diff --git a/.travis.yml b/.travis.yml index f7b811c..e4d5b28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ - +dist: xenial language: python python: - - "2.7" + - "3.5" # Need to support debian stretch for edison 32-bit installs install: - - pip install -e . - - sudo python setup.py install -script: make travis + - make ci-install +script: + - make test diff --git a/Makefile b/Makefile index 7206a0f..9498e31 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,19 @@ TESTS = $(wildcard decocare/*.py decocare/*/*.py) test: - python -m doctest -v ${TESTS} + python3 -m doctest -v ${TESTS} install: - python setup.py develop + python3 setup.py develop install decocare/etc/80-medtronic-carelink.rules /etc/udev/rules.d/ udevadm control --reload-rules ci-install: - python setup.py develop + python3 -V + python3 setup.py develop ci-test: ci-install test - # do the travis dance docs: (cd doc; make) -travis: test - # do the travis dance .PHONY: test docs diff --git a/README.markdown b/README.markdown index 88fc47e..56a8fc6 100644 --- a/README.markdown +++ b/README.markdown @@ -50,15 +50,15 @@ This only needs to be done once: ```bash $ easy_install decocare # or -$ pip install decocare +$ pip3 install decocare ``` ### From source ```bash git clone https://github.com/bewest/decoding-carelink.git cd decoding-carelink -sudo python ez_setup.py # only if you rarely use python -sudo python setup.py develop +sudo python3 ez_setup.py # only if you rarely use python +sudo python3 setup.py develop ``` ### Contribute your logs @@ -722,23 +722,23 @@ Each experiment is saved in the `./logs` directory, which is tracked by git. Source a bunch of helper functions, notably: ##### `run_stick`_ -Runs `python decocare/stick.py /dev/ttyUSB0` +Runs `python3 decocare/stick.py /dev/ttyUSB0` and saves results in `logs/stick.log`. When run by `status-quo.sh`, it creates `./logs/baseline.stick.log` before continuing. At end of experiments, it records `./logs/postmortem.stick.log` ##### `run_session`_ -Runs `python decocare/session.py /dev/ttyUSB0 ` +Runs `python3 decocare/session.py /dev/ttyUSB0 ` and saves results in `logs/session.log`. ##### `run_commands`_ -Runs `python decocare/commands.py /dev/ttyUSB0 ` +Runs `python3 decocare/commands.py /dev/ttyUSB0 ` and saves results in `logs/commands.log`. ##### `run_download`_ -Runs `python decocare/download.py /dev/ttyUSB0 ` +Runs `python3 decocare/download.py /dev/ttyUSB0 ` and saves results in `logs/download.log`. The download script is configured to save each page of historical data as a raw @@ -757,7 +757,7 @@ is saved in `./status-quo.log`. #### `run_regress` After `. bin/common`, `export SERIAL=511888` with your serial number. -Runs `python list_history.py` on each binary file found in `logs/`, saves +Runs `python3 list_history.py` on each binary file found in `logs/`, saves results in `./analysis//...`. This is what I use to render the markdown files in analysis. diff --git a/analysis/experiments/basal-hist-2006/munge.py b/analysis/experiments/basal-hist-2006/munge.py index 30dadaf..fbea751 100644 --- a/analysis/experiments/basal-hist-2006/munge.py +++ b/analysis/experiments/basal-hist-2006/munge.py @@ -1,19 +1,20 @@ #!/bin/python - import sys + def main(ifile, ofile): - B = ifile.read( ) - end = len(B) - cur = 0 - while cur < end: - ofile.write(B[cur+2:cur+10]) - cur += 10 + B = ifile.read() + end = len(B) + cur = 0 + while cur < end: + ofile.write(B[cur + 2 : cur + 10]) + cur += 10 + -if __name__ == '__main__': - ifile = open(sys.argv[1], 'r', 10) - ofile = open(sys.argv[2], 'w') - main(ifile, ofile) +if __name__ == "__main__": + ifile = open(sys.argv[1], 10) + ofile = open(sys.argv[2], "w") + main(ifile, ofile) ##### # EOF diff --git a/analysis/experiments/cl2.py b/analysis/experiments/cl2.py index 7fac789..e0548e0 100755 --- a/analysis/experiments/cl2.py +++ b/analysis/experiments/cl2.py @@ -1,24 +1,23 @@ #!/usr/bin/python -import user +import logging -#import struct +# import struct import sys -import serial import time -import logging -from pprint import pprint, pformat +from pprint import pformat, pprint -from decocare import link +import serial +import user +from decocare import lib, link from decocare.commands import * -from decocare.stick import ProductInfo, InterfaceStats -from decocare import lib +from decocare.stick import InterfaceStats, ProductInfo -logging.basicConfig( stream=sys.stdout ) -log = logging.getLogger( 'auditor' ) -log.setLevel( logging.DEBUG ) -log.info( 'hello world' ) -io = logging.getLogger( 'auditor.io' ) -io.setLevel( logging.DEBUG ) +logging.basicConfig(stream=sys.stdout) +log = logging.getLogger("auditor") +log.setLevel(logging.DEBUG) +log.info("hello world") +io = logging.getLogger("auditor.io") +io.setLevel(logging.DEBUG) """ ###################### @@ -41,116 +40,134 @@ """ -class ProtocolError(Exception): pass -class BadDeviceCommError(ProtocolError): pass +class ProtocolError(Exception): + pass + + +class BadDeviceCommError(ProtocolError): + pass + + +class DeviceCommsError(ProtocolError): + pass + + +class RFFailed(DeviceCommsError): + pass + + +class AckError(DeviceCommsError): + pass -class DeviceCommsError(ProtocolError): pass -class RFFailed(DeviceCommsError): pass -class AckError(DeviceCommsError): pass def retry(block, retry=3, sleep=0): - r = None - for i in xrange(retry): - log.info('retry:%s:%i' % (block, i)) - r = block( ) - if r: - return r - if sleep: - time.sleep(sleep) - return r - -class Link( link.Link ): - class ID: - VENDOR = 0x0a21 - PRODUCT = 0x8001 - timeout = .500 - def __init__( self, port, timeout=None ): - super(type(self), self).__init__(port, timeout) - self.setTimeout(.500) - - def setTimeout(self, timeout): - self.serial.setTimeout(timeout) - def getTimeout(self): - return self.serial.getTimeout() - - def initUSBComms(self): - self.initCommunicationsIO() - #self.initDevice() - - def getSignalStrength(self): - result = self.readSignalStrength() - signal = result[0] - - def readSignalStrength(self): - result = self.sendComLink2Command(6, 0) - # result[0] is signal strength - log.info('%r:readSignalStrength:%s' % (self, int(result[0]))) - return int(result[0]) - - def initCommunicationsIO(self): - # close/open serial - self.readProductInfo( ) - sig = 0 - while sig < 50: - sig = self.readSignalStrength() - def endCommunicationsIO(self): - self.readSignalStrength() - self.readInterfaceStatistics() - # close port - self.close() - - def readProductInfo(self): - result = self.sendComLink2Command(4) - # 1/0/255 - log.info('readProductInfo:result') - freq = result[5] - info = self.decodeProductInfo(result) - log.info('product info: %s' % pformat(info)) - # decodeInterface stats - - def decodeProductInfo(self, data): - return ProductInfo.decode(data) - - def sendComLink2Command(self, msg, a2=0x00, a3=0x00): - # generally commands are 3 bytes, most often CMD, 0x00, 0x00 - msg = bytearray([ msg, a2, a3 ]) - io.info('sendComLink2Command:write:%02x' % msg[0]) - self.write(msg) - return retry(self.checkAck, sleep=.01) - return self.checkAck() - # throw local usb exception - - def checkAck(self): - log.info('checkAck sleeping .100') - time.sleep(.100) - result = bytearray(self.read(64)) - io.info('checkAck:read') - if len(result) == 0: - log.error("ACK is zero bytes!") - return False - raise BadDeviceCommError("ACK is not 64 bytes: %s" % lib.hexdump(result)) - commStatus = result[0] - # usable response - assert commStatus == 1, ('commStatus: %02x expected 0x1' % commStatus) - status = result[1] - # status == 102 'f' NAK, look up NAK - if status == 85: # 'U' - log.info('checkACK OK, found %s total bytes' % len(result)) - return result[3:] - assert False, "NAK!!" - - def decodeIFaceStats(self, data): - return InterfaceStats.decode(data) - - def readInterfaceStatistics(self): - # decode and log stats - result = self.sendComLink2Command(5, 0) - info = self.decodeIFaceStats(result) - log.info("read radio Interface Stats: %s" % pformat(info)) - result = self.sendComLink2Command(5, 1) - info = self.decodeIFaceStats(result) - log.info("read stick Interface Stats: %s" % pformat(info)) + r = None + for i in range(retry): + log.info("retry:%s:%i" % (block, i)) + r = block() + if r: + return r + if sleep: + time.sleep(sleep) + return r + + +class Link(link.Link): + class ID: + VENDOR = 0x0A21 + PRODUCT = 0x8001 + + timeout = 0.500 + + def __init__(self, port, timeout=None): + super(type(self), self).__init__(port, timeout) + self.setTimeout(0.500) + + def setTimeout(self, timeout): + self.serial.setTimeout(timeout) + + def getTimeout(self): + return self.serial.getTimeout() + + def initUSBComms(self): + self.initCommunicationsIO() + # self.initDevice() + + def getSignalStrength(self): + result = self.readSignalStrength() + signal = result[0] + + def readSignalStrength(self): + result = self.sendComLink2Command(6, 0) + # result[0] is signal strength + log.info("{!r}:readSignalStrength:{}".format(self, int(result[0]))) + return int(result[0]) + + def initCommunicationsIO(self): + # close/open serial + self.readProductInfo() + sig = 0 + while sig < 50: + sig = self.readSignalStrength() + + def endCommunicationsIO(self): + self.readSignalStrength() + self.readInterfaceStatistics() + # close port + self.close() + + def readProductInfo(self): + result = self.sendComLink2Command(4) + # 1/0/255 + log.info("readProductInfo:result") + freq = result[5] + info = self.decodeProductInfo(result) + log.info("product info: %s" % pformat(info)) + # decodeInterface stats + + def decodeProductInfo(self, data): + return ProductInfo.decode(data) + + def sendComLink2Command(self, msg, a2=0x00, a3=0x00): + # generally commands are 3 bytes, most often CMD, 0x00, 0x00 + msg = bytearray([msg, a2, a3]) + io.info("sendComLink2Command:write:%02x" % msg[0]) + self.write(msg) + return retry(self.checkAck, sleep=0.01) + return self.checkAck() + # throw local usb exception + + def checkAck(self): + log.info("checkAck sleeping .100") + time.sleep(0.100) + result = bytearray(self.read(64)) + io.info("checkAck:read") + if len(result) == 0: + log.error("ACK is zero bytes!") + return False + raise BadDeviceCommError("ACK is not 64 bytes: %s" % lib.hexdump(result)) + commStatus = result[0] + # usable response + assert commStatus == 1, "commStatus: %02x expected 0x1" % commStatus + status = result[1] + # status == 102 'f' NAK, look up NAK + if status == 85: # 'U' + log.info("checkACK OK, found %s total bytes" % len(result)) + return result[3:] + assert False, "NAK!!" + + def decodeIFaceStats(self, data): + return InterfaceStats.decode(data) + + def readInterfaceStatistics(self): + # decode and log stats + result = self.sendComLink2Command(5, 0) + info = self.decodeIFaceStats(result) + log.info("read radio Interface Stats: %s" % pformat(info)) + result = self.sendComLink2Command(5, 1) + info = self.decodeIFaceStats(result) + log.info("read stick Interface Stats: %s" % pformat(info)) ####################### @@ -158,317 +175,292 @@ def readInterfaceStatistics(self): # # def CRC8(data): - return lib.CRC8.compute(data) + return lib.CRC8.compute(data) + ################################ # Remote Stuff # -class Device(object): - def __init__(self, link): - self.link = link - - def execute(self, command): - self.command = command - for i in xrange(max(1, self.command.retries)): - log.info('execute attempt: %s' % (i + 1)) - try: - self.allocateRawData() - self.sendAndRead() - return - except BadDeviceCommError, e: - log.critical("ERROR: %s" % e) - self.clearBuffers( ) - - def sendAndRead(self): - self.sendDeviceCommand() - if self.expectedLength > 0: - # in original code, this modifies the length tested in the previous if - # statement - self.command.data = self.readDeviceData() - - def sendDeviceCommand(self): - packet = self.buildTransmitPacket() - io.info('sendDeviceCommand:write:%r' % (self.command)) - self.link.write(packet) - log.info('sleeping: %s' % self.command.effectTime) - time.sleep(self.command.effectTime) - #time.sleep(.001) - code = self.command.code - params = self.command.params - if code != 93 or params[0] != 0: - self.link.checkAck() - - def allocateRawData(self): - self.command.allocateRawData() - self.expectedLength = self.command.bytesPerRecord * self.command.maxRecords - - def readDeviceData(self): - self.eod = False - results = bytearray( ) - while not self.eod: - data = self.readDeviceDataIO( ) - results.extend(data) - return results - - def readDeviceDataIO(self): - results = self.readData() - lb, hb = results[5] & 0x7F, results[6] - self.eod = (results[5] & 0x80) > 0 - resLength = lib.BangInt((lb, hb)) - log.info('XXX resLength: %s' % resLength) - #assert resLength < 64, ("cmd low byte count:\n%s" % lib.hexdump(results)) - - data = results[13:13+resLength] - assert len(data) == resLength - crc = results[-1] - # crc check - log.info('readDeviceDataIO:msgCRC:%r:expectedCRC:%r:data:%s' % (crc, CRC8(data), lib.hexdump(data))) - assert crc == CRC8(data) - return data - - def readData(self): - bytesAvailable = self.getNumBytesAvailable() - packet = [12, 0, lib.HighByte(bytesAvailable), lib.LowByte(bytesAvailable)] - packet.append( CRC8(packet) ) - - response = self.writeAndRead(packet, bytesAvailable) - # assert response.length > 14 - log.info('readData validating remote response: %02x' % response[0]) - log.info('readData; foreign response should be at least 14 bytes? %s %s' % (len(response), len(response) > 14)) - log.info('readData; retries %s' % int(response[3])) - dl_status = int(response[0]) - if dl_status != 0x02: - raise BadDeviceCommError("bad dl response! %r" % response) - assert (int(response[0]) == 2), repr(response) - # response[1] != 0 # interface number !=0 - # response[2] == 5 # timeout occurred - # response[2] == 2 # NAK - # response[2] # should be within 0..4 - log.info("readData ACK") - return response - - def writeAndRead(self, msg, length): - io.info("writeAndRead:") - self.link.write(bytearray(msg)) - #time.sleep(.250) - #self.link.setTimeout(self.command.timeout) - return bytearray(self.link.read(length)) - - def getNumBytesAvailable(self): - result = self.readStatus( ) - start = time.time() - i = 0 - while result == 0 and time.time() - start < 1: - log.debug('%r:getNumBytesAvailable:attempt:%s' % (self, i)) - result = self.readStatus( ) - log.info('sleeping in getNumBytesAvailable, .100') - time.sleep(.10) - i += 1 - log.info('getNumBytesAvailable:%s' % result) - return result - - def clearBuffers(self): - garbage = -1 - while garbage: - garbage = bytearray(self.link.read(64)) - log.error("found garbage:\n%s" % lib.hexdump(garbage)) - - - def readStatus(self): - result = self.link.sendComLink2Command(3) - commStatus = result[0] # 0 indicates success - if commStatus != 0: - log.error("readStatus: non-zero status: %02x" % commStatus) - raise BadDeviceCommError("readStatus: non-zero status: %02x" % commStatus) - status = result[2] - lb, hb = result[3], result[4] - bytesAvailable = lib.BangInt((lb, hb)) - self.status = status - log.info('status byte: %02x' % status) - if (status & 0x2) > 0: - log.info('STATUS: receive in progress!') - if (status & 0x4) > 0: - log.info('STATUS: transmit in progress!') - if (status & 0x8) > 0: - log.info('STATUS: interface error!') - if (status & 0x10) > 0: - log.info('STATUS: recieve overflow!') - if (status & 0x20) > 0: - log.info('STATUS: transmit overflow!') - assert commStatus == 0 - if (status & 0x1) > 0: - return bytesAvailable - return 0 - """ - def readStatus(self): - result = [ ] - def fetch_status( ): - res = self.link.sendComLink2Command(3) - log.info("res: %r" % res) - #if res and res[0] == 0: # 0 indicates success - if res and len(res) > 0: - return res - return False - - result = retry(fetch_status) - if not result: - raise RFFailed("rf read header indicates failure %s" % lib.hexdump(result)) - commStatus = result[0] # 0 indicates success - if commStatus != 0: - log.error("readStatus: non-zero status: %02x" % commStatus) - status = result[2] - lb, hb = result[3], result[4] - bytesAvailable = lib.BangInt((lb, hb)) - self.status = status - log.info('status byte: %02x' % status) - if (status & 0x2) > 0: - log.info('STATUS: receive in progress!') - if (status & 0x4) > 0: - log.info('STATUS: transmit in progress!') - if (status & 0x8) > 0: - log.info('STATUS: interface error!') - if (status & 0x10) > 0: - log.info('STATUS: recieve overflow!') - if (status & 0x20) > 0: - log.info('STATUS: transmit overflow!') - assert commStatus == 0, "commStatus must be 0x00 (%02x)" % commStatus - if (status & 0x1) > 0: - return bytesAvailable - return 0 - """ - - def buildTransmitPacket(self): - return self.command.format( ) +class Device: + def __init__(self, link): + self.link = link + + def execute(self, command): + self.command = command + for i in range(max(1, self.command.retries)): + log.info("execute attempt: %s" % (i + 1)) + try: + self.allocateRawData() + self.sendAndRead() + return + except BadDeviceCommError as e: + log.critical("ERROR: %s" % e) + self.clearBuffers() + + def sendAndRead(self): + self.sendDeviceCommand() + if self.expectedLength > 0: + # in original code, this modifies the length tested in the previous if + # statement + self.command.data = self.readDeviceData() + + def sendDeviceCommand(self): + packet = self.buildTransmitPacket() + io.info("sendDeviceCommand:write:%r" % (self.command)) + self.link.write(packet) + log.info("sleeping: %s" % self.command.effectTime) + time.sleep(self.command.effectTime) + # time.sleep(.001) + code = self.command.code + params = self.command.params + if code != 93 or params[0] != 0: + self.link.checkAck() + + def allocateRawData(self): + self.command.allocateRawData() + self.expectedLength = self.command.bytesPerRecord * self.command.maxRecords + + def readDeviceData(self): + self.eod = False + results = bytearray() + while not self.eod: + data = self.readDeviceDataIO() + results.extend(data) + return results + + def readDeviceDataIO(self): + results = self.readData() + lb, hb = results[5] & 0x7F, results[6] + self.eod = (results[5] & 0x80) > 0 + resLength = lib.BangInt((lb, hb)) + log.info("XXX resLength: %s" % resLength) + # assert resLength < 64, ("cmd low byte count:\n%s" % lib.hexdump(results)) + + data = results[13 : 13 + resLength] + assert len(data) == resLength + crc = results[-1] + # crc check + log.info( + "readDeviceDataIO:msgCRC:{!r}:expectedCRC:{!r}:data:{}".format( + crc, CRC8(data), lib.hexdump(data) + ) + ) + assert crc == CRC8(data) + return data + + def readData(self): + bytesAvailable = self.getNumBytesAvailable() + packet = [12, 0, lib.HighByte(bytesAvailable), lib.LowByte(bytesAvailable)] + packet.append(CRC8(packet)) + + response = self.writeAndRead(packet, bytesAvailable) + # assert response.length > 14 + log.info("readData validating remote response: %02x" % response[0]) + log.info( + "readData; foreign response should be at least 14 bytes? {} {}".format( + len(response), len(response) > 14 + ) + ) + log.info("readData; retries %s" % int(response[3])) + dl_status = int(response[0]) + if dl_status != 0x02: + raise BadDeviceCommError("bad dl response! %r" % response) + assert int(response[0]) == 2, repr(response) + # response[1] != 0 # interface number !=0 + # response[2] == 5 # timeout occurred + # response[2] == 2 # NAK + # response[2] # should be within 0..4 + log.info("readData ACK") + return response + + def writeAndRead(self, msg, length): + io.info("writeAndRead:") + self.link.write(bytearray(msg)) + # time.sleep(.250) + # self.link.setTimeout(self.command.timeout) + return bytearray(self.link.read(length)) + + def getNumBytesAvailable(self): + result = self.readStatus() + start = time.time() + i = 0 + while result == 0 and time.time() - start < 1: + log.debug("{!r}:getNumBytesAvailable:attempt:{}".format(self, i)) + result = self.readStatus() + log.info("sleeping in getNumBytesAvailable, .100") + time.sleep(0.10) + i += 1 + log.info("getNumBytesAvailable:%s" % result) + return result + + def clearBuffers(self): + garbage = -1 + while garbage: + garbage = bytearray(self.link.read(64)) + log.error("found garbage:\n%s" % lib.hexdump(garbage)) + + def readStatus(self): + result = self.link.sendComLink2Command(3) + commStatus = result[0] # 0 indicates success + if commStatus != 0: + log.error("readStatus: non-zero status: %02x" % commStatus) + raise BadDeviceCommError("readStatus: non-zero status: %02x" % commStatus) + status = result[2] + lb, hb = result[3], result[4] + bytesAvailable = lib.BangInt((lb, hb)) + self.status = status + log.info("status byte: %02x" % status) + if (status & 0x2) > 0: + log.info("STATUS: receive in progress!") + if (status & 0x4) > 0: + log.info("STATUS: transmit in progress!") + if (status & 0x8) > 0: + log.info("STATUS: interface error!") + if (status & 0x10) > 0: + log.info("STATUS: recieve overflow!") + if (status & 0x20) > 0: + log.info("STATUS: transmit overflow!") + assert commStatus == 0 + if (status & 0x1) > 0: + return bytesAvailable + return 0 + + def buildTransmitPacket(self): + return self.command.format() + def initDevice(link): - device = Device(link) + device = Device(link) + + # log.info("TURN POWER ON") + # comm = PowerControl() + # device.execute(comm) + # log.info('comm:%s:data:%s' % (comm, getattr(comm, 'data', None))) + # time.sleep(6) - #log.info("TURN POWER ON") - #comm = PowerControl() - #device.execute(comm) - #log.info('comm:%s:data:%s' % (comm, getattr(comm, 'data', None))) - #time.sleep(6) + comm = ReadErrorStatus() + device.execute(comm) + log.info("comm:{}:data:{}".format(comm, getattr(comm, "data", None))) - comm = ReadErrorStatus() - device.execute(comm) - log.info('comm:%s:data:%s' % (comm, getattr(comm, 'data', None))) + comm = ReadPumpState() + device.execute(comm) + log.info("comm:{}:data:{}".format(comm, getattr(comm, "data", None))) - comm = ReadPumpState() - device.execute(comm) - log.info('comm:%s:data:%s' % (comm, getattr(comm, 'data', None))) + return device - return device def do_commands(device): - comm = ReadPumpModel( ) - device.execute(comm) - log.info('comm:%s:data:%s' % (comm, getattr(comm.getData( ), 'data', None))) - log.info('REMOTE PUMP MODEL NUMBER: %s' % comm.getData( )) - - log.info("READ RTC") - comm = ReadRTC( ) - device.execute(comm) - log.info('comm:RTC:%s' % (comm.getData( ))) - - log.info("READ PUMP ID") - comm = ReadPumpID( ) - device.execute(comm) - log.info('comm:READ PUMP ID: ID: %s' % (comm.getData( ))) - - - log.info("Battery Status") - comm = ReadBatteryStatus( ) - device.execute(comm) - log.info('comm:READ Battery Status: %r' % (comm.getData( ))) - - log.info("Firmware Version") - comm = ReadFirmwareVersion( ) - device.execute(comm) - log.info('comm:READ Firmware Version: %r' % (comm.getData( ))) - - log.info("remaining insulin") - comm = ReadRemainingInsulin( ) - device.execute(comm) - log.info('comm:READ Remaining Insulin: %r' % (comm.getData( ))) - - log.info("read totals today") - comm = ReadTotalsToday( ) - device.execute(comm) - log.info('comm:READ totals today: %r' % (comm.getData( ))) - - log.info("read remote IDS") - comm = ReadRadioCtrlACL( ) - device.execute(comm) - log.info('comm:READ radio ACL: %r' % (comm.getData( ))) - - log.info("read temporary basal") - comm = ReadBasalTemp( ) - device.execute(comm) - log.info('comm:READ temp basal: %r' % (comm.getData( ))) - - log.info("read settings") - comm = ReadSettings( ) - device.execute(comm) - log.info('comm:READ settings!: %r' % (comm.getData( ))) - - log.info("read contrast") - comm = ReadContrast( ) - device.execute(comm) - log.info('comm:READ contrast: %r' % (comm.getData( ))) - - log.info("read cur page number") - comm = ReadCurPageNumber( ) - device.execute(comm) - log.info('comm:READ page number!!!: %r' % (comm.getData( ))) - - log.info("read HISTORY DATA") - comm = ReadHistoryData( ) - device.execute(comm) - #log.info('comm:READ history data!!!: %r' % (comm.getData( ))) + comm = ReadPumpModel() + device.execute(comm) + log.info("comm:{}:data:{}".format(comm, getattr(comm.getData(), "data", None))) + log.info("REMOTE PUMP MODEL NUMBER: %s" % comm.getData()) + + log.info("READ RTC") + comm = ReadRTC() + device.execute(comm) + log.info("comm:RTC:%s" % (comm.getData())) + + log.info("READ PUMP ID") + comm = ReadPumpID() + device.execute(comm) + log.info("comm:READ PUMP ID: ID: %s" % (comm.getData())) + + log.info("Battery Status") + comm = ReadBatteryStatus() + device.execute(comm) + log.info("comm:READ Battery Status: %r" % (comm.getData())) + + log.info("Firmware Version") + comm = ReadFirmwareVersion() + device.execute(comm) + log.info("comm:READ Firmware Version: %r" % (comm.getData())) + + log.info("remaining insulin") + comm = ReadRemainingInsulin() + device.execute(comm) + log.info("comm:READ Remaining Insulin: %r" % (comm.getData())) + + log.info("read totals today") + comm = ReadTotalsToday() + device.execute(comm) + log.info("comm:READ totals today: %r" % (comm.getData())) + + log.info("read remote IDS") + comm = ReadRadioCtrlACL() + device.execute(comm) + log.info("comm:READ radio ACL: %r" % (comm.getData())) + + log.info("read temporary basal") + comm = ReadBasalTemp() + device.execute(comm) + log.info("comm:READ temp basal: %r" % (comm.getData())) + + log.info("read settings") + comm = ReadSettings() + device.execute(comm) + log.info("comm:READ settings!: %r" % (comm.getData())) + + log.info("read contrast") + comm = ReadContrast() + device.execute(comm) + log.info("comm:READ contrast: %r" % (comm.getData())) + + log.info("read cur page number") + comm = ReadCurPageNumber() + device.execute(comm) + log.info("comm:READ page number!!!: %r" % (comm.getData())) + + log.info("read HISTORY DATA") + comm = ReadHistoryData() + device.execute(comm) + # log.info('comm:READ history data!!!: %r' % (comm.getData( ))) + def get_pages(device): - log.info("read cur page number") - comm = ReadCurPageNumber( ) - device.execute(comm) - pages = comm.getData( ) - log.info('attempting to read %s pages of history' % pages) - - for x in range(pages + 1): - log.info('comm:READ HISTORY DATA page number: %r' % (x)) - comm = ReadHistoryData( params=[ x ] ) + log.info("read cur page number") + comm = ReadCurPageNumber() device.execute(comm) - page = comm.getData( ) - log.info("XXX: READ HISTORY DATA!!:\n%s" % page) - #log.info('comm:READ history data!!!: %r' % (comm.getData( ))) + pages = comm.getData() + log.info("attempting to read %s pages of history" % pages) + + for x in range(pages + 1): + log.info("comm:READ HISTORY DATA page number: %r" % (x)) + comm = ReadHistoryData(params=[x]) + device.execute(comm) + page = comm.getData() + log.info("XXX: READ HISTORY DATA!!:\n%s" % page) + # log.info('comm:READ history data!!!: %r' % (comm.getData( ))) + def shutdownDevice(device): - comm = PowerControlOff() - device.execute(comm) - log.info('comm:%s:data:%s' % (comm, getattr(comm, 'data', None))) - - -if __name__ == '__main__': - io.info("hello world") - import doctest - doctest.testmod( ) - - port = None - try: - port = sys.argv[1] - except IndexError, e: - print "usage:\n%s /dev/ttyUSB0" % sys.argv[0] - sys.exit(1) - - link = Link(port) - link.initUSBComms() - device = initDevice(link) - #do_commands(device) - get_pages(device) - #shutdownDevice(device) - link.endCommunicationsIO() - #pprint( carelink( ProductInfo( ) ).info ) + comm = PowerControlOff() + device.execute(comm) + log.info("comm:{}:data:{}".format(comm, getattr(comm, "data", None))) + + +if __name__ == "__main__": + io.info("hello world") + import doctest + + doctest.testmod() + + port = None + try: + port = sys.argv[1] + except IndexError as e: + print("usage:\n%s /dev/ttyUSB0" % sys.argv[0]) + sys.exit(1) + + link = Link(port) + link.initUSBComms() + device = initDevice(link) + # do_commands(device) + get_pages(device) + # shutdownDevice(device) + link.endCommunicationsIO() + # pprint( carelink( ProductInfo( ) ).info ) ##### diff --git a/analysis/experiments/mypump.py b/analysis/experiments/mypump.py index bb96102..9039a5f 100644 --- a/analysis/experiments/mypump.py +++ b/analysis/experiments/mypump.py @@ -1,76 +1,70 @@ - -import serial import argparse +import logging import sys +from pprint import pformat, pprint + +import pump +import serial -from pprint import pprint, pformat +io = logging.getLogger() -import pump -import logging +def get_argparser(): + parser = argparse.ArgumentParser() -io = logging.getLogger( ) + parser.add_argument( + "--port", "-p", default="/dev/ttyUSB0", help="path to serial port" + ) -def get_argparser( ): - parser = argparse.ArgumentParser( ) + parser.add_argument( + "--verbosity", "-v", default=0, action="count", help="verbosity" + ) - parser.add_argument( - '--port', '-p', default='/dev/ttyUSB0', - help="path to serial port" - ) + parser.add_argument( + "--log", + "-l", + default=sys.stdout, + type=argparse.FileType("a"), + help="log output (stdout default)", + ) - parser.add_argument( - '--verbosity', '-v', - default=0, + parser.add_argument( + "serial", default="208850", help="serial number of target pump (eg. 208850)" + ) - action="count", - help="verbosity" - ) + return parser - parser.add_argument( - '--log', '-l', - default=sys.stdout, - type=argparse.FileType('a'), - help="log output (stdout default)" - ) - parser.add_argument( - 'serial', default='208850', - help="serial number of target pump (eg. 208850)" - ) +class App: + _log_map = {0: logging.INFO, 1: logging.DEBUG, 2: logging.WARN, 3: logging.DEBUG} - return parser + def __init__(self): + self.parser = get_argparser() + self.opts = self.parser.parse_args() + logging.basicConfig(stream=self.opts.log) + if self.opts.log.name != "": + stdout = logging.StreamHandler(stream=sys.stdout) + io.addHandler(stdout) + io.setLevel(self._log_map.get(self.opts.verbosity, logging.DEBUG)) -class App(object): - _log_map = { 0: logging.INFO, 1: logging.DEBUG, - 2: logging.WARN, 3: logging.DEBUG } - def __init__(self): - self.parser = get_argparser( ) - self.opts = self.parser.parse_args( ) - logging.basicConfig(stream=self.opts.log) - if self.opts.log.name != '': - stdout = logging.StreamHandler(stream=sys.stdout) - io.addHandler(stdout) + def main(self): - io.setLevel(self._log_map.get(self.opts.verbosity, logging.DEBUG)) + io.debug("debug hello world!!!") + io.info("info hello world!!!") + io.warn("warn hello world!!!") + io.error("error hello world!!!") + io.critical("critical hello world!!!") + io.fatal("fatal hello world!!!") - def main(self): - io.debug( "debug hello world!!!" ) - io.info( "info hello world!!!" ) - io.warn( "warn hello world!!!" ) - io.error( "error hello world!!!" ) - io.critical( "critical hello world!!!" ) - io.fatal( "fatal hello world!!!" ) +if __name__ == "__main__": + import doctest + doctest.testmod() -if __name__ == '__main__': - import doctest - doctest.testmod( ) - - app = App( ) - app.main( ) + app = App() + app.main() ##### # EOF diff --git a/analysis/experiments/rosetta-july-1-2006/read-hexcap.py b/analysis/experiments/rosetta-july-1-2006/read-hexcap.py index 5605b47..87b3282 100644 --- a/analysis/experiments/rosetta-july-1-2006/read-hexcap.py +++ b/analysis/experiments/rosetta-july-1-2006/read-hexcap.py @@ -1,44 +1,37 @@ #!/usr/bin/python - - +import logging +import sys import scapy import scapy.all -import sys -import logging logging.basicConfig(level=logging.INFO) import argparse +from pprint import pformat, pprint -from pprint import pprint, pformat -def get_argparse( ): +def get_argparse(): - parser = argparse.ArgumentParser( ) - parser.add_argument( - 'inputs', nargs="+", type=argparse.FileType('r') - ) - return parser + parser = argparse.ArgumentParser() + parser.add_argument("inputs", nargs="+", type=argparse.FileType("r")) + return parser def do_input(stream): - #pprint(stream) - print stream.name - text = stream.read( ) - #scapy.all.import_hexcap( ) - -def main(*args): - parser = get_argparse( ) - args = parser.parse_args( ) - pprint(args) - for stream in args.inputs: - do_input(stream) - + # pprint(stream) + print(stream.name) + text = stream.read() + # scapy.all.import_hexcap( ) +def main(*args): + parser = get_argparse() + args = parser.parse_args() + pprint(args) + for stream in args.inputs: + do_input(stream) if __name__ == "__main__": - main(*sys.argv) - logging.info("hello world") - + main(*sys.argv) + logging.info("hello world") diff --git a/bin/command_control_demo.sh b/bin/command_control_demo.sh index 27e5eb3..2d78973 100755 --- a/bin/command_control_demo.sh +++ b/bin/command_control_demo.sh @@ -14,7 +14,7 @@ INIT="$STATUS --init query" SERIAL=208850 # PORT is the device file to use. My udev rules create a nice # persistent device. You can try something like this: -# PORT=$(python decocare/scan.py) +# PORT=$(python3 decocare/scan.py) PORT=/dev/ttyUSB.Carelink0 # just send power, and query basic status diff --git a/bin/common b/bin/common index ebc5c86..b18deae 100644 --- a/bin/common +++ b/bin/common @@ -350,7 +350,7 @@ TOOL_ARGS="" function opcodes ( ) { x=${1} - python $TOOL $TOOL_ARGS $x 2>&1 + python3 $TOOL $TOOL_ARGS $x 2>&1 } diff --git a/bin/mm-bolus.py b/bin/mm-bolus.py index 2364e51..7a5472d 100755 --- a/bin/mm-bolus.py +++ b/bin/mm-bolus.py @@ -1,59 +1,53 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from decocare import commands -from decocare import lib +from decocare import commands, lib from decocare.helpers import cli -class BolusApp (cli.CommandApp): - """ %(prog)s - Send bolus command to a pump. - - XXX: Be careful please! - - Units might be wrong. Keep disconnected from pump until you trust it by - observing the right amount first. - """ - def customize_parser (self, parser): - parser.add_argument('units', - type=float, - help="Amount of insulin to bolus." - ) - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('--515', - dest='strokes_per_unit', - action='store_const', - const=10 - ) - group.add_argument('--554', - dest='strokes_per_unit', - action='store_const', - const=40 - ) - group.add_argument('--strokes', - dest='strokes_per_unit', - type=int - ) - - return parser - def main (self, args): - print args - self.bolus(args); - - def bolus (self, args): - query = commands.Bolus - kwds = dict(params=fmt_params(args)) - - resp = self.exec_request(self.pump, query, args=kwds, - dryrun=args.dryrun, render_hexdump=False) - return resp - -def fmt_params (args): - strokes = int(float(args.units) * args.strokes_per_unit) - if (args.strokes_per_unit > 10): - return [lib.HighByte(strokes), lib.LowByte(strokes)] - return [strokes] - -if __name__ == '__main__': - app = BolusApp( ) - app.run(None) + +class BolusApp(cli.CommandApp): + """ %(prog)s - Send bolus command to a pump. + + XXX: Be careful please! + + Units might be wrong. Keep disconnected from pump until you trust it by + observing the right amount first. + """ + + def customize_parser(self, parser): + parser.add_argument("units", type=float, help="Amount of insulin to bolus.") + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--515", dest="strokes_per_unit", action="store_const", const=10 + ) + group.add_argument( + "--554", dest="strokes_per_unit", action="store_const", const=40 + ) + group.add_argument("--strokes", dest="strokes_per_unit", type=int) + + return parser + + def main(self, args): + print(args) + self.bolus(args) + + def bolus(self, args): + query = commands.Bolus + kwds = dict(params=fmt_params(args)) + + resp = self.exec_request( + self.pump, query, args=kwds, dryrun=args.dryrun, render_hexdump=False + ) + return resp + + +def fmt_params(args): + strokes = int(float(args.units) * args.strokes_per_unit) + if args.strokes_per_unit > 10: + return [lib.HighByte(strokes), lib.LowByte(strokes)] + return [strokes] + + +if __name__ == "__main__": + app = BolusApp() + app.run(None) diff --git a/bin/mm-decode-history-page.py b/bin/mm-decode-history-page.py index f7a43e6..0afab77 100755 --- a/bin/mm-decode-history-page.py +++ b/bin/mm-decode-history-page.py @@ -1,55 +1,61 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK +import argparse +import datetime -import sys -import argparse, argcomplete -import textwrap - -from pprint import pprint, pformat -from binascii import hexlify # from datetime import datetime # from scapy.all import * import json -import datetime -json.JSONEncoder.default = lambda self,obj: (obj.isoformat() if isinstance(obj, datetime.datetime) else None) - -from decocare import lib, history, models - -from decocare.history import parse_record, HistoryPage - -def get_model (spec): - return models.known.get(spec, models.PumpModel)(spec, None) - -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") - - parser.add_argument('--collate', - dest='collate', - default=False, - action='store_true') - - parser.add_argument('--data', - choices=['glucose', 'pump'], - default='pump') - parser.add_argument('--model', - # type=get_model, - choices=models.known.keys( )) - parser.add_argument('--larger', - dest='larger', action='store_true') - parser.add_argument('--no-larger', - dest='larger', action='store_false') - - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - parser.set_defaults(larger=False) - argcomplete.autocomplete(parser) - return parser +import sys +import textwrap +from binascii import hexlify +from pprint import pformat, pprint + +import argcomplete + +json.JSONEncoder.default = lambda self, obj: ( + obj.isoformat() if isinstance(obj, datetime.datetime) else None +) + +from decocare import history, lib, models +from decocare.history import HistoryPage, parse_record + + +def get_model(spec): + return models.known.get(spec, models.PumpModel)(spec, None) + + +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) + + parser.add_argument("--collate", dest="collate", default=False, action="store_true") + + parser.add_argument("--data", choices=["glucose", "pump"], default="pump") + parser.add_argument( + "--model", + # type=get_model, + choices=list(models.known.keys()), + ) + parser.add_argument("--larger", dest="larger", action="store_true") + parser.add_argument("--no-larger", dest="larger", action="store_false") + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + parser.set_defaults(larger=False) + argcomplete.autocomplete(parser) + return parser + ## # move to history.py @@ -57,89 +63,97 @@ def get_opt_parser( ): def eat_nulls(fd): - nulls = bytearray( ) - for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): - if B[0] == 0x00: - nulls.extend(B) - else: - fd.seek(fd.tell( ) - 1) - break - print "found %s nulls" % len(nulls) - return nulls + nulls = bytearray() + for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): + if B[0] == 0x00: + nulls.extend(B) + else: + fd.seek(fd.tell() - 1) + break + print("found %s nulls" % len(nulls)) + return nulls + def find_records(stream, opts): - records = [ ] - errors = [ ] - bolus = bytearray( ) - extra = bytearray( ) - opcode = '' - - for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): - - if B == bytearray( [ 0x00, 0x00 ] ): - print ("#### STOPPING DOUBLE NULLS @ %s," % stream.tell( )), - nulls = eat_nulls(stream) - print "reading more to debug %#04x" % B[0] - print lib.hexdump(B, indent=4) - print lib.int_dump(B, indent=11) - - extra = bytearray(stream.read(32)) - print "##### DEBUG HEX" - print lib.hexdump(extra, indent=4) - print "##### DEBUG DECIMAL" - print lib.int_dump(extra, indent=11) - # print "XXX:???:XXX", history.parse_date(bolus).isoformat( ) - break - record = parse_record( stream, B, model=opts.model, larger=opts.larger ) - records.append(record) - - return records + records = [] + errors = [] + bolus = bytearray() + extra = bytearray() + opcode = "" + + for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): + + if B == bytearray([0x00, 0x00]): + print(("#### STOPPING DOUBLE NULLS @ %s," % stream.tell()), end=" ") + nulls = eat_nulls(stream) + print("reading more to debug %#04x" % B[0]) + print(lib.hexdump(B, indent=4)) + print(lib.int_dump(B, indent=11)) + + extra = bytearray(stream.read(32)) + print("##### DEBUG HEX") + print(lib.hexdump(extra, indent=4)) + print("##### DEBUG DECIMAL") + print(lib.int_dump(extra, indent=11)) + # print("XXX:???:XXX", history.parse_date(bolus).isoformat( )) + break + record = parse_record(stream, B, model=opts.model, larger=opts.larger) + records.append(record) + + return records + import collections -Response = collections.namedtuple('Response', 'data') - -def main( ): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - opts.model = get_model(opts.model) - tw_opts = { - 'width': 50, - 'subsequent_indent': ' ', - 'initial_indent': ' ', - } - wrapper = textwrap.TextWrapper(**tw_opts) - records = [ ] - for stream in opts.infile: - print "## START %s" % (stream.name) - if opts.collate and opts.data == 'pump': - page = HistoryPage(bytearray(stream.read( )), opts.model) - records.extend(page.decode(larger=opts.larger )) - elif opts.data == 'glucose': - page = Response(data=bytearray(stream.read( ))) - records.extend(opts.model.iter_glucose_pages.Cursor(opts.model).find_records(page)) - else: - records = find_records(stream, opts) - i = 0 - for record in records: - - prefix = '#### RECORD {} {}'.format(i, str(record)) - if getattr(record, 'pformat', None): - print record.pformat(prefix) - else: - # json.dumps(record, indent=2) - print prefix - i += 1 - print "`end %s: %s records`" % (stream.name, len(records)) - stream.close( ) - if opts.collate: - opts.out.write(json.dumps(records, indent=2)) - -if __name__ == '__main__': - import doctest - failures, tests = doctest.testmod( ) - if failures > 0: - print "REFUSING TO RUN DUE TO FAILED TESTS" - sys.exit(1) - main( ) + +Response = collections.namedtuple("Response", "data") + + +def main(): + parser = get_opt_parser() + opts = parser.parse_args() + opts.model = get_model(opts.model) + tw_opts = { + "width": 50, + "subsequent_indent": " ", + "initial_indent": " ", + } + wrapper = textwrap.TextWrapper(**tw_opts) + records = [] + for stream in opts.infile: + print("## START %s" % (stream.name)) + if opts.collate and opts.data == "pump": + page = HistoryPage(bytearray(stream.read()), opts.model) + records.extend(page.decode(larger=opts.larger)) + elif opts.data == "glucose": + page = Response(data=bytearray(stream.read())) + records.extend( + opts.model.iter_glucose_pages.Cursor(opts.model).find_records(page) + ) + else: + records = find_records(stream, opts) + i = 0 + for record in records: + + prefix = "#### RECORD {} {}".format(i, str(record)) + if getattr(record, "pformat", None): + print(record.pformat(prefix)) + else: + # json.dumps(record, indent=2) + print(prefix) + i += 1 + print("`end {}: {} records`".format(stream.name, len(records))) + stream.close() + if opts.collate: + opts.out.write(json.dumps(records, indent=2)) + + +if __name__ == "__main__": + import doctest + + failures, tests = doctest.testmod() + if failures > 0: + print("REFUSING TO RUN DUE TO FAILED TESTS") + sys.exit(1) + main() ##### # EOF diff --git a/bin/mm-glucose.py b/bin/mm-glucose.py index bdbf6fc..9803842 100755 --- a/bin/mm-glucose.py +++ b/bin/mm-glucose.py @@ -1,69 +1,89 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from decocare import commands, models, cgm -import json, argparse, sys - -from decocare.history import HistoryPage -from decocare.helpers import cli +import argparse +import json +import sys from dateutil.parser import parse from dateutil.tz import gettz +from decocare import cgm, commands, models +from decocare.helpers import cli +from decocare.history import HistoryPage + -class DownloadHistory (cli.CommandApp): +class DownloadHistory(cli.CommandApp): """%(prog)s - Grab history """ - def customize_parser (self, parser): - parser.add_argument('--model', - # type=get_model, - choices=models.known.keys( )) - - parser.add_argument('--timezone', - default=gettz( ), - type=gettz, - help="Timezone to use") - parser.add_argument('--parser-out', - dest="parsed_data", - default='-', - type=argparse.FileType('w'), - help="Put history json in this file") + def customize_parser(self, parser): + + parser.add_argument( + "--model", + # type=get_model, + choices=list(models.known.keys()), + ) + + parser.add_argument( + "--timezone", default=gettz(), type=gettz, help="Timezone to use" + ) + parser.add_argument( + "--parser-out", + dest="parsed_data", + default="-", + type=argparse.FileType("w"), + help="Put history json in this file", + ) return parser - - def download_page (self, number): - return self.exec_request(self.pump, commands.ReadGlucoseHistory, args=dict(page=number), render_decoded=False,render_hexdump=False) - - def download_history (self, args, pageRange): - records = [ ] - for i in range(1 + pageRange['page'] - pageRange['isig'], pageRange['page']): - print "Next page ", i + def download_page(self, number): + return self.exec_request( + self.pump, + commands.ReadGlucoseHistory, + args=dict(page=number), + render_decoded=False, + render_hexdump=False, + ) + + def download_history(self, args, pageRange): + records = [] + for i in range(1 + pageRange["page"] - pageRange["isig"], pageRange["page"]): + print("Next page ", i) try: pageRaw = self.download_page(i).data pageResult = cgm.PagedData.Data(pageRaw) records.extend(pageResult.decode()) except: - print "Unexpected error when downloading cgm-page ", i, " from pump:", sys.exc_info()[0] - - recordsJson = json.dumps(records); + print( + "Unexpected error when downloading cgm-page ", + i, + " from pump:", + sys.exc_info()[0], + ) + + recordsJson = json.dumps(records) args.parsed_data.write(recordsJson) - handle = open('glucose.json', 'wb') + handle = open("glucose.json", "wb") handle.write(recordsJson) - handle.close( ) + handle.close() def getPagesRange(self): - range = self.exec_request(self.pump, commands.ReadCurGlucosePageNumber, render_decoded=False,render_hexdump=False) - return range.getData( ) - - def main (self, args): + range = self.exec_request( + self.pump, + commands.ReadCurGlucosePageNumber, + render_decoded=False, + render_hexdump=False, + ) + return range.getData() + + def main(self, args): # Set Global variables.. self.timezone = args.timezone info = self.getPagesRange() self.download_history(args, info) -if __name__ == '__main__': - app = DownloadHistory( ) - app.run(None) +if __name__ == "__main__": + app = DownloadHistory() + app.run(None) diff --git a/bin/mm-history.py b/bin/mm-history.py index d8f1286..c11e614 100755 --- a/bin/mm-history.py +++ b/bin/mm-history.py @@ -1,82 +1,102 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK +import argparse +import json +import sys +from dateutil.parser import parse +from dateutil.tz import gettz from decocare import commands, models -import json, argparse, sys - -from decocare.history import HistoryPage from decocare.helpers import cli +from decocare.history import HistoryPage -from dateutil.parser import parse -from dateutil.tz import gettz -class DownloadHistory (cli.CommandApp): +class DownloadHistory(cli.CommandApp): """%(prog)s - Grab history """ - def customize_parser (self, parser): - - parser.add_argument('--model', - # type=get_model, - choices=models.known.keys( )) - - parser.add_argument('--timezone', - default=gettz( ), - type=gettz, - help="Timezone to use") - parser.add_argument('--parser-out', - dest="parsed_data", - default='-', - type=argparse.FileType('w'), - help="Put history json in this file") - return parser + def customize_parser(self, parser): + + parser.add_argument( + "--model", + # type=get_model, + choices=list(models.known.keys()), + ) + + parser.add_argument( + "--timezone", default=gettz(), type=gettz, help="Timezone to use" + ) + parser.add_argument( + "--parser-out", + dest="parsed_data", + default="-", + type=argparse.FileType("w"), + help="Put history json in this file", + ) + return parser - def download_page (self, number): - return self.exec_request(self.pump, commands.ReadHistoryData, args=dict(page=number), render_decoded=False,render_hexdump=False) + def download_page(self, number): + return self.exec_request( + self.pump, + commands.ReadHistoryData, + args=dict(page=number), + render_decoded=False, + render_hexdump=False, + ) - def find_records (self, page): + def find_records(self, page): - records = decoder.decode( ) + records = decoder.decode() - print "Found " , len(records), " records." + print("Found ", len(records), " records.") for record in records: - print " * found record", record.get('timestamp'), record['_type'] - if record.get('timestamp'): - dt = parse(record['timestamp']) + print(" * found record", record.get("timestamp"), record["_type"]) + if record.get("timestamp"): + dt = parse(record["timestamp"]) dt = dt.replace(tzinfo=self.timezone) - record.update(timestamp=dt.isoformat( )) + record.update(timestamp=dt.isoformat()) return records - def download_history (self, args, nrPages): - records = [ ] + def download_history(self, args, nrPages): + records = [] for i in range(1, nrPages): - print "Next page ", i + print("Next page ", i) try: pageRaw = self.download_page(i) pageResult = HistoryPage(pageRaw.data, self.pump.model) records.extend(pageResult.decode()) except: - print "Unexpected error when downloading cgm-page ", i, " from pump:", sys.exc_info()[0] - - recordsJson = json.dumps(records); + print( + "Unexpected error when downloading cgm-page ", + i, + " from pump:", + sys.exc_info()[0], + ) + + recordsJson = json.dumps(records) args.parsed_data.write(recordsJson) - handle = open('history.json', 'wb') + handle = open("history.json", "wb") handle.write(recordsJson) - handle.close( ) + handle.close() def getNumberOfPages(self): - range = self.exec_request(self.pump, commands.ReadCurPageNumber, render_decoded=False,render_hexdump=False) + range = self.exec_request( + self.pump, + commands.ReadCurPageNumber, + render_decoded=False, + render_hexdump=False, + ) return range.getData() - def main (self, args): + def main(self, args): # Set Global variables.. self.timezone = args.timezone nrPages = self.getNumberOfPages() self.download_history(args, nrPages) -if __name__ == '__main__': - app = DownloadHistory( ) - app.run(None) +if __name__ == "__main__": + app = DownloadHistory() + app.run(None) diff --git a/bin/mm-latest.py b/bin/mm-latest.py index 673cf6b..1100a50 100755 --- a/bin/mm-latest.py +++ b/bin/mm-latest.py @@ -1,212 +1,224 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from decocare import commands +import argparse import io import json -import argparse - from datetime import datetime + from dateutil import relativedelta from dateutil.parser import parse from dateutil.tz import gettz - -from decocare import lib -from decocare.history import parse_record, HistoryPage +from decocare import commands, lib from decocare.helpers import cli +from decocare.history import HistoryPage, parse_record + -class LatestActivity (cli.CommandApp): - """%(prog)s - Grab latest activity +class LatestActivity(cli.CommandApp): + """%(prog)s - Grab latest activity - Query pump for latest activity. - """ - def customize_parser (self, parser): - parser.add_argument('--no-clock', + Query pump for latest activity. + """ + + def customize_parser(self, parser): + parser.add_argument( + "--no-clock", dest="clock", action="store_false", default=True, - help="Also report current time on pump." - ) - parser.add_argument('--no-basal', + help="Also report current time on pump.", + ) + parser.add_argument( + "--no-basal", dest="basal", action="store_false", default=True, - help="Also report basal rates." - ) - parser.add_argument('--no-temp', + help="Also report basal rates.", + ) + parser.add_argument( + "--no-temp", dest="temp", action="store_false", default=True, - help="Also report temp basal rates." - ) - parser.add_argument('--no-reservoir', + help="Also report temp basal rates.", + ) + parser.add_argument( + "--no-reservoir", dest="reservoir", action="store_false", default=True, - help="Also report remaining insulin in reservoir." - ) - parser.add_argument('--no-status', + help="Also report remaining insulin in reservoir.", + ) + parser.add_argument( + "--no-status", dest="status", action="store_false", default=True, - help="Also report current suspend/bolus status." - ) - parser.add_argument('--parser-out', + help="Also report current suspend/bolus status.", + ) + parser.add_argument( + "--parser-out", dest="parsed_data", - default='-', - type=argparse.FileType('w'), - help="Put history json in this file" - ) - parser.add_argument('--rtc-out', + default="-", + type=argparse.FileType("w"), + help="Put history json in this file", + ) + parser.add_argument( + "--rtc-out", dest="rtc_archive", - default='-', - type=argparse.FileType('w'), - help="Put clock json in this file" - ) - parser.add_argument('--reservoir-out', + default="-", + type=argparse.FileType("w"), + help="Put clock json in this file", + ) + parser.add_argument( + "--reservoir-out", dest="reservoir_archive", - default='-', - type=argparse.FileType('w'), - help="Put reservoir json in this file" - ) - parser.add_argument('--settings-out', + default="-", + type=argparse.FileType("w"), + help="Put reservoir json in this file", + ) + parser.add_argument( + "--settings-out", dest="settings", - default='-', - type=argparse.FileType('w'), - help="Put settings json in this file" - ) - parser.add_argument('--temp-basal-status-out', + default="-", + type=argparse.FileType("w"), + help="Put settings json in this file", + ) + parser.add_argument( + "--temp-basal-status-out", dest="tempbasal", - default='-', - type=argparse.FileType('w'), - help="Put temp basal status json in this file" - ) - parser.add_argument('--basals-out', + default="-", + type=argparse.FileType("w"), + help="Put temp basal status json in this file", + ) + parser.add_argument( + "--basals-out", dest="basals", - default='-', - type=argparse.FileType('w'), - help="Put basal schedules json in this file" - ) - parser.add_argument('--status-out', + default="-", + type=argparse.FileType("w"), + help="Put basal schedules json in this file", + ) + parser.add_argument( + "--status-out", dest="status", - default='-', - type=argparse.FileType('w'), - help="Put status json in this file" - ) - parser.add_argument('--timezone', - default=gettz( ), - type=gettz, - help="Timezone to use" - ) - parser.add_argument('minutes', - type=int, - nargs="?", - default=30, - help="[default: %(default)s)]" - ) - return parser - - - def report_clock (self, args): - self.clock = self.exec_request(self.pump, commands.ReadRTC) - self.time = lib.parse.date(self.clock.getData( )) - self.time = self.time.replace(tzinfo=args.timezone) - self.timezone = args.timezone - self.since = self.time - self.delta - results = dict(now=self.time.isoformat( ) - , observed_at=datetime.now(args.timezone).isoformat( ) - , model=self.pump.modelNumber - , _type='RTC') - - print "```json" - args.rtc_archive.write(json.dumps(results, indent=2)) - print '' - print "```" - - def report_status (self, args): - status = self.exec_request(self.pump, commands.ReadPumpStatus) - self.status = status.getData( ) - args.status.write(json.dumps(self.status, indent=2)) - - def report_temp (self, args): - temp = self.exec_request(self.pump, commands.ReadBasalTemp) - self.temp = temp.getData( ) - args.tempbasal.write(json.dumps(self.temp, indent=2)) - - def report_settings (self, args): - settings = self.exec_request(self.pump, commands.ReadSettings) - self.settings = settings.getData( ) - args.settings.write(json.dumps(self.settings, indent=2)) - - def report_reservoir (self, args): - reservoir = self.exec_request(self.pump, commands.ReadRemainingInsulin) - self.reservoir = reservoir.getData( ) - args.reservoir_archive.write(json.dumps(self.reservoir, indent=2)) - - def report_basal (self, args): - profile = self.settings['selected_pattern'] - query = { 0: commands.ReadProfile_STD512 - , 1: commands.ReadProfile_A512 - , 2: commands.ReadProfile_B512 - } - basals = self.exec_request(self.pump, query[profile]) - self.basals = basals.getData( ) - args.basals.write(json.dumps(self.basals, indent=2)) - - def download_page (self, number): - kwds = dict(page=number) - page = self.exec_request(self.pump, commands.ReadHistoryData, args=kwds) - return page - - def find_records (self, page, larger=None): - decoder = HistoryPage(page, self.pump.model) - records = decoder.decode( ) - print "SINCE", self.since.isoformat( ) - for record in records: - print " * found record", record['_type'], record.get('timestamp') - print " * should quit", record.get('timestamp') < self.since.isoformat( ), self.enough_history - if record.get('timestamp'): - dt = parse(record['timestamp']) - dt = dt.replace(tzinfo=self.timezone) - record.update(timestamp=dt.isoformat( )) - if record['timestamp'] < self.since.isoformat( ): - self.enough_history = True - if record['timestamp'] >= self.since.isoformat( ): - self.records.append(record) - return records - - def download_history (self, args): - i = 0 - print "find records since", self.since.isoformat( ) - self.enough_history = False - self.records = [ ] - while not self.enough_history: - history = self.download_page(i) - remainder = self.find_records(history.data) - i = i + 1 - results = self.records - print "```json" - args.parsed_data.write(json.dumps(results, indent=2)) - print '' - print "```" - - def main (self, args): - self.delta = relativedelta.relativedelta(minutes=args.minutes) - self.report_settings(args) - if args.clock: - self.report_clock(args ) - if args.status: - self.report_status(args) - if args.temp: - self.report_temp(args) - if args.basal: - self.report_basal(args) - if args.reservoir: - self.report_reservoir(args) - self.download_history(args) - -if __name__ == '__main__': - app = LatestActivity( ) - app.run(None) - + default="-", + type=argparse.FileType("w"), + help="Put status json in this file", + ) + parser.add_argument( + "--timezone", default=gettz(), type=gettz, help="Timezone to use" + ) + parser.add_argument( + "minutes", type=int, nargs="?", default=30, help="[default: %(default)s)]" + ) + return parser + + def report_clock(self, args): + self.clock = self.exec_request(self.pump, commands.ReadRTC) + self.time = lib.parse.date(self.clock.getData()) + self.time = self.time.replace(tzinfo=args.timezone) + self.timezone = args.timezone + self.since = self.time - self.delta + results = dict( + now=self.time.isoformat(), + observed_at=datetime.now(args.timezone).isoformat(), + model=self.pump.modelNumber, + _type="RTC", + ) + + print("```json") + args.rtc_archive.write(json.dumps(results, indent=2)) + print("") + print("```") + + def report_status(self, args): + status = self.exec_request(self.pump, commands.ReadPumpStatus) + self.status = status.getData() + args.status.write(json.dumps(self.status, indent=2)) + + def report_temp(self, args): + temp = self.exec_request(self.pump, commands.ReadBasalTemp) + self.temp = temp.getData() + args.tempbasal.write(json.dumps(self.temp, indent=2)) + + def report_settings(self, args): + settings = self.exec_request(self.pump, commands.ReadSettings) + self.settings = settings.getData() + args.settings.write(json.dumps(self.settings, indent=2)) + + def report_reservoir(self, args): + reservoir = self.exec_request(self.pump, commands.ReadRemainingInsulin) + self.reservoir = reservoir.getData() + args.reservoir_archive.write(json.dumps(self.reservoir, indent=2)) + + def report_basal(self, args): + profile = self.settings["selected_pattern"] + query = { + 0: commands.ReadProfile_STD512, + 1: commands.ReadProfile_A512, + 2: commands.ReadProfile_B512, + } + basals = self.exec_request(self.pump, query[profile]) + self.basals = basals.getData() + args.basals.write(json.dumps(self.basals, indent=2)) + + def download_page(self, number): + kwds = dict(page=number) + page = self.exec_request(self.pump, commands.ReadHistoryData, args=kwds) + return page + + def find_records(self, page, larger=None): + decoder = HistoryPage(page, self.pump.model) + records = decoder.decode() + print("SINCE", self.since.isoformat()) + for record in records: + print(" * found record", record["_type"], record.get("timestamp")) + print( + " * should quit", + record.get("timestamp") < self.since.isoformat(), + self.enough_history, + ) + if record.get("timestamp"): + dt = parse(record["timestamp"]) + dt = dt.replace(tzinfo=self.timezone) + record.update(timestamp=dt.isoformat()) + if record["timestamp"] < self.since.isoformat(): + self.enough_history = True + if record["timestamp"] >= self.since.isoformat(): + self.records.append(record) + return records + + def download_history(self, args): + i = 0 + print("find records since", self.since.isoformat()) + self.enough_history = False + self.records = [] + while not self.enough_history: + history = self.download_page(i) + remainder = self.find_records(history.data) + i = i + 1 + results = self.records + print("```json") + args.parsed_data.write(json.dumps(results, indent=2)) + print("") + print("```") + + def main(self, args): + self.delta = relativedelta.relativedelta(minutes=args.minutes) + self.report_settings(args) + if args.clock: + self.report_clock(args) + if args.status: + self.report_status(args) + if args.temp: + self.report_temp(args) + if args.basal: + self.report_basal(args) + if args.reservoir: + self.report_reservoir(args) + self.download_history(args) + + +if __name__ == "__main__": + app = LatestActivity() + app.run(None) diff --git a/bin/mm-pages.py b/bin/mm-pages.py index 94aeeec..977b096 100755 --- a/bin/mm-pages.py +++ b/bin/mm-pages.py @@ -1,90 +1,87 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from decocare import commands import io import json - from datetime import datetime -from dateutil import relativedelta -from decocare import lib -from decocare.history import parse_record, HistoryPage +from dateutil import relativedelta +from decocare import commands, lib from decocare.helpers import cli +from decocare.history import HistoryPage, parse_record + + +def page_range(spec): + r = spec.split(",") + pages = [] + for s in r: + p = s.split("..") + if len(p) == 2: + pages.extend(list(range(int(p[0]), int(p[1]) + 1))) + continue + p = s.split("-") + if len(p) == 2: + pages.extend(list(range(int(p[0]), int(p[1]) + 1))) + continue + pages.append(int(s)) + return pages + -def page_range (spec): - r = spec.split(',') - pages = [ ] - for s in r: - p = s.split('..') - if len(p) is 2: - pages.extend(range(int(p[0]), int(p[1]) + 1)) - continue - p = s.split('-') - if len(p) is 2: - pages.extend(range(int(p[0]), int(p[1]) + 1)) - continue - pages.append(int(s)) - return pages +class PumpPager(cli.CommandApp): + """%(prog)s - Download and decode pages of history from pump. -class PumpPager (cli.CommandApp): - """%(prog)s - Download and decode pages of history from pump. + Download history pages from the pump. + """ - Download history pages from the pump. - """ - def customize_parser (self, parser): - parser.add_argument('--query', + def customize_parser(self, parser): + parser.add_argument( + "--query", dest="query", action="store_true", default=False, - help="Query number of pages available." - ) - # subparsers = parser.add_subparsers(help="Type of pages", dest="command") - # history_parser = subparsers.add_parser("history", help="ReadHistoryData pages") - # history_parser.add_argument('pages' - # ) - choices = [ 'history', 'cgm', 'vcntr' ] - parser.add_argument('variant', - # action="append", - choices=choices, - help="Type of history pages to retrieve." - ) - parser.add_argument('range', - help="Page range", - nargs='+', - type=page_range - ) - return parser + help="Query number of pages available.", + ) + # subparsers = parser.add_subparsers(help="Type of pages", dest="command") + # history_parser = subparsers.add_parser("history", help="ReadHistoryData pages") + # history_parser.add_argument('pages' + # ) + choices = ["history", "cgm", "vcntr"] + parser.add_argument( + "variant", + # action="append", + choices=choices, + help="Type of history pages to retrieve.", + ) + parser.add_argument("range", help="Page range", nargs="+", type=page_range) + return parser - def download_page (self, number): - kwds = dict(page=number) - page = self.exec_request(self.pump, commands.ReadHistoryData, args=kwds) - return page + def download_page(self, number): + kwds = dict(page=number) + page = self.exec_request(self.pump, commands.ReadHistoryData, args=kwds) + return page - def query_pages (self): - pages = self.exec_request(self.pump, commands.ReadCurPageNumber) - return pages.getData( ) - - def main (self, args): - print args - print args.variant - print args.range[0] - if args.query: - self.query_pages( ) - pages = args.range[0] - larger = int(self.pump.model.getData( )[1:]) > 22 - records = [ ] - for n in pages: - history = self.download_page(n) - page = HistoryPage(history.data) - records.extend(page.decode( )) - print "```json" - print json.dumps(records, indent=2) - print "```" + def query_pages(self): + pages = self.exec_request(self.pump, commands.ReadCurPageNumber) + return pages.getData() + def main(self, args): + print(args) + print(args.variant) + print(args.range[0]) + if args.query: + self.query_pages() + pages = args.range[0] + larger = int(self.pump.model.getData()[1:]) > 22 + records = [] + for n in pages: + history = self.download_page(n) + page = HistoryPage(history.data) + records.extend(page.decode()) + print("```json") + print(json.dumps(records, indent=2)) + print("```") -if __name__ == '__main__': - app = PumpPager( ) - app.run(None) +if __name__ == "__main__": + app = PumpPager() + app.run(None) diff --git a/bin/mm-press-key.py b/bin/mm-press-key.py index 0203ee1..efc5c57 100755 --- a/bin/mm-press-key.py +++ b/bin/mm-press-key.py @@ -1,40 +1,44 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - from decocare import commands from decocare.helpers import cli -class PressKeysApp (cli.CommandApp): - """%(prog)s - Simulate presses on the keypad. +class PressKeysApp(cli.CommandApp): + """%(prog)s - Simulate presses on the keypad. + + + Press keys on the keypad. + """ - Press keys on the keypad. - """ - def customize_parser (self, parser): - parser.add_argument('commands', - nargs="+", - choices=['act', 'esc', 'up', 'down', 'easy' ], - # default='act', - help="buttons to press [default: %(default)s)]" - ) - return parser + def customize_parser(self, parser): + parser.add_argument( + "commands", + nargs="+", + choices=["act", "esc", "up", "down", "easy"], + # default='act', + help="buttons to press [default: %(default)s)]", + ) + return parser + + def exec_request(self, pump, msg, **kwds): + msg = lookup_command(msg) + super().exec_request(pump, msg, **kwds) - def exec_request (self, pump, msg, **kwds): - msg = lookup_command(msg) - super(PressKeysApp, self).exec_request(pump, msg, **kwds) command_map = { - 'easy': commands.PushEASY, - 'esc': commands.PushESC, - 'act': commands.PushACT, - 'down': commands.PushDOWN, - 'up': commands.PushUP + "easy": commands.PushEASY, + "esc": commands.PushESC, + "act": commands.PushACT, + "down": commands.PushDOWN, + "up": commands.PushUP, } -def lookup_command (name): - return command_map.get(name) -if __name__ == '__main__': - app = PressKeysApp( ) - app.run(None) +def lookup_command(name): + return command_map.get(name) + +if __name__ == "__main__": + app = PressKeysApp() + app.run(None) diff --git a/bin/mm-send-comm.py b/bin/mm-send-comm.py index a285ea5..b784b09 100755 --- a/bin/mm-send-comm.py +++ b/bin/mm-send-comm.py @@ -1,9 +1,7 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - from decocare.helpers import messages -if __name__ == '__main__': - app = messages.SendMsgApp( ) - app.run(None) - +if __name__ == "__main__": + app = messages.SendMsgApp() + app.run(None) diff --git a/bin/mm-set-rtc.py b/bin/mm-set-rtc.py index d08fc70..0740d79 100755 --- a/bin/mm-set-rtc.py +++ b/bin/mm-set-rtc.py @@ -1,94 +1,101 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from decocare import commands -from decocare.helpers import cli import argparse -from decocare import lib -from dateutil.tz import gettz -from datetime import datetime -import time import json +import time +from datetime import datetime -class SetRTCApp (cli.CommandApp): - """ %(prog)s - query or set RTC +from dateutil.tz import gettz +from decocare import commands, lib +from decocare.helpers import cli + + +class SetRTCApp(cli.CommandApp): + """ %(prog)s - query or set RTC - Set or query RTC. - """ - def customize_parser (self, parser): - parser.add_argument('command', - choices=['query', 'set', ], - default='query', - help="Set or query pump status [default: %(default)s)]" - ) - parser.add_argument('--rtc-out', + Set or query RTC. + """ + + def customize_parser(self, parser): + parser.add_argument( + "command", + choices=["query", "set",], + default="query", + help="Set or query pump status [default: %(default)s)]", + ) + parser.add_argument( + "--rtc-out", dest="rtc_archive", - default='-', - type=argparse.FileType('w'), - help="Put clock json in this file" - ) - parser.add_argument('--timezone', - default=gettz( ), - type=gettz, - help="Timezone to use" - ) - parser.add_argument('--set', + default="-", + type=argparse.FileType("w"), + help="Put clock json in this file", + ) + parser.add_argument( + "--timezone", default=gettz(), type=gettz, help="Timezone to use" + ) + parser.add_argument( + "--set", dest="set", required=True, type=lib.parse.date, - help="Set clock to new value (iso8601)" - ) - parser.add_argument('--out', + help="Set clock to new value (iso8601)", + ) + parser.add_argument( + "--out", dest="out", - default='-', - type=argparse.FileType('w'), - help="Put basal in this file" - ) - return parser - def report_clock (self, args): - self.clock = self.exec_request(self.pump, commands.ReadRTC) - new_time = lib.parse.date(self.clock.getData( )) - self.time = new_time - return self.render_clock(self.clock) + default="-", + type=argparse.FileType("w"), + help="Put basal in this file", + ) + return parser + + def report_clock(self, args): + self.clock = self.exec_request(self.pump, commands.ReadRTC) + new_time = lib.parse.date(self.clock.getData()) + self.time = new_time + return self.render_clock(self.clock) + + def render_clock(self, clock): + new_time = lib.parse.date(clock.getData()) + new_time = new_time.replace(tzinfo=self.args.timezone) + results = dict( + clock=new_time.isoformat(), + observed_at=datetime.now(self.args.timezone).isoformat(), + model=self.pump.model.model, + _type="RTC", + ) - def render_clock (self, clock): - new_time = lib.parse.date(clock.getData( )) - new_time = new_time.replace(tzinfo=self.args.timezone) - results = dict(clock=new_time.isoformat( ) - , observed_at=datetime.now(self.args.timezone).isoformat( ) - , model=self.pump.model.model - , _type='RTC') + print("```json") + self.args.rtc_archive.write(json.dumps(results, indent=2)) + print("") + print("```") + return results - print "```json" - self.args.rtc_archive.write(json.dumps(results, indent=2)) - print '' - print "```" - return results + def set_clock(self, args): + msg = commands.SetRTC + kwds = dict(params=commands.SetRTC.fmt_datetime(args.set)) + new_clock = self.exec_request(self.pump, msg, args=kwds) + # self.render_clock(new_clock) + return new_clock - def set_clock (self, args): - msg = commands.SetRTC - kwds = dict(params=commands.SetRTC.fmt_datetime(args.set)) - new_clock = self.exec_request(self.pump, msg, args=kwds) - # self.render_clock(new_clock) - return new_clock - def main (self, args): - if args.dryrun: - print args.set - print lib.hexdump(bytearray(commands.SetRTC.fmt_datetime(args.set))) - return - clock = self.report_clock(args) - if args.command == "query": - pass - if args.command == "set": + def main(self, args): + if args.dryrun: + print(args.set) + print(lib.hexdump(bytearray(commands.SetRTC.fmt_datetime(args.set)))) + return + clock = self.report_clock(args) + if args.command == "query": + pass + if args.command == "set": - new_clock = self.set_clock(args) + new_clock = self.set_clock(args) - time.sleep(1) - clock = self.report_clock(args) + time.sleep(1) + clock = self.report_clock(args) - args.out.write(json.dumps(clock, indent=2)) + args.out.write(json.dumps(clock, indent=2)) -if __name__ == '__main__': - app = SetRTCApp( ) - app.run(None) +if __name__ == "__main__": + app = SetRTCApp() + app.run(None) diff --git a/bin/mm-set-suspend.py b/bin/mm-set-suspend.py index 53ffd3a..7b7c6c3 100755 --- a/bin/mm-set-suspend.py +++ b/bin/mm-set-suspend.py @@ -1,37 +1,41 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - from decocare import commands from decocare.helpers import cli -class SetSuspendResumeApp (cli.CommandApp): - """ %(prog)s - query or set suspend/resume status - Pause or resume pump. - """ - def customize_parser (self, parser): - parser.add_argument('commands', - nargs="+", - choices=['query', 'suspend', 'resume'], - default='query', - help="Set or query pump status [default: %(default)s)]" - ) - return parser +class SetSuspendResumeApp(cli.CommandApp): + """ %(prog)s - query or set suspend/resume status + + Pause or resume pump. + """ + + def customize_parser(self, parser): + parser.add_argument( + "commands", + nargs="+", + choices=["query", "suspend", "resume"], + default="query", + help="Set or query pump status [default: %(default)s)]", + ) + return parser + + def exec_request(self, pump, msg, **kwds): + msg = lookup_command(msg) + super().exec_request(pump, msg, **kwds) - def exec_request (self, pump, msg, **kwds): - msg = lookup_command(msg) - super(SetSuspendResumeApp, self).exec_request(pump, msg, **kwds) command_map = { - 'query': commands.ReadPumpStatus - , 'suspend': commands.PumpSuspend - , 'resume': commands.PumpResume + "query": commands.ReadPumpStatus, + "suspend": commands.PumpSuspend, + "resume": commands.PumpResume, } -def lookup_command (name): - return command_map.get(name) -if __name__ == '__main__': - app = SetSuspendResumeApp( ) - app.run(None) +def lookup_command(name): + return command_map.get(name) + +if __name__ == "__main__": + app = SetSuspendResumeApp() + app.run(None) diff --git a/bin/mm-temp-basals.py b/bin/mm-temp-basals.py index a5164cf..8e4f0ef 100755 --- a/bin/mm-temp-basals.py +++ b/bin/mm-temp-basals.py @@ -1,39 +1,47 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK +import argparse +import json from decocare import commands from decocare.helpers import cli -import argparse -import json -class TempBasalApp (cli.CommandApp): - """ %(prog)s - query or set temp basals - Set or query temp basals. - """ - def customize_parser (self, parser): - parser.add_argument('command', - choices=['query', 'set', 'percent'], - default='query', - help="Set or query pump status [default: %(default)s)]" - ) - parser.add_argument('--duration', - dest='duration', - type=int, default=0, - help="Duration of temp rate [default: %(default)s)]" - ) - parser.add_argument('--rate', - dest='rate', - type=float, default=0, - help="Rate of temp basal [default: %(default)s)]" - ) - parser.add_argument('--out', - dest="out", - default='-', - type=argparse.FileType('w'), - help="Put basal in this file" - ) +class TempBasalApp(cli.CommandApp): + """ %(prog)s - query or set temp basals + + Set or query temp basals. """ + + def customize_parser(self, parser): + parser.add_argument( + "command", + choices=["query", "set", "percent"], + default="query", + help="Set or query pump status [default: %(default)s)]", + ) + parser.add_argument( + "--duration", + dest="duration", + type=int, + default=0, + help="Duration of temp rate [default: %(default)s)]", + ) + parser.add_argument( + "--rate", + dest="rate", + type=float, + default=0, + help="Rate of temp basal [default: %(default)s)]", + ) + parser.add_argument( + "--out", + dest="out", + default="-", + type=argparse.FileType("w"), + help="Put basal in this file", + ) + """ subparsers = parser.add_subparsers(help="Main thing to do", dest="command") basals_parser = subparsers.add_parser("set", help="Just basals between command sets") basals_parser.add_argument('--duration', @@ -47,50 +55,56 @@ def customize_parser (self, parser): help="Rate of temp basal [default: %(default)s)]" ) """ - return parser - def main (self, args): - basals = self.query_temp(args) - if args.command == "query": - pass - if args.command == "percent": - params = format_percent_params(args) - kwds = dict(params=params) - msg = commands.TempBasalPercent - resp = self.exec_request(self.pump, msg, args=kwds, - dryrun=args.dryrun, render_hexdump=True ) - basals = self.query_temp(args) - if args.command == "set": - params = format_params(args) - kwds = dict(params=params) - msg = commands.TempBasal - resp = self.exec_request(self.pump, msg, args=kwds, - dryrun=args.dryrun, render_hexdump=True ) - basals = self.query_temp(args) + return parser + + def main(self, args): + basals = self.query_temp(args) + if args.command == "query": + pass + if args.command == "percent": + params = format_percent_params(args) + kwds = dict(params=params) + msg = commands.TempBasalPercent + resp = self.exec_request( + self.pump, msg, args=kwds, dryrun=args.dryrun, render_hexdump=True + ) + basals = self.query_temp(args) + if args.command == "set": + params = format_params(args) + kwds = dict(params=params) + msg = commands.TempBasal + resp = self.exec_request( + self.pump, msg, args=kwds, dryrun=args.dryrun, render_hexdump=True + ) + basals = self.query_temp(args) + + # (serial=pump.serial, params=params) + args.out.write(json.dumps(basals, indent=2)) + + def query_temp(self, args): + query = commands.ReadBasalTemp - # (serial=pump.serial, params=params) - args.out.write(json.dumps(basals, indent=2)) + resp = self.exec_request( + self.pump, query, dryrun=args.dryrun, render_hexdump=False + ) + results = resp.getData() + return results - def query_temp (self, args): - query = commands.ReadBasalTemp - resp = self.exec_request(self.pump, query, - dryrun=args.dryrun, render_hexdump=False) - results = resp.getData( ) - return results; +def format_percent_params(args): + duration = int(args.duration / 30) + rate = int(args.rate) + params = [rate, duration] + return params -def format_percent_params (args): - duration = int(args.duration / 30) - rate = int(args.rate) - params = [rate, duration] - return params -def format_params (args): - duration = args.duration / 30 - rate = int(args.rate / 0.025) - params = [0x00, rate, duration] - return params +def format_params(args): + duration = args.duration // 30 + rate = int(args.rate / 0.025) + params = [0x00, rate, duration] + return params -if __name__ == '__main__': - app = TempBasalApp( ) - app.run(None) +if __name__ == "__main__": + app = TempBasalApp() + app.run(None) diff --git a/bin/scapy_dump.py b/bin/scapy_dump.py index eec99f9..630f055 100755 --- a/bin/scapy_dump.py +++ b/bin/scapy_dump.py @@ -6,169 +6,173 @@ scapy. """ -# stdlib -import user -import sys import argparse -import logging import binascii +import logging +import sys from os.path import getsize +import user + logging.basicConfig(stream=sys.stdout) -logger = logging.getLogger('decoder') +logger = logging.getLogger("decoder") logger.setLevel(logging.INFO) -# Requires scapy to be in your PYATHONPATH -#import scapy +from scapy import automaton, utils + +# Requires scapy to be in your PYTHONPATH +# import scapy from scapy.all import * -from scapy import utils -from scapy import automaton -_usb_response = { 0x00: 'OUT', 0x55: "Success", 0x66: "Fail" } +_usb_response = {0x00: "OUT", 0x55: "Success", 0x66: "Fail"} _usb_commands = { - 0x00: 'nil', - 0x01: 'MCPY', - 0x02: 'XFER', - 0x03: 'POLL', - 0x04: 'INFO', - 0x05: 'stat', - 0x06: 'SIGNAL', - 0x0C: 'RFLEN', + 0x00: "nil", + 0x01: "MCPY", + 0x02: "XFER", + 0x03: "POLL", + 0x04: "INFO", + 0x05: "stat", + 0x06: "SIGNAL", + 0x0C: "RFLEN", } class ProdInfo(Packet): - name = "ProductInfo" - fields_desc = [ - XByteField('version.major', 0), - XByteField('version.minor', 0), - ] + name = "ProductInfo" + fields_desc = [ + XByteField("version.major", 0), + XByteField("version.minor", 0), + ] + class USBReq(Packet): - name = "USBRequest" + name = "USBRequest" - fields_desc = [ - ByteEnumField("code", 0x00, _usb_commands), - ByteEnumField("resp", 0x00, _usb_response), - XByteField("error", 0), - ] + fields_desc = [ + ByteEnumField("code", 0x00, _usb_commands), + ByteEnumField("resp", 0x00, _usb_response), + XByteField("error", 0), + ] class CLMMComm(Packet): - name ="CLMM Hexline" - fields_desc = [ - StrStopField('dir', 'error', ','), - PacketField('stick', None, USBReq), - ] - -class CLMMPair(Packet): - name = "CLMM command/response" - fields_desc = [ - PacketField('send', None, CLMMComm), - PacketField('recv', None, CLMMComm), - ] - -class Handler(object): - def __init__(self, path, opts): - self.path = path - self.opts = opts - - def __call__(self): - self.open( ) - self.decode( ) - self.close( ) - - def open(self): - self.handle = None - if self.path == '-': - self.handle = sys.stdin - else: - self.handle = open(self.path, 'rU') - - def close(self): - if self.path != '-': - self.handle.close( ) + name = "CLMM Hexline" + fields_desc = [ + StrStopField("dir", "error", ","), + PacketField("stick", None, USBReq), + ] -class Decoder(Handler): - def clean(self, line): - line = line.strip( ) - method, data = line.split(',') - data = binascii.unhexlify(data) - return ','.join([ method, data ]) - - def decode(self): - lines = self.handle.readlines( ) - self.lines = [ ] - self.decoded = [ ] - L = len(lines) - - for x in range(L): - line = self.clean(lines[x]) - p = CLMMComm(line) - logger.debug("Line: %s" % x) - logger.debug(utils.hexstr(str(p.stick))) - if len(self.lines) <= 1: - self.lines.append(p) - else: - if len(self.lines) == 2 and self.lines[0].dir == p.dir: - one = self.lines[0] - two = self.lines[1] - pair = CLMMPair( send = one, recv = two) - #pair. - #pair. - pair.show( ) - self.decoded.append(pair) - self.lines = [p] +class CLMMPair(Packet): + name = "CLMM command/response" + fields_desc = [ + PacketField("send", None, CLMMComm), + PacketField("recv", None, CLMMComm), + ] + + +class Handler: + def __init__(self, path, opts): + self.path = path + self.opts = opts + + def __call__(self): + self.open() + self.decode() + self.close() + + def open(self): + self.handle = None + if self.path == "-": + self.handle = sys.stdin else: - self.lines.append(p) + self.handle = open(self.path) + + def close(self): + if self.path != "-": + self.handle.close() - for p in self.lines: - print "###", "XXX unusual line!", p.dir - p.show( ) +class Decoder(Handler): + def clean(self, line): + line = line.strip() + method, data = line.split(",") + data = binascii.unhexlify(data) + return ",".join([method, data]) + + def decode(self): + lines = self.handle.readlines() + self.lines = [] + self.decoded = [] + L = len(lines) + + for x in range(L): + line = self.clean(lines[x]) + p = CLMMComm(line) + logger.debug("Line: %s" % x) + logger.debug(utils.hexstr(str(p.stick))) + if len(self.lines) <= 1: + self.lines.append(p) + else: + if len(self.lines) == 2 and self.lines[0].dir == p.dir: + one = self.lines[0] + two = self.lines[1] + pair = CLMMPair(send=one, recv=two) + # pair. + # pair. + pair.show() + self.decoded.append(pair) + self.lines = [p] + else: + self.lines.append(p) + + for p in self.lines: + print("###", "XXX unusual line!", p.dir) + p.show() class Console: - _log_map = { 0: logging.ERROR, 1: logging.WARN, - 2: logging.INFO, 3: logging.DEBUG } - def __init__(self, args): - self.raw_args = args - self.parser = self.get_argparser( ) - args = list(args) - cmd, args = args[0], args[1:] - self.opts = self.parser.parse_args((args)) - logger.setLevel(self._log_map.get(self.opts.verbose, 3)) - cmdverbose = '' - if self.opts.verbose > 0: - cmdverbose = '-' + ('v' * self.opts.verbose) - - #logger.info('opts: %s' % (pformat(args))) - cmdline = [ cmd, cmdverbose ] + self.opts.input - print ' '.join(cmdline) - - def main(self): - logger.info('opening %s' % (self.opts.input)) - - for item in self.opts.input: - self.do_input(item) - - def do_input(self, item): - decode = Decoder(item, self.opts) - decode( ) - - def get_argparser(self): - """Prepare an argument parser.""" - parser = argparse.ArgumentParser(description=__doc__ ) - parser.add_argument('-v', '--verbose', action='count', default=0, - help="Verbosity.") - parser.add_argument('input', nargs='+', help="Input files") - return parser - -if __name__ == '__main__': - app = Console(sys.argv) - app.main( ) + _log_map = {0: logging.ERROR, 1: logging.WARN, 2: logging.INFO, 3: logging.DEBUG} + + def __init__(self, args): + self.raw_args = args + self.parser = self.get_argparser() + args = list(args) + cmd, args = args[0], args[1:] + self.opts = self.parser.parse_args(args) + logger.setLevel(self._log_map.get(self.opts.verbose, 3)) + cmdverbose = "" + if self.opts.verbose > 0: + cmdverbose = "-" + ("v" * self.opts.verbose) + + # logger.info('opts: %s' % (pformat(args))) + cmdline = [cmd, cmdverbose] + self.opts.input + print(" ".join(cmdline)) + + def main(self): + logger.info("opening %s" % (self.opts.input)) + + for item in self.opts.input: + self.do_input(item) + + def do_input(self, item): + decode = Decoder(item, self.opts) + decode() + + def get_argparser(self): + """Prepare an argument parser.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-v", "--verbose", action="count", default=0, help="Verbosity." + ) + parser.add_argument("input", nargs="+", help="Input files") + return parser + + +if __name__ == "__main__": + app = Console(sys.argv) + app.main() ##### # EOF diff --git a/bin/scapy_layer.py b/bin/scapy_layer.py index e2ce280..1a268f6 100644 --- a/bin/scapy_layer.py +++ b/bin/scapy_layer.py @@ -1,18 +1,17 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - import argparse - -from pprint import pprint, pformat from binascii import hexlify from datetime import datetime -from scapy.all import * +from pprint import pformat, pprint from decocare import lib from decocare.history import * +from scapy.all import * + class MMMomentBase(XByteField): - """ + """ A field should be considered in different states: i (nternal) : this is the way Scapy manipulates it. @@ -57,274 +56,321 @@ def getfield(self, pkt, s): - """ - def i2h(self, pkt, pay): - pass - def i2m(self, pkt, pay): - pass - def m2i(self, pkt, pay): - pass + """ + + def i2h(self, pkt, pay): + pass + + def i2m(self, pkt, pay): + pass + + def m2i(self, pkt, pay): + pass + class XXXStrField(Field): def __init__(self, name, default, fmt="H", remain=0): - Field.__init__(self,name,default,fmt) + Field.__init__(self, name, default, fmt) self.remain = remain + def i2len(self, pkt, i): return len(i) + def i2m(self, pkt, x): if x is None: x = "" - elif type(x) is not str: - x=str(x) + elif not isinstance(x, str): + x = str(x) return x + def addfield(self, pkt, s, val): - return s+self.i2m(pkt, val) + return s + self.i2m(pkt, val) + def getfield(self, pkt, s): if self.remain == 0: - return "",self.m2i(pkt, s) + return "", self.m2i(pkt, s) else: - return s[-self.remain:],self.m2i(pkt, s[:-self.remain]) + return s[-self.remain :], self.m2i(pkt, s[: -self.remain]) + def randval(self): - return RandBin(RandNum(0,1200)) + return RandBin(RandNum(0, 1200)) + class XXStrLenField(StrField): def __init__(self, name, default, fld=None, length_from=None): StrField.__init__(self, name, default) self.length_from = length_from + def getfield(self, pkt, s): l = self.length_from(pkt) - return s[l:], self.m2i(pkt,s[:l]) + return s[l:], self.m2i(pkt, s[:l]) + class XXXStrFixedLenField(StrField): def __init__(self, name, default, length=None, length_from=None): StrField.__init__(self, name, default) - self.length_from = length_from + self.length_from = length_from if length is not None: - self.length_from = lambda pkt,length=length: length + self.length_from = lambda pkt, length=length: length + def i2repr(self, pkt, v): - if type(v) is str: + if isinstance(v, str): v = v.rstrip("\0") return repr(v) + def getfield(self, pkt, s): l = self.length_from(pkt) - return s[l:], self.m2i(pkt,s[:l]) + return s[l:], self.m2i(pkt, s[:l]) + def addfield(self, pkt, s, val): l = self.length_from(pkt) - return s+struct.pack("%is"%l,self.i2m(pkt, val)) + return s + struct.pack("%is" % l, self.i2m(pkt, val)) + def randval(self): try: l = self.length_from(None) except: - l = RandNum(0,200) + l = RandNum(0, 200) return RandBin(l) class XXXStrStopField(StrField): def __init__(self, name, default, stop, additionnal=0): Field.__init__(self, name, default) - self.stop=stop - self.additionnal=additionnal + self.stop = stop + self.additionnal = additionnal + def getfield(self, pkt, s): l = s.find(self.stop) if l < 0: - return "",s -# raise Scapy_Exception,"StrStopField: stop value [%s] not found" %stop - l += len(self.stop)+self.additionnal - return s[l:],s[:l] + return "", s + # raise Scapy_Exception,"StrStopField: stop value [%s] not found" %stop + l += len(self.stop) + self.additionnal + return s[l:], s[:l] + def randval(self): - return RandTermString(RandNum(0,1200),self.stop) + return RandTermString(RandNum(0, 1200), self.stop) class MMMomentField(StrField): - def __init__(self, name, default, fmt="B", remain=0): - StrField.__init__(self, name, default, fmt='B', remain=remain) + def __init__(self, name, default, fmt="B", remain=0): + StrField.__init__(self, name, default, fmt="B", remain=remain) class MaskedMoment(MMMomentField): - MASK = Mask.time - def mask_value(self, value): - return value & self.MASK - def remaining_value(value): - return value & ( ~self.Mask & 0xff ) + MASK = Mask.time + + def mask_value(self, value): + return value & self.MASK + + def remaining_value(value): + return value & (~self.Mask & 0xFF) + class Year(MaskedMoment): - """ + """ # >>> Year( ) / str(bytearray([0x06])) - """ - MASK = Mask.year - - def m2i(self, pkt, raw): - candidate = raw[0] - value = candidate & self.MASK - result = value + 2000 - return result - - return str(bytearray(parse_years(raw))) - - def i2m(self, pkt, x): - print "XXX: i2m %r" % repr(x) - if x is None or x == "": - return "" - return str(bytearray( [ x - 2000 ] )) - - def addfield(self, pkt, s, val): - print "XXXX: addfield: pkt: %r" % pkt - print "XXXX: addfield: s: %r" % s - print "XXXX: addfield: val: %r" % val - return s+self.i2m(pkt, val) - - def getfield(self, pkt, s): - y = s[0] - print "XXX: getfield: s: %r len(%s)" % (s, len(s)) - val = bytearray( s[0] ) - print "XXX: getfield: %s" % repr(val) - return s[1:], self.m2i(pkt, val) + """ + + MASK = Mask.year + + def m2i(self, pkt, raw): + candidate = raw[0] + value = candidate & self.MASK + result = value + 2000 + return result + + return str(bytearray(parse_years(raw))) + + def i2m(self, pkt, x): + print("XXX: i2m %r" % repr(x)) + if x is None or x == "": + return "" + return str(bytearray([x - 2000])) + + def addfield(self, pkt, s, val): + print("XXXX: addfield: pkt: %r" % pkt) + print("XXXX: addfield: s: %r" % s) + print("XXXX: addfield: val: %r" % val) + return s + self.i2m(pkt, val) + + def getfield(self, pkt, s): + y = s[0] + print("XXX: getfield: s: {!r} len({})".format(s, len(s))) + val = bytearray(s[0]) + print("XXX: getfield: %s" % repr(val)) + return s[1:], self.m2i(pkt, val) + class TestYearPacket(Packet): - """ + """ >>> TestYearPacket( ) / str(bytearray( [ 0x06 ] )) >>> (TestYearPacket( ) / str(bytearray( [ 0x06 ] ))).show2( ) >>> TestYearPacket( str(bytearray( [ 0x06 ] )) ) - """ - fields_desc = [ - Year('year', '') - ] + """ + + fields_desc = [Year("year", "")] + class Month(MMMomentField): - def __init__(self, name, default, high=None, low=None): - MMMomentField.__init__(self, name, default, fmt='BB') - self.high = high - self.low = low + def __init__(self, name, default, high=None, low=None): + MMMomentField.__init__(self, name, default, fmt="BB") + self.high = high + self.low = low - def addfield(self, pkt, s, val): def addfield(self, pkt, s, val): - l = self.length_from(pkt) - return s+struct.pack("%is"%l,self.i2m(pkt, val)) + def addfield(self, pkt, s, val): + l = self.length_from(pkt) + return s + struct.pack("%is" % l, self.i2m(pkt, val)) + class Day(MaskedMoment): - MASK = Mask.year - pass + MASK = Mask.year + pass + class Hour(MMMomentField): - pass + pass + class Minute(MaskedMoment): - MASK = Mask.invert - pass + MASK = Mask.invert + pass + class Second(MMMomentField): - MASK = Mask.invert - pass + MASK = Mask.invert + pass class TestSecondPacket(Packet): - """ - >>> TestSecondPacket( ) / str(bytearray( [ 0x6f ] )) - """ - fields_desc = [ - Second("second", None), - ] + """ + >>> TestSecondPacket( ) / str(bytearray( [ 0x6f ] )) + """ + + fields_desc = [ + Second("second", None), + ] + class TestMinutePacket(Packet): - """ - >>> TestMinutePacket( ) / str(bytearray( [ 0xd7 ] )) - """ - fields_desc = [ - Minute("minute", None), - ] + """ + >>> TestMinutePacket( ) / str(bytearray( [ 0xd7 ] )) + """ + + fields_desc = [ + Minute("minute", None), + ] + class TestMonthPacket(Packet): - """ - >>> TestMonthPacket( ) / str(bytearray( [ 0x6f, 0xd7 ] )) - """ - fields_desc = [ - Month("month", None), - ] + """ + >>> TestMonthPacket( ) / str(bytearray( [ 0x6f, 0xd7 ] )) + """ + + fields_desc = [ + Month("month", None), + ] + class TestTimeHeadPacket(Packet): - """ - >>> TestTimeHeadPacket( ) / str(bytearray( [ 0x6f, 0xd7 ] )) - """ - fields_desc = [ - Second("second", None), - Minute("minute", None), - Month("month", None, high="second", low="minute"), - ] + """ + >>> TestTimeHeadPacket( ) / str(bytearray( [ 0x6f, 0xd7 ] )) + """ + + fields_desc = [ + Second("second", None), + Minute("minute", None), + Month("month", None, high="second", low="minute"), + ] class MMDateField(Field): """ # >>> MMDateField[(str(bytearray([0x06]))) """ + def __init__(self, name, default, fmt="H", remain=0): - Field.__init__(self,name,default,fmt) + Field.__init__(self, name, default, fmt) self.remain = remain + def i2len(self, pkt, i): return len(i) + def i2m(self, pkt, x): if x is None: x = "" - elif type(x) is not str: - x=str(x) + elif not isinstance(x, str): + x = str(x) return x + def addfield(self, pkt, s, val): - return s+self.i2m(pkt, val) + return s + self.i2m(pkt, val) + def getfield(self, pkt, s): if self.remain == 0: - return "",self.m2i(pkt, s) + return "", self.m2i(pkt, s) else: - return s[-self.remain:],self.m2i(pkt, s[:-self.remain]) - def randval(self): - return RandBin(RandNum(0,1200)) + return s[-self.remain :], self.m2i(pkt, s[: -self.remain]) + def randval(self): + return RandBin(RandNum(0, 1200)) class XXMMDate(Field): - def i2h(self, pkt, x): - pass - def i2m(self, pkt, x): - pass - def m2i(self, pkt, x): - if x is None: - return None, 0 + def i2h(self, pkt, x): + pass + + def i2m(self, pkt, x): + pass + + def m2i(self, pkt, x): + if x is None: + return None, 0 + + pass - pass class MMDateTime(Packet): - """ - >>> MMDateTime( ) / str(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) - """ - name = "TIME" - - fields_desc = [ - Second("second", None), - Minute("minute", None), - Month("month", None, high="second", low="minute"), - Hour("hour", None), - Day("day", None), - Year("year", None), - ] + """ + >>> MMDateTime( ) / str(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) + """ + + name = "TIME" + + fields_desc = [ + Second("second", None), + Minute("minute", None), + Month("month", None, high="second", low="minute"), + Hour("hour", None), + Day("day", None), + Year("year", None), + ] + class MMAction(Packet): - name = 'Logged Action %s' % __name__ - - fields_desc = [ - PacketField('datetime', None, MMDateTime), - ] - -def main( ): - print "I do nothing." - -if __name__ == '__main__': - import doctest - failures, tests = doctest.testmod( ) - if failures > 0: - print "REFUSING TO RUN DUE TO FAILED TESTS" - sys.exit(1) - main( ) + name = "Logged Action %s" % __name__ + + fields_desc = [ + PacketField("datetime", None, MMDateTime), + ] + + +def main(): + print("I do nothing.") + + +if __name__ == "__main__": + import doctest + + failures, tests = doctest.testmod() + if failures > 0: + print("REFUSING TO RUN DUE TO FAILED TESTS") + sys.exit(1) + main() ##### # EOF diff --git a/circle.yml b/circle.yml deleted file mode 100644 index b5a49c7..0000000 --- a/circle.yml +++ /dev/null @@ -1,3 +0,0 @@ -test: - override: - - make ci-test diff --git a/debug_list_cgm_py.sh b/debug_list_cgm_py.sh index d06541a..34daa0d 100755 --- a/debug_list_cgm_py.sh +++ b/debug_list_cgm_py.sh @@ -1 +1 @@ -sudo python -m pudb list_cgm.py logs/2014-05-01-full-hist/20140501_225820-pg-16-ReadGlucoseHistory-page-0.data +sudo python3 -m pudb list_cgm.py logs/2014-05-01-full-hist/20140501_225820-pg-16-ReadGlucoseHistory-page-0.data diff --git a/decocare/__init__.py b/decocare/__init__.py index 7365109..8bc41cd 100644 --- a/decocare/__init__.py +++ b/decocare/__init__.py @@ -1,4 +1,3 @@ - """ decocare - a pure python module for serial communication with insulin pumps. @@ -16,4 +15,3 @@ """ - diff --git a/decocare/cgm/__init__.py b/decocare/cgm/__init__.py index 481d44e..a57d5d4 100644 --- a/decocare/cgm/__init__.py +++ b/decocare/cgm/__init__.py @@ -1,4 +1,3 @@ - """ cgm - package for decoding cgm pages @@ -7,19 +6,17 @@ * decoders - decoder classes? * PageDecoder - stateful decoder """ - -from binascii import hexlify - # TODO: all this stuff could be refactored into re-usable and tested # module. import io +from binascii import hexlify from datetime import datetime -from dateutil.relativedelta import relativedelta +from pprint import pprint +from dateutil.relativedelta import relativedelta from decocare import lib -from decocare.records import times from decocare.errors import DataTransferCorruptionError -from pprint import pprint +from decocare.records import times ################### # @@ -28,239 +25,274 @@ # TODO: tests, ideally ones showing bits flip over ################### -def parse_minutes (one): - minute = (one & 0b111111 ) - return minute -def parse_hours (one): - return (one & 0x1F ) +def parse_minutes(one): + minute = one & 0b111111 + return minute -def parse_day (one): - return one & 0x1F -def parse_months (first_byte, second_byte): - first_two_bits = first_byte >> 6 - second_two_bits = second_byte >> 6 - return (first_two_bits << 2) + second_two_bits - +def parse_hours(one): + return one & 0x1F -def parse_date (data, unmask=False, strict=False, minute_specific=False): - """ - Some dates are formatted/stored down to the second (Sensor CalBGForPH) while + +def parse_day(one): + return one & 0x1F + + +def parse_months(first_byte, second_byte): + first_two_bits = first_byte >> 6 + second_two_bits = second_byte >> 6 + return (first_two_bits << 2) + second_two_bits + + +def parse_date(data, unmask=False, strict=False, minute_specific=False): + """ + Some dates are formatted/stored down to the second (Sensor CalBGForPH) while others are stored down to the minute (CGM SensorTimestamp dates). - """ - data = data[:] - seconds = 0 - minutes = 0 - hours = 0 - - year = times.parse_years(data[0]) - day = parse_day(data[1]) - minutes = parse_minutes(data[2]) - - hours = parse_hours(data[3]) - - month = parse_months(data[3], data[2]) - - try: - date = datetime(year, month, day, hours, minutes, seconds) - return date - except ValueError, e: - if strict: - raise - if unmask: - return (year, month, day, hours, minutes, seconds) - pass - return None - -class PagedData (object): - """ + """ + data = data[:] + seconds = 0 + minutes = 0 + hours = 0 + + year = times.parse_years(data[0]) + day = parse_day(data[1]) + minutes = parse_minutes(data[2]) + + hours = parse_hours(data[3]) + + month = parse_months(data[3], data[2]) + + try: + date = datetime(year, month, day, hours, minutes, seconds) + return date + except ValueError as e: + if strict: + raise + if unmask: + return (year, month, day, hours, minutes, seconds) + pass + return None + + +class PagedData: + """ PagedData - context for parsing a page of cgm data. - """ - - @classmethod - def Data (klass, data, **kwds): - stream = io.BufferedReader(io.BytesIO(data)) - return klass(stream, **kwds) - def __init__ (self, stream, larger=False): - raw = bytearray(stream.read(1024)) - data, crc = raw[0:1022], raw[1022:] - computed = lib.CRC16CCITT.compute(bytearray(data)) - self.larger = larger - if lib.BangInt(crc) != computed: - raise DataTransferCorruptionError("CRC does not match page data") - - data.reverse( ) - self.data = self.eat_nulls(data) - self.stream = io.BufferedReader(io.BytesIO(self.data)) - - def eat_nulls (self, data): - i = 0 - while data[i] == 0x00: - i = i+1 - return data[i:] - def suggest (self, op): """ + + @classmethod + def Data(klass, data, **kwds): + stream = io.BufferedReader(io.BytesIO(data)) + return klass(stream, **kwds) + + def __init__(self, stream, larger=False): + raw = bytearray(stream.read(1024)) + data, crc = raw[0:1022], raw[1022:] + computed = lib.CRC16CCITT.compute(bytearray(data)) + self.larger = larger + if lib.BangInt(crc) != computed: + raise DataTransferCorruptionError("CRC does not match page data") + + data.reverse() + self.data = self.eat_nulls(data) + self.stream = io.BufferedReader(io.BytesIO(self.data)) + + def eat_nulls(self, data): + i = 0 + while data[i] == 0x00: + i = i + 1 + return data[i:] + + def suggest(self, op): + """ return a partially filled in acket/opcode info: name, packet size, date_type, op some info will be added later when the record is parsed: GlucoseSensorData, cal factor. amount, prefix, data """ - # TODO: would it be possible to turn these into classes which know hwo - # to decode time and describe themselves to PagedData? - # that way we can write tests for each type individually and tests for - # the thing as a whole easily. - records = { - 0x01: dict(name='DataEnd',packet_size=0,date_type='none',op='0x01'), - 0x02: dict(name='SensorWeakSignal',packet_size=0,date_type='prevTimestamp',op='0x02'), - 0x03: dict(name='SensorCal',packet_size=1,date_type='prevTimestamp',op='0x03'), - 0x07: dict(name='Fokko-07',packet_size=1,date_type='prevTimestamp',op='0x07'), - 0x08: dict(name='SensorTimestamp',packet_size=4,date_type='minSpecific',op='0x08'), - 0x0a: dict(name='BatteryChange',packet_size=4,date_type='minSpecific',op='0x0a'), - 0x0b: dict(name='SensorStatus',packet_size=4,date_type='minSpecific',op='0x0b'), - 0x0c: dict(name='DateTimeChange',packet_size=4,date_type='secSpecific',op='0x0c'), - 0x0d: dict(name='SensorSync',packet_size=4,date_type='minSpecific',op='0x0d'), - 0x0e: dict(name='CalBGForGH',packet_size=5,date_type='minSpecific',op='0x0e'), - 0x0f: dict(name='SensorCalFactor',packet_size=6,date_type='minSpecific',op='0x0f'), - # 0x10: dict(name='10-Something',packet_size=7,date_type='minSpecific',op='0x10'), - 0x10: dict(name='10-Something',packet_size=4,date_type='minSpecific',op='0x10'), - 0x13: dict(name='19-Something',packet_size=0,date_type='prevTimestamp',op='0x13') - } - if self.larger: - # record[08][] - pass - - if op > 0 and op < 20: - record = records.get(op, None) - if record is None: - return dict(name='Could Not Decode',packet_size=0,op=op) - else: - return record - else: - record = dict(name='GlucoseSensorData',packet_size=0,date_type='prevTimestamp',op=op) - record.update(sgv=(int(op) * 2)) - return record - - def decode (self): - """ + # TODO: would it be possible to turn these into classes which know hwo + # to decode time and describe themselves to PagedData? + # that way we can write tests for each type individually and tests for + # the thing as a whole easily. + # fmt:off + records = { + 0x01: dict(name="DataEnd", packet_size=0, date_type="none", op="0x01"), + 0x02: dict(name="SensorWeakSignal", packet_size=0, date_type="prevTimestamp", op="0x02"), + 0x03: dict(name="SensorCal", packet_size=1, date_type="prevTimestamp", op="0x03"), + 0x07: dict(name="Fokko-07", packet_size=1, date_type="prevTimestamp", op="0x07"), + 0x08: dict(name="SensorTimestamp", packet_size=4, date_type="minSpecific", op="0x08"), + 0x0A: dict(name="BatteryChange", packet_size=4, date_type="minSpecific", op="0x0a"), + 0x0B: dict(name="SensorStatus", packet_size=4, date_type="minSpecific", op="0x0b"), + 0x0C: dict(name="DateTimeChange", packet_size=4, date_type="secSpecific", op="0x0c"), + 0x0D: dict(name="SensorSync", packet_size=4, date_type="minSpecific", op="0x0d"), + 0x0E: dict(name="CalBGForGH", packet_size=5, date_type="minSpecific", op="0x0e"), + 0x0F: dict(name="SensorCalFactor", packet_size=6, date_type="minSpecific", op="0x0f"), + # 0x10: dict(name="10-Something", packet_size=7, date_type="minSpecific", op="0x10"), + 0x10: dict(name="10-Something", packet_size=4, date_type="minSpecific", op="0x10"), + 0x13: dict(name="19-Something", packet_size=0, date_type="prevTimestamp", op="0x13"), + } + # fmt:on + if self.larger: + # record[08][] + pass + + if op > 0 and op < 20: + record = records.get(op, None) + if record is None: + return dict(name="Could Not Decode", packet_size=0, op=op) + else: + return record + else: + record = dict( + name="GlucoseSensorData", + packet_size=0, + date_type="prevTimestamp", + op=op, + ) + record.update(sgv=(int(op) * 2)) + return record + + def decode(self): + """ XXX: buggy code * fails to acknowledge gaps in time * fails to acknowledge SensorSync """ - records = [ ] - prefix_records = [] - for B in iter(lambda: self.stream.read(1), ""): - B = bytearray(B) - - # eat nulls within the page to avoid 0-value sgv records - if B[0] == 0x00: - continue - - record = self.suggest(B[0]) - record['_tell'] = self.stream.tell( ) - # read packet if needed - if not record is None and record['packet_size'] > 0: - raw_packet = bytearray(self.stream.read(record['packet_size'])) - - if record['name'] == 'DataEnd': - prefix_records.append(record) - continue - - elif record['name'] == 'GlucoseSensorData' or record['name'] == 'SensorWeakSignal' \ - or record['name'] == 'SensorCal' or record['name'] == '19-Something': - # add to prefixed records to add to the next sensor minute timestamped record - if record['name'] == 'SensorCal': - record.update(raw=self.byte_to_str(raw_packet)) - if int(raw_packet[0]) == 1: - record.update(waiting='waiting') - else: - record.update(waiting='meter_bg_now') - prefix_records.append(record) - - elif record['name'] == 'SensorTimestamp' or record['name'] == 'SensorCalFactor' or record['name'] in ['10-Something']: - # TODO: maybe this is like a ResetGlucose base command - # these are sensor minute timestamped records thus create the record - # and map prefixed elements based on the timedelta - record.update(raw=self.byte_to_str(raw_packet)) - date, body = raw_packet[:4], raw_packet[4:] - date.reverse() - date = parse_date(date) - if date: - record.update(date=date.isoformat()) - else: - print "@@@", self.stream.tell( ) - pprint(dict(raw=hexlify(raw_packet))) - pprint(dict(date=hexlify(date or bytearray( )))) - pprint(dict(body=hexlify(body))) - break - prefix_records.reverse() - mapped_glucose_records = self.map_glucose(prefix_records, start=date, delta=self.delta_ago(reverse=True)) - mapped_glucose_records.reverse() - # And this ResetGlucose has a payload indicating calibration factor - # Update sensor cal factor - if record['name'] == 'SensorCalFactor': - factor = lib.BangInt([ body[0], body[1] ]) / 1000.0 - record.update(factor=factor) - records.extend(mapped_glucose_records) - records.append(record) + records = [] prefix_records = [] + for B in iter(lambda: self.stream.read(1), ""): + B = bytearray(B) + # eat nulls within the page to avoid 0-value sgv records + if B[0] == 0x00: + continue - elif record['name'] in ['SensorStatus', 'DateTimeChange', 'SensorSync', 'CalBGForGH', 'BatteryChange' ]: - # independent record => parse and add to records list - record.update(raw=self.byte_to_str(raw_packet)) - if record['name'] in ['SensorStatus', 'SensorSync', 'CalBGForGH', 'BatteryChange', 'DateTimeChange']: - date, body = raw_packet[:4], raw_packet[4:] - date.reverse() - date = parse_date(date) - if date is not None: - record.update(date=date.isoformat()) - else: - record.update(_date=str(raw_packet[:4]).encode('hex')) - record.update(body=self.byte_to_str(body)) - # Update cal amount - if record['name'] == 'DateTimeChange': - """ + record = self.suggest(B[0]) + record["_tell"] = self.stream.tell() + # read packet if needed + if not record is None and record["packet_size"] > 0: + raw_packet = bytearray(self.stream.read(record["packet_size"])) + + if record["name"] == "DataEnd": + prefix_records.append(record) + continue + + elif ( + record["name"] == "GlucoseSensorData" + or record["name"] == "SensorWeakSignal" + or record["name"] == "SensorCal" + or record["name"] == "19-Something" + ): + # add to prefixed records to add to the next sensor minute timestamped record + if record["name"] == "SensorCal": + record.update(raw=self.byte_to_str(raw_packet)) + if int(raw_packet[0]) == 1: + record.update(waiting="waiting") + else: + record.update(waiting="meter_bg_now") + prefix_records.append(record) + + elif ( + record["name"] == "SensorTimestamp" + or record["name"] == "SensorCalFactor" + or record["name"] in ["10-Something"] + ): + # TODO: maybe this is like a ResetGlucose base command + # these are sensor minute timestamped records thus create the record + # and map prefixed elements based on the timedelta + record.update(raw=self.byte_to_str(raw_packet)) + date, body = raw_packet[:4], raw_packet[4:] + date.reverse() + date = parse_date(date) + if date: + record.update(date=date.isoformat()) + else: + print("@@@", self.stream.tell()) + pprint(dict(raw=hexlify(raw_packet))) + pprint(dict(date=hexlify(date or bytearray()))) + pprint(dict(body=hexlify(body))) + break + prefix_records.reverse() + mapped_glucose_records = self.map_glucose( + prefix_records, start=date, delta=self.delta_ago(reverse=True) + ) + mapped_glucose_records.reverse() + # And this ResetGlucose has a payload indicating calibration factor + # Update sensor cal factor + if record["name"] == "SensorCalFactor": + factor = lib.BangInt([body[0], body[1]]) / 1000.0 + record.update(factor=factor) + records.extend(mapped_glucose_records) + records.append(record) + prefix_records = [] + + elif record["name"] in [ + "SensorStatus", + "DateTimeChange", + "SensorSync", + "CalBGForGH", + "BatteryChange", + ]: + # independent record => parse and add to records list + record.update(raw=self.byte_to_str(raw_packet)) + if record["name"] in [ + "SensorStatus", + "SensorSync", + "CalBGForGH", + "BatteryChange", + "DateTimeChange", + ]: + date, body = raw_packet[:4], raw_packet[4:] + date.reverse() + date = parse_date(date) + if date is not None: + record.update(date=date.isoformat()) + else: + record.update(_date=str(raw_packet[:4]).encode("hex")) + record.update(body=self.byte_to_str(body)) + # Update cal amount + if record["name"] == "DateTimeChange": + """ changed = body[1:5] changed.reverse( ) changed = parse_date(changed) record.update(change=changed.isoformat( ), body=self.byte_to_str(body[5:])) """ - if record['name'] == 'CalBGForGH': - amount = lib.BangInt([ (raw_packet[2] & 0b00100000) >> 5, body[0] ]) - record.update(body=self.byte_to_str(body)) - record.update(amount=amount) - records.append(record) - else: - # could not decode - records.append(record) - # End For - records.reverse() - self.records = records - return self.records - - - def byte_to_str (self, byte_array): - # convert byte array to a string - hex_bytes = [] - for i in range(0, len(byte_array)): - hex_bytes.append('{0:02x}'.format(byte_array[i])) - return '-'.join(hex_bytes) - - def map_glucose (self, values, start=None, delta=None): - last = start - if delta is None: - delta = self.delta_ago() - for x in list(values): - if x['name'] != '19-Something': - last = last - delta - x.update(date=last.isoformat()) - return values - - def delta_ago (self, reverse=False, offset=1): - delta = relativedelta(minutes=5*offset) - if reverse: - delta = relativedelta(minutes=-5*offset) - return delta + if record["name"] == "CalBGForGH": + amount = lib.BangInt( + [(raw_packet[2] & 0b00100000) >> 5, body[0]] + ) + record.update(body=self.byte_to_str(body)) + record.update(amount=amount) + records.append(record) + else: + # could not decode + records.append(record) + # End For + records.reverse() + self.records = records + return self.records + + def byte_to_str(self, byte_array): + # convert byte array to a string + hex_bytes = [] + for i in range(0, len(byte_array)): + hex_bytes.append("{:02x}".format(byte_array[i])) + return "-".join(hex_bytes) + + def map_glucose(self, values, start=None, delta=None): + last = start + if delta is None: + delta = self.delta_ago() + for x in list(values): + if x["name"] != "19-Something": + last = last - delta + x.update(date=last.isoformat()) + return values + def delta_ago(self, reverse=False, offset=1): + delta = relativedelta(minutes=5 * offset) + if reverse: + delta = relativedelta(minutes=-5 * offset) + return delta diff --git a/decocare/commands.py b/decocare/commands.py index 24d6456..b289b24 100644 --- a/decocare/commands.py +++ b/decocare/commands.py @@ -1,11 +1,14 @@ - +import codecs import logging +import textwrap import time -import lib +from decocare import lib + + +class BadResponse(Exception): + pass -class BadResponse (Exception): - pass """ @@ -22,478 +25,593 @@ class BadResponse (Exception): """ -log = logging.getLogger( ).getChild(__name__) +log = logging.getLogger().getChild(__name__) + def CRC8(data): - return lib.CRC8.compute(data) - -class BaseCommand(object): - code = 0x00 - descr = "(error)" - retries = 2 - timeout = 3 - params = [ ] - bytesPerRecord = 0 - maxRecords = 0 - effectTime = 0 - - responded = False - - def __init__(self, code, descr, *args): - self.code = code - self.descr = descr - self.params = [ ] - - def done(self): - found = len(self.data or [ ]) - expect = int(self.maxRecords * self.bytesPerRecord) - expect_size = "found[{}] expected[{}]".format(found, expect) - log.info("%s:download:done?explain=%s" % (self, expect_size)) - return found >= expect - def format(self): - pass + return lib.CRC8.compute(data) + + +class BaseCommand: + code = 0x00 + descr = "(error)" + retries = 2 + timeout = 3 + params = [] + bytesPerRecord = 0 + maxRecords = 0 + effectTime = 0 + + responded = False + + def __init__(self, code, descr, *args): + self.code = code + self.descr = descr + self.params = [] + + def done(self): + found = len(self.data or []) + expect = int(self.maxRecords * self.bytesPerRecord) + expect_size = "found[{}] expected[{}]".format(found, expect) + log.info("{}:download:done?explain={}".format(self, expect_size)) + return found >= expect + + def format(self): + pass + + def respond(self, data): + if getattr(self, "data", None): + self.data.extend(data) + else: + self.data = data + self.getData() + self.responded = True + + def hexdump(self): + return lib.hexdump(self.data) + + +class FieldChecker: + def __init__(self, msg, required=[]): + self.msg = msg + self.required = required + + def check_fields(self, data): + for field in self.required: + if field not in data: + raise BadResponse() + + def __call__(self, data): + self.msg.validate(data) + self.check_fields(data) + return True - def respond(self, data): - if getattr(self, 'data', None): - self.data.extend(data) - else: - self.data = data - self.getData( ) - self.responded = True - - def hexdump (self): - return lib.hexdump(self.data) - -class FieldChecker (object): - def __init__ (self, msg, required=[]): - self.msg = msg - self.required = required - - def check_fields (self, data): - for field in self.required: - if field not in data: - raise BadResponse( ) - def __call__ (self, data): - self.msg.validate(data) - self.check_fields(data) - return True class PumpCommand(BaseCommand): - #serial = '665455' - #serial = '206525' - serial = '208850' - - params = [ ] - bytesPerRecord = 64 - maxRecords = 1 - retries = 2 - effectTime = .500 - data = bytearray( ) - Validator = FieldChecker - output_fields = [ ] - __fields__ = ['maxRecords', 'code', 'descr', - 'serial', 'bytesPerRecord', 'retries', 'params'] - def __init__(self, **kwds): - for k in self.__fields__: - value = kwds.get(k, getattr(self, k)) - setattr(self, k, value) - self.allocateRawData( ) - self.data = bytearray( ) - self.name = self.log_name( ) - self.checker = self.Validator(self, required=self.output_fields) - - def log_name(self, prefix=''): - return prefix + '{}.data'.format(self.__class__.__name__) - - def save(self, prefix=''): - name = '{}'.format(self.log_name(prefix)) - handle = open(name, 'wb') - handle.write(self.data) - handle.close( ) - - def __str__(self): - if self.responded: - return '{}:size[{}]:data:{}'.format(self.__class__.__name__, - self.size, repr(self.getData( ))) - return '{}:data:unknown'.format(self.__class__.__name__) - - def __repr__(self): - return '<{0}>'.format( self) - - def validate (self, data): - return True - def check_output (self, data): - return self.checker(data) - - def getData(self): - return self.data - - def allocateRawData(self): - self.size = self.bytesPerRecord * self.maxRecords - - def format(self): - params = self.params - code = self.code - maxRetries = self.retries - serial = list(bytearray(self.serial.decode('hex'))) - paramsCount = len(params) - head = [ 1, 0, 167, 1 ] - # serial - packet = head + serial - # paramCount 2 bytes - packet.extend( [ (0x80 | lib.HighByte(paramsCount)), - lib.LowByte(paramsCount) ] ) - # not sure what this byte means - button = 0 - # special case command 93 - if code == 93: - button = 85 - packet.append(button) - packet.append(maxRetries) - # how many packets/frames/pages/flows will this take? - responseSize = self.calcRecordsRequired() - # really only 1 or 2? - pages = responseSize - if responseSize > 1: - pages = 2 - packet.append(pages) - packet.append(0) - # command code goes here - packet.append(code) - packet.append(CRC8(packet)) - packet.extend(params) - packet.append(CRC8(params)) - log.debug(packet) - return bytearray(packet) - - def calcRecordsRequired(self): - length = self.bytesPerRecord * self.maxRecords - i = length / 64 - j = length % 64 - if j > 0: - return i + 1 - return i + # serial = '665455' + # serial = '206525' + serial = "208850" + + params = [] + bytesPerRecord = 64 + maxRecords = 1 + retries = 2 + effectTime = 0.500 + data = bytearray() + Validator = FieldChecker + output_fields = [] + __fields__ = [ + "maxRecords", + "code", + "descr", + "serial", + "bytesPerRecord", + "retries", + "params", + ] + + def __init__(self, **kwds): + for k in self.__fields__: + value = kwds.get(k, getattr(self, k)) + setattr(self, k, value) + self.allocateRawData() + self.data = bytearray() + self.name = self.log_name() + self.checker = self.Validator(self, required=self.output_fields) + + def log_name(self, prefix=""): + return prefix + "{}.data".format(self.__class__.__name__) + + def save(self, prefix=""): + name = "{}".format(self.log_name(prefix)) + handle = open(name, "wb") + handle.write(self.data) + handle.close() + + def __str__(self): + if self.responded: + return "{}:size[{}]:data:{}".format( + self.__class__.__name__, self.size, repr(self.getData()) + ) + return "{}:data:unknown".format(self.__class__.__name__) + + def __repr__(self): + return "<{}>".format(self) + + def validate(self, data): + return True + + def check_output(self, data): + return self.checker(data) + + def getData(self): + return self.data + + def allocateRawData(self): + self.size = self.bytesPerRecord * self.maxRecords + + def format(self): + params = self.params + code = self.code + maxRetries = self.retries + serial = list(bytearray(codecs.decode(self.serial, "hex"))) + paramsCount = len(params) + head = [1, 0, 167, 1] + # serial + packet = head + serial + # paramCount 2 bytes + packet.extend([(0x80 | lib.HighByte(paramsCount)), lib.LowByte(paramsCount)]) + # not sure what this byte means + button = 0 + # special case command 93 + if code == 93: + button = 85 + packet.append(button) + packet.append(maxRetries) + # how many packets/frames/pages/flows will this take? + responseSize = self.calcRecordsRequired() + # really only 1 or 2? + pages = responseSize + if responseSize > 1: + pages = 2 + packet.append(pages) + packet.append(0) + # command code goes here + packet.append(code) + packet.append(CRC8(packet)) + packet.extend(params) + packet.append(CRC8(params)) + log.debug(packet) + return bytearray(packet) + + def calcRecordsRequired(self): + length = self.bytesPerRecord * self.maxRecords + i = length // 64 + j = length % 64 + if j > 0: + return i + 1 + return i + class ManualCommand(PumpCommand): - def __init__(self, **kwds): - self.name = kwds.get('name', self.__class__.__name__) - super(type(self), self).__init__(**kwds) - self.kwds = kwds - self.name = kwds.get('name', self.__class__.__name__) - def __str__(self): - if self.responded: - return '{}:{}:size[{}]:'.format(self.name, self.kwds, - self.size) - return '{}:{}:data:unknown'.format(self.name, self.kwds) - - def log_name(self, prefix=''): - return prefix + '{}.data'.format(self.name) - def __repr__(self): - return '<{0}>'.format(self) - - def getData(self): - return self.hexdump( ) + def __init__(self, **kwds): + self.name = kwds.get("name", self.__class__.__name__) + super(type(self), self).__init__(**kwds) + self.kwds = kwds + self.name = kwds.get("name", self.__class__.__name__) + + def __str__(self): + if self.responded: + return "{}:{}:size[{}]:".format(self.name, self.kwds, self.size) + return "{}:{}:data:unknown".format(self.name, self.kwds) + + def log_name(self, prefix=""): + return prefix + "{}.data".format(self.name) + + def __repr__(self): + return "<{}>".format(self) + + def getData(self): + return self.hexdump() + class PowerControl(PumpCommand): - """ + """ >>> PowerControl(serial='665455').format() == PowerControl._test_ok True - """ - _test_ok = bytearray( [ 0x01, 0x00, 0xA7, 0x01, 0x66, 0x54, 0x55, 0x80, - 0x02, 0x55, 0x00, 0x00, 0x00, 0x5D, 0xE6, 0x01, - 0x0A, 0xA2 ] ) - code = 93 - descr = "RF Power On" - params = [ 0x01, 0x0A ] - retries = 0 - maxRecords = 0 - #timeout = 1 - # effectTime = 7 - effectTime = 12 - def __init__(self, minutes=None, **kwds): - if minutes is not None: - self.minutes = int(minutes) - kwds['params'] = [ 0x01, self.minutes ] - super(PowerControl, self).__init__(**kwds) + """ + + _test_ok = bytearray( + [ + 0x01, + 0x00, + 0xA7, + 0x01, + 0x66, + 0x54, + 0x55, + 0x80, + 0x02, + 0x55, + 0x00, + 0x00, + 0x00, + 0x5D, + 0xE6, + 0x01, + 0x0A, + 0xA2, + ] + ) + code = 93 + descr = "RF Power On" + params = [0x01, 0x0A] + retries = 0 + maxRecords = 0 + # timeout = 1 + # effectTime = 7 + effectTime = 12 + + def __init__(self, minutes=None, **kwds): + if minutes is not None: + self.minutes = int(minutes) + kwds["params"] = [0x01, self.minutes] + super().__init__(**kwds) + class PowerControlOff(PowerControl): - """ - Here's an example where arguments clearly modify behavior. - """ - params = [ 0x00, 0x00 ] + """ + Here's an example where arguments clearly modify behavior. + """ + + params = [0x00, 0x00] + # MMPump???/ CMD_???????? 69 0x45 ('E') ?? -class PumpExperiment_OP69 (PumpCommand): - code = 69 +class PumpExperiment_OP69(PumpCommand): + code = 69 + # MMPump???/ CMD_???????? 70 0x46 ('F') ?? -class PumpExperiment_OP70 (PumpCommand): - code = 70 +class PumpExperiment_OP70(PumpCommand): + code = 70 + # MMPump???/ CMD_???????? 71 0x47 ('G') ?? -class PumpExperiment_OP71 (PumpCommand): - code = 71 +class PumpExperiment_OP71(PumpCommand): + code = 71 + # MMPump???/ CMD_???????? 72 0x48 ('H') ?? -class PumpExperiment_OP72 (PumpCommand): - code = 72 +class PumpExperiment_OP72(PumpCommand): + code = 72 + # MMPump???/ CMD_???????? 73 0x49 ('I') ?? -class PumpExperiment_OP73 (PumpCommand): - code = 73 +class PumpExperiment_OP73(PumpCommand): + code = 73 + # MMPump???/ SelectBasalProfile 74 0x4a ('J') OK -class SelectBasalProfile (PumpCommand): - code = 74 +class SelectBasalProfile(PumpCommand): + code = 74 + -class SelectBasalProfileSTD (SelectBasalProfile): - params = [ 0 ] +class SelectBasalProfileSTD(SelectBasalProfile): + params = [0] -class SelectBasalProfileA (SelectBasalProfile): - params = [ 1 ] -class SelectBasalProfileB (SelectBasalProfile): - params = [ 2 ] +class SelectBasalProfileA(SelectBasalProfile): + params = [1] + + +class SelectBasalProfileB(SelectBasalProfile): + params = [2] + # MMPump???/ CMD_???????? 75 0x4b ('K') ?? -class PumpExperiment_OP75 (PumpCommand): - code = 75 +class PumpExperiment_OP75(PumpCommand): + code = 75 + class TempBasal(PumpCommand): - """ - - """ - - code = 76 - descr = "Set temp basal" - params = [ 0x00, 0x00, 0x00 ] - retries = 0 - #maxRecords = 0 - #timeout = 1 - - def getData(self): - status = { 0: 'absolute' } - received = True if (len(self.data) > 0 and self.data[0] is 0) else False - return dict(recieved=received, temp=status.get(self.params[0], 'percent')) - @classmethod - def Program (klass, rate=None, duration=None, temp=None, **kwds): - assert duration % 30 is 0, "duration {0} is not a whole multiple of 30".format(duration) - assert temp in [ 'percent', 'absolute' ], "temp field <{0}> should be one of {1}".format(temp, ['percent', 'absolute' ]) - if temp in [ 'percent' ]: - return TempBasalPercent(params=klass.format_percent_params(rate, duration), **kwds) - - return klass(params=klass.format_params(rate, duration), **kwds) - @classmethod - def format_percent_params (klass, rate, duration): - duration = int(duration / 30) - rate = int(rate) - params = [rate, duration] - return params - - @classmethod - def format_params (klass, rate, duration): - duration = duration / 30 - rate = int(round(rate / 0.025)) - params = [lib.HighByte(rate), lib.LowByte(rate), duration] - return params + """ + """ + + code = 76 + descr = "Set temp basal" + params = [0x00, 0x00, 0x00] + retries = 0 + # maxRecords = 0 + # timeout = 1 + + def getData(self): + status = {0: "absolute"} + received = True if (len(self.data) > 0 and self.data[0] == 0) else False + return dict(recieved=received, temp=status.get(self.params[0], "percent")) + + @classmethod + def Program(klass, rate=None, duration=None, temp=None, **kwds): + assert duration % 30 == 0, "duration {} is not a whole multiple of 30".format( + duration + ) + assert temp in [ + "percent", + "absolute", + ], "temp field <{}> should be one of {}".format(temp, ["percent", "absolute"]) + if temp in ["percent"]: + return TempBasalPercent( + params=klass.format_percent_params(rate, duration), **kwds + ) + + return klass(params=klass.format_params(rate, duration), **kwds) + + @classmethod + def format_percent_params(klass, rate, duration): + duration = int(duration / 30) + rate = int(rate) + params = [rate, duration] + return params + + @classmethod + def format_params(klass, rate, duration): + duration = duration // 30 + rate = int(round(rate / 0.025)) + params = [lib.HighByte(rate), lib.LowByte(rate), duration] + return params class SetSuspend(PumpCommand): - code = 77 - descr = "Set Pump Suspend/Resume status" - params = [ ] - retries = 2 - maxRecords = 1 - def getData(self): - status = { 0: 'resumed', 1: 'suspended' } - received = True if self.data[0] is 0 else False - return dict(recieved=received, status=status.get(self.params[0])) + code = 77 + descr = "Set Pump Suspend/Resume status" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + status = {0: "resumed", 1: "suspended"} + received = True if self.data[0] == 0 else False + return dict(recieved=received, status=status.get(self.params[0])) + class PumpSuspend(SetSuspend): - descr = "Suspend pump" - params = [ 1 ] + descr = "Suspend pump" + params = [1] + class PumpResume(SetSuspend): - descr = "Resume pump (cancel suspend)" - params = [ 0 ] + descr = "Resume pump (cancel suspend)" + params = [0] + -class SetAutoOff (PumpCommand): - code = 78 - maxRecords = 0 +class SetAutoOff(PumpCommand): + code = 78 + maxRecords = 0 -class SetEnabledEasyBolus (PumpCommand): - code = 79 - maxRecords = 0 -class SetBasalType (PumpCommand): - code = 104 +class SetEnabledEasyBolus(PumpCommand): + code = 79 + maxRecords = 0 -class TempBasalPercent (TempBasal): - """ - """ +class SetBasalType(PumpCommand): + code = 104 + + +class TempBasalPercent(TempBasal): + """ + + """ + + code = 105 + descr = "Set temp basal by percent" + params = [0x00, 0x00] + retries = 0 + # maxRecords = 0 + # timeout = 1 - code = 105 - descr = "Set temp basal by percent" - params = [ 0x00, 0x00 ] - retries = 0 - #maxRecords = 0 - #timeout = 1 class KeypadPush(PumpCommand): - code = 91 - descr = "Press buttons on the keypad" - params = [ ] - retries = 1 - maxRecords = 0 + code = 91 + descr = "Press buttons on the keypad" + params = [] + retries = 1 + maxRecords = 0 + + @classmethod + def ACT(klass, **kwds): + return klass(params=[0x02], **kwds) + + @classmethod + def ESC(klass, **kwds): + return klass(params=[0x01], **kwds) + + @classmethod + def DOWN(klass, **kwds): + return klass(params=[0x04], **kwds) + + @classmethod + def UP(klass, **kwds): + return klass(params=[0x03], **kwds) + + @classmethod + def EASY(klass, **kwds): + return klass(params=[0x00], **kwds) + + +def PushACT(**kwds): + return KeypadPush.ACT(**kwds) + + +def PushESC(**kwds): + return KeypadPush.ESC(**kwds) + + +def PushDOWN(**kwds): + return KeypadPush.DOWN(**kwds) + + +def PushUP(**kwds): + return KeypadPush.UP(**kwds) + + +def PushEASY(**kwds): + return KeypadPush.EASY(**kwds) + + +class ReadErrorStatus508(PumpCommand): + """ + + """ + + code = 38 + descr = "error status" + params = [] + + +class ReadBolusHistory(PumpCommand): + """ + + """ + + code = 39 + descr = "bolus history" + params = [] + + +class ReadDailyTotals(PumpCommand): + """ - @classmethod - def ACT(klass, **kwds): - return klass(params=[0x02], **kwds) + """ - @classmethod - def ESC(klass, **kwds): - return klass(params=[0x01], **kwds) + code = 40 + descr = "..." + params = [] - @classmethod - def DOWN(klass, **kwds): - return klass(params=[0x04], **kwds) - @classmethod - def UP(klass, **kwds): - return klass(params=[0x03], **kwds) +class ReadPrimeBoluses(PumpCommand): + """ - @classmethod - def EASY(klass, **kwds): - return klass(params=[0x00], **kwds) + """ -def PushACT (**kwds): - return KeypadPush.ACT(**kwds) + code = 41 + descr = "..." + params = [] -def PushESC (**kwds): - return KeypadPush.ESC(**kwds) -def PushDOWN (**kwds): - return KeypadPush.DOWN(**kwds) +class ReadAlarms(PumpCommand): + """ -def PushUP (**kwds): - return KeypadPush.UP(**kwds) + """ -def PushEASY (**kwds): - return KeypadPush.EASY(**kwds) + code = 42 + descr = "..." + params = [] -class ReadErrorStatus508 (PumpCommand): - """ +class ReadProfileSets(PumpCommand): + """ - """ - code = 38 - descr = "error status" - params = [ ] + """ + + code = 43 + descr = "..." + params = [] -class ReadBolusHistory (PumpCommand): - """ - """ - code = 39 - descr = "bolus history" - params = [ ] +class ReadUserEvents(PumpCommand): + """ + + """ -class ReadDailyTotals (PumpCommand): - """ + code = 44 + descr = "..." + params = [] - """ - code = 40 - descr = "..." - params = [ ] -class ReadPrimeBoluses (PumpCommand): - """ +class ReadRemoteControlID(PumpCommand): + """ - """ - code = 41 - descr = "..." - params = [ ] + """ -class ReadAlarms (PumpCommand): - """ + code = 46 + descr = "..." + params = [] - """ - code = 42 - descr = "..." - params = [ ] -class ReadProfileSets (PumpCommand): - """ +class Read128KMem(PumpCommand): + """ - """ - code = 43 - descr = "..." - params = [ ] + """ -class ReadUserEvents (PumpCommand): - """ + code = 55 + descr = "..." + params = [] - """ - code = 44 - descr = "..." - params = [ ] -class ReadRemoteControlID (PumpCommand): - """ +class Read256KMem(PumpCommand): + """ - """ - code = 46 - descr = "..." - params = [ ] + """ -class Read128KMem (PumpCommand): - """ + code = 56 + descr = "..." + params = [] - """ - code = 55 - descr = "..." - params = [ ] -class Read256KMem (PumpCommand): - """ +class Bolus(PumpCommand): + """ + Bolus some insulin. - """ - code = 56 - descr = "..." - params = [ ] + XXX: Be careful please. + Best trying this not connected to the pump until you trust it. + """ -class Bolus (PumpCommand): - """ - Bolus some insulin. + code = 66 + descr = "Bolus" + params = [] - XXX: Be careful please. - Best trying this not connected to the pump until you trust it. - """ - code = 66 - descr = "Bolus" - params = [ ] - def getData(self): - received = True if self.data[0] is 0x0c else False - return dict(recieved=received, _type='BolusRequest') + def getData(self): + received = True if self.data[0] == 0x0C else False + return dict(recieved=received, _type="BolusRequest") class ReadErrorStatus(PumpCommand): - """ + """ >>> ReadErrorStatus(serial='665455').format() == ReadErrorStatus._test_ok True - """ - _test_ok = bytearray([ 0x01, 0x00, 0xA7, 0x01, 0x66, 0x54, 0x55, 0x80, - 0x00, 0x00, 0x02, 0x01, 0x00, 0x75, 0xD7, 0x00 ]) - code = 117 - descr = "Read Error Status any current alarms set?" - params = [ ] - retries = 2 - maxRecords = 1 + """ + + _test_ok = bytearray( + [ + 0x01, + 0x00, + 0xA7, + 0x01, + 0x66, + 0x54, + 0x55, + 0x80, + 0x00, + 0x00, + 0x02, + 0x01, + 0x00, + 0x75, + 0xD7, + 0x00, + ] + ) + code = 117 + descr = "Read Error Status any current alarms set?" + params = [] + retries = 2 + maxRecords = 1 + class ReadHistoryData(PumpCommand): - """ + """ >>> ReadHistoryData(serial='208850', params=[ 0x03 ]).format() == ReadHistoryData._test_ok True >>> ReadHistoryData(params=[ 0x01 ]).params @@ -508,512 +626,651 @@ class ReadHistoryData(PumpCommand): [2] >>> ReadHistoryData(page=0x03).params [3] - """ - __fields__ = PumpCommand.__fields__ + ['page'] - _test_ok = bytearray([ 0x01, 0x00, 0xA7, 0x01, 0x20, 0x88, 0x50, 0x80, 0x01, 0x00, 0x02, 0x02, 0x00, 0x80, 0x9B, 0x03, 0x36, ]) - - page = None - def __init__(self, page=None, **kwds): - if page is None and kwds.get('params', [ ]): - page = kwds.pop('params')[0] or 0 - - if page is not None: - self.page = int(page) - kwds['params'] = [ self.page ] - super(ReadHistoryData, self).__init__(**kwds) - - def log_name(self, prefix=''): - return prefix + '{}-page-{}.data'.format(self.__class__.__name__, self.page) - - def __str__(self): - base = ''.join([ self.__class__.__name__, - ':size[%s]:' % self.size, - '[page][%s]' % self.page ]) - return '{}:data[{}]:'.format(base, len(self.data)) - - def done(self): - eod = False - found = len(self.data or [ ]) - expect = int(self.maxRecords * self.bytesPerRecord) - expect_crc = CRC8(self.data[:-1]) - expect_size = "size check found[{}] expected[{}]".format(found, expect) - found_crc = 0 - if self.responded and len(self.data) > 5: - found_crc = self.data[-1] - self.eod = eod = (self.data[5] & 0x80) > 0 - explain_crc = "CRC ACK check found[{}] expected[{}]".format(found_crc, expect_crc) - is_eod = 'and has eod set? %s' % (eod) - log.info("%s:download:done %s:%s:%s" % (self, expect_size, explain_crc, is_eod)) - return found >= expect - - def respond(self, raw): - log.info('{} extending original {} with found {}'.format(str(self), len(self.data), len(raw))) - if len(raw) == self.size: - log.info('{} download respond replace original {} with found {}'.format(str(self), len(self.data), len(raw))) - self.data = raw - elif len(self.data) == self.size: - log.info('{} download respond original {}, XXX IGNORE found {}'.format(str(self), len(self.data), len(raw))) - pass - else: - log.info('{} download respond extend original {} with found {}'.format(str(self), len(self.data), len(raw))) - self.data.extend(raw) - self.responded = True - - code = 128 - descr = "Read History Data" - params = [ ] - retries = 2 - maxRecords = 16 - effectTime = .100 - data = bytearray( ) - - def getData(self): - data = self.data - # log.info("XXX: READ HISTORY DATA!!:\n%s" % lib.hexdump(data)) - return self.hexdump( ) + """ + + __fields__ = PumpCommand.__fields__ + ["page"] + _test_ok = bytearray( + [ + 0x01, + 0x00, + 0xA7, + 0x01, + 0x20, + 0x88, + 0x50, + 0x80, + 0x01, + 0x00, + 0x02, + 0x02, + 0x00, + 0x80, + 0x9B, + 0x03, + 0x36, + ] + ) + + page = None + + def __init__(self, page=None, **kwds): + if page is None and kwds.get("params", []): + page = kwds.pop("params")[0] or 0 + + if page is not None: + self.page = int(page) + kwds["params"] = [self.page] + super().__init__(**kwds) + + def log_name(self, prefix=""): + return prefix + "{}-page-{}.data".format(self.__class__.__name__, self.page) + + def __str__(self): + base = "".join( + [ + self.__class__.__name__, + ":size[%s]:" % self.size, + "[page][%s]" % self.page, + ] + ) + return "{}:data[{}]:".format(base, len(self.data)) + + def done(self): + eod = False + found = len(self.data or []) + expect = int(self.maxRecords * self.bytesPerRecord) + expect_crc = CRC8(self.data[:-1]) + expect_size = "size check found[{}] expected[{}]".format(found, expect) + found_crc = 0 + if self.responded and len(self.data) > 5: + found_crc = self.data[-1] + self.eod = eod = (self.data[5] & 0x80) > 0 + explain_crc = "CRC ACK check found[{}] expected[{}]".format( + found_crc, expect_crc + ) + is_eod = "and has eod set? %s" % (eod) + log.info( + "{}:download:done {}:{}:{}".format(self, expect_size, explain_crc, is_eod) + ) + return found >= expect + + def respond(self, raw): + log.info( + "{} extending original {} with found {}".format( + str(self), len(self.data), len(raw) + ) + ) + if len(raw) == self.size: + log.info( + "{} download respond replace original {} with found {}".format( + str(self), len(self.data), len(raw) + ) + ) + self.data = raw + elif len(self.data) == self.size: + log.info( + "{} download respond original {}, XXX IGNORE found {}".format( + str(self), len(self.data), len(raw) + ) + ) + pass + else: + log.info( + "{} download respond extend original {} with found {}".format( + str(self), len(self.data), len(raw) + ) + ) + self.data.extend(raw) + self.responded = True + + code = 128 + descr = "Read History Data" + params = [] + retries = 2 + maxRecords = 16 + effectTime = 0.100 + data = bytearray() + + def getData(self): + data = self.data + # log.info("XXX: READ HISTORY DATA!!:\n%s" % lib.hexdump(data)) + return self.hexdump() + class ReadCurPageNumber(PumpCommand): - """ - """ - - code = 157 - descr = "Read Cur Page Number" - params = [ ] - retries = 2 - maxRecords = 1 - pages = 'unknown' - - def __str__(self): - return ':pages:'.join([self.__class__.__name__, str(self.pages) ]) - - def respond(self, data): - self.data = data - self.pages = self.getData( ) - self.responded = True - def getData(self): - data = self.data - log.info("XXX: READ cur page number:\n%s" % lib.hexdump(data)) - # MM12 does not support this command, but has 31 pages - # Thanks to @amazaheri - page = 32 - if len(data) == 1: - return int(data[0]) - if len(data) > 3: - page = lib.BangLong(data[0:4]) - # https://bitbucket.org/bewest/carelink/src/419fbf23495a/ddmsDTWApplet.src/minimed/ddms/deviceportreader/MMX15.java#cl-157 - if page <= 0 or page > 36: - page = 36 - return page + """ + """ + + code = 157 + descr = "Read Cur Page Number" + params = [] + retries = 2 + maxRecords = 1 + pages = "unknown" + + def __str__(self): + return ":pages:".join([self.__class__.__name__, str(self.pages)]) + + def respond(self, data): + self.data = data + self.pages = self.getData() + self.responded = True + + def getData(self): + data = self.data + log.info("XXX: READ cur page number:\n%s" % lib.hexdump(data)) + # MM12 does not support this command, but has 31 pages + # Thanks to @amazaheri + page = 32 + if len(data) == 1: + return int(data[0]) + if len(data) > 3: + page = lib.BangLong(data[0:4]) + # https://bitbucket.org/bewest/carelink/src/419fbf23495a/ddmsDTWApplet.src/minimed/ddms/deviceportreader/MMX15.java#cl-157 + if page <= 0 or page > 36: + page = 36 + return page # MMX22/ CMD_READ_CURRENT_GLUCOSE_HISTORY_PAGE_NUMBER 205 0xcd ('\xcd') OK class ReadCurGlucosePageNumber(PumpCommand): - """ - """ + """ + """ - code = 205 - descr = "Read Cur Glucose Page Number" - params = [ ] - retries = 2 - maxRecords = 1 + code = 205 + descr = "Read Cur Glucose Page Number" + params = [] + retries = 2 + maxRecords = 1 - def getData(self): - data = self.data - log.info("XXX: READ cur page number:\n%s" % lib.hexdump(data)) - if len(data) == 1: - return int(data[0]) - return dict(page= lib.BangLong(data[0:4]), glucose=data[5], isig=data[7]) + def getData(self): + data = self.data + log.info("XXX: READ cur page number:\n%s" % lib.hexdump(data)) + if len(data) == 1: + return int(data[0]) + return dict(page=lib.BangLong(data[0:4]), glucose=data[5], isig=data[7]) class ReadRTC(PumpCommand): - """ - """ - - code = 112 - descr = "Read RTC" - params = [ ] - retries = 2 - maxRecords = 1 - - - def getData(self): - data = self.data - d = { - 'hour' : int(data[0]), - 'minute': int(data[1]), - 'second': int(data[2]), - # XXX - 'year' : lib.BangInt([data[3], data[4]]), - 'month' : int(data[5]), - 'day' : int(data[6]), - } - return "{year:#04}-{month:#02}-{day:#02}T{hour:#02}:{minute:#02}:{second:#02}".format(**d) - -class SetRTC (PumpCommand): - """ - Set clock - """ - code = 64 - descr = "Set RTC" - retries = 2 - maxRecords = 0 - - __fields__ = PumpCommand.__fields__ + ['clock'] - def __init__(self, clock=None, **kwds): - params = kwds.get('params', [ ]) - self.clock = kwds.get('clock', None) - if len(params) == 0: - params.extend(SetRTC.fmt_datetime(clock)) - - kwds['params'] = params - super(SetRTC, self).__init__(**kwds) - @classmethod - def fmt_datetime (klass, dt): - return [dt.hour, dt.minute, dt.second, lib.HighByte(dt.year), lib.LowByte(dt.year), dt.month, dt.day] + """ + """ + + code = 112 + descr = "Read RTC" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + d = { + "hour": int(data[0]), + "minute": int(data[1]), + "second": int(data[2]), + # XXX + "year": lib.BangInt([data[3], data[4]]), + "month": int(data[5]), + "day": int(data[6]), + } + return "{year:#04}-{month:#02}-{day:#02}T{hour:#02}:{minute:#02}:{second:#02}".format( + **d + ) + + +class SetRTC(PumpCommand): + """ + Set clock + """ + + code = 64 + descr = "Set RTC" + retries = 2 + maxRecords = 0 + + __fields__ = PumpCommand.__fields__ + ["clock"] + + def __init__(self, clock=None, **kwds): + params = kwds.get("params", []) + self.clock = kwds.get("clock", None) + if len(params) == 0: + params.extend(SetRTC.fmt_datetime(clock)) + + kwds["params"] = params + super().__init__(**kwds) + + @classmethod + def fmt_datetime(klass, dt): + return [ + dt.hour, + dt.minute, + dt.second, + lib.HighByte(dt.year), + lib.LowByte(dt.year), + dt.month, + dt.day, + ] + class ReadPumpID(PumpCommand): - """ - """ + """ + """ - code = 113 - descr = "Read Pump ID" - params = [ ] - retries = 2 - maxRecords = 1 + code = 113 + descr = "Read Pump ID" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + return str(data[0:6]) - def getData(self): - data = self.data - return str(data[0:6]) class ReadBatteryStatus(PumpCommand): - """ - """ + """ + """ - code = 114 - descr = "Read Battery Status" - params = [ ] - retries = 2 - maxRecords = 1 + code = 114 + descr = "Read Battery Status" + params = [] + retries = 2 + maxRecords = 1 - def getData(self): - data = self.data - bd = bytearray(data) - volt = lib.BangInt((bd[1], bd[2])) - indicator = bd[0] - battery = {'status': {0: 'normal', 1: 'low'}[indicator], 'voltage': volt/100.0 } - return battery + def getData(self): + data = self.data + bd = bytearray(data) + volt = lib.BangInt((bd[1], bd[2])) + indicator = bd[0] + battery = { + "status": {0: "normal", 1: "low"}[indicator], + "voltage": volt / 100.0, + } + return battery class ReadFirmwareVersion(PumpCommand): - """ - """ + """ + """ + + code = 116 + descr = "Read Firmware Version" + params = [] + retries = 2 + maxRecords = 1 - code = 116 - descr = "Read Firmware Version" - params = [ ] - retries = 2 - maxRecords = 1 + def getData(self): + data = self.data + log.debug("READ FIRMWARE HEX:\n%s" % lib.hexdump(data)) + return str(data.split(chr(0x0B))[0]).strip() - def getData(self): - data = self.data - log.debug("READ FIRMWARE HEX:\n%s" % lib.hexdump(data)) - return str(data.split( chr(0x0b) )[0]).strip( ) class ReadRemainingInsulin(PumpCommand): - """ - """ + """ + """ - code = 115 - descr = "Read Remaining Insulin" - params = [ ] - retries = 2 - maxRecords = 1 - basalStrokes = 10.0 - startByte = 0 - endByte = 2 + code = 115 + descr = "Read Remaining Insulin" + params = [] + retries = 2 + maxRecords = 1 + basalStrokes = 10.0 + startByte = 0 + endByte = 2 - def getData(self): - data = self.data - log.info("READ remaining insulin:\n%s" % lib.hexdump(data)) - return lib.BangInt(data[self.startByte:self.endByte])/self.basalStrokes + def getData(self): + data = self.data + log.info("READ remaining insulin:\n%s" % lib.hexdump(data)) + return lib.BangInt(data[self.startByte : self.endByte]) / self.basalStrokes class ReadRemainingInsulin523(ReadRemainingInsulin): - """ - """ - - basalStrokes = 40.0 - startByte = 2 - endByte = 4 - - -class ReadBasalTemp508 (PumpCommand): - """ - """ - - code = 64 - descr = "Read Temp Basal 508 (old)" - params = [ ] - retries = 2 - maxRecords = 1 - - def getData(self): - data = self.data - rate = lib.BangInt(data[2:4])/40.0 - duration = lib.BangInt(data[4:6]) - log.info("READ temporary basal:\n%s" % lib.hexdump(data)) - return { 'rate': rate, 'duration': duration } - - -class ReadTodayTotals508 (PumpCommand): - """ - """ - - code = 65 - descr = "Read Totals Today" - params = [ ] - retries = 2 - maxRecords = 1 - - def getData(self): - data = self.data - log.info("READ totals today:\n%s" % lib.hexdump(data)) - totals = { - 'today': lib.BangInt(data[0:2]) / 10.0, - 'yesterday': lib.BangInt(data[2:4]) / 10.0 - } - return totals + """ + """ + + basalStrokes = 40.0 + startByte = 2 + endByte = 4 + + +class ReadBasalTemp508(PumpCommand): + """ + """ + + code = 64 + descr = "Read Temp Basal 508 (old)" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + rate = lib.BangInt(data[2:4]) / 40.0 + duration = lib.BangInt(data[4:6]) + log.info("READ temporary basal:\n%s" % lib.hexdump(data)) + return {"rate": rate, "duration": duration} + + +class ReadTodayTotals508(PumpCommand): + """ + """ + + code = 65 + descr = "Read Totals Today" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + log.info("READ totals today:\n%s" % lib.hexdump(data)) + totals = { + "today": lib.BangInt(data[0:2]) / 10.0, + "yesterday": lib.BangInt(data[2:4]) / 10.0, + } + return totals + # MMPump511/ ReadTotalsToday 121 0x79 ('y') OK class ReadTotalsToday(PumpCommand): - """ - """ - - code = 121 - descr = "Read Totals Today" - params = [ ] - retries = 2 - maxRecords = 1 - - def getData(self): - data = self.data - log.info("READ totals today:\n%s" % lib.hexdump(data)) - totals = { - 'today': lib.BangInt(data[0:2]) / 10.0, - 'yesterday': lib.BangInt(data[2:4]) / 10.0 - } - return totals + """ + """ + + code = 121 + descr = "Read Totals Today" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + log.info("READ totals today:\n%s" % lib.hexdump(data)) + totals = { + "today": lib.BangInt(data[0:2]) / 10.0, + "yesterday": lib.BangInt(data[2:4]) / 10.0, + } + return totals + # MMPump511/ ReadProfiles_STD 122 0x7a ('z') OK -class ReadProfiles511_STD (PumpCommand): - code = 122 +class ReadProfiles511_STD(PumpCommand): + code = 122 + + # MMPump511/ ReadProfiles_A 123 0x7b ('{') ?? -class ReadProfiles511_A (PumpCommand): - code = 123 +class ReadProfiles511_A(PumpCommand): + code = 123 + + # MMPump511/ ReadProfiles_B 124 0x7c ('|') ?? -class ReadProfiles511_B (PumpCommand): - code = 124 +class ReadProfiles511_B(PumpCommand): + code = 124 + + # MMPump???/ CMD_????? 125 0x7d ('}') ?? -class Model511_ExperimentOP125 (PumpCommand): - code = 125 +class Model511_ExperimentOP125(PumpCommand): + code = 125 + + # MMPump???/ CMD_????? 126 0x7e ('~') ?? -class Model511_ExperimentOP126 (PumpCommand): - code = 126 +class Model511_ExperimentOP126(PumpCommand): + code = 126 + + # MMPump511/ ReadSettings 127 0x7f DEL -class ReadSettings511 (PumpCommand): - code = 127 +class ReadSettings511(PumpCommand): + code = 127 + # MMX11/ CMD_ENABLE_DISABLE_DETAIL_TRACE 160 0x9f ('\x9f') ?? -class PumpTraceSelect (PumpCommand): - code = 160 +class PumpTraceSelect(PumpCommand): + code = 160 + + +class PumpEnableDetailTrace(PumpTraceSelect): + params = [1] + -class PumpEnableDetailTrace (PumpTraceSelect): - params = [ 1 ] +class PumpDisableDetailTrace(PumpTraceSelect): + params = [0] -class PumpDisableDetailTrace (PumpTraceSelect): - params = [ 0 ] -class Experiment_OP161 (PumpCommand): - code = 161 +class Experiment_OP161(PumpCommand): + code = 161 + + +class Experiment_OP162(PumpCommand): + code = 162 -class Experiment_OP162 (PumpCommand): - code = 162 # MMPump511/ ReadPumpTrace 163 0xa3 ('\xa3') ?? -class ReadPumpTrace (PumpCommand): - code = 163 - maxRecords = 16 +class ReadPumpTrace(PumpCommand): + code = 163 + maxRecords = 16 + + # MMPump511/ ReadDetailTrace 164 0xa4 ('\xa4') ?? -class ReadDetailTrace (PumpCommand): - code = 164 - maxRecords = 16 +class ReadDetailTrace(PumpCommand): + code = 164 + maxRecords = 16 + # MMPump11??/ CMD_???????????? 165 0xa5 0xa5 ?? -class Model511_Experiment_OP165 (PumpCommand): - code = 165 +class Model511_Experiment_OP165(PumpCommand): + code = 165 + # MMPump511/ ReadNewTraceAlarm 166 0xa6 ('\xa6') ?? -class ReadNewTraceAlarm (PumpCommand): - code = 166 - maxRecords = 16 +class ReadNewTraceAlarm(PumpCommand): + code = 166 + maxRecords = 16 + # MMPump511/ ReadOldTraceAlarm 167 0xa7 ('\xa7') ?? -class ReadOldTraceAlarm (PumpCommand): - maxRecords = 16 - code = 167 +class ReadOldTraceAlarm(PumpCommand): + maxRecords = 16 + code = 167 + # MMPump???/ CMD_????? 36 0x24 ('$') ?? -class PumpExperimentSelfCheck_OP36 (PumpCommand): - code = 36 +class PumpExperimentSelfCheck_OP36(PumpCommand): + code = 36 + # MMX22/ CMD_WRITE_GLUCOSE_HISTORY_TIMESTAMP 40 0x28 ('(') ?? -class WriteGlucoseHistoryTimestamp (PumpCommand): - code = 40 +class WriteGlucoseHistoryTimestamp(PumpCommand): + code = 40 + class ReadRadioCtrlACL(PumpCommand): - """ - """ + """ + """ + + code = 118 + descr = "Read Radio ACL" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + ids = [] + ids.append(str(data[0:6])) + ids.append(str(data[6:12])) + ids.append(str(data[12:18])) + log.info("READ radio ACL:\n%s" % lib.hexdump(data)) + return ids + + +class Model511_Experiment_OP119(PumpCommand): + code = 119 - code = 118 - descr = "Read Radio ACL" - params = [ ] - retries = 2 - maxRecords = 1 - def getData(self): - data = self.data - ids = [ ] - ids.append( str(data[0:6]) ) - ids.append( str(data[6:12]) ) - ids.append( str(data[12:18]) ) - log.info("READ radio ACL:\n%s" % lib.hexdump(data)) - return ids +class Model511_Experiment_OP120(PumpCommand): + code = 120 -class Model511_Experiment_OP119 (PumpCommand): - code = 119 -class Model511_Experiment_OP120 (PumpCommand): - code = 120 +class Model511_Experiment_OP121(PumpCommand): + code = 121 -class Model511_Experiment_OP121 (PumpCommand): - code = 121 -class Model511_Experiment_OP122 (PumpCommand): - code = 122 +class Model511_Experiment_OP122(PumpCommand): + code = 122 -class Model511_Experiment_OP123 (PumpCommand): - code = 123 -class Model511_Experiment_OP124 (PumpCommand): - code = 124 +class Model511_Experiment_OP123(PumpCommand): + code = 123 -class Model511_Experiment_OP125 (PumpCommand): - code = 125 -class Model511_Experiment_OP126 (PumpCommand): - code = 126 +class Model511_Experiment_OP124(PumpCommand): + code = 124 -class Model511_Experiment_OP127 (PumpCommand): - code = 127 -class Model511_Experiment_OP128 (PumpCommand): - code = 128 +class Model511_Experiment_OP125(PumpCommand): + code = 125 -class Model511_Experiment_OP129 (PumpCommand): - code = 129 -class Model511_Experiment_OP130 (PumpCommand): - code = 130 +class Model511_Experiment_OP126(PumpCommand): + code = 126 + + +class Model511_Experiment_OP127(PumpCommand): + code = 127 + + +class Model511_Experiment_OP128(PumpCommand): + code = 128 + + +class Model511_Experiment_OP129(PumpCommand): + code = 129 + + +class Model511_Experiment_OP130(PumpCommand): + code = 130 + # MMPump512/ CMD_READ_LANGUAGE 134 0x86 ('\x86') ?? -class ReadLanguage (PumpCommand): - code = 134 +class ReadLanguage(PumpCommand): + code = 134 + + # MMPump512/ CMD_READ_BOLUS_WIZARD_SETUP_STATUS 135 0x87 ('\x87') ?? -class ReadBolusWizardSetupStatus (PumpCommand): - code = 135 +class ReadBolusWizardSetupStatus(PumpCommand): + code = 135 + # MMPump512/ CMD_READ_CARB_UNITS 136 0x88 ('\x88') OK -class ReadCarbUnits (PumpCommand): - code = 136 - def getData (self): - labels = { 1 : 'grams', 2: 'exchanges' } - return dict(carb_units=labels.get(self.data[0], self.data[0])) +class ReadCarbUnits(PumpCommand): + code = 136 + + def getData(self): + labels = {1: "grams", 2: "exchanges"} + return dict(carb_units=labels.get(self.data[0], self.data[0])) + # MMPump512/ CMD_READ_BG_UNITS 137 0x89 ('\x89') ?? -class ReadBGUnits (PumpCommand): - code = 137 - def getData (self): - labels = { 1 : 'mg/dL', 2: 'mmol/L' } - return dict(bg_units=labels.get(self.data[0], self.data[0])) +class ReadBGUnits(PumpCommand): + code = 137 + + def getData(self): + labels = {1: "mg/dL", 2: "mmol/L"} + return dict(bg_units=labels.get(self.data[0], self.data[0])) + # MMPump512/ CMD_READ_CARB_RATIOS 138 0x8a ('\x8a') OK -class ReadCarbRatios512 (PumpCommand): - code = 138 - output_fields = ['units', 'schedule' ] - def getData (self): - # return self.model.decode_carb_ratios(self.data[:]) - units = self.data[0] - labels = { 1 : 'grams', 2: 'exchanges' } - fixed = self.data[1] - data = self.data[1:1+(8 *2)] - return dict(schedule=self.decode_ratios(data[0:], units=units), units=labels.get(units), first=self.data[0], raw=' '.join('0x{:02x}'.format(x) for x in self.data)) - - item_size = 2 - num_items = 8 - @classmethod - def decode_ratios (klass, data, units=0): - data = data[0:(8 *2)] - schedule = [ ] - for x in range(len(data)/ 2): - start = x * 2 - end = start + 2 - (i, r) = data[start:end] - if x > 0 and i == 0: - break - ratio = int(r) - if units == 2: - ratio = r / 10.0 - schedule.append(dict(x=x, i=i, start=lib.basal_time(i), offset=i*30, ratio=ratio, r=r)) - return schedule - -class ReadCarbRatios (PumpCommand): - code = 138 - item_size = 3 - num_items = 8 - output_fields = ['units', 'schedule' ] - def getData (self): - units = self.data[0] - labels = { 1 : 'grams', 2: 'exchanges' } - fixed = self.data[1] - data = self.data[2:2+(fixed *3)] - return dict(schedule=self.decode_ratios(data, units=units), units=labels.get(units), first=self.data[0]) - - @classmethod - def decode_ratios (klass, data, units=0): - schedule = [ ] - for x in range(len(data)/ 3): - start = x * 3 - end = start + 3 - (i, q, r) = data[start:end] - if x > 0 and i == 0: - break - ratio = r/10.0 - if q: - ratio = lib.BangInt([q, r]) / 1000.0 - schedule.append(dict(x=x, i=i, start=lib.basal_time(i), offset=i*30, q=q, ratio=ratio, r=r)) - return schedule +class ReadCarbRatios512(PumpCommand): + code = 138 + output_fields = ["units", "schedule"] + + def getData(self): + # return self.model.decode_carb_ratios(self.data[:]) + units = self.data[0] + labels = {1: "grams", 2: "exchanges"} + fixed = self.data[1] + data = self.data[1 : 1 + (8 * 2)] + return dict( + schedule=self.decode_ratios(data[0:], units=units), + units=labels.get(units), + first=self.data[0], + raw=" ".join("0x{:02x}".format(x) for x in self.data), + ) + + item_size = 2 + num_items = 8 + + @classmethod + def decode_ratios(klass, data, units=0): + data = data[0 : (8 * 2)] + schedule = [] + for x in range(len(data) // 2): + start = x * 2 + end = start + 2 + (i, r) = data[start:end] + if x > 0 and i == 0: + break + ratio = int(r) + if units == 2: + ratio = r / 10.0 + schedule.append( + dict(x=x, i=i, start=lib.basal_time(i), offset=i * 30, ratio=ratio, r=r) + ) + return schedule + + +class ReadCarbRatios(PumpCommand): + code = 138 + item_size = 3 + num_items = 8 + output_fields = ["units", "schedule"] + + def getData(self): + units = self.data[0] + labels = {1: "grams", 2: "exchanges"} + fixed = self.data[1] + data = self.data[2 : 2 + (fixed * 3)] + return dict( + schedule=self.decode_ratios(data, units=units), + units=labels.get(units), + first=self.data[0], + ) + + @classmethod + def decode_ratios(klass, data, units=0): + schedule = [] + for x in range(len(data) // 3): + start = x * 3 + end = start + 3 + (i, q, r) = data[start:end] + if x > 0 and i == 0: + break + ratio = r / 10.0 + if q: + ratio = lib.BangInt([q, r]) / 1000.0 + schedule.append( + dict( + x=x, + i=i, + start=lib.basal_time(i), + offset=i * 30, + q=q, + ratio=ratio, + r=r, + ) + ) + return schedule + # MMPump512/ CMD_READ_INSULIN_SENSITIVITIES 139 0x8b ('\x8b') OK -class ReadInsulinSensitivities (PumpCommand): - """ +class ReadInsulinSensitivities(PumpCommand): + """ >>> import json >>> sens = ReadInsulinSensitivities.decode(ReadInsulinSensitivities.resp_1) - >>> print json.dumps(sens) - {"units": "mg/dL", "sensitivities": [{"i": 0, "start": "00:00:00", "sensitivity": 45, "offset": 0, "x": 0}], "first": 1} + >>> print(json.dumps(sens, sort_keys=True)) + {"first": 1, "sensitivities": [{"i": 0, "offset": 0, "sensitivity": 45, "start": "00:00:00", "x": 0}], "units": "mg/dL"} >>> sens = ReadInsulinSensitivities.decode(ReadInsulinSensitivities.resp_uk_1) - >>> print json.dumps(sens) - {"units": "mmol/L", "sensitivities": [{"i": 0, "start": "00:00:00", "sensitivity": 2.2, "offset": 0, "x": 0}], "first": 2} + >>> print(json.dumps(sens, sort_keys=True)) + {"first": 2, "sensitivities": [{"i": 0, "offset": 0, "sensitivity": 2.2, "start": "00:00:00", "x": 0}], "units": "mmol/L"} >>> sens = ReadInsulinSensitivities.decode(ReadInsulinSensitivities.resp_high_bits) >>> sens == ReadInsulinSensitivities.resp_high_bit_broken @@ -1023,427 +1280,534 @@ class ReadInsulinSensitivities (PumpCommand): >>> sens == ReadInsulinSensitivities.resp_high_bit_fixed True + """ + + code = 139 + resp_1 = bytearray( + b"\x01\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ) + resp_uk_1 = bytearray( + codecs.decode( + ( + "02001600000000" + "00000000000000" + "00000000000000" + "00000000000000" + "00000000000000" + "00000000000000" + "00000000000000" + "00000000000000" + ), + "hex", + ) + ) + + # Thanks to Mitchell Slep. + resp_high_bits = bytearray( + codecs.decode( + ( + "01400c000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000" + "00000000" + ), + "hex", + ) + ) + resp_high_bit_broken = { + "units": "mg/dL", + "sensitivities": [ + {"i": 64, "start": "08:00:00", "sensitivity": 12, "offset": 1920, "x": 0} + ], + "first": 1, + } + resp_high_bit_fixed = { + "units": "mg/dL", + "sensitivities": [ + {"i": 0, "start": "00:00:00", "sensitivity": 268, "offset": 0, "x": 0} + ], + "first": 1, + } + + output_fields = ["units", "sensitivities"] + UNITS = {1: "mg/dL", 2: "mmol/L"} + + def getData(self): + return self.decode(self.data) + + @staticmethod + def decode(data): + units = data[0] + data = data[1 : 1 + 16] + schedule = [] + for x in range(8): + # Each entry in the sensitivity schedule consists of 2 bytes. + # The first byte is used as follows: + # - the highest bit is 0 + # - the next highest bit is an overflow bit that is set when the sensitivity is > 255 + # - the low 6 bits are an index from 0-47 representing the scheduled time + # The second byte is the insulin sensitivity. When the sensitivity is >255, the overflow bit above will be set and this byte will + # show the remainder (e.g. for 256 the overflow bit will be set and this byte will be 0). + start = x * 2 + i = data[start] & 0x3F + sensitivity_overflow = (data[start] & 0x40) << 2 + sensitivity = data[start + 1] + sensitivity_overflow + if x > 0 and i == 0: + break + if units == 2: + sensitivity = sensitivity / 10.0 + schedule.append( + dict( + x=x, + i=i, + start=str(lib.basal_time(i)), + offset=i * 30, + sensitivity=sensitivity, + ) + ) + return dict( + sensitivities=schedule, + first=units, + units=ReadInsulinSensitivities.UNITS.get(units), + ) - """ - - code = 139 - resp_1 = bytearray(b'\x01\x00-\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - resp_uk_1 = bytearray(str(""" -02001600000000 -00000000000000 -00000000000000 -00000000000000 -00000000000000 -00000000000000 -00000000000000 -00000000000000 -""".strip( ).replace("\n", "").decode('hex'))) - - # Thanks to Mitchell Slep. - resp_high_bits = bytearray(str(""" -01400c000000000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000000000000000 -00000000 -""".strip( ).replace("\n", "").decode('hex'))) - resp_high_bit_broken = {"units": "mg/dL", "sensitivities": [{"i": 64, "start": "08:00:00", "sensitivity": 12, "offset": 1920, "x": 0}], "first": 1} - resp_high_bit_fixed = {"units": "mg/dL", "sensitivities": [{"i": 0, "start": "00:00:00", "sensitivity": 268, "offset": 0, "x": 0}], "first": 1} - - - output_fields = ['units', 'sensitivities' ] - UNITS = { - 1: 'mg/dL', - 2: 'mmol/L' - } - - def getData (self): - return self.decode(self.data) - - @staticmethod - def decode(data): - units = data[0] - data = data[1:1+16] - schedule = [ ] - for x in range(8): - # Each entry in the sensitivity schedule consists of 2 bytes. - # The first byte is used as follows: - # - the highest bit is 0 - # - the next highest bit is an overflow bit that is set when the sensitivity is > 255 - # - the low 6 bits are an index from 0-47 representing the scheduled time - # The second byte is the insulin sensitivity. When the sensitivity is >255, the overflow bit above will be set and this byte will - # show the remainder (e.g. for 256 the overflow bit will be set and this byte will be 0). - start = x * 2 - i = data[start] & 0x3F - sensitivity_overflow = (data[start] & 0x40) << 2 - sensitivity = data[start+1] + sensitivity_overflow - if x > 0 and i == 0: - break - if units == 2: - sensitivity = sensitivity / 10.0 - schedule.append(dict(x=x, i=i, start=str(lib.basal_time(i)), offset=i*30, sensitivity=sensitivity)) - return dict(sensitivities=schedule, first=units, units=ReadInsulinSensitivities.UNITS.get(units)) # MMPump512/ CMD_READ_BG_TARGETS 140 0x8c ('\x8c') ?? -class ReadBGTargets (PumpCommand): - code = 140 -class ReadBGTargets515 (PumpCommand): - code = 159 - output_fields = ['units', 'targets' ] - def getData (self): - units = self.data[0] - labels = { 1 : 'mg/dL', 2: 'mmol/L' } - data = self.data[1:1+24] - schedule = [ ] - for x in range(8): - start = x * 3 - end = start + 3 - (i, low, high) = data[start:end] - if x > 0 and i == 0: - break - if units is 2: - low = low / 10.0 - high = high / 10.0 - schedule.append(dict(x=x, i=i, start=lib.basal_time(i), offset=i*30, low=low, high=high)) - return dict(targets=schedule, units=labels.get(units), first=self.data[0], raw=' '.join('0x{:02x}'.format(x) for x in self.data)) +class ReadBGTargets(PumpCommand): + code = 140 + + +class ReadBGTargets515(PumpCommand): + code = 159 + output_fields = ["units", "targets"] + + def getData(self): + units = self.data[0] + labels = {1: "mg/dL", 2: "mmol/L"} + data = self.data[1 : 1 + 24] + schedule = [] + for x in range(8): + start = x * 3 + end = start + 3 + (i, low, high) = data[start:end] + if x > 0 and i == 0: + break + if units == 2: + low = low / 10.0 + high = high / 10.0 + schedule.append( + dict( + x=x, i=i, start=lib.basal_time(i), offset=i * 30, low=low, high=high + ) + ) + return dict( + targets=schedule, + units=labels.get(units), + first=self.data[0], + raw=" ".join("0x{:02x}".format(x) for x in self.data), + ) + # MMPump512/ CMD_READ_BG_ALARM_CLOCKS 142 0x8e ('\x8e') ?? -class ReadBGAlarmCLocks (PumpCommand): - code = 142 +class ReadBGAlarmCLocks(PumpCommand): + code = 142 + + # MMPump512/ CMD_READ_RESERVOIR_WARNING 143 0x8f ('\x8f') ?? -class ReadReservoirWarning (PumpCommand): - code = 143 +class ReadReservoirWarning(PumpCommand): + code = 143 + + # MMPump512/ CMD_READ_BG_REMINDER_ENABLE 144 0x90 ('\x90') ?? -class ReadBGReminderEnable (PumpCommand): - code = 144 +class ReadBGReminderEnable(PumpCommand): + code = 144 + + # MMPump512/ CMD_READ_SETTINGS 145 0x91 ('\x91') ?? -class ReadSettings512 (PumpCommand): - code = 145 +class ReadSettings512(PumpCommand): + code = 145 + + # MMPump512/ CMD_READ_STD_PROFILES 146 0x92 ('\x92') ?? -class ReadProfile_STD512 (PumpCommand): - """ +class ReadProfile_STD512(PumpCommand): + """ >>> import json >>> schedule = ReadProfile_STD512.decode(ReadProfile_STD512._test_result_1) >>> len(schedule) 4 - >>> print json.dumps(schedule[0]) - {"i": 0, "start": "00:00:00", "rate": 0.8, "minutes": 0} - >>> print json.dumps(schedule[1]) - {"i": 1, "start": "06:30:00", "rate": 0.9500000000000001, "minutes": 390} - >>> print json.dumps(schedule[2]) - {"i": 2, "start": "09:30:00", "rate": 1.1, "minutes": 570} - >>> print json.dumps(schedule[3]) - {"i": 3, "start": "14:00:00", "rate": 0.9500000000000001, "minutes": 840} - - """ - _test_result_1 = bytearray([ - 32, 0, 0, - 38, 0, 13, - 44, 0, 19, - 38, 0, 28, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 - ]) - _test_schedule = {'total': 22.50, 'schedule': [ - { 'start': '12:00A', 'rate': 0.80 }, - { 'start': '6:30A', 'rate': 0.95 }, - { 'start': '9:30A', 'rate': 1.10 }, - { 'start': '2:00P', 'rate': 0.95 }, - ]} - code = 146 - maxRecords = 2 - output_fields = [ ] - def validate (self, data): - i = 0 - valid = True - last = None - for profile in data: - start = str(lib.basal_time(profile['minutes']/30)) - if 'rate' in profile and profile['i'] == i and start == profile['start']: - if i == 0: - if profile['minutes'] != 0: - template = "{name} first scheduled item should be 00:00:00." - msg = template.format(name=self.__class__.__name__) - msg = "%s\n%s" % (msg, profile) - raise BadResponse(msg) - if last and profile['minutes'] <= last['minutes']: - template = "{name} next scheduled item occurs before previous" - msg = template.format(name=self.__class__.__name__) - msg = "%s\n%s" % (msg, profile) - raise BadResponse(msg) - else: - bad_profile = """Current profile: %s""" % (profile) - template = """{bad_profile} Found in response to {name} - i: {i} matches? {matches_i} - our calcstart: {start} - profile start: {profile_start} - has a rate: {has_rate} - start matches: {matches_start} - """ - raise BadResponse(template.format(bad_profile=bad_profile - , name=self.__class__.__name__ - , start=start - , profile_start=profile['start'] - , has_rate= 'rate' in profile - , matches_i= profile['i'] == i - , matches_start= start == profile['start'] - , i=i)) - valid = False - last = profile - i = i + 1 - return True - @staticmethod - def decode (data): - i = 0 - schedule = [ ] - end = [ 0, 0, 0 ] - none = [ 0, 0, 0x3F ] - for i in xrange(len(data)/3): - off = i*3 - r, z, m = data[off : off + 3] - if i > 0 and [r,z,m] in [end, none]: - break - schedule.append(dict(i=i, minutes=m*30, start=str(lib.basal_time(m)), rate=r*.025)) - return schedule - def getData (self): - return self.decode(self.data) + >>> print(json.dumps(schedule[0], sort_keys=True)) + {"i": 0, "minutes": 0, "rate": 0.8, "start": "00:00:00"} + >>> print(json.dumps(schedule[1], sort_keys=True)) + {"i": 1, "minutes": 390, "rate": 0.9500000000000001, "start": "06:30:00"} + >>> print(json.dumps(schedule[2], sort_keys=True)) + {"i": 2, "minutes": 570, "rate": 1.1, "start": "09:30:00"} + >>> print(json.dumps(schedule[3], sort_keys=True)) + {"i": 3, "minutes": 840, "rate": 0.9500000000000001, "start": "14:00:00"} + + """ + + _test_result_1 = bytearray([32, 0, 0, 38, 0, 13, 44, 0, 19, 38, 0, 28] + [0] * 52) + _test_schedule = { + "total": 22.50, + "schedule": [ + {"start": "12:00A", "rate": 0.80}, + {"start": "6:30A", "rate": 0.95}, + {"start": "9:30A", "rate": 1.10}, + {"start": "2:00P", "rate": 0.95}, + ], + } + code = 146 + maxRecords = 2 + output_fields = [] + + def validate(self, data): + i = 0 + valid = True + last = None + for profile in data: + start = str(lib.basal_time(profile["minutes"] // 30)) + if "rate" in profile and profile["i"] == i and start == profile["start"]: + if i == 0: + if profile["minutes"] != 0: + template = "{name} first scheduled item should be 00:00:00." + msg = template.format(name=self.__class__.__name__) + msg = "{}\n{}".format(msg, profile) + raise BadResponse(msg) + if last and profile["minutes"] <= last["minutes"]: + template = "{name} next scheduled item occurs before previous" + msg = template.format(name=self.__class__.__name__) + msg = "{}\n{}".format(msg, profile) + raise BadResponse(msg) + else: + bad_profile = "Current profile: %s" % (profile) + template = textwrap.dedent("""\ + {bad_profile} Found in response to {name} + i: {i} matches? {matches_i} + our calcstart: {start} + profile start: {profile_start} + has a rate: {has_rate} + start matches: {matches_start} + """) + raise BadResponse( + template.format( + bad_profile=bad_profile, + name=self.__class__.__name__, + start=start, + profile_start=profile["start"], + has_rate="rate" in profile, + matches_i=profile["i"] == i, + matches_start=start == profile["start"], + i=i, + ) + ) + valid = False + last = profile + i = i + 1 + return True + + @staticmethod + def decode(data): + i = 0 + schedule = [] + end = [0, 0, 0] + none = [0, 0, 0x3F] + for i in range(len(data) // 3): + off = i * 3 + r, z, m = data[off : off + 3] + if i > 0 and [r, z, m] in [end, none]: + break + schedule.append( + dict(i=i, minutes=m * 30, start=str(lib.basal_time(m)), rate=r * 0.025) + ) + return schedule + + def getData(self): + return self.decode(self.data) + + # MMPump512/ CMD_READ_A_PROFILES 147 0x93 ('\x93') OK -class ReadProfile_A512 (ReadProfile_STD512): - code = 147 +class ReadProfile_A512(ReadProfile_STD512): + code = 147 + + # MMPump512/ CMD_READ_B_PROFILES 148 0x94 ('\x94') OK -class ReadProfile_B512 (ReadProfile_STD512): - code = 148 +class ReadProfile_B512(ReadProfile_STD512): + code = 148 + + # MMPump512/ CMD_READ_LOGIC_LINK_IDS 149 0x95 ('\x95') OK -class ReadLogicLinkIDS (PumpCommand): - code = 149 +class ReadLogicLinkIDS(PumpCommand): + code = 149 + # MMPump512??/ CMD_???????????????? 150 0x96 ('\x96') ?? -class Model512Experiment_OP150 (PumpCommand): - code = 150 +class Model512Experiment_OP150(PumpCommand): + code = 150 + # MMPump512/ CMD_READ_BG_ALARM_ENABLE 151 0x97 ('\x97') ?? -class ReadBGAlarmEnable (PumpCommand): - code = 151 +class ReadBGAlarmEnable(PumpCommand): + code = 151 + # MMPump512/ CMD_READ_TEMP_BASAL 152 0x98 ('\x98') OK class ReadBasalTemp(PumpCommand): - """ - MM511 - 120 - MM512 and up - opcode 152 - # strokes per basalunit = 40 - mm12, 10 in mm11 - """ - - code = 152 - descr = "Read Temp Basal" - params = [ ] - retries = 2 - maxRecords = 1 - - def getData(self): - data = self.data - temp = { 0: 'absolute', 1: 'percent' }[self.data[0]] - status = dict(temp=temp) - if temp is 'absolute': - rate = lib.BangInt(data[2:4])/40.0 - duration = lib.BangInt(data[4:6]) - status.update(rate=rate, duration=duration) - if temp is 'percent': - rate = int(data[1]) - duration = lib.BangInt(data[4:6]) - status.update(rate=rate, duration=duration) - log.info("READ temporary basal:\n%s" % lib.hexdump(data)) - return status + """ + MM511 - 120 + MM512 and up - opcode 152 + # strokes per basalunit = 40 - mm12, 10 in mm11 + """ + + code = 152 + descr = "Read Temp Basal" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + temp = {0: "absolute", 1: "percent"}[self.data[0]] + status = dict(temp=temp) + if temp == "absolute": + rate = lib.BangInt(data[2:4]) / 40.0 + duration = lib.BangInt(data[4:6]) + status.update(rate=rate, duration=duration) + if temp == "percent": + rate = int(data[1]) + duration = lib.BangInt(data[4:6]) + status.update(rate=rate, duration=duration) + log.info("READ temporary basal:\n%s" % lib.hexdump(data)) + return status + # MMGuardian3/ CMD_READ_SENSOR_SETTINGS 207 0xcf ('\xcf') ?? -class GuardianSensorSettings (PumpCommand): - code = 207 +class GuardianSensorSettings(PumpCommand): + code = 207 + + # MMGuardian3/ CMD_READ_SENSOR_PREDICTIVE_ALERTS 209 0xd1 ('\xd1') ?? -class GuardianSensorSettings (PumpCommand): - code = 209 +class GuardianSensorSettings(PumpCommand): + code = 209 + + # MMGuardian3/ CMD_READ_SENSOR_DEMO_AND_GRAPH_TIMEOUT 210 0xd2 ('\xd2') ?? -class GuardianSensorDemoGraphTimeout (PumpCommand): - code = 210 +class GuardianSensorDemoGraphTimeout(PumpCommand): + code = 210 + + # MMGuardian3/ CMD_READ_SENSOR_ALARM_SILENCE 211 0xd3 ('\xd3') ?? -class GuardianSensorAlarmSilence (PumpCommand): - code = 211 +class GuardianSensorAlarmSilence(PumpCommand): + code = 211 + + # MMGuardian3/ CMD_READ_SENSOR_RATE_OF_CHANGE_ALERTS 212 0xd4 ('\xd4') ?? -class GuardianSensorRateChangeAlerts (PumpCommand): - code = 212 +class GuardianSensorRateChangeAlerts(PumpCommand): + code = 212 + class ReadSettings(PumpCommand): - """ - XXX: changed in MM512 to 192 - - """ - - code = 192 - descr = "Read Settings" - params = [ ] - retries = 2 - maxRecords = 1 - - output_fields = ['maxBolus', 'maxBasal', 'insulin_action_curve' ] - byte_map = { - - } - - def alarm(self, alarm): - d = { 'volume': alarm, 'mode': 2 } - if alarm == 4: - d = { 'volume': -1, 'mode': 1 } - return d - def temp_basal_type(self, data): - temp = { 'type': data[0] == 1 and "Percent" or "Units/hour", - 'percent': data[1] - } - return temp - def getData(self): - data = self.data - log.info("READ pump settings:\n%s" % lib.hexdump(data)) - - if len(data) < 2: - log.info("pump settings: unsupported version, sorry") - return data - - auto_off_duration_hrs = data[0] - alarm = self.alarm(data[1]) - audio_bolus_enable = data[2] == 1 - audio_bolus_size = 0 - if audio_bolus_enable: - audio_bolus_size = data[3] / 10.0 - variable_bolus_enable = data[4] == 1 - #MM23 is different - maxBolus = data[5]/ 10.0 - # MM512 and up - maxBasal = lib.BangInt(data[6:8]) / 40.0 - timeformat = data[8] - insulinConcentration = {0: 100, 1: 50}[data[9]] - patterns_enabled = data[10] == 1 - selected_pattern = data[11] - rf_enable = data[12] == 1 - block_enable = data[13] == 1 - temp_basal = self.temp_basal_type(data[14:16]) - paradigm_enabled = data[16] - """ - # MM12 - insulin_action_type = data[17] == 0 and 'Fast' or 'Regular' - """ - #MM15 - # insulin_action_type = data[17] - insulin_action_curve = data[17] - low_reservoir_warn_type = data[18] - low_reservoir_warn_point = data[19] - keypad_lock_status = data[20] - - values = locals( ) - # safety - values.pop('self') - values.pop('data') - - return values + """ + XXX: changed in MM512 to 192 + + """ + + code = 192 + descr = "Read Settings" + params = [] + retries = 2 + maxRecords = 1 + + output_fields = ["maxBolus", "maxBasal", "insulin_action_curve"] + byte_map = {} + + def alarm(self, alarm): + d = {"volume": alarm, "mode": 2} + if alarm == 4: + d = {"volume": -1, "mode": 1} + return d + + def temp_basal_type(self, data): + temp = {"type": data[0] == 1 and "Percent" or "Units/hour", "percent": data[1]} + return temp + + def getData(self): + data = self.data + log.info("READ pump settings:\n%s" % lib.hexdump(data)) + + if len(data) < 2: + log.info("pump settings: unsupported version, sorry") + return data + + auto_off_duration_hrs = data[0] + alarm = self.alarm(data[1]) + audio_bolus_enable = data[2] == 1 + audio_bolus_size = 0 + if audio_bolus_enable: + audio_bolus_size = data[3] / 10.0 + variable_bolus_enable = data[4] == 1 + # MM23 is different + maxBolus = data[5] / 10.0 + # MM512 and up + maxBasal = lib.BangInt(data[6:8]) / 40.0 + timeformat = data[8] + insulinConcentration = {0: 100, 1: 50}[data[9]] + patterns_enabled = data[10] == 1 + selected_pattern = data[11] + rf_enable = data[12] == 1 + block_enable = data[13] == 1 + temp_basal = self.temp_basal_type(data[14:16]) + paradigm_enabled = data[16] + """ + # MM12 + insulin_action_type = data[17] == 0 and 'Fast' or 'Regular' + """ + # MM15 + # insulin_action_type = data[17] + insulin_action_curve = data[17] + low_reservoir_warn_type = data[18] + low_reservoir_warn_point = data[19] + keypad_lock_status = data[20] + + values = locals() + # safety + values.pop("self") + values.pop("data") + + return values + class ReadSettings523(ReadSettings): + def getData(self): + values = super().getData() + data = self.data - def getData(self): - values = super(ReadSettings523, self).getData() - data = self.data + values["maxBasal"] = lib.BangInt(data[7:9]) / 40.0 + values["maxBolus"] = data[6] / 10.0 - values['maxBasal'] = lib.BangInt(data[7:9]) / 40.0 - values['maxBolus'] = data[6]/ 10.0 + return values - return values # MMX15/ CMD_READ_SAVED_SETTINGS_DATE 193 0xc1 ('\xc1') ?? -class ReadSavedSettingsDate (PumpCommand): - code = 193 +class ReadSavedSettingsDate(PumpCommand): + code = 193 -class ReadContrast(PumpCommand): - """ - """ - code = 195 - descr = "Read Contrast" - params = [ ] - retries = 2 - maxRecords = 1 +class ReadContrast(PumpCommand): + """ + """ - def getData(self): - data = self.data - log.info("READ contrast:\n%s" % lib.hexdump(data)) - return data + code = 195 + descr = "Read Contrast" + params = [] + retries = 2 + maxRecords = 1 + def getData(self): + data = self.data + log.info("READ contrast:\n%s" % lib.hexdump(data)) + return data # MMX15/ CMD_READ_BOLUS_REMINDER_ENABLE 197 0xc5 ('\xc5') ?? -class ReadBolusReminderEnable (PumpCommand): - code = 197 +class ReadBolusReminderEnable(PumpCommand): + code = 197 + # MMX15/ CMD_READ_BOLUS_REMINDERS 198 0xc6 ('\xc6') ?? -class ReadBolusReminders (PumpCommand): - code = 198 +class ReadBolusReminders(PumpCommand): + code = 198 + # MMX15/ CMD_READ_FACTORY_PARAMETERS 199 0xc7 ('\xc7') ?? -class ReadFactoryParameters (PumpCommand): - code = 199 +class ReadFactoryParameters(PumpCommand): + code = 199 + class ReadPumpStatus(PumpCommand): - """ - """ - - - code = 206 - descr = "Read Pump Status" - params = [ ] - retries = 2 - maxRecords = 1 - def getData(self): - data = self.data - normal = { 03: 'normal' } - status = { 'status': normal.get(data[0], 'error'), - 'bolusing': data[1] == 1, - 'suspended': data[2] == 1 - } - return status + """ + """ + + code = 206 + descr = "Read Pump Status" + params = [] + retries = 2 + maxRecords = 1 + + def getData(self): + data = self.data + normal = {0o3: "normal"} + status = { + "status": normal.get(data[0], "error"), + "bolusing": data[1] == 1, + "suspended": data[2] == 1, + } + return status + class ReadPumpState(PumpCommand): - """ + """ >>> ReadPumpState(serial='665455').format() == ReadPumpState._test_ok True - """ - _test_ok = bytearray([ 0x01, 0x00, 0xA7, 0x01, 0x66, 0x54, 0x55, 0x80, - 0x00, 0x00, 0x02, 0x01, 0x00, 0x83, 0x2E, 0x00 ]) - - code = 131 - descr = "Read Pump State" - params = [ ] - retries = 2 - maxRecords = 1 + """ + _test_ok = bytearray( + [ + 0x01, + 0x00, + 0xA7, + 0x01, + 0x66, + 0x54, + 0x55, + 0x80, + 0x00, + 0x00, + 0x02, + 0x01, + 0x00, + 0x83, + 0x2E, + 0x00, + ] + ) + + code = 131 + descr = "Read Pump State" + params = [] + retries = 2 + maxRecords = 1 # MMX22/ CMD_READ_SENSOR_SETTINGS 153 0x99 ('\x99') ?? -class ReadSensorSettings (PumpCommand): - """ - """ - descr = "Read sensor settings" - code = 153 - params = [ ] - retries = 2 - -class ReadSensorHistoryData (ReadHistoryData): - def __init__(self, page=None, **kwds): - params = kwds.pop('params', [ ]) - if page is not None: - params = [ lib.LowByte(page >> 24), lib.LowByte(page >> 16), - lib.LowByte(page >> 8), lib.LowByte(page) ] - - self.page = page - super(ReadSensorHistoryData, self).__init__(params=params, **kwds) - self.params = params - self.page = page +class ReadSensorSettings(PumpCommand): + """ + """ + + descr = "Read sensor settings" + code = 153 + params = [] + retries = 2 + + +class ReadSensorHistoryData(ReadHistoryData): + def __init__(self, page=None, **kwds): + params = kwds.pop("params", []) + if page is not None: + params = [ + lib.LowByte(page >> 24), + lib.LowByte(page >> 16), + lib.LowByte(page >> 8), + lib.LowByte(page), + ] + + self.page = page + super().__init__(params=params, **kwds) + self.params = params + self.page = page + # MMX22/ CMD_READ_GLUCOSE_HISTORY 154 0x9a ('\x9a') ?? -class ReadGlucoseHistory (ReadSensorHistoryData): - """ +class ReadGlucoseHistory(ReadSensorHistoryData): + """ >>> ReadGlucoseHistory(page=1).params [0, 0, 0, 1] >>> list(ReadGlucoseHistory(page=1).format( )) @@ -1462,14 +1826,16 @@ class ReadGlucoseHistory (ReadSensorHistoryData): [2] >>> ReadGlucoseHistory(params=[3]).params [3] - """ - descr = "Read glucose history" - code = 154 - params = [ ] + """ + + descr = "Read glucose history" + code = 154 + params = [] + # MMX22/ CMD_READ_ISIG_HISTORY 155 0x9b ('\x9b') ?? -class ReadISIGHistory (ReadSensorHistoryData): - """ +class ReadISIGHistory(ReadSensorHistoryData): + """ >>> ReadISIGHistory(page=0).params [0, 0, 0, 0] @@ -1479,382 +1845,445 @@ class ReadISIGHistory (ReadSensorHistoryData): >>> ReadISIGHistory(page=2).params [0, 0, 0, 2] - """ - descr = "Read ISIG history" - code = 155 - params = [ ] - maxRecords = 32 + """ + + descr = "Read ISIG history" + code = 155 + params = [] + maxRecords = 32 + # MMX22/ CMD_READ_CALIBRATION_FACTOR 156 0x9c ('\x9c') ?? -class ReadCalibrationFactor (PumpCommand): - """ - """ - code = 156 +class ReadCalibrationFactor(PumpCommand): + """ + """ + + code = 156 + # MMX23/ CMD_READ_VCNTR_HISTORY 213 0xd5 ('\xd5') ?? -class ReadVCNTRHistory (ReadSensorHistoryData): - code = 213 +class ReadVCNTRHistory(ReadSensorHistoryData): + code = 213 + # MMX23/ CMD_READ_OTHER_DEVICES_IDS 240 0xf0 ('\xf0') ?? -class ReadOtherDevicesIDS (PumpCommand): - code = 240 +class ReadOtherDevicesIDS(PumpCommand): + code = 240 + +class ReadCaptureEventEnabled(PumpCommand): + code = 241 -class ReadCaptureEventEnabled (PumpCommand): - code = 241 +class ChangeCaptureEventEnable(PumpCommand): + code = 242 + params = [0] -class ChangeCaptureEventEnable (PumpCommand): - code = 242 - params = [0] + def __init__(self, enabled=True, **kwds): + self.params[0] = int(enabled) + super().__init__(**kwds) - def __init__(self, enabled=True, **kwds): - self.params[0] = int(enabled) - super(ChangeCaptureEventEnable, self).__init__(**kwds) +class ReadConnectDevicesOtherDevicesStatus(PumpCommand): + code = 243 -class ReadConnectDevicesOtherDevicesStatus (PumpCommand): - code = 243 +class FilterHistory(PumpCommand): + """ + + """ -class FilterHistory (PumpCommand): - """ + code = None + begin = None + end = None + __fields__ = PumpCommand.__fields__ + ["begin", "end"] - """ - code = None - begin = None - end = None - __fields__ = PumpCommand.__fields__ + ['begin', 'end'] + def __init__(self, begin=None, end=None, **kwds): + params = kwds.get("params", []) + if len(params) == 0: + params.extend(lib.format_filter_date(begin)) + params.extend(lib.format_filter_date(end)) - def __init__(self, begin=None, end=None, **kwds): - params = kwds.get('params', [ ]) - if len(params) == 0: - params.extend(lib.format_filter_date(begin)) - params.extend(lib.format_filter_date(end)) + kwds["params"] = params + super().__init__(**kwds) - kwds['params'] = params - super(FilterHistory, self).__init__(**kwds) + def getData(self): + data = self.data + if len(data) < 4: + return bytearray(data) + begin = lib.BangInt(data[0:2]) + end = lib.BangInt(data[2:4]) + return dict(begin=begin, end=end, params=self.params) - def getData(self): - data = self.data - if len(data) < 4: - return bytearray(data) - begin = lib.BangInt(data[0:2]) - end = lib.BangInt(data[2:4]) - return dict(begin=begin, end=end, params=self.params) + @classmethod + def ISO(klass, begin=None, end=None, **kwds): + return klass(begin=lib.parse.date(begin), end=lib.parse.date(end), **kwds) - @classmethod - def ISO (klass, begin=None, end=None, **kwds): - return klass(begin=lib.parse.date(begin), end=lib.parse.date(end), **kwds) # MMX22??/ CMD_FILTER_BG 168 0xa8 ('\xa8') ?? -class FilterGlucoseHistory (FilterHistory): - """ +class FilterGlucoseHistory(FilterHistory): + """ >>> FilterGlucoseHistory.ISO(begin='2014-04-13', end='2014-04-14').params [7, 222, 4, 13, 7, 222, 4, 14] - """ - code = 168 + """ + + code = 168 + # MMX22??/ CMD_FILTER_ISIG 169 0xa9 ('\xa9') ?? -class FilterISIGHistory (FilterHistory): - """ +class FilterISIGHistory(FilterHistory): + """ >>> FilterISIGHistory.ISO(begin='2014-04-13', end='2014-04-14').params [7, 222, 4, 13, 7, 222, 4, 14] - """ - code = 169 - -class TweakAnotherCommand (ManualCommand): - @classmethod - def get_kwds (klass, Other, args): - kwds = { } - fields = list(set(Other.__fields__) - set(['serial', ])) - for k in fields: - value = getattr(args, k, None) - if value is not None: - kwds[k] = value - return kwds - - @classmethod - def config_argparse (klass, parser): - parser.add_argument('--params', type=int, action="append", - help="parameters to format into sent message" - ) - parser.add_argument('--params_hexline', dest='params', type=lib.decode_hexline, - help="hex string, parameters to format into sent message" - # default=commands.ManualCommand.params - ) - parser.add_argument('--descr', type=str, - help="Description of command" - ) - parser.add_argument('--name', type=str, - help="Proposed name of command" - ) - parser.add_argument('--save', action="store_true", default=False, - help="Save response in a file." - ) - parser.add_argument('--effectTime', type=float, - help="time to sleep before responding to message, float in seconds" - ) - parser.add_argument('--maxRecords', type=int, - help="number of frames in a packet composing payload response" - ) - parser.add_argument('--bytesPerRecord', type=int, - help="bytes per frame" - ) - - parser.add_argument('--page', type=int, - help="Page to fetch (for ReadHistoryData)" - ) - - parser.add_argument('--begin', type=lib.parse.date, - help="begin date for FilterHistory" - ) - parser.add_argument('--end', type=lib.parse.date, - help="end date for FilterHistory" - ) - - - return parser + """ + + code = 169 + + +class TweakAnotherCommand(ManualCommand): + @classmethod + def get_kwds(klass, Other, args): + kwds = {} + fields = list(set(Other.__fields__) - {"serial"}) + for k in fields: + value = getattr(args, k, None) + if value is not None: + kwds[k] = value + return kwds + + @classmethod + def config_argparse(klass, parser): + parser.add_argument( + "--params", + type=int, + action="append", + help="parameters to format into sent message", + ) + parser.add_argument( + "--params_hexline", + dest="params", + type=lib.decode_hexline, + help="hex string, parameters to format into sent message" + # default=commands.ManualCommand.params + ) + parser.add_argument("--descr", type=str, help="Description of command") + parser.add_argument("--name", type=str, help="Proposed name of command") + parser.add_argument( + "--save", + action="store_true", + default=False, + help="Save response in a file.", + ) + parser.add_argument( + "--effectTime", + type=float, + help="time to sleep before responding to message, float in seconds", + ) + parser.add_argument( + "--maxRecords", + type=int, + help="number of frames in a packet composing payload response", + ) + parser.add_argument("--bytesPerRecord", type=int, help="bytes per frame") + + parser.add_argument( + "--page", type=int, help="Page to fetch (for ReadHistoryData)" + ) + + parser.add_argument( + "--begin", type=lib.parse.date, help="begin date for FilterHistory" + ) + parser.add_argument( + "--end", type=lib.parse.date, help="end date for FilterHistory" + ) + + return parser class ReadPumpModel(PumpCommand): - """ + """ >>> ReadPumpModel(serial='665455').format() == ReadPumpModel._test_ok True - """ - code = 141 - descr = "Read Pump Model Number" - params = [ ] - retries = 2 - maxRecords = 1 - _test_ok = bytearray([ 0x01, 0x00, 0xA7, 0x01, 0x66, 0x54, 0x55, 0x80, - 0x00, 0x00, 0x02, 0x01, 0x00, 0x8D, 0x5B, 0x00 ]) - - def getData(self): - data = self.data - if len(data) == 0: - return '' - length = data[0] - msg = data[1:1+length] - self.model = msg - return str(msg) + """ + + code = 141 + descr = "Read Pump Model Number" + params = [] + retries = 2 + maxRecords = 1 + _test_ok = bytearray( + [ + 0x01, + 0x00, + 0xA7, + 0x01, + 0x66, + 0x54, + 0x55, + 0x80, + 0x00, + 0x00, + 0x02, + 0x01, + 0x00, + 0x8D, + 0x5B, + 0x00, + ] + ) + + def getData(self): + data = self.data + if len(data) == 0: + return "" + length = data[0] + msg = data[1 : 1 + length] + self.model = msg + return str(msg) + def do_commands(device): - comm = ReadPumpModel( serial=device.serial ) - device.execute(comm) - log.info('comm:%s:data:%s' % (comm, getattr(comm.getData( ), 'data', None))) - log.info('REMOTE PUMP MODEL NUMBER: %s' % comm.getData( )) - - log.info("READ RTC") - comm = ReadRTC( serial=device.serial ) - device.execute(comm) - log.info('comm:RTC:%s' % (comm.getData( ))) - - log.info("READ PUMP ID") - comm = ReadPumpID( serial=device.serial ) - device.execute(comm) - log.info('comm:READ PUMP ID: ID: %s' % (comm.getData( ))) - - - log.info("Battery Status") - comm = ReadBatteryStatus( serial=device.serial ) - device.execute(comm) - log.info('comm:READ Battery Status: %r' % (comm.getData( ))) - - log.info("Firmware Version") - comm = ReadFirmwareVersion( serial=device.serial ) - device.execute(comm) - log.info('comm:READ Firmware Version: %r' % (comm.getData( ))) - - log.info("remaining insulin") - comm = ReadRemainingInsulin( serial=device.serial ) - device.execute(comm) - log.info('comm:READ Remaining Insulin: %r' % (comm.getData( ))) - - log.info("read totals today") - comm = ReadTotalsToday( serial=device.serial ) - device.execute(comm) - log.info('comm:READ totals today: %r' % (comm.getData( ))) - - log.info("read remote IDS") - comm = ReadRadioCtrlACL( serial=device.serial ) - device.execute(comm) - log.info('comm:READ radio ACL: %r' % (comm.getData( ))) - - log.info("read temporary basal") - comm = ReadBasalTemp( serial=device.serial ) - device.execute(comm) - log.info('comm:READ temp basal: %r' % (comm.getData( ))) - - log.info("read settings") - comm = ReadSettings( serial=device.serial ) - device.execute(comm) - log.info('comm:READ settings!: %r' % (comm.getData( ))) - - log.info("read contrast") - comm = ReadContrast( serial=device.serial ) - device.execute(comm) - log.info('comm:READ contrast: %r' % (comm.getData( ))) - - log.info("read cur page number") - comm = ReadCurPageNumber(serial=device.serial ) - device.execute(comm) - log.info('comm:READ page number!!!: %r' % (comm.getData( ))) - - log.info("read HISTORY DATA") - comm = ReadHistoryData(serial=device.serial, page=0) - device.execute(comm) - log.info('comm:READ history data page!!!:\n%s' % (comm.getData( ))) + comm = ReadPumpModel(serial=device.serial) + device.execute(comm) + log.info("comm:{}:data:{}".format(comm, getattr(comm.getData(), "data", None))) + log.info("REMOTE PUMP MODEL NUMBER: %s" % comm.getData()) + + log.info("READ RTC") + comm = ReadRTC(serial=device.serial) + device.execute(comm) + log.info("comm:RTC:%s" % (comm.getData())) + + log.info("READ PUMP ID") + comm = ReadPumpID(serial=device.serial) + device.execute(comm) + log.info("comm:READ PUMP ID: ID: %s" % (comm.getData())) + + log.info("Battery Status") + comm = ReadBatteryStatus(serial=device.serial) + device.execute(comm) + log.info("comm:READ Battery Status: %r" % (comm.getData())) + + log.info("Firmware Version") + comm = ReadFirmwareVersion(serial=device.serial) + device.execute(comm) + log.info("comm:READ Firmware Version: %r" % (comm.getData())) + + log.info("remaining insulin") + comm = ReadRemainingInsulin(serial=device.serial) + device.execute(comm) + log.info("comm:READ Remaining Insulin: %r" % (comm.getData())) + + log.info("read totals today") + comm = ReadTotalsToday(serial=device.serial) + device.execute(comm) + log.info("comm:READ totals today: %r" % (comm.getData())) + + log.info("read remote IDS") + comm = ReadRadioCtrlACL(serial=device.serial) + device.execute(comm) + log.info("comm:READ radio ACL: %r" % (comm.getData())) + + log.info("read temporary basal") + comm = ReadBasalTemp(serial=device.serial) + device.execute(comm) + log.info("comm:READ temp basal: %r" % (comm.getData())) + + log.info("read settings") + comm = ReadSettings(serial=device.serial) + device.execute(comm) + log.info("comm:READ settings!: %r" % (comm.getData())) + + log.info("read contrast") + comm = ReadContrast(serial=device.serial) + device.execute(comm) + log.info("comm:READ contrast: %r" % (comm.getData())) + + log.info("read cur page number") + comm = ReadCurPageNumber(serial=device.serial) + device.execute(comm) + log.info("comm:READ page number!!!: %r" % (comm.getData())) + + log.info("read HISTORY DATA") + comm = ReadHistoryData(serial=device.serial, page=0) + device.execute(comm) + log.info("comm:READ history data page!!!:\n%s" % (comm.getData())) + def get_pages(device): - log.info("read cur page number") - comm = ReadCurPageNumber(serial=device.serial ) - device.execute(comm) - pages = comm.getData( ) - log.info('attempting to read %s pages of history' % pages) - - for x in range(pages + 1): - log.info('comm:READ HISTORY DATA page number: %r' % (x)) - comm = ReadHistoryData(serial=device.serial, params=[ x ] ) + log.info("read cur page number") + comm = ReadCurPageNumber(serial=device.serial) device.execute(comm) - page = comm.getData( ) - log.info("XXX: READ HISTORY DATA!!:\n%s" % lib.hexdump(page)) - time.sleep(.100) + pages = comm.getData() + log.info("attempting to read %s pages of history" % pages) + + for x in range(pages + 1): + log.info("comm:READ HISTORY DATA page number: %r" % (x)) + comm = ReadHistoryData(serial=device.serial, params=[x]) + device.execute(comm) + page = comm.getData() + log.info("XXX: READ HISTORY DATA!!:\n%s" % lib.hexdump(page)) + time.sleep(0.100) + __all__ = [ - 'BaseCommand', 'KeypadPush', 'PowerControl', 'PowerControlOff', - 'PumpCommand', 'PumpResume', 'PumpSuspend', - 'ReadBasalTemp', 'ReadBatteryStatus', 'ReadContrast', - 'ReadCurPageNumber', 'ReadErrorStatus', 'ReadFirmwareVersion', - 'ReadGlucoseHistory', 'ReadHistoryData', 'ReadPumpID', - 'ReadPumpModel', 'ReadPumpState', 'ReadPumpStatus', - 'ReadRTC', 'ReadRadioCtrlACL', 'ReadRemainingInsulin', - 'ReadRemainingInsulin523', - 'ReadSettings', 'ReadSettings523', 'ReadTotalsToday', 'SetSuspend', - 'PushEASY', 'PushUP', 'PushDOWN', 'PushACT', 'PushESC', - 'TempBasal', 'ManualCommand', 'ReadCurGlucosePageNumber', - 'SetAutoOff', - 'SetEnabledEasyBolus', - 'SetBasalType', - 'TempBasalPercent', - 'Bolus', - 'ReadErrorStatus508', - 'ReadBolusHistory', - 'ReadDailyTotals', - 'ReadPrimeBoluses', - 'ReadAlarms', - 'ReadProfileSets', - 'ReadUserEvents', - 'ReadRemoteControlID', - 'Read128KMem', - 'Read256KMem', - 'ReadBasalTemp508', - 'ReadTodayTotals508', - 'ReadSensorSettings', - 'ReadSensorHistoryData', - 'ReadISIGHistory', - 'FilterHistory', - 'FilterGlucoseHistory', - 'FilterISIGHistory', - - 'ReadProfiles511_STD', - 'ReadProfiles511_A', - 'ReadProfiles511_B', - 'Model511_ExperimentOP125', - 'Model511_ExperimentOP126', - 'ReadSettings511', - 'ReadPumpTrace', - 'ReadDetailTrace', - 'Model511_Experiment_OP165', - 'ReadNewTraceAlarm', - 'ReadOldTraceAlarm', - 'WriteGlucoseHistoryTimestamp', - 'ReadLanguage', - 'ReadBolusWizardSetupStatus', - 'ReadCarbUnits', - 'ReadBGUnits', - 'ReadCarbRatios', - 'ReadCarbRatios512', - 'ReadInsulinSensitivities', - 'ReadBGTargets', - 'ReadBGTargets515', - 'ReadBGAlarmCLocks', - 'ReadReservoirWarning', - 'ReadBGReminderEnable', - 'ReadSettings512', - 'ReadProfile_STD512', - 'ReadProfile_A512', - 'ReadProfile_B512', - 'ReadLogicLinkIDS', - 'Model512Experiment_OP150', - 'ReadBGAlarmEnable', - 'GuardianSensorSettings', - 'GuardianSensorSettings', - 'GuardianSensorDemoGraphTimeout', - 'GuardianSensorAlarmSilence', - 'GuardianSensorRateChangeAlerts', - 'ReadSavedSettingsDate', - 'ReadBolusReminderEnable', - 'ReadBolusReminders', - 'ReadFactoryParameters', - 'ReadCalibrationFactor', - 'ReadVCNTRHistory', - 'ReadOtherDevicesIDS', - 'PumpTraceSelect', - 'PumpEnableDetailTrace', - 'PumpDisableDetailTrace', - 'Experiment_OP161', - 'Experiment_OP162', - 'Model511_Experiment_OP119', - 'Model511_Experiment_OP120', - 'Model511_Experiment_OP121', - 'Model511_Experiment_OP122', - 'Model511_Experiment_OP123', - 'Model511_Experiment_OP124', - 'Model511_Experiment_OP125', - 'Model511_Experiment_OP126', - 'Model511_Experiment_OP127', - 'Model511_Experiment_OP128', - 'Model511_Experiment_OP129', - 'Model511_Experiment_OP130', - 'SelectBasalProfile', - 'SelectBasalProfileSTD', - 'SelectBasalProfileA', - 'SelectBasalProfileB', - 'PumpExperiment_OP69', - 'PumpExperiment_OP70', - 'PumpExperiment_OP71', - 'PumpExperiment_OP72', - 'PumpExperiment_OP73', - 'PumpExperiment_OP75', + "BaseCommand", + "KeypadPush", + "PowerControl", + "PowerControlOff", + "PumpCommand", + "PumpResume", + "PumpSuspend", + "ReadBasalTemp", + "ReadBatteryStatus", + "ReadContrast", + "ReadCurPageNumber", + "ReadErrorStatus", + "ReadFirmwareVersion", + "ReadGlucoseHistory", + "ReadHistoryData", + "ReadPumpID", + "ReadPumpModel", + "ReadPumpState", + "ReadPumpStatus", + "ReadRTC", + "ReadRadioCtrlACL", + "ReadRemainingInsulin", + "ReadRemainingInsulin523", + "ReadSettings", + "ReadSettings523", + "ReadTotalsToday", + "SetSuspend", + "PushEASY", + "PushUP", + "PushDOWN", + "PushACT", + "PushESC", + "TempBasal", + "ManualCommand", + "ReadCurGlucosePageNumber", + "SetAutoOff", + "SetEnabledEasyBolus", + "SetBasalType", + "TempBasalPercent", + "Bolus", + "ReadErrorStatus508", + "ReadBolusHistory", + "ReadDailyTotals", + "ReadPrimeBoluses", + "ReadAlarms", + "ReadProfileSets", + "ReadUserEvents", + "ReadRemoteControlID", + "Read128KMem", + "Read256KMem", + "ReadBasalTemp508", + "ReadTodayTotals508", + "ReadSensorSettings", + "ReadSensorHistoryData", + "ReadISIGHistory", + "FilterHistory", + "FilterGlucoseHistory", + "FilterISIGHistory", + "ReadProfiles511_STD", + "ReadProfiles511_A", + "ReadProfiles511_B", + "Model511_ExperimentOP125", + "Model511_ExperimentOP126", + "ReadSettings511", + "ReadPumpTrace", + "ReadDetailTrace", + "Model511_Experiment_OP165", + "ReadNewTraceAlarm", + "ReadOldTraceAlarm", + "WriteGlucoseHistoryTimestamp", + "ReadLanguage", + "ReadBolusWizardSetupStatus", + "ReadCarbUnits", + "ReadBGUnits", + "ReadCarbRatios", + "ReadCarbRatios512", + "ReadInsulinSensitivities", + "ReadBGTargets", + "ReadBGTargets515", + "ReadBGAlarmCLocks", + "ReadReservoirWarning", + "ReadBGReminderEnable", + "ReadSettings512", + "ReadProfile_STD512", + "ReadProfile_A512", + "ReadProfile_B512", + "ReadLogicLinkIDS", + "Model512Experiment_OP150", + "ReadBGAlarmEnable", + "GuardianSensorSettings", + "GuardianSensorSettings", + "GuardianSensorDemoGraphTimeout", + "GuardianSensorAlarmSilence", + "GuardianSensorRateChangeAlerts", + "ReadSavedSettingsDate", + "ReadBolusReminderEnable", + "ReadBolusReminders", + "ReadFactoryParameters", + "ReadCalibrationFactor", + "ReadVCNTRHistory", + "ReadOtherDevicesIDS", + "PumpTraceSelect", + "PumpEnableDetailTrace", + "PumpDisableDetailTrace", + "Experiment_OP161", + "Experiment_OP162", + "Model511_Experiment_OP119", + "Model511_Experiment_OP120", + "Model511_Experiment_OP121", + "Model511_Experiment_OP122", + "Model511_Experiment_OP123", + "Model511_Experiment_OP124", + "Model511_Experiment_OP125", + "Model511_Experiment_OP126", + "Model511_Experiment_OP127", + "Model511_Experiment_OP128", + "Model511_Experiment_OP129", + "Model511_Experiment_OP130", + "SelectBasalProfile", + "SelectBasalProfileSTD", + "SelectBasalProfileA", + "SelectBasalProfileB", + "PumpExperiment_OP69", + "PumpExperiment_OP70", + "PumpExperiment_OP71", + "PumpExperiment_OP72", + "PumpExperiment_OP73", + "PumpExperiment_OP75", ] -if __name__ == '__main__': - import doctest - doctest.testmod( ) - - import sys - port = None - port = sys.argv[1:] and sys.argv[1] or False - serial_num = sys.argv[2:] and sys.argv[2] or False - if not port or not serial_num: - print "usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0] - sys.exit(1) - import link - import stick - import session - from pprint import pformat - logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - log.info("howdy! I'm going to take a look at your pump and grab lots of info.") - stick = stick.Stick(link.Link(port, timeout=.400)) - stick.open( ) - session = session.Pump(stick, serial_num) - log.info(pformat(stick.interface_stats( ))) - log.info('PUMP MODEL: %s' % session.read_model( )) - do_commands(session) - log.info(pformat(stick.interface_stats( ))) - #get_pages(session) - #log.info(pformat(stick.interface_stats( ))) - log.info("howdy! we downloaded a lot of pump info successfully.") - # stick.open( ) +if __name__ == "__main__": + import doctest + + doctest.testmod() + + import sys + + port = None + port = sys.argv[1:] and sys.argv[1] or False + serial_num = sys.argv[2:] and sys.argv[2] or False + if not port or not serial_num: + print("usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0]) + sys.exit(1) + from pprint import pformat + + from decocare import link, session, stick + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + log.info("howdy! I'm going to take a look at your pump and grab lots of info.") + stick = stick.Stick(link.Link(port, timeout=0.400)) + stick.open() + session = session.Pump(stick, serial_num) + log.info(pformat(stick.interface_stats())) + log.info("PUMP MODEL: %s" % session.read_model()) + do_commands(session) + log.info(pformat(stick.interface_stats())) + # get_pages(session) + # log.info(pformat(stick.interface_stats( ))) + log.info("howdy! we downloaded a lot of pump info successfully.") + # stick.open( ) diff --git a/decocare/download.py b/decocare/download.py index 52b81bb..0b1757d 100644 --- a/decocare/download.py +++ b/decocare/download.py @@ -1,90 +1,95 @@ - -import time import logging -log = logging.getLogger( ).getChild(__name__) +import time -import lib -import commands +log = logging.getLogger().getChild(__name__) -class Downloader(object): - log_format = 'logs/' - def __init__(self, stick=None, device=None, log_format=log_format): - self.stick = stick - stick.open( ) - self.device = device - self.log_format = log_format +from decocare import commands, lib - def download(self): - """ + +class Downloader: + log_format = "logs/" + + def __init__(self, stick=None, device=None, log_format=log_format): + self.stick = stick + stick.open() + self.device = device + self.log_format = log_format + + def download(self): + """ Download a single page, copy paste from elsewhere. """ - log.info("read HISTORY DATA") - comm = commands.ReadHistoryData(serial=self.device.serial, page=0) - self.device.execute(comm) - log.info('comm:READ history data page!!!:\n%s' % (comm.getData( ))) - comm.save(prefix=self.log_format) + log.info("read HISTORY DATA") + comm = commands.ReadHistoryData(serial=self.device.serial, page=0) + self.device.execute(comm) + log.info("comm:READ history data page!!!:\n%s" % (comm.getData())) + comm.save(prefix=self.log_format) + class PageDownloader(Downloader): - log_format = 'logs/' - def __init__(self, stick=None, device=None, log_format=log_format): - self.stick = stick - stick.open( ) - self.device = device - self.log_format = log_format - - def read_current(self): - - log.info("read cur page number") - comm = commands.ReadCurPageNumber( ) - self.device.execute(comm) - self.pages = comm.getData( ) - log.info('attempting to read %s pages of history' % self.pages) - return self.pages - - - def download_page(self, x): - log.info('comm:XXX:READ HISTORY DATA page number: %r' % (x)) - comm = commands.ReadHistoryData(serial=self.device.serial, params=[ x ] ) - self.device.execute(comm) - page = comm.getData( ) - comm.save(prefix=self.log_format) - log.info("XXX: READ HISTORY DATA page number %r!!:\n%s" % (x, page)) - time.sleep(.100) - - def download(self): - self.read_current( ) - for x in range(self.pages + 1): - log.info("read page %s" % x) - self.download_page(x) - -if __name__ == '__main__': - import doctest - doctest.testmod( ) - - import sys - port = None - port = sys.argv[1:] and sys.argv[1] or False - serial_num = sys.argv[2:] and sys.argv[2] or False - if not port or not serial_num: - print "usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0] - sys.exit(1) - import link - import stick - import session - from pprint import pformat - logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - log.info("howdy! I'm going to take a look at your pump download something info.") - stick = stick.Stick(link.Link(port, timeout=.400)) - stick.open( ) - session = session.Pump(stick, serial_num) - log.info(pformat(stick.interface_stats( ))) - - downloader = Downloader(stick, session) - downloader.download( ) - - downloader = PageDownloader(stick, session) - downloader.download( ) - - log.info(pformat(stick.interface_stats( ))) - log.info("howdy! we downloaded everything.") + log_format = "logs/" + + def __init__(self, stick=None, device=None, log_format=log_format): + self.stick = stick + stick.open() + self.device = device + self.log_format = log_format + + def read_current(self): + + log.info("read cur page number") + comm = commands.ReadCurPageNumber() + self.device.execute(comm) + self.pages = comm.getData() + log.info("attempting to read %s pages of history" % self.pages) + return self.pages + + def download_page(self, x): + log.info("comm:XXX:READ HISTORY DATA page number: %r" % (x)) + comm = commands.ReadHistoryData(serial=self.device.serial, params=[x]) + self.device.execute(comm) + page = comm.getData() + comm.save(prefix=self.log_format) + log.info("XXX: READ HISTORY DATA page number {!r}!!:\n{}".format(x, page)) + time.sleep(0.100) + + def download(self): + self.read_current() + for x in range(self.pages + 1): + log.info("read page %s" % x) + self.download_page(x) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + import sys + + port = None + port = sys.argv[1:] and sys.argv[1] or False + serial_num = sys.argv[2:] and sys.argv[2] or False + if not port or not serial_num: + print("usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0]) + sys.exit(1) + from pprint import pformat + + from decocare import link, session, stick + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + log.info("howdy! I'm going to take a look at your pump download something info.") + stick = stick.Stick(link.Link(port, timeout=0.400)) + stick.open() + session = session.Pump(stick, serial_num) + log.info(pformat(stick.interface_stats())) + + downloader = Downloader(stick, session) + downloader.download() + + downloader = PageDownloader(stick, session) + downloader.download() + + log.info(pformat(stick.interface_stats())) + log.info("howdy! we downloaded everything.") diff --git a/decocare/errors.py b/decocare/errors.py index 4361938..5e97534 100644 --- a/decocare/errors.py +++ b/decocare/errors.py @@ -1,11 +1,18 @@ +class StickError(Exception): + pass -class StickError(Exception): pass -class AckError(StickError): pass +class AckError(StickError): + pass -class BadDeviceCommError(AckError): pass -class DataTransferCorruptionError(Exception): pass +class BadDeviceCommError(AckError): + pass + + +class DataTransferCorruptionError(Exception): + pass + ##### # EOF diff --git a/decocare/fuser.py b/decocare/fuser.py index cde7660..5308c87 100644 --- a/decocare/fuser.py +++ b/decocare/fuser.py @@ -1,19 +1,20 @@ - import sys -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen + + +def in_use(device): + if "windows" in sys.platform: + # TODO: use Handle + # http://stackoverflow.com/questions/18059798/windows-batch-equivalent-of-fuser-k-folder + # https://technet.microsoft.com/en-us/sysinternals/bb896655 + return False + pipe = Popen(["fuser", device], stdout=PIPE, stderr=PIPE) + stdout, stderr = pipe.communicate() + return stdout != "" -def in_use (device): - if 'windows' in sys.platform: - # TODO: use Handle - # http://stackoverflow.com/questions/18059798/windows-batch-equivalent-of-fuser-k-folder - # https://technet.microsoft.com/en-us/sysinternals/bb896655 - return False - pipe = Popen(['fuser', device], stdout=PIPE, stderr=PIPE) - stdout, stderr = pipe.communicate( ) - return stdout is not "" -if __name__ == '__main__': - from scan import scan - candidate = (sys.argv[1:2] or [scan( )]).pop( ) - print in_use(candidate) +if __name__ == "__main__": + from decocare.scan import scan + candidate = (sys.argv[1:2] or [scan()]).pop() + print(in_use(candidate)) diff --git a/decocare/helpers/cli.py b/decocare/helpers/cli.py index 13a212c..4cf9581 100644 --- a/decocare/helpers/cli.py +++ b/decocare/helpers/cli.py @@ -1,220 +1,253 @@ -import sys, os +import argparse import logging +import os +import sys import time -import argparse from pprint import pformat -log = logging.getLogger( ).getChild(__name__) - -from decocare import link, stick, session, commands, lib, scan - -class CommandApp(object): - def __init__(self): - self.env = self.parse_env( ) - self.parser = self.get_parser( ) - self.autocomplete( ) - - @classmethod - def parse_env (klass): - return { - "serial": os.environ.get('SERIAL', ''), - "port": os.environ.get('PORT', '') - } - - def autocomplete (self): - try: - import argcomplete - argcomplete.autocomplete(self.parser) - except ImportError: - # no auto completion - pass - - def help (self): - return self.__class__.__doc__ - - def get_parser (self): - helptext = self.help( ).split("\n") - description = '\n'.join(helptext[0:2]) - epilog = '\n'.join(helptext[2:]) - parser = argparse.ArgumentParser(description=description, epilog=epilog) - parser.add_argument('--serial', type=str, - dest='serial', - default=self.env.get('serial', ''), - help="serial number of pump [default: %(default)s]") - parser.add_argument('--port', type=str, - dest='port', - default=(self.env.get('port') or 'scan'), - help="Path to device [default: %(default)s]") - parser.add_argument('--no-op', - dest='dryrun', - action='store_true', default=False, - help="Dry run, don't do main function" - ) - parser.add_argument('--skip-prelude', - dest='no_prelude', - action='store_true', default=False, - help="Don't do the normal prelude." - ) - parser.add_argument('--no-rf-prelude', - dest='no_rf_prelude', - action='store_true', default=False, - help="Do the prelude, but don't query the pump." - ) - parser.add_argument('--skip-postlude', - dest='no_postlude', - action='store_true', default=False, - help="Don't do the normal postlude." - ) - parser.add_argument('-v', '--verbose', - dest='verbose', - action='append_const', const=1, - help="Verbosity" - ) - parser.add_argument('--rf-minutes', - dest='session_life', - type=int, default=10, - help="How long RF sessions should last" - ) - parser.add_argument('--auto-init', - dest='autoinit', - action='store_true', default=False, - help="Send power ctrl to initialize RF session." - ) - parser.add_argument('--init', - dest='init', - action='store_true', default=False, - help="Send power ctrl to initialize RF session." - ) - parser = self.customize_parser(parser) - return parser - - def customize_parser (self, parser): - parser.add_argument('commands', - nargs="+", - #dest='status', - choices=['act', 'esc', 'up', 'down', 'easy' ], - default='act', - help="buttons to press [default: %(default)s)]" - ) - return parser - - def configure (self): - args = self.parser.parse_args( ) - level = None - if args.verbose > 0: - level = args.verbose > 1 and logging.DEBUG or logging.INFO - logging.basicConfig(stream=sys.stdout, level=level) - self.args = args - return args - - def run (self, args): - if not args: - args = self.configure( ) - self.prelude(args) - self.main(args) - self.postlude(args) - - def prelude (self, args): - if args.no_prelude: - print "##### skipping prelude" - return - print "## do stuff with an insulin pump over RF" - print "using", "`", args, "`" - - print "```" - if args.port == 'scan' or args.port == "": - args.port = scan.scan( ) - uart = stick.Stick(link.Link(args.port)) - print "```" - print "```" - uart.open( ) - print "```" - print "```" - pump = session.Pump(uart, args.serial) - print "```" - print "```" - stats = uart.interface_stats( ) - print "```" - print "```javascript" - print pformat(stats) - print "```" - self.uart = uart - self.pump = pump - self.model = None - if args.no_rf_prelude: - print "##### skipping normal RF preludes" - return - print "```" - if not args.autoinit: - if args.init: - pump.power_control(minutes=args.session_life) - model = pump.read_model( ) - self.model = model - else: - self.autoinit(args) - print "```" - print '### PUMP MODEL: `%s`' % self.model - - def autoinit (self, args): - for n in xrange(3): - print "AUTO INIT", n - self.sniff_model( ) - if len(self.model.getData( )) != 3: - self.stats - print "SENDING POWER ON", n - self.pump.power_control(minutes=args.session_life) - else: - print '### PUMP MODEL: `%s`' % self.model - break - def sniff_model (self): - model = self.pump.read_model( ) - print "GOT MODEL", model - self.model = model - def postlude (self, args): - if args.no_postlude: - print "##### skipping postlude" - return - print "### end stats" - self.stats( ) - def stats (self): - print "```" - stats = self.uart.interface_stats( ) - print "```" - print "```javascript" - print pformat(stats) - print "```" - - def main (self, args): - for flow in args.commands: - print '### ', flow - if args.dryrun: - print "#### dry run, no action taken" - else: - self.exec_request(self.pump, flow) - - - def exec_request (self, pump, msg, args={}, - dryrun=False, - save=False, - prefix='', - render_decoded=True, - render_hexdump=True): - if dryrun: - print "skipping query", pump, msg, args - return False - response = pump.query(msg, **args) - print "response: %s" % response - if render_hexdump: - print "hexdump:" - print "```python" - print response.hexdump( ) - print "```" - if render_decoded: - print "#### decoded:" - print "```python" - print repr(response.getData( )) - print "```" - if save: - response.save(prefix=prefix) - return response - +log = logging.getLogger().getChild(__name__) + +from decocare import commands, lib, link, scan, session, stick + + +class CommandApp: + def __init__(self): + self.env = self.parse_env() + self.parser = self.get_parser() + self.autocomplete() + + @classmethod + def parse_env(klass): + return { + "serial": os.environ.get("SERIAL", ""), + "port": os.environ.get("PORT", ""), + } + + def autocomplete(self): + try: + import argcomplete + + argcomplete.autocomplete(self.parser) + except ImportError: + # no auto completion + pass + + def help(self): + return self.__class__.__doc__ + + def get_parser(self): + helptext = self.help().split("\n") + description = "\n".join(helptext[0:2]) + epilog = "\n".join(helptext[2:]) + parser = argparse.ArgumentParser(description=description, epilog=epilog) + parser.add_argument( + "--serial", + type=str, + dest="serial", + default=self.env.get("serial", ""), + help="serial number of pump [default: %(default)s]", + ) + parser.add_argument( + "--port", + type=str, + dest="port", + default=(self.env.get("port") or "scan"), + help="Path to device [default: %(default)s]", + ) + parser.add_argument( + "--no-op", + dest="dryrun", + action="store_true", + default=False, + help="Dry run, don't do main function", + ) + parser.add_argument( + "--skip-prelude", + dest="no_prelude", + action="store_true", + default=False, + help="Don't do the normal prelude.", + ) + parser.add_argument( + "--no-rf-prelude", + dest="no_rf_prelude", + action="store_true", + default=False, + help="Do the prelude, but don't query the pump.", + ) + parser.add_argument( + "--skip-postlude", + dest="no_postlude", + action="store_true", + default=False, + help="Don't do the normal postlude.", + ) + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="append_const", + const=1, + help="Verbosity", + ) + parser.add_argument( + "--rf-minutes", + dest="session_life", + type=int, + default=10, + help="How long RF sessions should last", + ) + parser.add_argument( + "--auto-init", + dest="autoinit", + action="store_true", + default=False, + help="Send power ctrl to initialize RF session.", + ) + parser.add_argument( + "--init", + dest="init", + action="store_true", + default=False, + help="Send power ctrl to initialize RF session.", + ) + parser = self.customize_parser(parser) + return parser + + def customize_parser(self, parser): + parser.add_argument( + "commands", + nargs="+", + # dest='status', + choices=["act", "esc", "up", "down", "easy"], + default="act", + help="buttons to press [default: %(default)s)]", + ) + return parser + + def configure(self): + args = self.parser.parse_args() + level = None + if args.verbose > 0: + level = args.verbose > 1 and logging.DEBUG or logging.INFO + logging.basicConfig(stream=sys.stdout, level=level) + self.args = args + return args + + def run(self, args): + if not args: + args = self.configure() + self.prelude(args) + self.main(args) + self.postlude(args) + + def prelude(self, args): + if args.no_prelude: + print("##### skipping prelude") + return + print("## do stuff with an insulin pump over RF") + print("using", "`", args, "`") + + print("```") + if args.port == "scan" or args.port == "": + args.port = scan.scan() + uart = stick.Stick(link.Link(args.port)) + print("```") + print("```") + uart.open() + print("```") + print("```") + pump = session.Pump(uart, args.serial) + print("```") + print("```") + stats = uart.interface_stats() + print("```") + print("```javascript") + print(pformat(stats)) + print("```") + self.uart = uart + self.pump = pump + self.model = None + if args.no_rf_prelude: + print("##### skipping normal RF preludes") + return + print("```") + if not args.autoinit: + if args.init: + pump.power_control(minutes=args.session_life) + model = pump.read_model() + self.model = model + else: + self.autoinit(args) + print("```") + print("### PUMP MODEL: `%s`" % self.model) + + def autoinit(self, args): + for n in range(3): + print("AUTO INIT", n) + self.sniff_model() + if len(self.model.getData()) != 3: + self.stats + print("SENDING POWER ON", n) + self.pump.power_control(minutes=args.session_life) + else: + print("### PUMP MODEL: `%s`" % self.model) + break + + def sniff_model(self): + model = self.pump.read_model() + print("GOT MODEL", model) + self.model = model + + def postlude(self, args): + if args.no_postlude: + print("##### skipping postlude") + return + print("### end stats") + self.stats() + + def stats(self): + print("```") + stats = self.uart.interface_stats() + print("```") + print("```javascript") + print(pformat(stats)) + print("```") + + def main(self, args): + for flow in args.commands: + print("### ", flow) + if args.dryrun: + print("#### dry run, no action taken") + else: + self.exec_request(self.pump, flow) + + def exec_request( + self, + pump, + msg, + args={}, + dryrun=False, + save=False, + prefix="", + render_decoded=True, + render_hexdump=True, + ): + if dryrun: + print("skipping query", pump, msg, args) + return False + response = pump.query(msg, **args) + print("response: %s" % response) + if render_hexdump: + print("hexdump:") + print("```python") + print(response.hexdump()) + print("```") + if render_decoded: + print("#### decoded:") + print("```python") + print(repr(response.getData())) + print("```") + if save: + response.save(prefix=prefix) + return response diff --git a/decocare/helpers/messages.py b/decocare/helpers/messages.py index 51a20d3..1890c03 100644 --- a/decocare/helpers/messages.py +++ b/decocare/helpers/messages.py @@ -1,130 +1,174 @@ import time from pprint import pformat -from cli import CommandApp -from decocare import link, stick, session, commands, lib, scan -def get_parser ( ): - app = SendMsgApp( ) - return app.get_parser( ) +from decocare import commands, lib, link, scan, session, stick -class SendMsgApp(CommandApp): - """ - %(prog)s - send messages to a compatible MM insulin pump +from decocare.helpers.cli import CommandApp + + +def get_parser(): + app = SendMsgApp() + return app.get_parser() - This tool is intended to help discover protocol behavior. - Under no circumstance is it intended to deliver therapy. - """ - def main (self, args): - if args.prefix: - self.execute_list(args.prefix, args.saveall) - if args.command == "ManualCommand": - kwds = { - 'params': map(int, getattr(args, 'params', [ ])), - 'retries': getattr(args, 'retries', 0), - 'effectTime': getattr(args, 'effectTime'), - 'maxRecords': getattr(args, 'maxRecords'), - 'bytesPerRecord': getattr(args, 'bytesPerRecord'), - 'code': args.code, - 'name': getattr(args, 'name', "ExperimentCode%02x" % args.code), - 'descr': getattr(args, 'descr', "Experimental msg") - } - msg = commands.ManualCommand - resp = self.exec_request(self.pump, msg, args=kwds, - dryrun=args.dryrun, render_hexdump=False, save=args.save, prefix=args.prefix_path) - if args.command == "sleep": - time.sleep(args.timeout) - if args.command == "tweak": - Other = getattr(commands, args.other) - kwds = commands.TweakAnotherCommand.get_kwds(Other, args) - print Other, kwds - resp = self.exec_request(self.pump, Other, args=kwds, - dryrun=args.dryrun, save=args.save, prefix=args.prefix_path) - if args.postfix: - self.execute_list(args.postfix, args.saveall) - def execute_list (self, messages, save=False): - for name in messages: - msg = getattr(commands, name) - print "###### sending `%s`" % getattr(msg, 'name', msg) - resp = self.exec_request(self.pump, msg, dryrun=self.args.dryrun, - render_hexdump=self.args.verbose>0, - save=save, prefix=self.args.prefix_path) +class SendMsgApp(CommandApp): + """ + %(prog)s - send messages to a compatible MM insulin pump + + This tool is intended to help discover protocol behavior. + Under no circumstance is it intended to deliver therapy. + """ - def customize_parser (self, parser): - choices = commands.__all__ - choices.sort( ) - parser.add_argument('--prefix-path', - dest="prefix_path", - type=str, - default="", - help="Prefix to store saved files when using --save or --saveall." - ) - parser.add_argument('--saveall', - action="store_true", - default=False, - help="Whether or not to save all responses.", - ) + def main(self, args): + if args.prefix: + self.execute_list(args.prefix, args.saveall) + if args.command == "ManualCommand": + kwds = { + "params": list(map(int, getattr(args, "params", []))), + "retries": getattr(args, "retries", 0), + "effectTime": getattr(args, "effectTime"), + "maxRecords": getattr(args, "maxRecords"), + "bytesPerRecord": getattr(args, "bytesPerRecord"), + "code": args.code, + "name": getattr(args, "name", "ExperimentCode%02x" % args.code), + "descr": getattr(args, "descr", "Experimental msg"), + } + msg = commands.ManualCommand + resp = self.exec_request( + self.pump, + msg, + args=kwds, + dryrun=args.dryrun, + render_hexdump=False, + save=args.save, + prefix=args.prefix_path, + ) + if args.command == "sleep": + time.sleep(args.timeout) + if args.command == "tweak": + Other = getattr(commands, args.other) + kwds = commands.TweakAnotherCommand.get_kwds(Other, args) + print(Other, kwds) + resp = self.exec_request( + self.pump, + Other, + args=kwds, + dryrun=args.dryrun, + save=args.save, + prefix=args.prefix_path, + ) + if args.postfix: + self.execute_list(args.postfix, args.saveall) - parser.add_argument('--prefix', - action="append", - choices=choices, - help="Built-in commands to run before the main one." - ) + def execute_list(self, messages, save=False): + for name in messages: + msg = getattr(commands, name) + print("###### sending `%s`" % getattr(msg, "name", msg)) + resp = self.exec_request( + self.pump, + msg, + dryrun=self.args.dryrun, + render_hexdump=self.args.verbose > 0, + save=save, + prefix=self.args.prefix_path, + ) - parser.add_argument('--postfix', - action="append", - choices=choices, - help="Built-in commands to run after the main one." - ) + def customize_parser(self, parser): + choices = sorted(commands.__all__) + parser.add_argument( + "--prefix-path", + dest="prefix_path", + type=str, + default="", + help="Prefix to store saved files when using --save or --saveall.", + ) + parser.add_argument( + "--saveall", + action="store_true", + default=False, + help="Whether or not to save all responses.", + ) - subparsers = parser.add_subparsers(help="Main thing to do between --prefix and--postfix", dest="command") - sleep_parser = subparsers.add_parser("sleep", help="Just sleep between command sets") - sleep_parser.add_argument('timeout', type=float, default=0, - help="Sleep in between running --prefix and --postfix" - ) - tweaker = subparsers.add_parser("tweak", help="Tweak a builtin command") - tweaker.add_argument('other', - choices=choices, - help="Command to tweak." - ) - commands.TweakAnotherCommand.config_argparse(tweaker) - all_parser = subparsers.add_parser("ManualCommand", help="Customize a command") - all_parser.add_argument('code', type=int, - help="The opcode to send to the pump." - ) - #__fields__ = ['maxRecords', 'code', 'descr', - # 'serial', 'bytesPerRecord', 'retries', 'params'] - all_parser.add_argument('--params', type=str, action="append", - help="parameters to format into sent message", - default=commands.ManualCommand.params - ) - all_parser.add_argument('--params_hexline', dest='params', type=lib.decode_hexline, - help="hex string, parameters to format into sent message" - # default=commands.ManualCommand.params - ) + parser.add_argument( + "--prefix", + action="append", + choices=choices, + help="Built-in commands to run before the main one.", + ) + parser.add_argument( + "--postfix", + action="append", + choices=choices, + help="Built-in commands to run after the main one.", + ) - all_parser.add_argument('--descr', type=str, default="Experimental command", - help="Description of command" - ) - all_parser.add_argument('--name', type=str, - help="Proposed name of command" - ) - all_parser.add_argument('--save', action="store_true", default=False, - help="Save response in a file." - ) - all_parser.add_argument('--effectTime', type=float, - help="time to sleep before responding to message, float in seconds", - default=commands.ManualCommand.effectTime - ) - all_parser.add_argument('--maxRecords', type=int, - help="number of frames in a packet composing payload response", - default=commands.ManualCommand.maxRecords - ) - all_parser.add_argument('--bytesPerRecord', type=int, - help="bytes per frame", - default=commands.ManualCommand.bytesPerRecord - ) + subparsers = parser.add_subparsers( + help="Main thing to do between --prefix and--postfix", dest="command" + ) + sleep_parser = subparsers.add_parser( + "sleep", help="Just sleep between command sets" + ) + sleep_parser.add_argument( + "timeout", + type=float, + default=0, + help="Sleep in between running --prefix and --postfix", + ) + tweaker = subparsers.add_parser("tweak", help="Tweak a builtin command") + tweaker.add_argument("other", choices=choices, help="Command to tweak.") + commands.TweakAnotherCommand.config_argparse(tweaker) + all_parser = subparsers.add_parser("ManualCommand", help="Customize a command") + all_parser.add_argument( + "code", type=int, help="The opcode to send to the pump." + ) + # __fields__ = ['maxRecords', 'code', 'descr', + # 'serial', 'bytesPerRecord', 'retries', 'params'] + all_parser.add_argument( + "--params", + type=str, + action="append", + help="parameters to format into sent message", + default=commands.ManualCommand.params, + ) + all_parser.add_argument( + "--params_hexline", + dest="params", + type=lib.decode_hexline, + help="hex string, parameters to format into sent message" + # default=commands.ManualCommand.params + ) - return parser + all_parser.add_argument( + "--descr", + type=str, + default="Experimental command", + help="Description of command", + ) + all_parser.add_argument("--name", type=str, help="Proposed name of command") + all_parser.add_argument( + "--save", + action="store_true", + default=False, + help="Save response in a file.", + ) + all_parser.add_argument( + "--effectTime", + type=float, + help="time to sleep before responding to message, float in seconds", + default=commands.ManualCommand.effectTime, + ) + all_parser.add_argument( + "--maxRecords", + type=int, + help="number of frames in a packet composing payload response", + default=commands.ManualCommand.maxRecords, + ) + all_parser.add_argument( + "--bytesPerRecord", + type=int, + help="bytes per frame", + default=commands.ManualCommand.bytesPerRecord, + ) + return parser diff --git a/decocare/history.py b/decocare/history.py index 20f4f3b..2a71173 100644 --- a/decocare/history.py +++ b/decocare/history.py @@ -6,236 +6,299 @@ """ import io from binascii import hexlify - -import lib -from records import * from datetime import date +from decocare import lib +from decocare.records import * + _remote_ids = [ - bytearray([ 0x01, 0xe2, 0x40 ]), - bytearray([ 0x03, 0x42, 0x2a ]), - bytearray([ 0x0c, 0x89, 0x92 ]), + bytearray([0x01, 0xE2, 0x40]), + bytearray([0x03, 0x42, 0x2A]), + bytearray([0x0C, 0x89, 0x92]), ] -def decode_remote_id(msg): - """ - practice decoding some remote ids: - | 0x27 - | 0x01 0xe2 0x40 - | 0x03 0x42 0x2a - | 0x28 0x0c 0x89 - | 0x92 0x00 0x00 0x00 +def decode_remote_id(msg): + """ + practice decoding some remote ids: - >>> decode_remote_id(_remote_ids[0]) - '123456' + | 0x27 + | 0x01 0xe2 0x40 + | 0x03 0x42 0x2a + | 0x28 0x0c 0x89 + | 0x92 0x00 0x00 0x00 - >>> decode_remote_id(_remote_ids[1]) - '213546' + >>> decode_remote_id(_remote_ids[0]) + '123456' - >>> decode_remote_id(_remote_ids[2]) - '821650' + >>> decode_remote_id(_remote_ids[1]) + '213546' + >>> decode_remote_id(_remote_ids[2]) + '821650' + """ + high = msg[0] * 256 * 256 + middle = msg[1] * 256 + low = msg[2] + return str(high + middle + low) - """ - high = msg[ 0 ] * 256 * 256 - middle = msg[ 1 ] * 256 - low = msg[ 2 ] - return str(high + middle + low) class AlarmPump(KnownRecord): - opcode = 0x06 - head_length = 4 -#class ResultTotals(KnownRecord): + opcode = 0x06 + head_length = 4 + + +# class ResultTotals(KnownRecord): class ResultDailyTotal(InvalidRecord): - """On 722 this seems like two records.""" - opcode = 0x07 - #head_length = 5 - head_length = 5 - date_length = 2 - #body_length = 37 + 4 - #body_length = 2 - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - if self.larger: - self.body_length = 3 - def parse_time(self): - date = parse_midnight(self.date) - self.datetime = date - if not hasattr(date, 'isoformat'): - self.datetime = None - return date - - def decode (self): - self.parse_time( ) - mid = unmask_m_midnight(self.date)[0:3] - return (dict(valid_date=date(*mid).isoformat())) - - def date_str(self): - result = 'unknown' - if self.datetime is not None: - result = self.datetime.isoformat( ) - else: - if len(self.date) >=2: - result = "{}".format(unmask_m_midnight(self.date)) - return result - -class ChangeBasalProfile_old_profile (KnownRecord): - opcode = 0x08 - # old/bad - # body_length = 46 - # XXX: on LeDanaScott's, 522, this seems right - body_length = 145 - # head_length = 3 # XXX: # for 554!? - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - if self.larger: - self.body_length = 145 - def decode (self): - self.parse_time( ) - rates = [ ] - i = 0 - for x in range(47): - start = x * 3 - end = start + 3 - (offset, rate, q) = self.body[start:end] - if [offset, rate, q] == [ 0x00, 0x00, 0x00]: - break - try: - rates.append(describe_rate(offset, rate, q)) - except TypeError, e: - remainder = [ offset, rate, q ] - rates.append(remainder) - return rates - -def describe_rate (offset, rate, q): - return (dict(offset=(30*1000*60)*offset, rate=rate/40.0)) - - -class ChangeBasalProfile_new_profile (KnownRecord): - opcode = 0x09 - body_length = 145 - # body_length = 144 # XXX: # for 554!? - # head_length = 3 # XXX: # for 554!? - def decode (self): - self.parse_time( ) - rates = [ ] - i = 0 - for x in range(47): - start = x * 3 - end = start + 3 - (offset, rate, q) = self.body[start:end] - if [offset, rate, q] == [ 0x00, 0x00, 0x00]: - break - rates.append(describe_rate(offset, rate, q)) - return rates + """On 722 this seems like two records.""" + + opcode = 0x07 + # head_length = 5 + head_length = 5 + date_length = 2 + # body_length = 37 + 4 + # body_length = 2 + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + if self.larger: + self.body_length = 3 + + def parse_time(self): + date = parse_midnight(self.date) + self.datetime = date + if not hasattr(date, "isoformat"): + self.datetime = None + return date + + def decode(self): + self.parse_time() + mid = unmask_m_midnight(self.date)[0:3] + return dict(valid_date=date(*mid).isoformat()) + + def date_str(self): + result = "unknown" + if self.datetime is not None: + result = self.datetime.isoformat() + else: + if len(self.date) >= 2: + result = "{}".format(unmask_m_midnight(self.date)) + return result + + +class ChangeBasalProfile_old_profile(KnownRecord): + opcode = 0x08 + # old/bad + # body_length = 46 + # XXX: on LeDanaScott's, 522, this seems right + body_length = 145 + # head_length = 3 # XXX: # for 554!? + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + if self.larger: + self.body_length = 145 + + def decode(self): + self.parse_time() + rates = [] + i = 0 + for x in range(47): + start = x * 3 + end = start + 3 + (offset, rate, q) = self.body[start:end] + if [offset, rate, q] == [0x00, 0x00, 0x00]: + break + try: + rates.append(describe_rate(offset, rate, q)) + except TypeError as e: + remainder = [offset, rate, q] + rates.append(remainder) + return rates + + +def describe_rate(offset, rate, q): + return dict(offset=(30 * 1000 * 60) * offset, rate=rate / 40.0) + + +class ChangeBasalProfile_new_profile(KnownRecord): + opcode = 0x09 + body_length = 145 + # body_length = 144 # XXX: # for 554!? + # head_length = 3 # XXX: # for 554!? + def decode(self): + self.parse_time() + rates = [] + i = 0 + for x in range(47): + start = x * 3 + end = start + 3 + (offset, rate, q) = self.body[start:end] + if [offset, rate, q] == [0x00, 0x00, 0x00]: + break + rates.append(describe_rate(offset, rate, q)) + return rates + + class ClearAlarm(KnownRecord): - opcode = 0x0C + opcode = 0x0C + + class SelectBasalProfile(KnownRecord): - opcode = 0x14 + opcode = 0x14 + + class ChangeTime(KnownRecord): - opcode = 0x17 + opcode = 0x17 + + class NewTimeSet(KnownRecord): - opcode = 0x18 + opcode = 0x18 + + class LowBattery(KnownRecord): - opcode = 0x19 + opcode = 0x19 + + class Battery(KnownRecord): - opcode = 0x1a + opcode = 0x1A + + class PumpSuspend(KnownRecord): - opcode = 0x1e + opcode = 0x1E + + class PumpResume(KnownRecord): - opcode = 0x1f + opcode = 0x1F + class Rewind(KnownRecord): - opcode = 0x21 + opcode = 0x21 + class EnableDisableRemote(KnownRecord): - opcode = 0x26 - # body_length = 14 - # head_length = 3 # XXX: for 554 - body_length = 14 + opcode = 0x26 + # body_length = 14 + # head_length = 3 # XXX: for 554 + body_length = 14 + + class ChangeRemoteID(KnownRecord): - opcode = 0x27 + opcode = 0x27 + class TempBasalDuration(KnownRecord): - opcode = 0x16 - _test_1 = bytearray([ ]) - def decode(self): - self.parse_time( ) - basal = { 'duration (min)': self.head[1] * 30, } - return basal -class ChangeMazaheri2e (KnownRecord): - opcode = 0x2e - body_length = 100 + opcode = 0x16 + _test_1 = bytearray([]) + + def decode(self): + self.parse_time() + basal = { + "duration (min)": self.head[1] * 30, + } + return basal + + +class ChangeMazaheri2e(KnownRecord): + opcode = 0x2E + body_length = 100 + # class BolusWizard512 (BolusWizard): -class BolusWizard512 (KnownRecord): - opcode = 0x2f - body_length = 12 - -class UnabsorbedInsulin512 (UnabsorbedInsulinBolus): - opcode = 0x30 - -class TempBasal (KnownRecord): - opcode = 0x33 - body_length = 1 - _test_1 = bytearray([ ]) - def decode(self): - self.parse_time( ) - temp = { 0: 'absolute', 1: 'percent' }[(self.body[0] >> 3)] - status = dict(temp=temp) - if temp is 'absolute': - rate = lib.BangInt([self.body[0]&0x7, self.head[1]]) / 40.0 - status.update(rate=rate) - if temp is 'percent': - rate = int(self.head[1]) - status.update(rate=rate) - return status +class BolusWizard512(KnownRecord): + opcode = 0x2F + body_length = 12 + + +class UnabsorbedInsulin512(UnabsorbedInsulinBolus): + opcode = 0x30 + + +class TempBasal(KnownRecord): + opcode = 0x33 + body_length = 1 + _test_1 = bytearray([]) + + def decode(self): + self.parse_time() + temp = {0: "absolute", 1: "percent"}[(self.body[0] >> 3)] + status = dict(temp=temp) + if temp == "absolute": + rate = lib.BangInt([self.body[0] & 0x7, self.head[1]]) / 40.0 + status.update(rate=rate) + if temp == "percent": + rate = int(self.head[1]) + status.update(rate=rate) + return status + class LowReservoir(KnownRecord): - """ - >>> rec = LowReservoir( LowReservoir._test_1[:2] ) - >>> decoded = rec.parse(LowReservoir._test_1) - >>> print str(rec) - LowReservoir 2012-12-07T11:02:43 head[2], body[0] op[0x34] - - >>> print pformat(decoded) - {'amount': 20.0} - """ - opcode = 0x34 - _test_1 = bytearray([ 0x34, 0xc8, - 0xeb, 0x02, 0x0b, 0x07, 0x0c, ]) - def decode(self): - self.parse_time( ) - reservoir = {'amount' : int(self.head[1]) / 10.0 } - return reservoir - - -class ChangeAlarmNotifyMode (KnownRecord): - opcode = 0x63 - body_length = 0 + """ + >>> rec = LowReservoir( LowReservoir._test_1[:2] ) + >>> decoded = rec.parse(LowReservoir._test_1) + >>> print(str(rec)) + LowReservoir 2012-12-07T11:02:43 head[2], body[0] op[0x34] + + >>> print(pformat(decoded)) + {'amount': 20.0} + """ + + opcode = 0x34 + _test_1 = bytearray([0x34, 0xC8, 0xEB, 0x02, 0x0B, 0x07, 0x0C,]) + + def decode(self): + self.parse_time() + reservoir = {"amount": int(self.head[1]) / 10.0} + return reservoir + + +class ChangeAlarmNotifyMode(KnownRecord): + opcode = 0x63 + body_length = 0 + + class ChangeTimeDisplay(KnownRecord): - opcode = 0x64 - def decode(self): - self.parse_time( ) - key = {1: "d24"} - info = dict(timeFormat=key.get(self.head[1], 'd12')) - return info - -class ChangeBolusWizardSetup (KnownRecord): - opcode = 0x4f - body_length = 40 - -_confirmed = [ Bolus, Prime, AlarmPump, ResultDailyTotal, - ChangeBasalProfile_old_profile, - ChangeBasalProfile_new_profile, - ClearAlarm, SelectBasalProfile, TempBasalDuration, ChangeTime, - NewTimeSet, LowBattery, Battery, PumpSuspend, - PumpResume, CalBGForPH, Rewind, EnableDisableRemote, - ChangeRemoteID, TempBasal, LowReservoir, BolusWizard, - UnabsorbedInsulinBolus, ChangeAlarmNotifyMode, ChangeTimeDisplay, - ChangeBolusWizardSetup, ] + opcode = 0x64 + + def decode(self): + self.parse_time() + key = {1: "d24"} + info = dict(timeFormat=key.get(self.head[1], "d12")) + return info + + +class ChangeBolusWizardSetup(KnownRecord): + opcode = 0x4F + body_length = 40 + + +_confirmed = [ + Bolus, + Prime, + AlarmPump, + ResultDailyTotal, + ChangeBasalProfile_old_profile, + ChangeBasalProfile_new_profile, + ClearAlarm, + SelectBasalProfile, + TempBasalDuration, + ChangeTime, + NewTimeSet, + LowBattery, + Battery, + PumpSuspend, + PumpResume, + CalBGForPH, + Rewind, + EnableDisableRemote, + ChangeRemoteID, + TempBasal, + LowReservoir, + BolusWizard, + UnabsorbedInsulinBolus, + ChangeAlarmNotifyMode, + ChangeTimeDisplay, + ChangeBolusWizardSetup, +] # _confirmed.append(DanaScott0x09) _confirmed.append(ChangeMazaheri2e) @@ -244,67 +307,92 @@ class ChangeBolusWizardSetup (KnownRecord): class JournalEntryMealMarker(KnownRecord): - """Capture Event > Meal marker""" - opcode = 0x40 - body_length = 2 + """Capture Event > Meal marker""" - def decode(self): - super(JournalEntryMealMarker, self).decode() + opcode = 0x40 + body_length = 2 + + def decode(self): + super().decode() + + return dict(carb_input=int(lib.BangInt([self.head[1], self.body[0]]))) - return dict(carb_input=int(lib.BangInt([self.head[1], self.body[0]]))) _confirmed.append(JournalEntryMealMarker) + class JournalEntryExerciseMarker(KnownRecord): - """Capture Event > Exercise marker""" - opcode = 0x41 - body_length = 1 + """Capture Event > Exercise marker""" + + opcode = 0x41 + body_length = 1 + + _confirmed.append(JournalEntryExerciseMarker) + class JournalEntryInsulinMarker(KnownRecord): - """Capture Event > Insulin marker""" - opcode = 0x42 - body_length = 1 - def decode(self): - super(JournalEntryInsulinMarker, self).decode() - # see https://github.com/ps2/rileylink_ios/pull/160/files - lowbits = self.head[1] - highbits = (self.date[2] & 0b1100000) << 3 # ?? - amount = (highbits + lowbits) / 10.0 - return dict(amount=amount) + """Capture Event > Insulin marker""" + + opcode = 0x42 + body_length = 1 + + def decode(self): + super().decode() + # see https://github.com/ps2/rileylink_ios/pull/160/files + lowbits = self.head[1] + highbits = (self.date[2] & 0b1100000) << 3 # ?? + amount = (highbits + lowbits) / 10.0 + return dict(amount=amount) + + _confirmed.append(JournalEntryInsulinMarker) class JournalEntryOtherMarker(KnownRecord): - """Capture Event > Other""" - opcode = 0x43 - body_length = 0 + """Capture Event > Other""" + + opcode = 0x43 + body_length = 0 + + _confirmed.append(JournalEntryOtherMarker) + class Ian69(KnownRecord): - opcode = 0x69 - body_length = 2 + opcode = 0x69 + body_length = 2 + + _confirmed.append(Ian69) -class ChangeSensorSetup2 (KnownRecord): - opcode = 0x50 - body_length = 34 - # XXX: tghoward testing on 723 at length 30 - body_length = 30 - def __init__ (self, head, model, **kwds): - super(ChangeSensorSetup2, self).__init__(head, model, **kwds) - self.body_length = model.Ian50Body +class ChangeSensorSetup2(KnownRecord): + opcode = 0x50 + body_length = 34 + + # XXX: tghoward testing on 723 at length 30 + body_length = 30 + + def __init__(self, head, model, **kwds): + super().__init__(head, model, **kwds) + self.body_length = model.Ian50Body + + _confirmed.append(ChangeSensorSetup2) + class Ian54(KnownRecord): - opcode = 0x54 - body_length = 34 + 23 - body_length = 57 + opcode = 0x54 + body_length = 34 + 23 + body_length = 57 + + _confirmed.append(Ian54) -class AlarmSensor (KnownRecord): - """Glucose sensor alarms. + +class AlarmSensor(KnownRecord): + """Glucose sensor alarms. The second byte of the head represents the alarm type. The third byte contains an alarm-specific value. @@ -315,569 +403,721 @@ class AlarmSensor (KnownRecord): 0x50 # 80: Glucose level (For a pump configured to mg/dL) ] """ - opcode = 0x0B - head_length = 3 - - alarm_types = { - 101: 'High Glucose', - 102: 'Low Glucose', - 104: 'Meter BG Now', - 105: 'Cal Reminder', - 106: 'Calibration Error', - 107: 'Sensor End', - 112: 'Weak Signal', - 113: 'Lost Sensor', - 115: 'Low Glucose Predicted' - } - - def decode(self): - super(AlarmSensor, self).decode() - - alarm_type = self.head[1] - - decoded_dict = { - 'alarm_type': alarm_type, - 'alarm_description': self.alarm_types.get(alarm_type, 'Unknown sensor alarm ({})'.format(alarm_type)) + + opcode = 0x0B + head_length = 3 + + alarm_types = { + 101: "High Glucose", + 102: "Low Glucose", + 104: "Meter BG Now", + 105: "Cal Reminder", + 106: "Calibration Error", + 107: "Sensor End", + 112: "Weak Signal", + 113: "Lost Sensor", + 115: "Low Glucose Predicted", } - if alarm_type in (101, 102,): - year_bits = extra_year_bits(self.date[4]) - decoded_dict['amount'] = int(lib.BangInt([year_bits[0], self.head[2]])) + def decode(self): + super().decode() + + alarm_type = self.head[1] + + decoded_dict = { + "alarm_type": alarm_type, + "alarm_description": self.alarm_types.get( + alarm_type, "Unknown sensor alarm ({})".format(alarm_type) + ), + } + + if alarm_type in (101, 102,): + year_bits = extra_year_bits(self.date[4]) + decoded_dict["amount"] = int(lib.BangInt([year_bits[0], self.head[2]])) + + return decoded_dict + - return decoded_dict _confirmed.append(AlarmSensor) -class BGReceived (KnownRecord): - opcode = 0x3F - body_length = 3 - def decode (self): - self.parse_time( ) - bg = (self.head[1] << 3) + (self.date[2] >> 5) - return dict(link=str(self.body).encode('hex'), amount=bg) + +class BGReceived(KnownRecord): + opcode = 0x3F + body_length = 3 + + def decode(self): + self.parse_time() + bg = (self.head[1] << 3) + (self.date[2] >> 5) + return dict(link=str(self.body).encode("hex"), amount=bg) + + _confirmed.append(BGReceived) + class IanA8(KnownRecord): - opcode = 0xA8 - head_length = 10 + opcode = 0xA8 + head_length = 10 + + _confirmed.append(IanA8) + class BasalProfileStart(KnownRecord): - opcode = 0x7b - body_length = 3 - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - if self.larger: - # body_length = 1 - pass - # self.body_length = 48 - def decode (self): - self.parse_time( ) - if (len(self.body) % 3 == 0): - rate = describe_rate(*self.body) - rate['profile_index'] = self.head[1] - return rate - else: - return dict(raw=hexlify(self.body)) + opcode = 0x7B + body_length = 3 + + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + if self.larger: + # body_length = 1 + pass + # self.body_length = 48 + + def decode(self): + self.parse_time() + if len(self.body) % 3 == 0: + rate = describe_rate(*self.body) + rate["profile_index"] = self.head[1] + return rate + else: + return dict(raw=hexlify(self.body)) + + _confirmed.append(BasalProfileStart) # 123, 143 -class OldBolusWizardChange (KnownRecord): - opcode = 0x5a - body_length = 117 - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - if self.larger: - self.body_length = 117 + 17 + 3 - pass - def decode (self): - self.parse_time( ) - half = (self.body_length - 1) / 2 - stale = self.body[0:half] - changed = self.body[half:-1] - tail = self.body[-1] - stale = decode_wizard_settings(stale, model=self.model) - changed = decode_wizard_settings(changed, model=self.model) - stale.update(InsulinActionHours=(tail & 0xF)) - changed.update(InsulinActionHours=(tail >>4)) - return dict(stale=stale - # , _changed=changed - , changed=changed - , tail=tail - ) +class OldBolusWizardChange(KnownRecord): + opcode = 0x5A + body_length = 117 + + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + if self.larger: + self.body_length = 117 + 17 + 3 + pass + + def decode(self): + self.parse_time() + half = (self.body_length - 1) / 2 + stale = self.body[0:half] + changed = self.body[half:-1] + tail = self.body[-1] + stale = decode_wizard_settings(stale, model=self.model) + changed = decode_wizard_settings(changed, model=self.model) + stale.update(InsulinActionHours=(tail & 0xF)) + changed.update(InsulinActionHours=(tail >> 4)) + return dict( + stale=stale + # , _changed=changed + , + changed=changed, + tail=tail, + ) + _confirmed.append(OldBolusWizardChange) -def decode_wizard_settings (data, num=8, model=None): - head = data[0:2] - tail = data[len(head):] - carb_reader = model.read_carb_ratios.msg - cr_size = carb_reader.item_size - carb_ratios = tail[0:num*cr_size] - tail = tail[num*cr_size:] - insulin_sensitivies = tail[0:(num*2)] - tail = tail[num*2:] - isMg = head[0] & 0b00000100 - isMmol = head[0] & 0b00001000 - bg_units = 1 - if isMmol and not isMg: - bg_units = 2 - bg_targets = tail[0:(num*3)+2] - if model and model.larger: - bg_targets = bg_targets[2:] - return dict(head=str(head).encode('hex') - # , carb_ratios=decode_carb_ratios(carb_ratios) - , carb_ratios=carb_reader.decode_ratios(carb_ratios) - # , _carb_ratios=str(carb_ratios).encode('hex') - # , cr_len=len(carb_ratios) - , insulin_sensitivies=decode_insulin_sensitivies(insulin_sensitivies) - # , _insulin_sensitivies=str(insulin_sensitivies).encode('hex') - # , is_len=len(insulin_sensitivies) - # , bg_len=len(bg_targets) - , bg_targets=decode_bg_targets(bg_targets, bg_units) - # , _o_len=len(data) - # , _bg_targets=str(bg_targets).encode('hex') - , _head = "{0:#010b} {1:#010b}".format(*head) - ) - -def decode_carb_ratios (data): - ratios = [ ] - for x in range(8): - start = x * 3 - end = start + 3 - (offset, q, r) = data[start:end] - ratio = r/10.0 - if q: - ratio = lib.BangInt([q, r]) / 1000.0 - ratios.append(dict(i=x, offset=offset*30, q=q, _offset=offset, - ratio=ratio, r=r)) - return ratios - -def decode_insulin_sensitivies (data): - sensitivities = [ ] - for x in range(8): - start = x * 2 - end = start + 2 - (offset, sensitivity) = data[start:end] - sensitivities.append(dict(i=x, offset=offset*30, _offset=offset, - sensitivity=sensitivity)) - return sensitivities - -def decode_bg_targets (data, bg_units): - # data = data[2:] - targets = [ ] - for x in range(8): - start = x * 3 - end = start + 3 - # (low, high, offset) = data[start:end] - (offset, low, high) = data[start:end] - if bg_units is 2: - low = low / 10.0 - high = high / 10.0 - targets.append(dict( #i=x, - offset=offset*30, _offset=offset, - # _raw=str(data[start:end]).encode('hex'), - low=low, high=high)) - return targets - -class BigBolusWizardChange (KnownRecord): - opcode = 0x5a - body_length = 143 - -class SetAutoOff (KnownRecord): - opcode = 0x1b + + +def decode_wizard_settings(data, num=8, model=None): + head = data[0:2] + tail = data[len(head) :] + carb_reader = model.read_carb_ratios.msg + cr_size = carb_reader.item_size + carb_ratios = tail[0 : num * cr_size] + tail = tail[num * cr_size :] + insulin_sensitivies = tail[0 : (num * 2)] + tail = tail[num * 2 :] + isMg = head[0] & 0b00000100 + isMmol = head[0] & 0b00001000 + bg_units = 1 + if isMmol and not isMg: + bg_units = 2 + bg_targets = tail[0 : (num * 3) + 2] + if model and model.larger: + bg_targets = bg_targets[2:] + return dict( + head=str(head).encode("hex") + # , carb_ratios=decode_carb_ratios(carb_ratios) + , + carb_ratios=carb_reader.decode_ratios(carb_ratios) + # , _carb_ratios=str(carb_ratios).encode('hex') + # , cr_len=len(carb_ratios) + , + insulin_sensitivies=decode_insulin_sensitivies(insulin_sensitivies) + # , _insulin_sensitivies=str(insulin_sensitivies).encode('hex') + # , is_len=len(insulin_sensitivies) + # , bg_len=len(bg_targets) + , + bg_targets=decode_bg_targets(bg_targets, bg_units) + # , _o_len=len(data) + # , _bg_targets=str(bg_targets).encode('hex') + , + _head="{:#010b} {:#010b}".format(*head), + ) + + +def decode_carb_ratios(data): + ratios = [] + for x in range(8): + start = x * 3 + end = start + 3 + (offset, q, r) = data[start:end] + ratio = r / 10.0 + if q: + ratio = lib.BangInt([q, r]) / 1000.0 + ratios.append( + dict(i=x, offset=offset * 30, q=q, _offset=offset, ratio=ratio, r=r) + ) + return ratios + + +def decode_insulin_sensitivies(data): + sensitivities = [] + for x in range(8): + start = x * 2 + end = start + 2 + (offset, sensitivity) = data[start:end] + sensitivities.append( + dict(i=x, offset=offset * 30, _offset=offset, sensitivity=sensitivity) + ) + return sensitivities + + +def decode_bg_targets(data, bg_units): + # data = data[2:] + targets = [] + for x in range(8): + start = x * 3 + end = start + 3 + # (low, high, offset) = data[start:end] + (offset, low, high) = data[start:end] + if bg_units == 2: + low = low / 10.0 + high = high / 10.0 + targets.append( + dict( # i=x, + offset=offset * 30, + _offset=offset, + # _raw=str(data[start:end]).encode('hex'), + low=low, + high=high, + ) + ) + return targets + + +class BigBolusWizardChange(KnownRecord): + opcode = 0x5A + body_length = 143 + + +class SetAutoOff(KnownRecord): + opcode = 0x1B + + _confirmed.append(SetAutoOff) -class ChangeAudioBolus (KnownRecord): - opcode = 0x5f - def decode (self): - self.parse_time( ) + +class ChangeAudioBolus(KnownRecord): + opcode = 0x5F + + def decode(self): + self.parse_time() + + _confirmed.append(ChangeAudioBolus) -class ChangeCaptureEventEnable (KnownRecord): - opcode = 0x83 - # body_length = 1 + +class ChangeCaptureEventEnable(KnownRecord): + opcode = 0x83 + # body_length = 1 + + _confirmed.append(ChangeCaptureEventEnable) -class hack53 (KnownRecord): - opcode = 0x53 - body_length = 1 + +class hack53(KnownRecord): + opcode = 0x53 + body_length = 1 + + _confirmed.append(hack53) -class hack52 (KnownRecord): - opcode = 0x52 - # body_length = 1 + +class hack52(KnownRecord): + opcode = 0x52 + # body_length = 1 + + _confirmed.append(hack52) -class hack51 (KnownRecord): - opcode = 0x51 - # body_length = 1 + +class hack51(KnownRecord): + opcode = 0x51 + # body_length = 1 + + _confirmed.append(hack51) -class hack55 (KnownRecord): - opcode = 0x55 - # body_length = 1 + 47 - # body_length = 2 + 46 - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - # self.larger = larger - self.body_length = (self.head[1] - 1) * 3 + +class hack55(KnownRecord): + opcode = 0x55 + # body_length = 1 + 47 + # body_length = 2 + 46 + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + # self.larger = larger + self.body_length = (self.head[1] - 1) * 3 + + _confirmed.append(hack55) -class hack56 (KnownRecord): - opcode = 0x56 - body_length = 5 +class hack56(KnownRecord): + opcode = 0x56 + body_length = 5 + + _confirmed.append(hack56) + class ChangeWatchdogMarriageProfile(KnownRecord): - opcode = 0x81 - body_length = 5 + opcode = 0x81 + body_length = 5 + + _confirmed.append(ChangeWatchdogMarriageProfile) -class DeleteOtherDeviceID (KnownRecord): - opcode = 0x82 - body_length = 5 + +class DeleteOtherDeviceID(KnownRecord): + opcode = 0x82 + body_length = 5 + + _confirmed.append(DeleteOtherDeviceID) -class ChangeOtherDeviceID (KnownRecord): - opcode = 0x7d - body_length = 30 + +class ChangeOtherDeviceID(KnownRecord): + opcode = 0x7D + body_length = 30 + + _confirmed.append(ChangeOtherDeviceID) -class SetBolusWizardEnabled (KnownRecord): - opcode = 0x2d - def decode (self): - self.parse_time( ) - return dict(enabled=self.head[1] is 1) + +class SetBolusWizardEnabled(KnownRecord): + opcode = 0x2D + + def decode(self): + self.parse_time() + return dict(enabled=self.head[1] == 1) + + _confirmed.append(SetBolusWizardEnabled) -class SettingSomething57 (KnownRecord): - opcode = 0x57 - # body_length = 1 +class SettingSomething57(KnownRecord): + opcode = 0x57 + # body_length = 1 + + _confirmed.append(SettingSomething57) -class ChangeMaxBasal (KnownRecord): - opcode = 0x2c - def decode (self): - self.parse_time( ) - return dict(maxBasal=self.head[1] / 40.0) + +class ChangeMaxBasal(KnownRecord): + opcode = 0x2C + + def decode(self): + self.parse_time() + return dict(maxBasal=self.head[1] / 40.0) + + _confirmed.append(ChangeMaxBasal) -class questionable22 (KnownRecord): - opcode = 0x22 + +class questionable22(KnownRecord): + opcode = 0x22 + + _confirmed.append(questionable22) -class questionable23 (KnownRecord): - opcode = 0x23 + +class questionable23(KnownRecord): + opcode = 0x23 + + _confirmed.append(questionable23) -class questionable24 (KnownRecord): - opcode = 0x24 + +class questionable24(KnownRecord): + opcode = 0x24 + + _confirmed.append(questionable24) -class ChangeBGReminderEnable (KnownRecord): - opcode = 0x60 - def decode (self): - self.parse_time( ) - enabled = self.head[1] is 1 - return dict(enabled=enabled) + +class ChangeBGReminderEnable(KnownRecord): + opcode = 0x60 + + def decode(self): + self.parse_time() + enabled = self.head[1] == 1 + return dict(enabled=enabled) + + _confirmed.append(ChangeBGReminderEnable) -class questionable61 (KnownRecord): - opcode = 0x61 + +class questionable61(KnownRecord): + opcode = 0x61 + + _confirmed.append(questionable61) -class ChangeTempBasalType (KnownRecord): - opcode = 0x62 - def decode (self): - self.parse_time( ) - temp = { 0: 'absolute', 1: 'percent' }[self.head[1]] - return dict(temp=temp) - # body_length = 1 + +class ChangeTempBasalType(KnownRecord): + opcode = 0x62 + + def decode(self): + self.parse_time() + temp = {0: "absolute", 1: "percent"}[self.head[1]] + return dict(temp=temp) + + # body_length = 1 + + _confirmed.append(ChangeTempBasalType) -class questionable65 (KnownRecord): - opcode = 0x65 + +class questionable65(KnownRecord): + opcode = 0x65 + + _confirmed.append(questionable65) -class questionable66 (KnownRecord): - opcode = 0x66 + +class questionable66(KnownRecord): + opcode = 0x66 + + _confirmed.append(questionable66) -class questionable6f (KnownRecord): - opcode = 0x6f + +class questionable6f(KnownRecord): + opcode = 0x6F + + _confirmed.append(questionable6f) -class SaveSettings (KnownRecord): - opcode = 0x5d + +class SaveSettings(KnownRecord): + opcode = 0x5D + + _confirmed.append(SaveSettings) -class questionable5e (KnownRecord): - opcode = 0x5e + +class questionable5e(KnownRecord): + opcode = 0x5E + + _confirmed.append(questionable5e) -class ChangeParadigmLinkID (KnownRecord): - opcode = 0x3c - body_length = 14 - def decode (self): - self.parse_time( ) - data = self.body[1:] - links = [ ] - links.append(str(data[0:3]).encode('hex')) - links.append(str(data[3:6]).encode('hex')) - links.append(str(data[7:10]).encode('hex')) - return dict(links=links) + +class ChangeParadigmLinkID(KnownRecord): + opcode = 0x3C + body_length = 14 + + def decode(self): + self.parse_time() + data = self.body[1:] + links = [] + links.append(str(data[0:3]).encode("hex")) + links.append(str(data[3:6]).encode("hex")) + links.append(str(data[7:10]).encode("hex")) + return dict(links=links) + + _confirmed.append(ChangeParadigmLinkID) -class ConnectDevicesOtherDevicesEnabled (KnownRecord): - opcode = 0x7c - def decode(self): - super(ConnectDevicesOtherDevicesEnabled, self).decode() - return dict(enabled=self.head[1] == 1) +class ConnectDevicesOtherDevicesEnabled(KnownRecord): + opcode = 0x7C + + def decode(self): + super().decode() + return dict(enabled=self.head[1] == 1) + _confirmed.append(ConnectDevicesOtherDevicesEnabled) + class Model522ResultTotals(KnownRecord): - opcode = 0x6d - head_length = 1 - date_length = 2 - body_length = 40 - def parse_time(self): - date = parse_midnight(self.date) - self.datetime = date - if not hasattr(date, 'isoformat'): - self.datetime = None - return date - - def date_str(self): - result = 'unknown' - if self.datetime is not None: - result = self.datetime.isoformat( ) - else: - if len(self.date) >=2: - result = "{}".format(unmask_m_midnight(self.date)) - return result + opcode = 0x6D + head_length = 1 + date_length = 2 + body_length = 40 + + def parse_time(self): + date = parse_midnight(self.date) + self.datetime = date + if not hasattr(date, "isoformat"): + self.datetime = None + return date + + def date_str(self): + result = "unknown" + if self.datetime is not None: + result = self.datetime.isoformat() + else: + if len(self.date) >= 2: + result = "{}".format(unmask_m_midnight(self.date)) + return result + # class Model522ResultTotals(KnownRecord): class old6c(Model522ResultTotals): - opcode = 0x6c - #head_length = 45 - #xxx non 515 - # body_length = 38 - # body_length = 34 - # XXX: 515 only? - # body_length = 31 - def __init__ (self, head, model, **kwds): - super(old6c, self).__init__(head, model, **kwds) - self.body_length = model.old6cBody + 3 + opcode = 0x6C + # head_length = 45 + # xxx non 515 + # body_length = 38 + # body_length = 34 + # XXX: 515 only? + # body_length = 31 + def __init__(self, head, model, **kwds): + super().__init__(head, model, **kwds) + self.body_length = model.old6cBody + 3 + + _confirmed.append(old6c) -class questionable3b (KnownRecord): - opcode = 0x3b + +class questionable3b(KnownRecord): + opcode = 0x3B + + _confirmed.append(questionable3b) from dateutil.relativedelta import relativedelta -def parse_midnight (data): + + +def parse_midnight(data): mid = unmask_m_midnight(data) oneday = relativedelta(days=1) try: - date = datetime(*mid) + oneday - return date - except ValueError, e: - print "ERROR", e, lib.hexdump(data) - pass + date = datetime(*mid) + oneday + return date + except ValueError as e: + print("ERROR", e, lib.hexdump(data)) + pass return mid + def unmask_m_midnight(data): - """ - Extract date values from a series of bytes. - Always returns tuple given a bytearray of at least 3 bytes. + """ + Extract date values from a series of bytes. + Always returns tuple given a bytearray of at least 3 bytes. + + Returns 6-tuple of scalar values year, month, day, hours, minutes, + seconds. - Returns 6-tuple of scalar values year, month, day, hours, minutes, - seconds. + """ + data = data[:] + seconds = 0 + minutes = 0 + hours = 0 - """ - data = data[:] - seconds = 0 - minutes = 0 - hours = 0 + day = parse_day(data[0]) - day = parse_day(data[0]) + high = data[0] >> 4 + low = data[0] & 0x1F - high = data[0] >> 4 - low = data[0] & 0x1F + year_high = data[1] >> 4 + # month = int(high) #+ year_high + # month = parse_months( data[0], data[1] ) + mhigh = (data[0] & 0xE0) >> 4 + mlow = (data[1] & 0x80) >> 7 + month = int(mhigh + mlow) + day = int(low) - year_high = data[1] >> 4 - # month = int(high) #+ year_high - # month = parse_months( data[0], data[1] ) - mhigh = (data[0] & 0xE0) >> 4 - mlow = (data[1] & 0x80) >> 7 - month = int(mhigh + mlow) - day = int(low) + year = parse_years(data[1]) + return (year, month, day, hours, minutes, seconds) - year = parse_years(data[1]) - return (year, month, day, hours, minutes, seconds) _confirmed.append(Model522ResultTotals) + class Sara6E(Model522ResultTotals): - """Seems specific to 722?""" - opcode = 0x6e - #head_length = 52 - 5 - # body_length = 1 - body_length = 48 - #body_length = 0 - def __init__(self, head, larger=False): - super(type(self), self).__init__(head, larger) - if self.larger: - self.body_length = 48 - def decode (self): - self.parse_time( ) - mid = unmask_m_midnight(self.date)[0:3] - try: - return (dict(valid_date=date(*mid).isoformat())) - except ValueError, e: - return (dict(error_date=mid, error=str(e))) + """Seems specific to 722?""" + + opcode = 0x6E + # head_length = 52 - 5 + # body_length = 1 + body_length = 48 + # body_length = 0 + def __init__(self, head, larger=False): + super(type(self), self).__init__(head, larger) + if self.larger: + self.body_length = 48 + + def decode(self): + self.parse_time() + mid = unmask_m_midnight(self.date)[0:3] + try: + return dict(valid_date=date(*mid).isoformat()) + except ValueError as e: + return dict(error_date=mid, error=str(e)) + _confirmed.append(Sara6E) -_known = { } +_known = {} -_variant = { } +_variant = {} for x in _confirmed: - _known[x.opcode] = x + _known[x.opcode] = x del x + def suggest(head, larger=False, model=None): - """ - Look in the known table of commands to find a suitable record type - for this opcode. - """ - klass = _known.get(head[0], Base) - record = klass(head, model) - return record - -def parse_record(fd, head=bytearray( ), larger=False, model=None): - """ - Given a file-like object, and the head of a record, parse the rest - of the record. - Look up the type of record, read in just enough data to parse it, - return the result. - """ - # head = bytearray(fd.read(2)) - date = bytearray( ) - body = bytearray( ) - record = suggest(head, larger, model=model) - remaining = record.head_length - len(head) - if remaining > 0: - head.extend(bytearray(fd.read(remaining))) - if record.date_length > 0: - date.extend(bytearray(fd.read(record.date_length))) - if record.body_length > 0: - body.extend(bytearray(fd.read(record.body_length))) - record.parse( head + date + body ) - # print str(record) - # print record.pformat(prefix=str(record) ) - return record - - -def describe( ): - keys = _known.keys( ) - out = [ ] - for k in keys: - out.append(_known[k].describe( )) - return out - -class PagedData (object): - """ + """ + Look in the known table of commands to find a suitable record type + for this opcode. + """ + klass = _known.get(head[0], Base) + record = klass(head, model) + return record + + +def parse_record(fd, head=bytearray(), larger=False, model=None): + """ + Given a file-like object, and the head of a record, parse the rest + of the record. + Look up the type of record, read in just enough data to parse it, + return the result. + """ + # head = bytearray(fd.read(2)) + date = bytearray() + body = bytearray() + record = suggest(head, larger, model=model) + remaining = record.head_length - len(head) + if remaining > 0: + head.extend(bytearray(fd.read(remaining))) + if record.date_length > 0: + date.extend(bytearray(fd.read(record.date_length))) + if record.body_length > 0: + body.extend(bytearray(fd.read(record.body_length))) + record.parse(head + date + body) + # print(str(record)) + # print(record.pformat(prefix=str(record) )) + return record + + +def describe(): + keys = list(_known.keys()) + out = [] + for k in keys: + out.append(_known[k].describe()) + return out + + +class PagedData: + """ PagedData - context for parsing a page of cgm data. - """ - - def __init__ (self, raw, model): - self.model = model - data, crc = raw[0:1022], raw[1022:] - computed = lib.CRC16CCITT.compute(bytearray(data)) - if lib.BangInt(crc) != computed: - assert lib.BangInt(crc) == computed, "CRC does not match page data" - - self.raw = raw - self.clean(data) - - def clean (self, data): - data.reverse( ) - self.data = self.eat_nulls(data) - self.stream = io.BufferedReader(io.BytesIO(self.data)) - - def eat_nulls (self, data): - i = 0 - while data[i] == 0x00: - i = i+1 - return data[i:] - -class HistoryPage (PagedData): - def clean (self, data): - # data.reverse( ) - # self.data = self.eat_nulls(data) - #self.data.reverse( ) - self.data = data[:] - # XXX: under some circumstances, zero is the correct value and - # eat_nulls actually eats valid data. This ugly hack restores two - # nulls back ot the end. """ + + def __init__(self, raw, model): + self.model = model + data, crc = raw[0:1022], raw[1022:] + computed = lib.CRC16CCITT.compute(bytearray(data)) + if lib.BangInt(crc) != computed: + assert lib.BangInt(crc) == computed, "CRC does not match page data" + + self.raw = raw + self.clean(data) + + def clean(self, data): + data.reverse() + self.data = self.eat_nulls(data) + self.stream = io.BufferedReader(io.BytesIO(self.data)) + + def eat_nulls(self, data): + i = 0 + while data[i] == 0x00: + i = i + 1 + return data[i:] + + +class HistoryPage(PagedData): + def clean(self, data): + # data.reverse( ) + # self.data = self.eat_nulls(data) + # self.data.reverse( ) + self.data = data[:] + # XXX: under some circumstances, zero is the correct value and + # eat_nulls actually eats valid data. This ugly hack restores two + # nulls back ot the end. + """ self.data.append(0x00) self.data.append(0x00) self.data.append(0x00) self.data.append(0x00) self.data.append(0x00) """ - self.stream = io.BufferedReader(io.BytesIO(self.data)) - def decode (self, larger=False): - records = [ ] - skipped = [ ] - for B in iter(lambda: bytearray(self.stream.read(2)), bytearray("")): - if B == bytearray( [ 0x00, 0x00 ] ): - if skipped: - if len(records) > 0: - last = records[-1] - last.update(appended=last.get('appended', [ ]) + skipped) - # records.extend(skipped) - skipped = [ ] - break - record = parse_record(self.stream, B, larger=larger, model=self.model) - data = record.decode( ) - if record.datetime: - rec = dict(timestamp=record.datetime.isoformat( ), - _type=str(record.__class__.__name__), - _body=lib.hexlify(record.body), - _head=lib.hexlify(record.head), - _date=lib.hexlify(record.date), - _description=str(record)) - if data is not None: - rec.update(data) - if skipped: - rec.update(appended=skipped) - skipped = [ ] - records.append(rec) - else: - rec = dict(_type=str(record.__class__.__name__), - _body=lib.hexlify(record.body), - _head=lib.hexlify(record.head), - _date=lib.hexlify(record.date), - _description=str(record)) - data = record.decode( ) - if data is not None: - rec.update(data=data) - skipped.append(rec) - records.reverse( ) - return records - -if __name__ == '__main__': - import doctest - doctest.testmod( ) + self.stream = io.BufferedReader(io.BytesIO(self.data)) + + def decode(self, larger=False): + records = [] + skipped = [] + for B in iter(lambda: bytearray(self.stream.read(2)), bytearray("")): + if B == bytearray([0x00, 0x00]): + if skipped: + if len(records) > 0: + last = records[-1] + last.update(appended=last.get("appended", []) + skipped) + # records.extend(skipped) + skipped = [] + break + record = parse_record(self.stream, B, larger=larger, model=self.model) + data = record.decode() + if record.datetime: + rec = dict( + timestamp=record.datetime.isoformat(), + _type=str(record.__class__.__name__), + _body=lib.hexlify(record.body), + _head=lib.hexlify(record.head), + _date=lib.hexlify(record.date), + _description=str(record), + ) + if data is not None: + rec.update(data) + if skipped: + rec.update(appended=skipped) + skipped = [] + records.append(rec) + else: + rec = dict( + _type=str(record.__class__.__name__), + _body=lib.hexlify(record.body), + _head=lib.hexlify(record.head), + _date=lib.hexlify(record.date), + _description=str(record), + ) + data = record.decode() + if data is not None: + rec.update(data=data) + skipped.append(rec) + records.reverse() + return records + + +if __name__ == "__main__": + import doctest + + doctest.testmod() ##### # EOF diff --git a/decocare/lib.py b/decocare/lib.py index 1da7aad..6fd8f4c 100644 --- a/decocare/lib.py +++ b/decocare/lib.py @@ -31,219 +31,230 @@ >>> BangInt( bytearray( [ 0x02, 0X02 ] ) ) 514 ->>> BangLong( bytearray( [ 0x0, 0X0, 0x02, 0x02 ] ) ) -514L +>>> BangLong( bytearray( [ 0x0, 0X0, 0x02, 0x02 ] ) ) # Python3 has no `long` integer anymore +514 """ - -from pprint import pformat -from datetime import time as clocks +import struct +from binascii import hexlify, unhexlify from datetime import datetime +from datetime import time as clocks +from pprint import pformat import dateutil.parser from dateutil import relativedelta -from binascii import unhexlify, hexlify -def _fmt_hex( bytez ): - return ' '.join( [ '%#04x' % x for x in list( bytez ) ] ) -def _fmt_txt( bytez ): - return ''.join( [ chr( x ) if 0x20 <= x < 0x7F else '.' \ - for x in bytez ] ) +def _fmt_hex(bytez): + return " ".join(["%#04x" % x for x in list(bytez)]) -def basal_time (raw): - midnight = clocks(0, 0) - offset = relativedelta.relativedelta(minutes=30*raw) - start = midnight.replace(hour=offset.hours, minute=offset.minutes) - return start -class Timer(object): - def __init__(self): - self.begin = datetime.now( ) - def millis(self): - dt = datetime.now() - self.begin - ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0 - return ms +def _fmt_txt(bytez): + return "".join([chr(x) if 0x20 <= x < 0x7F else "." for x in bytez]) -def format_filter_date (date): - """ + +def basal_time(raw): + midnight = clocks(0, 0) + offset = relativedelta.relativedelta(minutes=30 * raw) + start = midnight.replace(hour=offset.hours, minute=offset.minutes) + return start + + +class Timer: + def __init__(self): + self.begin = datetime.now() + + def millis(self): + dt = datetime.now() - self.begin + ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0 + return ms + + +def format_filter_date(date): + """ >>> format_filter_date(parse.date('2014-04-09')) [7, 222, 4, 9] - """ - params = [ HighByte(date.year), LowByte(date.year), - date.month, date.day ] - return params + """ + params = [HighByte(date.year), LowByte(date.year), date.month, date.day] + return params + + +def filter_date_today(): + return format_filter_date(datetime.now()) -def filter_date_today ( ): - return format_filter_date(datetime.now( )) class parse: - @staticmethod - def date( data ): - """ + @staticmethod + def date(data): + """ - >>> parse.date( '2010-11-10T01:46:00' ).isoformat( ) - '2010-11-10T01:46:00' + >>> parse.date( '2010-11-10T01:46:00' ).isoformat( ) + '2010-11-10T01:46:00' - >>> parse.date( '2010-11-10 01:46:00' ).isoformat( ) - '2010-11-10T01:46:00' + >>> parse.date( '2010-11-10 01:46:00' ).isoformat( ) + '2010-11-10T01:46:00' - >>> parse.date( '2010-11-10 01:46PM' ).isoformat( ) - '2010-11-10T13:46:00' + >>> parse.date( '2010-11-10 01:46PM' ).isoformat( ) + '2010-11-10T13:46:00' - >>> parse.date( '2010-11-10 13:46' ).isoformat( ) - '2010-11-10T13:46:00' + >>> parse.date( '2010-11-10 13:46' ).isoformat( ) + '2010-11-10T13:46:00' - >>> parse.date( '2010-11-10 1:46AM' ).isoformat( ) - '2010-11-10T01:46:00' + >>> parse.date( '2010-11-10 1:46AM' ).isoformat( ) + '2010-11-10T01:46:00' - """ - return dateutil.parser.parse( data ) - -def hexdump( src, length=8, indent=0 ): - """ - Return a string representing the bytes in src, length bytes per - line. - - """ - if len( src ) == 0: - return '' - result = [ ] - indent = ''.join( [ ' ' ] * indent ) - digits = 4 if isinstance( src, unicode ) else 2 - for i in xrange( 0, len( src ), length ): - s = src[i:i+length] - hexa = ' '.join( [ '%#04x' % x for x in list( s ) ] ) - text = ''.join( [ chr(x) if 0x20 <= x < 0x7F else '.' \ - for x in s ] ) - result.append( indent + "%04X %-*s %s" % \ - ( i, length * 5 - , hexa, text ) ) - return '\n'.join(result) + """ + return dateutil.parser.parse(data) -def int_dump(stream, indent=0): - """ - >>> int_dump(bytearray([0x01, 0x02])) - ' 1 2' +def hexdump(src, length=8, indent=0): + """ + Return a string representing the bytes in src, length bytes per line. + """ + if len(src) == 0: + return "" + result = [] + indent = "".join([" "] * indent) + digits = 4 if isinstance(src, str) else 2 + for i in range(0, len(src), length): + s = src[i : i + length] + hexa = " ".join(["%#04x" % x for x in list(s)]) + text = "".join([chr(x) if 0x20 <= x < 0x7F else "." for x in s]) + result.append(indent + "%04X %-*s %s" % (i, length * 5, hexa, text)) + return "\n".join(result) - """ - cells = [ '%#04s' % (x) for x in stream ] - lines = [ ] - indent = ''.join( [ ' ' ] * indent ) - while cells: - octet = cells[:8] - line = ' '.join(octet) - lines.append(indent + line) - cells = cells[8:] - out = ('\n').join([ line for line in lines ]) - return out +def int_dump(stream, indent=0): + """ + >>> int_dump(bytearray([0x01, 0x02])) + ' 1 2' + """ + cells = ["%#04s" % (x) for x in stream] + lines = [] + indent = "".join([" "] * indent) + while cells: + octet = cells[:8] + line = " ".join(octet) + lines.append(indent + line) + cells = cells[8:] + out = ("\n").join([line for line in lines]) + return out -def HighByte( arg ): - return arg >> 8 & 0xFF +def HighByte(arg): + return arg >> 8 & 0xFF -def LowByte( arg ): - return arg & 0xFF +def LowByte(arg): + return arg & 0xFF class CRC16CCITT: - lookup = [ 0, 4129, 8258, 12387, 16516, 20645, 24774, - 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, - 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, - 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, - 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, - 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, - 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, - 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, - 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, - 51660, 55789, 59790, 63919, 35144, 39273, 43274, 47403, - 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, - 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, - 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, - 60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, - 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, - 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, - 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, - 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, - 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, - 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, - 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, - 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, - 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, - 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, - 51565, 63758, 59695, 39368, 35305, 47498, 43435, 22596, - 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, - 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, - 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, - 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, - 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, - 65342, 53085, 57212, 44955, 49082, 36825, 40952, 28183, - 32310, 20053, 24180, 11923, 16050, 3793, 7920 ] - @classmethod - def compute( klass, block ): - result = 65535 - #result = 0 - for i in xrange( len( block ) ): - tmp = block[ i ] ^ result >> 8 - result = ( klass.lookup[ tmp ] ^ result << 8 ) & 0xFFFF - return result + # fmt:off + lookup = [ + 0, 4129, 8258, 12387, 16516, 20645, 24774, + 28903, 33032, 37161, 41290, 45419, 49548, 53677, 57806, + 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, + 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, + 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, + 42346, 46411, 34088, 38153, 58862, 62927, 50604, 54669, + 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, + 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, + 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, + 51660, 55789, 59790, 63919, 35144, 39273, 43274, 47403, + 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, + 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, + 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233, + 60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, + 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, + 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, + 37256, 33193, 45514, 41451, 53516, 49453, 61774, 57711, + 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, + 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, + 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, + 42443, 38312, 34185, 62830, 58703, 54572, 50445, 13538, + 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, + 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, + 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, + 51565, 63758, 59695, 39368, 35305, 47498, 43435, 22596, + 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, + 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, + 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, + 60687, 56684, 52557, 48554, 44427, 40424, 36297, 31782, + 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, + 65342, 53085, 57212, 44955, 49082, 36825, 40952, 28183, + 32310, 20053, 24180, 11923, 16050, 3793, 7920 + ] + # fmt:on + @classmethod + def compute(klass, block): + result = 65535 + # result = 0 + for i in range(len(block)): + tmp = block[i] ^ result >> 8 + result = (klass.lookup[tmp] ^ result << 8) & 0xFFFF + return result class CRC8: - lookup = [ 0, 155, 173, 54, 193, 90, 108, 247, 25, 130, 180, 47, - 216, 67, 117, 238, 50, 169, 159, 4, 243, 104, 94, 197, 43, 176, - 134, 29, 234, 113, 71, 220, 100, 255, 201, 82, 165, 62, 8, 147, - 125, 230, 208, 75, 188, 39, 17, 138, 86, 205, 251, 96, 151, 12, - 58, 161, 79, 212, 226, 121, 142, 21, 35, 184, 200, 83, 101, 254, - 9, 146, 164, 63, 209, 74, 124, 231, 16, 139, 189, 38, 250, 97, - 87, 204, 59, 160, 150, 13, 227, 120, 78, 213, 34, 185, 143, 20, - 172, 55, 1, 154, 109, 246, 192, 91, 181, 46, 24, 131, 116, 239, - 217, 66, 158, 5, 51, 168, 95, 196, 242, 105, 135, 28, 42, 177, - 70, 221, 235, 112, 11, 144, 166, 61, 202, 81, 103, 252, 18, 137, - 191, 36, 211, 72, 126, 229, 57, 162, 148, 15, 248, 99, 85, 206, - 32, 187, 141, 22, 225, 122, 76, 215, 111, 244, 194, 89, 174, 53, - 3, 152, 118, 237, 219, 64, 183, 44, 26, 129, 93, 198, 240, 107, - 156, 7, 49, 170, 68, 223, 233, 114, 133, 30, 40, 179, 195, 88, - 110, 245, 2, 153, 175, 52, 218, 65, 119, 236, 27, 128, 182, 45, - 241, 106, 92, 199, 48, 171, 157, 6, 232, 115, 69, 222, 41, 178, - 132, 31, 167, 60, 10, 145, 102, 253, 203, 80, 190, 37, 19, 136, - 127, 228, 210, 73, 149, 14, 56, 163, 84, 207, 249, 98, 140, 23, - 33, 186, 77, 214, 224, 123 ] - - @classmethod - def compute( klass, block ): - result = 0 - for i in xrange( len( block ) ): - result = klass.lookup[ ( result ^ block[ i ] & 0xFF ) ] - return result - - -def BangLong( bytez ): - ( a, b, c, d ) = bytez - l = a << 24 | b << 16 | c << 8 | d; - return long( l ) - + # fmt:off + lookup = [ + 0, 155, 173, 54, 193, 90, 108, 247, 25, 130, 180, 47, + 216, 67, 117, 238, 50, 169, 159, 4, 243, 104, 94, 197, 43, 176, + 134, 29, 234, 113, 71, 220, 100, 255, 201, 82, 165, 62, 8, 147, + 125, 230, 208, 75, 188, 39, 17, 138, 86, 205, 251, 96, 151, 12, + 58, 161, 79, 212, 226, 121, 142, 21, 35, 184, 200, 83, 101, 254, + 9, 146, 164, 63, 209, 74, 124, 231, 16, 139, 189, 38, 250, 97, + 87, 204, 59, 160, 150, 13, 227, 120, 78, 213, 34, 185, 143, 20, + 172, 55, 1, 154, 109, 246, 192, 91, 181, 46, 24, 131, 116, 239, + 217, 66, 158, 5, 51, 168, 95, 196, 242, 105, 135, 28, 42, 177, + 70, 221, 235, 112, 11, 144, 166, 61, 202, 81, 103, 252, 18, 137, + 191, 36, 211, 72, 126, 229, 57, 162, 148, 15, 248, 99, 85, 206, + 32, 187, 141, 22, 225, 122, 76, 215, 111, 244, 194, 89, 174, 53, + 3, 152, 118, 237, 219, 64, 183, 44, 26, 129, 93, 198, 240, 107, + 156, 7, 49, 170, 68, 223, 233, 114, 133, 30, 40, 179, 195, 88, + 110, 245, 2, 153, 175, 52, 218, 65, 119, 236, 27, 128, 182, 45, + 241, 106, 92, 199, 48, 171, 157, 6, 232, 115, 69, 222, 41, 178, + 132, 31, 167, 60, 10, 145, 102, 253, 203, 80, 190, 37, 19, 136, + 127, 228, 210, 73, 149, 14, 56, 163, 84, 207, 249, 98, 140, 23, + 33, 186, 77, 214, 224, 123 + ] + # fmt:on + + @classmethod + def compute(klass, block): + result = 0 + for i in range(len(block)): + result = klass.lookup[(result ^ block[i] & 0xFF)] + return result + + +def BangLong(bytez): + (a, b, c, d) = bytez + l = a << 24 | b << 16 | c << 8 | d + return int(l) + + +def BangInt(ints): + (x, y) = ints + return (x & 0xFF) << 8 | y & 0xFF -def BangInt( ints ): - ( x, y ) = ints - return ( x & 0xFF ) << 8 | y & 0xFF; def makeByte(highNibble, lowNibble): - """ + """ 0 <= highNibble <= 15 0 <= lowNibble <= 15 0 <= result <= 255 - """ - result = highNibble << 4 | lowNibble & 0xF - return result + """ + result = highNibble << 4 | lowNibble & 0xF + return result + +# fmt:off ENCODE_TABLE = [ 21, 49, 50, 35, 52, 37, 38, 22, 26, 25, 42, 11, 44, 13, 14, 28 ] @@ -266,47 +277,48 @@ def makeByte(highNibble, lowNibble): 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x56, 0xC5 ] - +# fmt:on _enc_test_2 = [0xA7, 0x47, 0x33, 0x62, 0x8D, 0x00, 0xA6] -_enc_result_2 = [0xA9, 0x6D, 0x16, 0x8E, 0x39, 0xB2, 0x68, 0xD5, 0x55, 0xAA, - 0x65] +_enc_result_2 = [0xA9, 0x6D, 0x16, 0x8E, 0x39, 0xB2, 0x68, 0xD5, 0x55, 0xAA, 0x65] + def encodeDC(msg): - """ + """ >>> encodeDC(_enc_test_1) == bytearray(_enc_result_1) True >>> encodeDC(_enc_test_2) == bytearray(_enc_result_2) True - """ - msg = bytearray(msg) - # realign bytes - nibbles = [ ] - result = [ ] - # collect nibbles - for b in msg: - highNibble = b >> 4 & 0xF - lowNibble = b & 0xF - dcValue1 = ENCODE_TABLE[highNibble] - dcValue2 = ENCODE_TABLE[lowNibble] - nibbles.append(dcValue1 >> 2) - - high2Bits = dcValue1 & 0x3 - low2Bits = dcValue2 >> 4 & 0x3 - nibbles.append( high2Bits << 2 | low2Bits ) - nibbles.append( dcValue2 & 0xF ) - - for i in xrange(0, len(nibbles), 2): - # last item gets a padding terminator - high, low = nibbles[i], 5 - # most elide the next item - if i < len(nibbles) - 1: - low = nibbles[i+1] - result.append(makeByte(high, low)) - return bytearray(result) - - + """ + msg = bytearray(msg) + # realign bytes + nibbles = [] + result = [] + # collect nibbles + for b in msg: + highNibble = b >> 4 & 0xF + lowNibble = b & 0xF + dcValue1 = ENCODE_TABLE[highNibble] + dcValue2 = ENCODE_TABLE[lowNibble] + nibbles.append(dcValue1 >> 2) + + high2Bits = dcValue1 & 0x3 + low2Bits = dcValue2 >> 4 & 0x3 + nibbles.append(high2Bits << 2 | low2Bits) + nibbles.append(dcValue2 & 0xF) + + for i in range(0, len(nibbles), 2): + # last item gets a padding terminator + high, low = nibbles[i], 5 + # most elide the next item + if i < len(nibbles) - 1: + low = nibbles[i + 1] + result.append(makeByte(high, low)) + return bytearray(result) + + +# fmt:off _decode_test_1 = [0xA9, 0x6D, 0x16, 0x8E, 0x39, 0xB2, 0x68, 0xD5, 0x59, 0x56, 0x38, 0xD6, 0x8F, 0x28, 0xF2, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, @@ -326,64 +338,70 @@ def encodeDC(msg): 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC2] -_decode_test_2 = [0xA9, 0x6D, 0x16, 0x8E, 0x39, 0xB2, 0x56, 0x65, 0x55, 0x56, - 0x35] +# fmt:on +_decode_test_2 = [0xA9, 0x6D, 0x16, 0x8E, 0x39, 0xB2, 0x56, 0x65, 0x55, 0x56, 0x35] _decode_result_2 = [0xA7, 0x47, 0x33, 0x62, 0x06, 0x00, 0x03] + + def decodeDC(msg): - """ + """ >>> decodeDC(_decode_test_1) == bytearray(_decode_result_1) True >>> decodeDC(_decode_test_2) == bytearray(_decode_result_2) True - """ - msg = bytearray(msg) - result = [ ] - nibbleCount = 0 - bitCount = 0 - sixBitValue = 0 - highValue = 0 - highNibble = 0 - # - for B in msg: - bP = 7 - while bP >= 0: - bitValue = B >> bP & 0x1 - sixBitValue = sixBitValue << 1 | bitValue - bitCount += 1 - if bitCount != 6: - bP -= 1 - continue; # next - nibbleCount += 1 - if nibbleCount == 1: - highNibble = decodeDCByte(sixBitValue) - else: - lowNibble = decodeDCByte(sixBitValue) - byteValue = makeByte(highNibble, lowNibble) - # append to result - result.append(byteValue) - nibbleCount = 0 - sixBitValue = 0 - bitCount = 0 - bP -= 1 - - return bytearray(result) + """ + msg = bytearray(msg) + result = [] + nibbleCount = 0 + bitCount = 0 + sixBitValue = 0 + highValue = 0 + highNibble = 0 + # + for B in msg: + bP = 7 + while bP >= 0: + bitValue = B >> bP & 0x1 + sixBitValue = sixBitValue << 1 | bitValue + bitCount += 1 + if bitCount != 6: + bP -= 1 + continue + # next + nibbleCount += 1 + if nibbleCount == 1: + highNibble = decodeDCByte(sixBitValue) + else: + lowNibble = decodeDCByte(sixBitValue) + byteValue = makeByte(highNibble, lowNibble) + # append to result + result.append(byteValue) + nibbleCount = 0 + sixBitValue = 0 + bitCount = 0 + bP -= 1 + + return bytearray(result) + def decodeDCByte(B): - # B should be 0 < B && B < 63 - # look up in decode table - return ENCODE_TABLE.index(B) + # B should be 0 < B && B < 63 + # look up in decode table + return ENCODE_TABLE.index(B) -def decode_hexline (line): - return bytearray(str(''.join(line.split( ))).decode('hex')) +def decode_hexline(line): + return bytearray(str("".join(line.split())).decode("hex")) -def hexbytes (hexstr): +def hexbytes(hexstr): return bytearray(unhexlify(hexstr)) -if __name__ == '__main__': - import doctest - doctest.testmod( ) + +if __name__ == "__main__": + import doctest + + doctest.testmod() ##### # EOF diff --git a/decocare/link.py b/decocare/link.py index 69cc8b7..1507e68 100644 --- a/decocare/link.py +++ b/decocare/link.py @@ -1,75 +1,82 @@ - # # TODO: move all constants to config module. # +import logging import serial -import logging -import lib -import fuser -io = logging.getLogger( ) + +from decocare import fuser, lib + +io = logging.getLogger() log = io.getChild(__name__) -class AlreadyInUseException (Exception): - pass - -class Link( object ): - __timeout__ = .500 - port = None - def __init__( self, port, timeout=None ): - if timeout is not None: - self.__timeout__ = timeout - if fuser.in_use(port): - raise AlreadyInUseException("{port} already in use".format(port=port)) - self.open( port, dsrdtr=True, rtscts=True ) - - - def open( self, newPort=False, **kwds ): - if newPort: - self.port = newPort - if 'timeout' not in kwds: - kwds['timeout'] = self.__timeout__ - kwds['rtscts'] = True - kwds['dsrdtr'] = True - - self.serial = serial.Serial( self.port, **kwds ) - - if self.serial.isOpen( ): - log.info( '{agent} opened serial port: {serial}'\ - .format( serial = repr( self.serial ), - agent =self.__class__.__name__ ) ) - - def close( self ): - io.info( 'closing serial port' ) - return self.serial.close( ) - - def write( self, string ): - r = self.serial.write( string ) - io.info( 'usb.write.len: %s\n%s' % ( len( string ), - lib.hexdump( bytearray( string ) ) ) ) - return r - - def read( self, c ): - r = self.serial.read( c ) - io.info( 'usb.read.len: %s' % ( len( r ) ) ) - io.info( 'usb.read.raw:\n%s' % ( lib.hexdump( bytearray( r ) ) ) ) - return r - - def readline( self ): - r = self.serial.readline( ) - io.info( 'usb.read.len: %s\n%s' % ( len( r ), - lib.hexdump( bytearray( r ) ) ) ) - return r - - def readlines( self ): - r = self.serial.readlines( ) - io.info( 'usb.read.len: %s\n%s' % ( len( r ), - lib.hexdump( bytearray( ''.join( r ) ) ) ) ) - return r - -if __name__ == '__main__': - import doctest - doctest.testmost( ) + +class AlreadyInUseException(Exception): + pass + + +class Link: + __timeout__ = 0.500 + port = None + + def __init__(self, port, timeout=None): + if timeout is not None: + self.__timeout__ = timeout + if fuser.in_use(port): + raise AlreadyInUseException("{port} already in use".format(port=port)) + self.open(port, dsrdtr=True, rtscts=True) + + def open(self, newPort=False, **kwds): + if newPort: + self.port = newPort + if "timeout" not in kwds: + kwds["timeout"] = self.__timeout__ + kwds["rtscts"] = True + kwds["dsrdtr"] = True + + self.serial = serial.Serial(self.port, **kwds) + + if self.serial.isOpen(): + log.info( + "{agent} opened serial port: {serial}".format( + serial=repr(self.serial), agent=self.__class__.__name__ + ) + ) + + def close(self): + io.info("closing serial port") + return self.serial.close() + + def write(self, string): + r = self.serial.write(string) + io.info( + "usb.write.len: {}\n{}".format(len(string), lib.hexdump(bytearray(string))) + ) + return r + + def read(self, c): + r = self.serial.read(c) + io.info("usb.read.len: %s" % (len(r))) + io.info("usb.read.raw:\n%s" % (lib.hexdump(bytearray(r)))) + return r + + def readline(self): + r = self.serial.readline() + io.info("usb.read.len: {}\n{}".format(len(r), lib.hexdump(bytearray(r)))) + return r + + def readlines(self): + r = self.serial.readlines() + io.info( + "usb.read.len: {}\n{}".format(len(r), lib.hexdump(bytearray("".join(r)))) + ) + return r + + +if __name__ == "__main__": + import doctest + + doctest.testmost() ##### diff --git a/decocare/models/__init__.py b/decocare/models/__init__.py index 25ca3d8..fb18b58 100644 --- a/decocare/models/__init__.py +++ b/decocare/models/__init__.py @@ -1,346 +1,384 @@ - -from decocare import commands, history, cgm -from decocare import lib import types -class Task (object): - def __init__ (self, msg, handler=None, **kwargs): - self.msg = msg - if handler: - self.func = handler - def __get__ (self, obj, objtype=None): - if obj is None: - return self - else: - return types.MethodType(self, obj) - def validate (self): - data = self.response.getData( ) - self.response.check_output(data) - @staticmethod - def func (self, response): - return response.getData( ) - def __call__ (self, inst, **kwds): - # print "__calll__", inst, self.func - # self.func( ) - self.response = inst.session.query(self.msg, **kwds) - self.validate( ) - return types.MethodType(self.func, inst)(self.response) - # return self.response.getData( ) - - @classmethod - def handler (klass, msg, **kwargs): - def closure (func): - return Task(msg, handler=func, **kwargs) - return closure - -class Cursor (object): - # Info - # Page - def __init__ (self, inst, **kwds): - self.inst = inst - self.kwds = kwds - def get_page_info (self): - self.info = self.inst.session.query(self.Info) - def download_page (self, num): - page = self.inst.session.query(self.Page, page=num) - for record in self.find_records(page): - yield record - def range (self, info): - raise NotImplemented( ) - def find_records (self, response): - raise NotImplemented( ) - def iter (self): - self.get_page_info( ) - for n in self.range(self.info.getData( )): - yield self.download_page(n) - -class PageIterator (Task): - - def __init__ (self, Cursor=None, handler=None): - self.Cursor = Cursor - if handler: - self.func = handler - - def __call__ (self, inst, **kwds): - self.pager = self.Cursor(inst, **kwds) - for page in self.pager.iter( ): - for record in page: - yield record - - @classmethod - def handler (klass, **kwargs): - def closure (func): - return klass(func, **kwargs) - return closure - - -class PumpModel (object): - bolus_strokes = 20 - basal_strokes = 40 - MMOL_DEFAULT = False - larger = False - Ian50Body = 30 - def __init__(self, model, session): - self.model = model - self.session = session - - read_model = Task(commands.ReadPumpModel) - read_status = Task(commands.ReadPumpStatus) - read_temp_basal = Task(commands.ReadBasalTemp) - read_settings = Task(commands.ReadSettings) - read_reservoir = Task(commands.ReadRemainingInsulin) - read_carb_ratios = Task(commands.ReadCarbRatios512) - read_bg_targets = Task(commands.ReadBGTargets) - read_insulin_sensitivies = Task(commands.ReadInsulinSensitivities) - read_insulin_sensitivities = Task(commands.ReadInsulinSensitivities) - read_current_glucose_pages = Task(commands.ReadCurGlucosePageNumber) - read_current_history_pages = Task(commands.ReadCurPageNumber) - suspend_pump = Task(commands.PumpSuspend) - resume_pump = Task(commands.PumpResume) - read_battery_status = Task(commands.ReadBatteryStatus) - - - KEYPAD = dict(ESC=commands.KeypadPush.ESC - , ACT= commands.KeypadPush.ACT - , UP= commands.KeypadPush.UP - , DOWN= commands.KeypadPush.DOWN - , EASY= commands.KeypadPush.EASY - ) - def press_key (self, key=None, **kwds): - press = self.KEYPAD.get(key, None) - err = "Tried to press {key}, but only support {keys}".format(key=key, keys=', '.join(self.KEYPAD.keys( ))) - assert press, err - # req = press(**kwds) - resp = self.session.query(press, **kwds) - packet = resp.getData( ) - raw = str(packet).encode('hex') - result = dict(raw=raw, received=len(packet) > 1, key=key) - - return result - def decode_unabsorbed (self, raw): - doses = [ ] - while raw and len(raw) > 2: - head, tail = raw[:3], raw[3:] - doses.append(self.decode_unabsorbed_component(*head) ) - raw = tail - return doses - - def decode_unabsorbed_component (self, amount, age, _curve,strokes=40.0): - curve = ((_curve & 0b110000) << 4) - unabsorbed = { 'amount': amount/strokes, - 'age': age + curve, - # 'curve': curve, - } - return unabsorbed - - @PageIterator.handler( ) - class iter_glucose_pages (Cursor): - Info = commands.ReadCurGlucosePageNumber - Page = commands.ReadGlucoseHistory - def range (self, info): - start = int(info['page']) - end = start - int(info['glucose']) - return xrange(start, end, -1) - def find_records (self, response): - page = cgm.PagedData.Data(response.data, larger=self.inst.larger) - return reversed(page.decode( )) - - @PageIterator.handler( ) - class iter_history_pages (Cursor): - Info = commands.ReadCurPageNumber - Page = commands.ReadHistoryData - def range (self, info): - start = 0 - end = int(info) - return xrange(start, end) - def find_records (self, response): - decoder = history.HistoryPage(response.data, self.inst) - records = decoder.decode( ) - return records - - filter_glucose_date = Task(commands.FilterGlucoseHistory.ISO) - filter_isig_date = Task(commands.FilterISIGHistory.ISO) - - @Task.handler(commands.ReadHistoryData) - def read_history_data (self, response): - decoder = history.HistoryPage(response.data, self) - records = decoder.decode( ) - return records - - @Task.handler(commands.ReadGlucoseHistory) - def read_glucose_data (self, response): - records = [ ] - page = cgm.PagedData.Data(response.data, larger=self.larger) - records.extend(page.decode( )) - return records - - @Task.handler(commands.ReadSettings) - def my_read_settings (self, response): - self.settings = response.getData( ) - return self.settings - - @Task.handler(commands.ReadRTC) - def read_clock (self, response): - clock = lib.parse.date(response.getData( )) - return clock - - @Task.handler(commands.SetRTC) - def _set_clock (self, response): - raw = response.getData( ) - # TODO: fix mmeowlink's zero responses - # print "SET RESPONSE RAW", raw, response - # clock = lib.parse.date(raw) - return raw - def set_clock (self, clock=None, **kwds): - params = commands.SetRTC.fmt_datetime(clock) - old = self.read_clock( ) - program = dict(requested=dict(clock=clock, params=list(params), old=old)) - results = self._set_clock(params=params, **kwds) - program.update(raw=list(results)) - program.update(clock=self.read_clock( )) - return program - - _set_temp_basal = Task(commands.TempBasal.Program) - - _bolus = Task(commands.Bolus) - strokes_per_unit = 10 - def bolus (self, units=None, **kwds): - params = self.fmt_bolus_params(units) - program = dict(requested=dict(units=units, params=list(params))) - results = self._bolus(params=params, **kwds) - program.update(**results) - program.update(**self.read_status( )) - return program - def fmt_bolus_params (self, units): - strokes = int(float(units) * self.strokes_per_unit) - if (self.larger or self.strokes_per_unit > 10): - return [lib.HighByte(strokes), lib.LowByte(strokes)] - return [strokes] - - def set_temp_basal (self, rate=None, duration=None, temp=None, **kwds): - basals = dict(rate=rate, duration=duration, temp=temp) - result = self._set_temp_basal(**basals) - if not result.get('recieved'): - result.update(requested=basals) - result.update(**self.read_temp_basal( )) - return result - - -class Model508 (PumpModel): - MMOL_DEFAULT = False - old6cBody = 31 - # XXX: hack to return something. - def read_status (self, **kwds): - number = self.read_model( ) - status = dict(model=number, error="not supported") - return status - -class Model511 (Model508): - read_basal_profile_std = Task(commands.ReadProfiles511_STD) - read_basal_profile_a = Task(commands.ReadProfiles511_A) - read_basal_profile_b = Task(commands.ReadProfiles511_B) - - def read_selected_basal_profile (self, **kwds): - settings = self.read_settings( ) - selected = settings['selected_pattern'] - patterns = { - 0 : self.read_basal_profile_std - , 1 : self.read_basal_profile_a - , 2 : self.read_basal_profile_b - } - return patterns[selected](**kwds) - - -class Model512 (Model511): - MMOL_DEFAULT = False - read_basal_profile_std = Task(commands.ReadProfile_STD512) - read_basal_profile_a = Task(commands.ReadProfile_A512) - read_basal_profile_b = Task(commands.ReadProfile_B512) - - -class Model515 (Model512): - MMOL_DEFAULT = False - read_bg_targets = Task(commands.ReadBGTargets515) - read_status = Task(commands.ReadPumpStatus) - pass - -class Model715 (Model515): - MMOL_DEFAULT = False - pass - -class Model522 (Model515): - MMOL_DEFAULT = False - old6cBody = 38 - pass - -class Model722 (Model522): - MMOL_DEFAULT = False - pass - -class Model523 (Model522): - strokes_per_unit = 40 - larger = True - read_carb_ratios = Task(commands.ReadCarbRatios) - read_reservoir = Task(commands.ReadRemainingInsulin523) - read_settings = Task(commands.ReadSettings523) - -class Model723 (Model523): - pass - -class Model530 (Model523): - Ian50Body = 34 - pass - -class Model730 (Model530): - Ian50Body = 34 - pass - -class Model540 (Model530): - MMOL_DEFAULT = True - pass - -class Model740 (Model540): - MMOL_DEFAULT = True - pass - -class Model551 (Model540): - MMOL_DEFAULT = True - pass - -class Model751 (Model551): - MMOL_DEFAULT = True - pass - -class Model554 (Model551): - MMOL_DEFAULT = True - pass - -class Model754 (Model554): - MMOL_DEFAULT = True - pass +from decocare import cgm, commands, history, lib + + +class Task: + def __init__(self, msg, handler=None, **kwargs): + self.msg = msg + if handler: + self.func = handler + + def __get__(self, obj, objtype=None): + if obj is None: + return self + else: + return types.MethodType(self, obj) + + def validate(self): + data = self.response.getData() + self.response.check_output(data) + + @staticmethod + def func(self, response): + return response.getData() + + def __call__(self, inst, **kwds): + # print("__call__", inst, self.func) + # self.func( ) + self.response = inst.session.query(self.msg, **kwds) + self.validate() + return types.MethodType(self.func, inst)(self.response) + # return self.response.getData( ) + + @classmethod + def handler(klass, msg, **kwargs): + def closure(func): + return Task(msg, handler=func, **kwargs) + + return closure + + +class Cursor: + # Info + # Page + def __init__(self, inst, **kwds): + self.inst = inst + self.kwds = kwds + + def get_page_info(self): + self.info = self.inst.session.query(self.Info) + + def download_page(self, num): + page = self.inst.session.query(self.Page, page=num) + yield from self.find_records(page) + + def range(self, info): + raise NotImplemented() + + def find_records(self, response): + raise NotImplemented() + + def iter(self): + self.get_page_info() + for n in self.range(self.info.getData()): + yield self.download_page(n) + + +class PageIterator(Task): + def __init__(self, Cursor=None, handler=None): + self.Cursor = Cursor + if handler: + self.func = handler + + def __call__(self, inst, **kwds): + self.pager = self.Cursor(inst, **kwds) + for page in self.pager.iter(): + yield from page + + @classmethod + def handler(klass, **kwargs): + def closure(func): + return klass(func, **kwargs) + + return closure + + +class PumpModel: + bolus_strokes = 20 + basal_strokes = 40 + MMOL_DEFAULT = False + larger = False + Ian50Body = 30 + + def __init__(self, model, session): + self.model = model + self.session = session + + read_model = Task(commands.ReadPumpModel) + read_status = Task(commands.ReadPumpStatus) + read_temp_basal = Task(commands.ReadBasalTemp) + read_settings = Task(commands.ReadSettings) + read_reservoir = Task(commands.ReadRemainingInsulin) + read_carb_ratios = Task(commands.ReadCarbRatios512) + read_bg_targets = Task(commands.ReadBGTargets) + read_insulin_sensitivies = Task(commands.ReadInsulinSensitivities) + read_insulin_sensitivities = Task(commands.ReadInsulinSensitivities) + read_current_glucose_pages = Task(commands.ReadCurGlucosePageNumber) + read_current_history_pages = Task(commands.ReadCurPageNumber) + suspend_pump = Task(commands.PumpSuspend) + resume_pump = Task(commands.PumpResume) + read_battery_status = Task(commands.ReadBatteryStatus) + + KEYPAD = dict( + ESC=commands.KeypadPush.ESC, + ACT=commands.KeypadPush.ACT, + UP=commands.KeypadPush.UP, + DOWN=commands.KeypadPush.DOWN, + EASY=commands.KeypadPush.EASY, + ) + + def press_key(self, key=None, **kwds): + press = self.KEYPAD.get(key, None) + err = "Tried to press {key}, but only support {keys}".format( + key=key, keys=", ".join(list(self.KEYPAD.keys())) + ) + assert press, err + # req = press(**kwds) + resp = self.session.query(press, **kwds) + packet = resp.getData() + raw = str(packet).encode("hex") + result = dict(raw=raw, received=len(packet) > 1, key=key) + + return result + + def decode_unabsorbed(self, raw): + doses = [] + while raw and len(raw) > 2: + head, tail = raw[:3], raw[3:] + doses.append(self.decode_unabsorbed_component(*head)) + raw = tail + return doses + + def decode_unabsorbed_component(self, amount, age, _curve, strokes=40.0): + curve = (_curve & 0b110000) << 4 + unabsorbed = { + "amount": amount / strokes, + "age": age + curve, + # 'curve': curve, + } + return unabsorbed + + @PageIterator.handler() + class iter_glucose_pages(Cursor): + Info = commands.ReadCurGlucosePageNumber + Page = commands.ReadGlucoseHistory + + def range(self, info): + start = int(info["page"]) + end = start - int(info["glucose"]) + return range(start, end, -1) + + def find_records(self, response): + page = cgm.PagedData.Data(response.data, larger=self.inst.larger) + return reversed(page.decode()) + + @PageIterator.handler() + class iter_history_pages(Cursor): + Info = commands.ReadCurPageNumber + Page = commands.ReadHistoryData + + def range(self, info): + start = 0 + end = int(info) + return range(start, end) + + def find_records(self, response): + decoder = history.HistoryPage(response.data, self.inst) + records = decoder.decode() + return records + + filter_glucose_date = Task(commands.FilterGlucoseHistory.ISO) + filter_isig_date = Task(commands.FilterISIGHistory.ISO) + + @Task.handler(commands.ReadHistoryData) + def read_history_data(self, response): + decoder = history.HistoryPage(response.data, self) + records = decoder.decode() + return records + + @Task.handler(commands.ReadGlucoseHistory) + def read_glucose_data(self, response): + records = [] + page = cgm.PagedData.Data(response.data, larger=self.larger) + records.extend(page.decode()) + return records + + @Task.handler(commands.ReadSettings) + def my_read_settings(self, response): + self.settings = response.getData() + return self.settings + + @Task.handler(commands.ReadRTC) + def read_clock(self, response): + clock = lib.parse.date(response.getData()) + return clock + + @Task.handler(commands.SetRTC) + def _set_clock(self, response): + raw = response.getData() + # TODO: fix mmeowlink's zero responses + # print("SET RESPONSE RAW", raw, response) + # clock = lib.parse.date(raw) + return raw + + def set_clock(self, clock=None, **kwds): + params = commands.SetRTC.fmt_datetime(clock) + old = self.read_clock() + program = dict(requested=dict(clock=clock, params=list(params), old=old)) + results = self._set_clock(params=params, **kwds) + program.update(raw=list(results)) + program.update(clock=self.read_clock()) + return program + + _set_temp_basal = Task(commands.TempBasal.Program) + + _bolus = Task(commands.Bolus) + strokes_per_unit = 10 + + def bolus(self, units=None, **kwds): + params = self.fmt_bolus_params(units) + program = dict(requested=dict(units=units, params=list(params))) + results = self._bolus(params=params, **kwds) + program.update(**results) + program.update(**self.read_status()) + return program + + def fmt_bolus_params(self, units): + strokes = int(float(units) * self.strokes_per_unit) + if self.larger or self.strokes_per_unit > 10: + return [lib.HighByte(strokes), lib.LowByte(strokes)] + return [strokes] + + def set_temp_basal(self, rate=None, duration=None, temp=None, **kwds): + basals = dict(rate=rate, duration=duration, temp=temp) + result = self._set_temp_basal(**basals) + if not result.get("recieved"): + result.update(requested=basals) + result.update(**self.read_temp_basal()) + return result + + +class Model508(PumpModel): + MMOL_DEFAULT = False + old6cBody = 31 + # XXX: hack to return something. + def read_status(self, **kwds): + number = self.read_model() + status = dict(model=number, error="not supported") + return status + + +class Model511(Model508): + read_basal_profile_std = Task(commands.ReadProfiles511_STD) + read_basal_profile_a = Task(commands.ReadProfiles511_A) + read_basal_profile_b = Task(commands.ReadProfiles511_B) + + def read_selected_basal_profile(self, **kwds): + settings = self.read_settings() + selected = settings["selected_pattern"] + patterns = { + 0: self.read_basal_profile_std, + 1: self.read_basal_profile_a, + 2: self.read_basal_profile_b, + } + return patterns[selected](**kwds) + + +class Model512(Model511): + MMOL_DEFAULT = False + read_basal_profile_std = Task(commands.ReadProfile_STD512) + read_basal_profile_a = Task(commands.ReadProfile_A512) + read_basal_profile_b = Task(commands.ReadProfile_B512) + + +class Model515(Model512): + MMOL_DEFAULT = False + read_bg_targets = Task(commands.ReadBGTargets515) + read_status = Task(commands.ReadPumpStatus) + pass + + +class Model715(Model515): + MMOL_DEFAULT = False + pass + + +class Model522(Model515): + MMOL_DEFAULT = False + old6cBody = 38 + pass + + +class Model722(Model522): + MMOL_DEFAULT = False + pass + + +class Model523(Model522): + strokes_per_unit = 40 + larger = True + read_carb_ratios = Task(commands.ReadCarbRatios) + read_reservoir = Task(commands.ReadRemainingInsulin523) + read_settings = Task(commands.ReadSettings523) + + +class Model723(Model523): + pass + + +class Model530(Model523): + Ian50Body = 34 + pass + + +class Model730(Model530): + Ian50Body = 34 + pass + + +class Model540(Model530): + MMOL_DEFAULT = True + pass + + +class Model740(Model540): + MMOL_DEFAULT = True + pass + + +class Model551(Model540): + MMOL_DEFAULT = True + pass + + +class Model751(Model551): + MMOL_DEFAULT = True + pass + + +class Model554(Model551): + MMOL_DEFAULT = True + pass + + +class Model754(Model554): + MMOL_DEFAULT = True + pass + known = { - '508': Model508 -, '511': Model511 -, '512': Model512 -, '515': Model515 -, '522': Model522 -, '523': Model523 -, '530': Model530 -, '540': Model540 -, '551': Model551 -, '554': Model554 -, '715': Model715 -, '722': Model722 -, '723': Model723 -, '723': Model723 -, '730': Model730 -, '740': Model740 -, '751': Model751 -, '754': Model754 + "508": Model508, + "511": Model511, + "512": Model512, + "515": Model515, + "522": Model522, + "523": Model523, + "530": Model530, + "540": Model540, + "551": Model551, + "554": Model554, + "715": Model715, + "722": Model722, + "723": Model723, + "723": Model723, + "730": Model730, + "740": Model740, + "751": Model751, + "754": Model754, } -def lookup (model, session): - klass = known.get(model, PumpModel) - return klass(model, session) +def lookup(model, session): + klass = known.get(model, PumpModel) + return klass(model, session) diff --git a/decocare/records/__init__.py b/decocare/records/__init__.py index 5f14327..a42c60e 100644 --- a/decocare/records/__init__.py +++ b/decocare/records/__init__.py @@ -1,5 +1,3 @@ - -from times import * -from base import * -from bolus import * - +from decocare.records.base import * +from decocare.records.bolus import * +from decocare.records.times import * diff --git a/decocare/records/base.py b/decocare/records/base.py index 71affde..ddc6409 100644 --- a/decocare/records/base.py +++ b/decocare/records/base.py @@ -1,86 +1,104 @@ from datetime import datetime -#from .. import lib + from decocare import lib -from times import * -class Base(object): - """ +from decocare.records.times import * + + +class Base: + """ >>> str( Base( bytearray([ 0x00, 0x00 ]) ) ) 'Base unknown head[2], body[0] op[0x00]' - Each record in the history seems to have a two byte head, possibly - some arguments, then a 5 byte description of the datetime, then - maybe a body. The most reliable way to identify records so far, - seems to be through the 2 byte head. - - """ - head_length = 2 - body_length = 0 - date_length = 5 - def __init__(self, head=bytearray( ), model=None, larger=False): - self.larger = getattr(model, 'larger', larger) - self.model = model - self.bolus = bytearray( ) - self.opcode = head[0] - self.head = head - self.date = bytearray( ) - self.datetime = None - self.body = bytearray( ) - - @classmethod - def describe(klass): - opstring = "0x%02x" % (getattr(klass, 'opcode', 0)) - name = klass.__name__ - out = [ klass.head_length, klass.date_length, klass.body_length ] - return ",".join([ opstring, name ] + map(str, out)) - - def __str__(self): - name = self.__class__.__name__ - lengths = 'head[{}], body[{}]'.format(len(self.head), len(self.body)) - # opcodes = ' '.join(['%#04x' % x for x in self.head[:1]]) - opcodes = 'op[%#04x]' % self.opcode - return ' '.join([name, self.date_str( ), lengths, opcodes ]) - - def date_str(self): - result = 'unknown' - if self.datetime is not None: - result = self.datetime.isoformat( ) - else: - if len(self.date) == 5: - result = "{}".format(unmask_date(self.date)) - return result - - def min_length(self): - return self.head_length + self.date_length - def parse(self, bolus): - if len(bolus) < self.min_length( ): - return - head_length = self.head_length - date_length = self.date_length - - self.bolus = bolus - self.head = bolus[:head_length] - body_offset = head_length + date_length - self.date = bolus[head_length:body_offset] - self.body = bolus[body_offset:] - return self.decode( ) - - def decode(self): - pass + Each record in the history seems to have a two byte head, possibly + some arguments, then a 5 byte description of the datetime, then + maybe a body. The most reliable way to identify records so far, + seems to be through the 2 byte head. - def pformat(self, prefix=''): - head = '\n'.join([ " op hex (%s)" % len(self.head), lib.hexdump(self.head, indent=4), - " decimal", lib.int_dump(self.head, indent=11) ]) - date = '\n'.join([ " datetime (%s)" % self.date_str( ), - lib.hexdump(self.date, indent=4) ]) - - body = " body (%s)" % len(self.body) - if len(self.body) > 0: - body = '\n'.join([ body, - " hex", lib.hexdump(self.body, indent=4), - " decimal", lib.int_dump(self.body, indent=11) ]) - extra = [ ] """ + + head_length = 2 + body_length = 0 + date_length = 5 + + def __init__(self, head=bytearray(), model=None, larger=False): + self.larger = getattr(model, "larger", larger) + self.model = model + self.bolus = bytearray() + self.opcode = head[0] + self.head = head + self.date = bytearray() + self.datetime = None + self.body = bytearray() + + @classmethod + def describe(klass): + opstring = "0x%02x" % (getattr(klass, "opcode", 0)) + name = klass.__name__ + out = [klass.head_length, klass.date_length, klass.body_length] + return ",".join([opstring, name] + list(map(str, out))) + + def __str__(self): + name = self.__class__.__name__ + lengths = "head[{}], body[{}]".format(len(self.head), len(self.body)) + # opcodes = ' '.join(['%#04x' % x for x in self.head[:1]]) + opcodes = "op[%#04x]" % self.opcode + return " ".join([name, self.date_str(), lengths, opcodes]) + + def date_str(self): + result = "unknown" + if self.datetime is not None: + result = self.datetime.isoformat() + else: + if len(self.date) == 5: + result = "{}".format(unmask_date(self.date)) + return result + + def min_length(self): + return self.head_length + self.date_length + + def parse(self, bolus): + if len(bolus) < self.min_length(): + return + head_length = self.head_length + date_length = self.date_length + + self.bolus = bolus + self.head = bolus[:head_length] + body_offset = head_length + date_length + self.date = bolus[head_length:body_offset] + self.body = bolus[body_offset:] + return self.decode() + + def decode(self): + pass + + def pformat(self, prefix=""): + head = "\n".join( + [ + " op hex (%s)" % len(self.head), + lib.hexdump(self.head, indent=4), + " decimal", + lib.int_dump(self.head, indent=11), + ] + ) + date = "\n".join( + [" datetime (%s)" % self.date_str(), lib.hexdump(self.date, indent=4)] + ) + + body = " body (%s)" % len(self.body) + if len(self.body) > 0: + body = "\n".join( + [ + body, + " hex", + lib.hexdump(self.body, indent=4), + " decimal", + lib.int_dump(self.body, indent=11), + ] + ) + extra = [] + """ hour_bits = self.date[1:] and extra_hour_bits(self.date[1]) or [ ] year_bits = self.date[4:] and extra_year_bits(self.date[4]) or [ ] day_bits = self.date[3:] and extra_hour_bits(self.date[3]) or [ ] @@ -91,59 +109,69 @@ def pformat(self, prefix=''): if 1 in year_bits: extra.append("YEAR BITS: {}".format(str(year_bits))) """ - decoded = None - if len(self.head + self.date + self.body) >= self.min_length( ): - decoded = self.decode( ) - decode_msg = '' - if decoded is not None: - decode_msg = '\n'.join([ '###### DECODED', - '```python', - '{}'.format(lib.pformat(self.decode( ))), - '```', ]) - if extra: - extra = ' ' + ' '.join(extra) - else: - extra = '' - return '\n'.join([ prefix, decode_msg, head, date, body, extra ]) + decoded = None + if len(self.head + self.date + self.body) >= self.min_length(): + decoded = self.decode() + decode_msg = "" + if decoded is not None: + decode_msg = "\n".join( + [ + "###### DECODED", + "```python", + "{}".format(lib.pformat(self.decode())), + "```", + ] + ) + if extra: + extra = " " + " ".join(extra) + else: + extra = "" + return "\n".join([prefix, decode_msg, head, date, body, extra]) class VariableHead(Base): - def __init__(self, head, model=None): - Base.__init__(self, head, model) - self.head_length = head[1] + def __init__(self, head, model=None): + Base.__init__(self, head, model) + self.head_length = head[1] -class KnownRecord(Base): - opcode = 0x00 - decodes_date = True - def parse_time(self): - if len(self.date) == 5: - self.datetime = parse_date(self.date) +class KnownRecord(Base): + opcode = 0x00 + decodes_date = True - def decode(self): - self.parse_time( ) + def parse_time(self): + if len(self.date) == 5: + self.datetime = parse_date(self.date) + def decode(self): + self.parse_time() class InvalidRecord(KnownRecord): - pass - # 0x0c: 22, - # 0x6d: 46, - # 0x6d: 46 - 5, + pass + # 0x0c: 22, + # 0x6d: 46, + # 0x6d: 46 - 5, + class Prime(KnownRecord): - opcode = 0x03 - head_length = 5 - def decode(self): - self.parse_time( ) - amount = self.head[4] / 10.0 - fixed = self.head[2] / 10.0 - t = {0:'manual'}.get(fixed, 'fixed') - prime = { 'type': t, - 'amount': amount, - 'fixed': fixed, } - return prime - -if __name__ == '__main__': - import doctest - doctest.testmod( ) + opcode = 0x03 + head_length = 5 + + def decode(self): + self.parse_time() + amount = self.head[4] / 10.0 + fixed = self.head[2] / 10.0 + t = {0: "manual"}.get(fixed, "fixed") + prime = { + "type": t, + "amount": amount, + "fixed": fixed, + } + return prime + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/decocare/records/bolus.py b/decocare/records/bolus.py index f198352..449ec33 100644 --- a/decocare/records/bolus.py +++ b/decocare/records/bolus.py @@ -1,243 +1,291 @@ -from base import KnownRecord, VariableHead -from decocare import lib -# from .. import lib -from times import extra_year_bits from pprint import pformat +from decocare import lib + +from decocare.records.base import KnownRecord, VariableHead +from decocare.records.times import extra_year_bits + + class Bolus(KnownRecord): - """ - >>> rec = Bolus(Bolus._test_1[:4]) - >>> decoded = rec.parse(Bolus._test_1) - >>> print str(rec) - Bolus 2012-12-18T15:05:28 head[4], body[0] op[0x01] - - >>> print pformat(decoded) - {'amount': 5.6, 'duration': 0, 'programmed': 5.6, 'type': 'normal'} - - """ - _test_1 = bytearray([ 0x01, 0x38, 0x38, 0x00, - 0xdc, 0x05, 0x4f, 0x12, 0x0c, ]) - opcode = 0x01 - head_length = 4 - def __init__(self, head, larger=False): - super(Bolus, self).__init__(head, larger) - # self.larger = larger - if self.larger: - self.head_length = 8 - def decode(self): - self.parse_time( ) - dose = { - 'amount': self.head[2]/10.0, - 'programmed': self.head[1]/10.0, - 'duration': self.head[3] * 30, - 'type': self.head[3] > 0 and 'square' or 'normal' - } - if self.larger: - duration = self.head[7] * 30 - dose = { 'amount': lib.BangInt(self.head[3:5])/40.0, - 'programmed': lib.BangInt(self.head[1:3])/40.0, - 'unabsorbed': lib.BangInt(self.head[5:7])/40.0, - 'duration': duration, - 'type': duration > 0 and 'square' or 'normal', - } - return dose + """ + >>> rec = Bolus(Bolus._test_1[:4]) + >>> decoded = rec.parse(Bolus._test_1) + >>> print(str(rec)) + Bolus 2012-12-18T15:05:28 head[4], body[0] op[0x01] + + >>> print(pformat(decoded)) + {'amount': 5.6, 'duration': 0, 'programmed': 5.6, 'type': 'normal'} + + """ + + _test_1 = bytearray([0x01, 0x38, 0x38, 0x00, 0xDC, 0x05, 0x4F, 0x12, 0x0C,]) + opcode = 0x01 + head_length = 4 + + def __init__(self, head, larger=False): + super().__init__(head, larger) + # self.larger = larger + if self.larger: + self.head_length = 8 + + def decode(self): + self.parse_time() + dose = { + "amount": self.head[2] / 10.0, + "programmed": self.head[1] / 10.0, + "duration": self.head[3] * 30, + "type": self.head[3] > 0 and "square" or "normal", + } + if self.larger: + duration = self.head[7] * 30 + dose = { + "amount": lib.BangInt(self.head[3:5]) / 40.0, + "programmed": lib.BangInt(self.head[1:3]) / 40.0, + "unabsorbed": lib.BangInt(self.head[5:7]) / 40.0, + "duration": duration, + "type": duration > 0 and "square" or "normal", + } + return dose + class BolusWizard(KnownRecord): - """ - Decode/parse bolus wizard records. - - >>> from decocare import models - >>> rec = BolusWizard(BolusWizard._test_1[:2], model=models.PumpModel('522', None)) - >>> decoded = rec.parse(BolusWizard._test_1) - >>> print str(rec) - BolusWizard 2013-01-20T13:07:45 head[2], body[13] op[0x5b] - - >>> print pformat(decoded) - {'_byte[5]': 0, - '_byte[7]': 0, - 'bg': 108, - 'bg_target_high': 125, - 'bg_target_low': 106, - 'bolus_estimate': 1.1, - 'carb_input': 15, - 'carb_ratio': 13, - 'correction_estimate': 0.0, - 'food_estimate': 1.1, - 'sensitivity': 45, - 'unabsorbed_insulin_count': '??', - 'unabsorbed_insulin_total': 4.8, - 'unknown_byte[10]': 0, - 'unknown_byte[8]': 0} - - """ - # missing unabsorbed_insulin_count = 4 - _test_1 = bytearray([ 0x5b, 0x6c, - 0x2d, 0x47, 0x0d, 0x14, 0x0d, - 0x0f, 0x50, 0x0d, 0x2d, 0x6a, 0x00, 0x0b, 0x00, - 0x00, 0x30, 0x00, 0x0b, 0x7d, ]) - - _test_2 = bytearray([ 0x5b, 0x8b, - 0xdc, 0x05, 0x0f, 0x12, 0x0c, - 0x45, 0x50, 0x0d, 0x2d, 0x6a, 0x03, 0x35, 0x00, - 0x00, 0x00, 0x00, 0x38, 0x7d, ]) - opcode = 0x5b - body_length = 13 - def __init__(self, head, model=None): - super(BolusWizard, self).__init__(head, model) - # self.larger = larger - self.MMOL_DEFAULT = model.MMOL_DEFAULT - if self.larger: - self.body_length = 15 - def decode(self): - self.parse_time( ) - bg = lib.BangInt([ self.body[1] & 0x0f, self.head[1] ]) - carb_input = int(self.body[0]) - # XXX: I have no idea if this is correct; it seems to produce correct results. - correction = ( twos_comp( self.body[7], 8 ) - + twos_comp( self.body[5] & 0x0f, 8 ) ) / 10.0 - wizard = { 'bg': bg, 'carb_input': carb_input, - 'carb_ratio': int(self.body[2]), - 'sensitivity': int(self.body[3]), - 'bg_target_low': int(self.body[4]), - 'bg_target_high': int(self.body[12]), - 'bolus_estimate': int(self.body[11])/10.0, - 'food_estimate': int(self.body[6])/10.0, - 'unabsorbed_insulin_total': int(self.body[9])/10.0, - 'unabsorbed_insulin_count': '??', - 'correction_estimate': correction, - '_byte[5]': self.body[5], - '_byte[7]': int(self.body[7]), # - 'unknown_byte[8]': self.body[8], - 'unknown_byte[10]': self.body[10], - # '??': '??', - # 'unabsorbed_insulin_total': int(self.body[9])/10.0, - # 'food_estimate': int(self.body[0]), - } - - if self.larger: - # correction = ( twos_comp( self.body[6], (self.body[9] & 0x38) << 5 ) ) / 40.0 - bg = ((self.body[1] & 0x03) << 8) + self.head[1] - carb_input = ((self.body[1] & 0x0c) << 6) + self.body[0] - carb_ratio = (((self.body[2] & 0x07) << 8) + self.body[3]) / 10.0 - # xxx: not sure about this - # https://github.com/ps2/minimed_rf/blob/master/lib/minimed_rf/log_entries/bolus_wizard.rb#L102 - sensitivity = int(self.body[4]) - wizard = { 'bg': bg, 'carb_input': carb_input, - 'carb_ratio': carb_ratio, - 'sensitivity': sensitivity, - 'bg_target_low': int(self.body[5]), - 'bg_target_high': int(self.body[14]), - # 'bolus_estimate': int(self.body[13])/40.0, - - 'correction_estimate': (((self.body[9] & 0x38) << 5) + self.body[6]) / 40.0, - # 'correction_maybe_estimate': correction, - - 'food_estimate': insulin_decode(self.body[7], self.body[8]), - 'unabsorbed_insulin_total': insulin_decode(self.body[10], self.body[11]), - 'bolus_estimate': insulin_decode(self.body[12], self.body[13]), - # 'unknown_bytes': map(int, list(self.body)), - } - - if self.MMOL_DEFAULT: - for key in [ 'bg', 'bg_target_high', 'bg_target_low', 'sensitivity' ]: - wizard[key] = wizard[key] / 10.0 - return wizard - -def insulin_decode (a, b, strokes=40.0): - return ((a << 8) + b) / strokes + """ + Decode/parse bolus wizard records. + + >>> from decocare import models + >>> rec = BolusWizard(BolusWizard._test_1[:2], model=models.PumpModel('522', None)) + >>> decoded = rec.parse(BolusWizard._test_1) + >>> print(str(rec)) + BolusWizard 2013-01-20T13:07:45 head[2], body[13] op[0x5b] + + >>> print(pformat(decoded)) + {'_byte[5]': 0, + '_byte[7]': 0, + 'bg': 108, + 'bg_target_high': 125, + 'bg_target_low': 106, + 'bolus_estimate': 1.1, + 'carb_input': 15, + 'carb_ratio': 13, + 'correction_estimate': 0.0, + 'food_estimate': 1.1, + 'sensitivity': 45, + 'unabsorbed_insulin_count': '??', + 'unabsorbed_insulin_total': 4.8, + 'unknown_byte[10]': 0, + 'unknown_byte[8]': 0} + + """ + + # missing unabsorbed_insulin_count = 4 + _test_1 = bytearray( + [ + 0x5B, + 0x6C, + 0x2D, + 0x47, + 0x0D, + 0x14, + 0x0D, + 0x0F, + 0x50, + 0x0D, + 0x2D, + 0x6A, + 0x00, + 0x0B, + 0x00, + 0x00, + 0x30, + 0x00, + 0x0B, + 0x7D, + ] + ) + + _test_2 = bytearray( + [ + 0x5B, + 0x8B, + 0xDC, + 0x05, + 0x0F, + 0x12, + 0x0C, + 0x45, + 0x50, + 0x0D, + 0x2D, + 0x6A, + 0x03, + 0x35, + 0x00, + 0x00, + 0x00, + 0x00, + 0x38, + 0x7D, + ] + ) + opcode = 0x5B + body_length = 13 + + def __init__(self, head, model=None): + super().__init__(head, model) + # self.larger = larger + self.MMOL_DEFAULT = model.MMOL_DEFAULT + if self.larger: + self.body_length = 15 + + def decode(self): + self.parse_time() + bg = lib.BangInt([self.body[1] & 0x0F, self.head[1]]) + carb_input = int(self.body[0]) + # XXX: I have no idea if this is correct; it seems to produce correct results. + correction = ( + twos_comp(self.body[7], 8) + twos_comp(self.body[5] & 0x0F, 8) + ) / 10.0 + wizard = { + "bg": bg, + "carb_input": carb_input, + "carb_ratio": int(self.body[2]), + "sensitivity": int(self.body[3]), + "bg_target_low": int(self.body[4]), + "bg_target_high": int(self.body[12]), + "bolus_estimate": int(self.body[11]) / 10.0, + "food_estimate": int(self.body[6]) / 10.0, + "unabsorbed_insulin_total": int(self.body[9]) / 10.0, + "unabsorbed_insulin_count": "??", + "correction_estimate": correction, + "_byte[5]": self.body[5], + "_byte[7]": int(self.body[7]), # + "unknown_byte[8]": self.body[8], + "unknown_byte[10]": self.body[10], + # '??': '??', + # 'unabsorbed_insulin_total': int(self.body[9])/10.0, + # 'food_estimate': int(self.body[0]), + } + + if self.larger: + # correction = ( twos_comp( self.body[6], (self.body[9] & 0x38) << 5 ) ) / 40.0 + bg = ((self.body[1] & 0x03) << 8) + self.head[1] + carb_input = ((self.body[1] & 0x0C) << 6) + self.body[0] + carb_ratio = (((self.body[2] & 0x07) << 8) + self.body[3]) / 10.0 + # xxx: not sure about this + # https://github.com/ps2/minimed_rf/blob/master/lib/minimed_rf/log_entries/bolus_wizard.rb#L102 + sensitivity = int(self.body[4]) + wizard = { + "bg": bg, + "carb_input": carb_input, + "carb_ratio": carb_ratio, + "sensitivity": sensitivity, + "bg_target_low": int(self.body[5]), + "bg_target_high": int(self.body[14]), + # 'bolus_estimate': int(self.body[13])/40.0, + "correction_estimate": (((self.body[9] & 0x38) << 5) + self.body[6]) + / 40.0, + # 'correction_maybe_estimate': correction, + "food_estimate": insulin_decode(self.body[7], self.body[8]), + "unabsorbed_insulin_total": insulin_decode( + self.body[10], self.body[11] + ), + "bolus_estimate": insulin_decode(self.body[12], self.body[13]), + # 'unknown_bytes': map(int, list(self.body)), + } + + if self.MMOL_DEFAULT: + for key in ["bg", "bg_target_high", "bg_target_low", "sensitivity"]: + wizard[key] = wizard[key] / 10.0 + return wizard + + +def insulin_decode(a, b, strokes=40.0): + return ((a << 8) + b) / strokes + + def twos_comp(val, bits): # http://stackoverflow.com/a/9147327 """compute the 2's compliment of int value val""" - if( (val&(1<<(bits-1))) != 0 ): - val = val - (1<>> from decocare import models + >>> model = models.PumpModel('522', None) + >>> rec = UnabsorbedInsulinBolus( UnabsorbedInsulinBolus._test_1[:2], model) + >>> print(str(rec)) + UnabsorbedInsulinBolus unknown head[2], body[0] op[0x5c] + + >>> print(pformat(rec.parse( UnabsorbedInsulinBolus._test_1 ))) + [{'age': 78, 'amount': 1.25}, {'age': 88, 'amount': 0.95}] + + >>> rec = UnabsorbedInsulinBolus( UnabsorbedInsulinBolus._test_2[:2], model ) + >>> print(str(rec)) + UnabsorbedInsulinBolus unknown head[2], body[0] op[0x5c] - >>> from decocare import models - >>> model = models.PumpModel('522', None) - >>> rec = UnabsorbedInsulinBolus( UnabsorbedInsulinBolus._test_1[:2], model) - >>> print str(rec) - UnabsorbedInsulinBolus unknown head[2], body[0] op[0x5c] + >>> print(pformat(rec.parse( UnabsorbedInsulinBolus._test_2 ))) + [{'age': 60, 'amount': 2.6}, {'age': 160, 'amount': 2.5}] - >>> print pformat(rec.parse( UnabsorbedInsulinBolus._test_1 )) - [{'age': 78, 'amount': 1.25}, {'age': 88, 'amount': 0.95}] - >>> rec = UnabsorbedInsulinBolus( UnabsorbedInsulinBolus._test_2[:2], model ) - >>> print str(rec) - UnabsorbedInsulinBolus unknown head[2], body[0] op[0x5c] - >>> print pformat(rec.parse( UnabsorbedInsulinBolus._test_2 )) - [{'age': 60, 'amount': 2.6}, {'age': 160, 'amount': 2.5}] + [{'age': 60, 'amount': 2.6, 'curve': 4}, + {'age': 160, 'amount': 2.5, 'curve': 4}] + """ + _test_1 = bytearray([0x5C, 0x08, 0x32, 0x4E, 0x04, 0x26, 0x58, 0x04,]) - [{'age': 60, 'amount': 2.6, 'curve': 4}, - {'age': 160, 'amount': 2.5, 'curve': 4}] + _test_2 = bytearray([0x5C, 0x08, 0x68, 0x3C, 0x04, 0x64, 0xA0, 0x04,]) + opcode = 0x5C + date_length = 0 - """ - _test_1 = bytearray([ 0x5c, 0x08, - 0x32, 0x4e, 0x04, 0x26, 0x58, 0x04, ]) + def decode(self): + raw = self.head[2:] + return self.model.decode_unabsorbed(raw) - _test_2 = bytearray([ 0x5c, 0x08, - 0x68, 0x3c, 0x04, - 0x64, 0xa0, 0x04, ]) - opcode = 0x5c - date_length = 0 - def decode(self): - raw = self.head[2:] - return self.model.decode_unabsorbed(raw) class CalBGForPH(KnownRecord): - """ + """ >>> rec = CalBGForPH( CalBGForPH._test_1[:2] ) >>> rec.parse( CalBGForPH._test_1 ) {'amount': 139} - >>> print str(rec) + >>> print(str(rec)) CalBGForPH 2012-12-18T15:04:46 head[2], body[0] op[0x0a] - """ - _test_1 = bytearray([ 0x0a, 0x8b, - 0xee, 0x04, 0x2f, 0x12, 0x0c, ]) - - _test_2 = bytearray([ 0x0a, 0xa7, - 0x22, 0x53, 0x30, 0x0e, 0x0d, ]) - - _test_3 = bytearray([ 0x0a, 0xb0, - 0x00, 0x6f, 0x2f, 0x0e, 0x0d, ]) - - _test_4 = bytearray([ 0x0a, 0x42, - 0x0c, 0x6c, 0x31, 0x0e, 0x8d, ]) - - _test_5 = bytearray([ 0x0a, 0x60, - 0x04, 0x59, 0x2b, 0x0e, 0x8d, ]) - _test_6 = bytearray([ 0x0a, 0x5b, - 0x16, 0x52, 0x2a, 0x0e, 0x8d, ]) - opcode = 0x0a - def decode(self): - self.parse_time( ) - # year_bits = extra_year_bits(self.date[4]) - - highbit = (self.date[2] & 0b10000000) << 2 - nibble = (self.date[4] & 0b10000000) << 1 - low = self.head[1] - amount = int(highbit + nibble + low) - return { 'amount': amount } - # return { 'amount': int(lib.BangInt([ year_bits[0], self.head[1] ])) } - pass - -if __name__ == '__main__': - import doctest - doctest.testmod( ) + """ + + _test_1 = bytearray([0x0A, 0x8B, 0xEE, 0x04, 0x2F, 0x12, 0x0C,]) + + _test_2 = bytearray([0x0A, 0xA7, 0x22, 0x53, 0x30, 0x0E, 0x0D,]) + + _test_3 = bytearray([0x0A, 0xB0, 0x00, 0x6F, 0x2F, 0x0E, 0x0D,]) + + _test_4 = bytearray([0x0A, 0x42, 0x0C, 0x6C, 0x31, 0x0E, 0x8D,]) + + _test_5 = bytearray([0x0A, 0x60, 0x04, 0x59, 0x2B, 0x0E, 0x8D,]) + _test_6 = bytearray([0x0A, 0x5B, 0x16, 0x52, 0x2A, 0x0E, 0x8D,]) + opcode = 0x0A + + def decode(self): + self.parse_time() + # year_bits = extra_year_bits(self.date[4]) + + highbit = (self.date[2] & 0b10000000) << 2 + nibble = (self.date[4] & 0b10000000) << 1 + low = self.head[1] + amount = int(highbit + nibble + low) + return {"amount": amount} + # return { 'amount': int(lib.BangInt([ year_bits[0], self.head[1] ])) } + pass + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/decocare/records/tests.py b/decocare/records/tests.py index 71a60ae..fc30061 100644 --- a/decocare/records/tests.py +++ b/decocare/records/tests.py @@ -1,14 +1,13 @@ #!/usr/bin/python - +import difflib +import json +import codecs from binascii import hexlify from datetime import datetime from pprint import pformat -import json -import difflib - -from times import * -from bolus import * +from decocare.records.bolus import * +from decocare.records.times import * # I don't know where else to put this. """ @@ -17,7 +16,7 @@ + bytearray([ ]) + bytearray([ ])), """ - +# fmt:off _midnights = { 'page_4': [ # record 14 (2013, 0, 7, 6, 5, 0) @@ -186,74 +185,75 @@ 0xe8, 0x00, 0x00, 0x00, ])) ], } - +# fmt:on _bewest_dates = { - # from https://github.com/bewest/decoding-carelink/blob/rewriting/analysis/bewest-pump/ReadHistoryData-page-19.data.list_opcodes.markdown - 'page-19': { - 0: [ 0xaa, 0xf7, 0x40, 0x0c, 0x0c, ], - 1: [ 0x40, 0x0c, 0x0c, 0x0a, 0x0c, ], - 2: [ 0x0c, 0x8b, 0xc3, 0x28, 0x0c, ], - 3: [ 0x8b, 0xc3, 0x28, 0x0c, 0x8c, ], - 4: [ 0x28, 0x0c, 0x8c, 0x5b, 0x0c, ], - 5: [ 0x8d, 0xc3, 0x08, 0x0c, 0x0c, ], - 6: [ 0xaa, 0xf7, 0x00, 0x0c, 0x0c, ], - } + # from https://github.com/bewest/decoding-carelink/blob/rewriting/analysis/bewest-pump/ReadHistoryData-page-19.data.list_opcodes.markdown + "page-19": { + 0: [0xAA, 0xF7, 0x40, 0x0C, 0x0C,], + 1: [0x40, 0x0C, 0x0C, 0x0A, 0x0C,], + 2: [0x0C, 0x8B, 0xC3, 0x28, 0x0C,], + 3: [0x8B, 0xC3, 0x28, 0x0C, 0x8C,], + 4: [0x28, 0x0C, 0x8C, 0x5B, 0x0C,], + 5: [0x8D, 0xC3, 0x08, 0x0C, 0x0C,], + 6: [0xAA, 0xF7, 0x00, 0x0C, 0x0C,], + } } -def _test_decode_bolus( ): - """ - ## correct - >>> parse_date( bytearray( _bewest_dates['page-19'][6] ) ).isoformat( ) - '2012-11-12T00:55:42' - ## correct - >>> parse_date( bytearray( _bewest_dates['page-19'][0] ) ).isoformat( ) - '2012-11-12T00:55:42' +def _test_decode_bolus(): + """ + ## correct + >>> parse_date( bytearray( _bewest_dates['page-19'][6] ) ).isoformat( ) + '2012-11-12T00:55:42' + + ## correct + >>> parse_date( bytearray( _bewest_dates['page-19'][0] ) ).isoformat( ) + '2012-11-12T00:55:42' - ## this is wrong - >>> parse_date( bytearray( _bewest_dates['page-19'][1] ) ).isoformat( ) - '2012-04-10T12:12:00' + ## this is wrong + >>> parse_date( bytearray( _bewest_dates['page-19'][1] ) ).isoformat( ) + '2012-04-10T12:12:00' - ## day,month is wrong, time H:M:S is correct - # expected: - >>> parse_date( bytearray( _bewest_dates['page-19'][2] ) ).isoformat( ) - '2012-02-08T03:11:12' + ## day,month is wrong, time H:M:S is correct + # expected: + >>> parse_date( bytearray( _bewest_dates['page-19'][2] ) ).isoformat( ) + '2012-02-08T03:11:12' - ## correct - >>> parse_date( bytearray( _bewest_dates['page-19'][3] ) ).isoformat( ) - '2012-11-12T08:03:11' + ## correct + >>> parse_date( bytearray( _bewest_dates['page-19'][3] ) ).isoformat( ) + '2012-11-12T08:03:11' - #### not a valid date - # >>> parse_date( bytearray( _bewest_dates['page-19'][4] ) ).isoformat( ) + #### not a valid date + # >>> parse_date( bytearray( _bewest_dates['page-19'][4] ) ).isoformat( ) - ## correct - >>> parse_date( bytearray( _bewest_dates['page-19'][5] ) ).isoformat( ) - '2012-11-12T08:03:13' + ## correct + >>> parse_date( bytearray( _bewest_dates['page-19'][5] ) ).isoformat( ) + '2012-11-12T08:03:13' + """ + """ - """ - """ + 0x5b 0x7e # bolus wizard, + 0xaa 0xf7 0x00 0x0c 0x0c # page-19[0] - 0x5b 0x7e # bolus wizard, - 0xaa 0xf7 0x00 0x0c 0x0c # page-19[0] + 0x0f 0x50 0x0d 0x2d 0x6a 0x00 0x0b 0x00 + 0x00 0x07 0x00 0x0b 0x7d 0x5c 0x08 0x58 + 0x97 0x04 0x30 0x05 0x14 0x34 0xc8 + 0x91 0xf8 # 0x91, 0xf8 = month=11, minute=56, seconds=17! + 0x00 # general parsing fails here + 0x00 + 0xaa 0xf7 0x40 0x0c 0x0c # expected - page-19[6] - 0x0f 0x50 0x0d 0x2d 0x6a 0x00 0x0b 0x00 - 0x00 0x07 0x00 0x0b 0x7d 0x5c 0x08 0x58 - 0x97 0x04 0x30 0x05 0x14 0x34 0xc8 - 0x91 0xf8 # 0x91, 0xf8 = month=11, minute=56, seconds=17! - 0x00 # general parsing fails here - 0x00 - 0xaa 0xf7 0x40 0x0c 0x0c # expected - page-19[6] + 0x0a 0x0c + 0x8b 0xc3 0x28 0x0c 0x8c # page-19[3] - 0x0a 0x0c - 0x8b 0xc3 0x28 0x0c 0x8c # page-19[3] + 0x5b 0x0c - 0x5b 0x0c + 0x8d 0xc3 0x08 0x0c 0x0c # page-19[5] + 0x00 0x51 0x0d 0x2d 0x6a 0x1f 0x00 0x00 0x00 0x00 0x00 + """ - 0x8d 0xc3 0x08 0x0c 0x0c # page-19[5] - 0x00 0x51 0x0d 0x2d 0x6a 0x1f 0x00 0x00 0x00 0x00 0x00 - """ ### csv deconstructed """ @@ -302,24 +302,21 @@ def _test_decode_bolus( ): """ - _bad_days = [ - bytearray([ 0xa9, 0xf5, 0x15, 0x14, 0x0c, ]), - bytearray([ 0xa6, 0xc7, 0x36, 0x14, 0x8c, ]), - bytearray([ 0xa9, 0xf5, 0x15, 0x14, 0x0c, ]), - bytearray([ 0xa6, 0xc7, 0x36, 0x14, 0x8c, ]), - - bytearray([ 0xa2, 0xe9, 0x10, 0x19, 0x0c, ]), - bytearray([ 0xa0, 0xf6, 0x0d, 0x19, 0x0c, ]), - bytearray([ 0xa5, 0xd9, 0x34, 0x1d, 0x0c, ]), - - bytearray([ 0xc2, 0x3b, 0x0e, 0x14, 0x0c, ]), - bytearray([ 0xd9, 0x1c, 0x0f, 0x14, 0x0c, ]), - ] + bytearray([0xA9, 0xF5, 0x15, 0x14, 0x0C,]), + bytearray([0xA6, 0xC7, 0x36, 0x14, 0x8C,]), + bytearray([0xA9, 0xF5, 0x15, 0x14, 0x0C,]), + bytearray([0xA6, 0xC7, 0x36, 0x14, 0x8C,]), + bytearray([0xA2, 0xE9, 0x10, 0x19, 0x0C,]), + bytearray([0xA0, 0xF6, 0x0D, 0x19, 0x0C,]), + bytearray([0xA5, 0xD9, 0x34, 0x1D, 0x0C,]), + bytearray([0xC2, 0x3B, 0x0E, 0x14, 0x0C,]), + bytearray([0xD9, 0x1C, 0x0F, 0x14, 0x0C,]), +] # days need 5 bits def big_days(x=0): - """ + """ # page 17, RECORD 11 >>> parse_date( big_days(0) ).isoformat( ) '2012-11-20T21:53:41' @@ -352,10 +349,11 @@ def big_days(x=0): >>> parse_date( big_days(8) ).isoformat( ) '2012-12-20T15:28:25' - """ - return _bad_days[x] + """ + return _bad_days[x] +# fmt:off _wizards = [ # 2382,1/19/13,21:50:15,1/19/13 # 21:50:15,5.9,125,106,13,45,87,75,-0.7,6.6,0.0,BolusWizardBolusEstimate,"BG_INPUT=75, @@ -384,391 +382,418 @@ def big_days(x=0): 0x00, 0x0a, 0x00, 0x43, 0x7d, ]), # bytearray([ ]), ] +# fmt:on from decocare import models -model522 = models.PumpModel('522', None) -def _test_bolus_wizards( ): - """ - >>> rec = BolusWizard( _wizards[0][:2], model522 ) - >>> print pformat(rec.parse( _wizards[0] )) - {'_byte[5]': 249, - '_byte[7]': 240, - 'bg': 75, - 'bg_target_high': 125, - 'bg_target_low': 106, - 'bolus_estimate': 5.9, - 'carb_input': 87, - 'carb_ratio': 13, - 'correction_estimate': -0.7, - 'food_estimate': 6.6, - 'sensitivity': 45, - 'unabsorbed_insulin_count': '??', - 'unabsorbed_insulin_total': 0.0, - 'unknown_byte[10]': 0, - 'unknown_byte[8]': 0} - >>> print str(rec) - BolusWizard 2013-01-19T21:50:15 head[2], body[13] op[0x5b] - - - >>> rec = BolusWizard( _wizards[1][:2], model522 ) - >>> print pformat(rec.parse( _wizards[1] )) - {'_byte[5]': 251, - '_byte[7]': 240, - 'bg': 83, - 'bg_target_high': 125, - 'bg_target_low': 106, - 'bolus_estimate': 6.7, - 'carb_input': 94, - 'carb_ratio': 13, - 'correction_estimate': -0.5, - 'food_estimate': 7.2, - 'sensitivity': 45, - 'unabsorbed_insulin_count': '??', - 'unabsorbed_insulin_total': 1.0, - 'unknown_byte[10]': 0, - 'unknown_byte[8]': 0} - - >>> print str(rec) - BolusWizard 2013-01-14T22:36:00 head[2], body[13] op[0x5b] - - """ - pass +model522 = models.PumpModel("522", None) -_bolus = [ - # 2381,1/19/13,21:50:15,1/19/13 - # 21:50:15,Dual/Normal,2.6,2.6,BolusNormal,"AMOUNT=2.6, - # CONCENTRATION=null, PROGRAMMED_AMOUNT=2.6, ACTION_REQUESTOR=pump, - # ENABLE=true, IS_DUAL_COMPONENT=true, - # UNABSORBED_INSULIN_TOTAL=null" - # 9942918054,51974238,108,Paradigm 522 - bytearray([ 0x01, 0x1a, 0x1a, 0x00, - 0x0f, 0x72, 0x95, 0x13, 0x0d, ]), - - # 2305,1/15/13,15:57:16,1/15/13 - # 15:57:16,Normal,1.7,1.7,BolusNormal,"AMOUNT=1.7, - # CONCENTRATION=null, PROGRAMMED_AMOUNT=1.7, ACTION_REQUESTOR=pump, - # ENABLE=true, IS_DUAL_COMPONENT=false, - # UNABSORBED_INSULIN_TOTAL=null" - # 9942918131,51974238,185,Paradigm 522 - bytearray([ 0x01, 0x11, 0x11, 0x00, - 0x10, 0x79, 0x4f, 0x0f, 0x0d, ]), - # bytearray([ ]), +def _test_bolus_wizards(): + """ + >>> rec = BolusWizard( _wizards[0][:2], model522 ) + >>> print(pformat(rec.parse( _wizards[0] ))) + {'_byte[5]': 249, + '_byte[7]': 240, + 'bg': 75, + 'bg_target_high': 125, + 'bg_target_low': 106, + 'bolus_estimate': 5.9, + 'carb_input': 87, + 'carb_ratio': 13, + 'correction_estimate': -0.7, + 'food_estimate': 6.6, + 'sensitivity': 45, + 'unabsorbed_insulin_count': '??', + 'unabsorbed_insulin_total': 0.0, + 'unknown_byte[10]': 0, + 'unknown_byte[8]': 0} + >>> print(str(rec)) + BolusWizard 2013-01-19T21:50:15 head[2], body[13] op[0x5b] + + + >>> rec = BolusWizard( _wizards[1][:2], model522 ) + >>> print(pformat(rec.parse( _wizards[1] ))) + {'_byte[5]': 251, + '_byte[7]': 240, + 'bg': 83, + 'bg_target_high': 125, + 'bg_target_low': 106, + 'bolus_estimate': 6.7, + 'carb_input': 94, + 'carb_ratio': 13, + 'correction_estimate': -0.5, + 'food_estimate': 7.2, + 'sensitivity': 45, + 'unabsorbed_insulin_count': '??', + 'unabsorbed_insulin_total': 1.0, + 'unknown_byte[10]': 0, + 'unknown_byte[8]': 0} + + >>> print(str(rec)) + BolusWizard 2013-01-14T22:36:00 head[2], body[13] op[0x5b] + + """ + pass + +_bolus = [ + # 2381,1/19/13,21:50:15,1/19/13 + # 21:50:15,Dual/Normal,2.6,2.6,BolusNormal,"AMOUNT=2.6, + # CONCENTRATION=null, PROGRAMMED_AMOUNT=2.6, ACTION_REQUESTOR=pump, + # ENABLE=true, IS_DUAL_COMPONENT=true, + # UNABSORBED_INSULIN_TOTAL=null" + # 9942918054,51974238,108,Paradigm 522 + bytearray([0x01, 0x1A, 0x1A, 0x00, 0x0F, 0x72, 0x95, 0x13, 0x0D,]), + # 2305,1/15/13,15:57:16,1/15/13 + # 15:57:16,Normal,1.7,1.7,BolusNormal,"AMOUNT=1.7, + # CONCENTRATION=null, PROGRAMMED_AMOUNT=1.7, ACTION_REQUESTOR=pump, + # ENABLE=true, IS_DUAL_COMPONENT=false, + # UNABSORBED_INSULIN_TOTAL=null" + # 9942918131,51974238,185,Paradigm 522 + bytearray([0x01, 0x11, 0x11, 0x00, 0x10, 0x79, 0x4F, 0x0F, 0x0D,]), + # bytearray([ ]), ] -def _test_bolus( ): - """ - >>> rec = Bolus( _bolus[0][:2] ) - >>> print pformat(rec.parse( _bolus[0] )) - {'amount': 2.6, 'duration': 0, 'programmed': 2.6, 'type': 'normal'} - >>> print str(rec) - Bolus 2013-01-19T21:50:15 head[4], body[0] op[0x01] +def _test_bolus(): + """ + >>> rec = Bolus( _bolus[0][:2] ) + >>> print(pformat(rec.parse( _bolus[0] ))) + {'amount': 2.6, 'duration': 0, 'programmed': 2.6, 'type': 'normal'} + + >>> print(str(rec)) + Bolus 2013-01-19T21:50:15 head[4], body[0] op[0x01] + + >>> rec = Bolus( _bolus[1][:2] ) + >>> print(pformat(rec.parse( _bolus[1] ))) + {'amount': 1.7, 'duration': 0, 'programmed': 1.7, 'type': 'normal'} + >>> print(str(rec)) + Bolus 2013-01-15T15:57:16 head[4], body[0] op[0x01] - >>> rec = Bolus( _bolus[1][:2] ) - >>> print pformat(rec.parse( _bolus[1] )) - {'amount': 1.7, 'duration': 0, 'programmed': 1.7, 'type': 'normal'} - >>> print str(rec) - Bolus 2013-01-15T15:57:16 head[4], body[0] op[0x01] + """ - """ class TestSaraBolus: - # model 722 - hexdump = """ - 5b 67 - a1 51 0e 04 0d - 0d 50 00 78 - 3c 64 00 00 28 00 00 14 00 28 78 - 5c 08 44 79 c0 3c 4b d0 - 01 00 28 00 28 00 14 00 - a1 51 4e 04 0d - 0a fc - b4 54 2f 04 0d - 5b fc - b7 54 0f 04 0d - 00 50 00 78 - 3c 64 58 00 00 00 00 1c 00 3c 78 - 5c 0b 28 40 c0 44 b8 c0 3c 8a d0 - 01 00 3c 00 3c 00 1c 00 - b7 54 4f 04 0d - """ - csv_breakdown = """ - 9/4/13 14:17:33,,,,,,,Normal,1.0,1.0,,,,,,,,,,,,,,,,,,,,,BolusNormal - "AMOUNT=1 - CONCENTRATION=null - PROGRAMMED_AMOUNT=1 - ACTION_REQUESTOR=pump - ENABLE=true - IS_DUAL_COMPONENT=false - UNABSORBED_INSULIN_TOTAL=0.5" - 11345487207,52554138,86,Paradigm Revel - 723 - - 9/4/13 14:17:33,,,,,,,,,,,,,,,1.0,120,100,12,60,13,103,0,1,0.5,,,,,,BolusWizardBolusEstimate,"BG_INPUT=103 - BG_UNITS=mg dl - CARB_INPUT=13 - CARB_UNITS=grams - CARB_RATIO=12 - INSULIN_SENSITIVITY=60 - BG_TARGET_LOW=100 - BG_TARGET_HIGH=120 - BOLUS_ESTIMATE=1 - CORRECTION_ESTIMATE=0 - FOOD_ESTIMATE=1 - UNABSORBED_INSULIN_TOTAL=0.5 - UNABSORBED_INSULIN_COUNT=2 - ACTION_REQUESTOR=pump" - 11345487208,52554138,87,Paradigm Revel - 723 - - 9/4/13 14:17:33,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487208 - INDEX=0 - AMOUNT=1.7 - RECORD_AGE=121 - INSULIN_TYPE=null - INSULIN_ACTION_CURVE=180" - 11345487209,52554138,88,Paradigm Revel - 723 - - 9/4/13 14:17:33,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487208 - INDEX=1 - AMOUNT=1.5 - RECORD_AGE=331 - INSULIN_TYPE=null - INSULIN_ACTION_CURVE=180" - 11345487210,52554138,89,Paradigm Revel - 723 - - 9/4/13 15:20:52,,,,,,,,,,,,,,,,,,,,,,,,,,252,,,,CalBGForPH,"AMOUNT=252, ACTION_REQUESTOR=pump" - 11345487206,52554138,85,Paradigm Revel - 723 - - 9/4/13 15:20:55,,,,,,,Normal,1.5,1.5,,,,,,,,,,,,,,,,,,,,,BolusNormal,"AMOUNT=1.5 - CONCENTRATION=null - PROGRAMMED_AMOUNT=1.5 - ACTION_REQUESTOR=pump - ENABLE=true - IS_DUAL_COMPONENT=false - UNABSORBED_INSULIN_TOTAL=0.7" - 11345487201,52554138,80,Paradigm Revel - 723 - - 9/4/13 15:20:55,,,,,,,,,,,,,,,1.5,120,100,12,60,0,252,2.2,0,0.7,,,,,,BolusWizardBolusEstimate,"BG_INPUT=252 - BG_UNITS=mg dl - CARB_INPUT=0 - CARB_UNITS=grams - CARB_RATIO=12 - INSULIN_SENSITIVITY=60 - BG_TARGET_LOW=100 - BG_TARGET_HIGH=120 - BOLUS_ESTIMATE=1.5 - CORRECTION_ESTIMATE=2.2 - FOOD_ESTIMATE=0 - UNABSORBED_INSULIN_TOTAL=0.7 - UNABSORBED_INSULIN_COUNT=3 - ACTION_REQUESTOR=pump" - 11345487202,52554138,81,Paradigm Revel - 723 - - 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 - INDEX=0 - AMOUNT=1 - RECORD_AGE=64 - INSULIN_TYPE=null - INSULIN_ACTION_CURVE=180" - 11345487203,52554138,82,Paradigm Revel - 723 - - 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 - INDEX=1 - AMOUNT=1.7 - RECORD_AGE=184 - INSULIN_TYPE=null - INSULIN_ACTION_CURVE=180" - 11345487204,52554138,83,Paradigm Revel - 723 - - 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 - INDEX=2 - AMOUNT=1.5 - RECORD_AGE=394 - INSULIN_TYPE=null - INSULIN_ACTION_CURVE=180" - 11345487205,52554138,84,Paradigm Revel - 723 - 9/4/13 16:11:57,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CurrentSensorMissedDataTime,TIME=1800000,11345487185,52554138,64,Paradigm Revel - 723 - """ - bolus_1_ok = { - 'bg': 103, - # 'BG_UNITS': 'mg dl' - 'carb_input': 13, - #'CARB_UNITS': 'grams', - 'carb_ratio': 12, - 'sensitivity': 60, - 'bg_target_low': 100, - 'bg_target_high': 120, - 'bolus_estimate': 1, - 'correction_estimate': 0, - 'food_estimate': 1, - 'unabsorbed_insulin_total': 0.5, - 'unabsorbed_insulin_count': 2, - #'action_requestor': 'pump' - } - bw_1_bytes = bytearray(''.join(""" - 5b 67 - a1 51 0e 04 0d - 0d 50 00 78 + # model 722 + hexdump = """ + 5b 67 + a1 51 0e 04 0d + 0d 50 00 78 3c 64 00 00 28 00 00 14 00 28 78 - """.strip( ).split( )).decode('hex')) - bw_2_bytes = bytearray(''.join(""" - 5b fc - b7 54 0f 04 0d - 00 50 00 78 + 5c 08 44 79 c0 3c 4b d0 + 01 00 28 00 28 00 14 00 + a1 51 4e 04 0d + 0a fc + b4 54 2f 04 0d + 5b fc + b7 54 0f 04 0d + 00 50 00 78 3c 64 58 00 00 00 00 1c 00 3c 78 - """.strip( ).split( )).decode('hex')) - - cal_bg_bytes = bytearray(''.join(""" - 0a fc - b4 54 2f 04 0d - """.strip( ).split( )).decode('hex')) - @classmethod - def test_cal_bg(klass): + 5c 0b 28 40 c0 44 b8 c0 3c 8a d0 + 01 00 3c 00 3c 00 1c 00 + b7 54 4f 04 0d """ - >>> TestSaraBolus.test_cal_bg( ) - CalBGForPH 2013-09-04T15:20:52 head[2], body[0] op[0x0a] - { - "amount": 252 - } + csv_breakdown = """ + 9/4/13 14:17:33,,,,,,,Normal,1.0,1.0,,,,,,,,,,,,,,,,,,,,,BolusNormal + "AMOUNT=1 + CONCENTRATION=null + PROGRAMMED_AMOUNT=1 + ACTION_REQUESTOR=pump + ENABLE=true + IS_DUAL_COMPONENT=false + UNABSORBED_INSULIN_TOTAL=0.5" + 11345487207,52554138,86,Paradigm Revel - 723 + + 9/4/13 14:17:33,,,,,,,,,,,,,,,1.0,120,100,12,60,13,103,0,1,0.5,,,,,,BolusWizardBolusEstimate,"BG_INPUT=103 + BG_UNITS=mg dl + CARB_INPUT=13 + CARB_UNITS=grams + CARB_RATIO=12 + INSULIN_SENSITIVITY=60 + BG_TARGET_LOW=100 + BG_TARGET_HIGH=120 + BOLUS_ESTIMATE=1 + CORRECTION_ESTIMATE=0 + FOOD_ESTIMATE=1 + UNABSORBED_INSULIN_TOTAL=0.5 + UNABSORBED_INSULIN_COUNT=2 + ACTION_REQUESTOR=pump" + 11345487208,52554138,87,Paradigm Revel - 723 + + 9/4/13 14:17:33,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487208 + INDEX=0 + AMOUNT=1.7 + RECORD_AGE=121 + INSULIN_TYPE=null + INSULIN_ACTION_CURVE=180" + 11345487209,52554138,88,Paradigm Revel - 723 + + 9/4/13 14:17:33,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487208 + INDEX=1 + AMOUNT=1.5 + RECORD_AGE=331 + INSULIN_TYPE=null + INSULIN_ACTION_CURVE=180" + 11345487210,52554138,89,Paradigm Revel - 723 + + 9/4/13 15:20:52,,,,,,,,,,,,,,,,,,,,,,,,,,252,,,,CalBGForPH,"AMOUNT=252, ACTION_REQUESTOR=pump" + 11345487206,52554138,85,Paradigm Revel - 723 + + 9/4/13 15:20:55,,,,,,,Normal,1.5,1.5,,,,,,,,,,,,,,,,,,,,,BolusNormal,"AMOUNT=1.5 + CONCENTRATION=null + PROGRAMMED_AMOUNT=1.5 + ACTION_REQUESTOR=pump + ENABLE=true + IS_DUAL_COMPONENT=false + UNABSORBED_INSULIN_TOTAL=0.7" + 11345487201,52554138,80,Paradigm Revel - 723 + + 9/4/13 15:20:55,,,,,,,,,,,,,,,1.5,120,100,12,60,0,252,2.2,0,0.7,,,,,,BolusWizardBolusEstimate,"BG_INPUT=252 + BG_UNITS=mg dl + CARB_INPUT=0 + CARB_UNITS=grams + CARB_RATIO=12 + INSULIN_SENSITIVITY=60 + BG_TARGET_LOW=100 + BG_TARGET_HIGH=120 + BOLUS_ESTIMATE=1.5 + CORRECTION_ESTIMATE=2.2 + FOOD_ESTIMATE=0 + UNABSORBED_INSULIN_TOTAL=0.7 + UNABSORBED_INSULIN_COUNT=3 + ACTION_REQUESTOR=pump" + 11345487202,52554138,81,Paradigm Revel - 723 + + 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 + INDEX=0 + AMOUNT=1 + RECORD_AGE=64 + INSULIN_TYPE=null + INSULIN_ACTION_CURVE=180" + 11345487203,52554138,82,Paradigm Revel - 723 + + 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 + INDEX=1 + AMOUNT=1.7 + RECORD_AGE=184 + INSULIN_TYPE=null + INSULIN_ACTION_CURVE=180" + 11345487204,52554138,83,Paradigm Revel - 723 + + 9/4/13 15:20:55,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,UnabsorbedInsulin,"BOLUS_ESTIMATE_DATUM=11345487202 + INDEX=2 + AMOUNT=1.5 + RECORD_AGE=394 + INSULIN_TYPE=null + INSULIN_ACTION_CURVE=180" + 11345487205,52554138,84,Paradigm Revel - 723 + 9/4/13 16:11:57,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CurrentSensorMissedDataTime,TIME=1800000,11345487185,52554138,64,Paradigm Revel - 723 """ - # 9/4/13 15:20:52,,,,,,,,,,,,,,,,,,,,,,,,,,252,,,,CalBGForPH,"AMOUNT=252, ACTION_REQUESTOR=pump" - data = klass.cal_bg_bytes - rec = CalBGForPH(data[:2]) - d = rec.parse(data) - print str(rec) - print json.dumps(d, indent=2) + bolus_1_ok = { + "bg": 103, + # 'BG_UNITS': 'mg dl' + "carb_input": 13, + #'CARB_UNITS': 'grams', + "carb_ratio": 12, + "sensitivity": 60, + "bg_target_low": 100, + "bg_target_high": 120, + "bolus_estimate": 1, + "correction_estimate": 0, + "food_estimate": 1, + "unabsorbed_insulin_total": 0.5, + "unabsorbed_insulin_count": 2, + #'action_requestor': 'pump' + } + bw_1_bytes = bytearray( + codecs.decode( + ( + "5b 67" + "a1 51 0e 04 0d" + "0d 50 00 78" + "3c 64 00 00 28 00 00 14 00 28 78" + ).replace(" ", ""), + "hex", + ) + ) + bw_2_bytes = bytearray( + codecs.decode( + ( + "5b fc" + "b7 54 0f 04 0d" + "00 50 00 78" + "3c 64 58 00 00 00 00 1c 00 3c 78" + ).replace(" ", ""), + "hex", + ) + ) + + cal_bg_bytes = bytearray( + # fmt: off + codecs.decode( + ( + "0a fc" + "b4 54 2f 04 0d" + ).replace(" ", ""), + "hex", + ) + # fmt: on + ) + + @classmethod + def test_cal_bg(klass): + """ + >>> TestSaraBolus.test_cal_bg( ) + CalBGForPH 2013-09-04T15:20:52 head[2], body[0] op[0x0a] + { + "amount": 252 + } + """ + # 9/4/13 15:20:52,,,,,,,,,,,,,,,,,,,,,,,,,,252,,,,CalBGForPH,"AMOUNT=252, ACTION_REQUESTOR=pump" + data = klass.cal_bg_bytes + rec = CalBGForPH(data[:2]) + d = rec.parse(data) + print(str(rec)) + print(json.dumps(d, indent=2)) + def dictlines(d): - items = d.items( ) - items.sort( ) - d = [ "%s: %s\n" % (k, v) for (k, v) in items ] - return d - -def unsolved_bolus_wizard( ): - """ - # >>> unsolved_bolus_wizard( ) - """ - # these byte sequences line up with these records: - bw_ok_1 = { - 'bg_input': 103, - 'carb_input': 13, - 'carb_ratio': 12, - 'insulin_sensitivity': 60, - 'bg_target_low': 100, - 'bg_target_high': 120, - 'bolus_estimate': 1, - 'correction_estimate': 0, - 'food_estimate': 1, - 'unabsorbed_insulin_total': 0.5, - 'unabsorbed_insulin_count': 2, - } - bw_ok_2 = { - 'bg_input': 252, - 'carb_input': 0, - 'carb_ratio': 12, - 'insulin_sensitivity': 60, - 'bg_target_low': 100, - 'bg_target_high': 120, - 'bolus_estimate': 1.5, - 'correction_estimate': 2.2, - 'food_estimate': 0, - 'unabsorbed_insulin_total': 0.7, - 'unabsorbed_insulin_count': 3, - } - found = decode_wizard(TestSaraBolus.bw_1_bytes) - if found != bw_ok_1: - print "FOUND:" - print json.dumps(found, indent=2) - print "EXPECTED:" - print json.dumps(bw_ok_1, indent=2) - found = decode_wizard(TestSaraBolus.bw_2_bytes) - if found != bw_ok_2: - print "FOUND:" - print json.dumps(found, indent=2) - print "EXPECTED:" - print json.dumps(bw_ok_2, indent=2) + items = sorted(list(d.items())) + d = ["{}: {}\n".format(k, v) for (k, v) in items] + return d -def decode_wizard(data): - """ - BYTE - 01: - 02: - 03: - 04: - 05: - 06: - 07: - 08: - 09: - 10: - 12: - 13: - 14: - 15: - 16: - 17: - 18: - 19: - 20: - 21: - 22: - """ - head = data[:2] - date = data[2:7] - datetime = parse_date(date) - body = data[7:] - bg = lib.BangInt([ body[1] & 0x0f, head[1] ]) - carb_input = int(body[0]) - carb_ratio = int(body[2]) - bg_target_low = int(body[5]) - bg_target_high = int(body[3]) - sensitivity = int(body[4]) - - print "BOLUS WIZARD", datetime.isoformat( ) - wizard = { 'bg_input': bg, 'carb_input': carb_input, - 'carb_ratio': carb_ratio, - 'insulin_sensitivity': sensitivity, - 'bg_target_low': bg_target_low, - 'bg_target_high': bg_target_high, - } - return wizard -class BW722(BolusWizard): - def decode(self): - self.parse_time( ) - bg = lib.BangInt([ self.body[1] & 0x0f, self.head[1] ]) - carb_input = int(self.body[0]) - carb_ratio = int(self.body[2]) - bg_target_low = int(self.body[5]) - bg_target_high = int(self.body[3]) - sensitivity = int(self.body[12]) - - # XXX: Most likely incorrect. - correction = ( twos_comp( self.body[7], 8 ) - + twos_comp( self.body[5] & 0x0f, 8 ) ) / 10.0 - wizard = { 'bg': bg, 'carb_input': carb_input, - 'carb_ratio': carb_ratio, - 'sensitivity': sensitivity, - 'bg_target_low': bg_target_low, - 'bg_target_high': bg_target_high, - #'bolus_estimate': int(self.body[6])/10.0, - #'food_estimate': int(self.body[13])/10.0, - #'unabsorbed_insulin_total': int(self.body[9])/10.0, - #'unabsorbed_insulin_count': self.body[11], - 'correction_estimate': correction, - # '??': '??', - # 'unabsorbed_insulin_total': int(self.body[9])/10.0, - # 'food_estimate': int(self.body[0]), - } - return wizard +def unsolved_bolus_wizard(): + """ + # >>> unsolved_bolus_wizard( ) + """ + # these byte sequences line up with these records: + bw_ok_1 = { + "bg_input": 103, + "carb_input": 13, + "carb_ratio": 12, + "insulin_sensitivity": 60, + "bg_target_low": 100, + "bg_target_high": 120, + "bolus_estimate": 1, + "correction_estimate": 0, + "food_estimate": 1, + "unabsorbed_insulin_total": 0.5, + "unabsorbed_insulin_count": 2, + } + bw_ok_2 = { + "bg_input": 252, + "carb_input": 0, + "carb_ratio": 12, + "insulin_sensitivity": 60, + "bg_target_low": 100, + "bg_target_high": 120, + "bolus_estimate": 1.5, + "correction_estimate": 2.2, + "food_estimate": 0, + "unabsorbed_insulin_total": 0.7, + "unabsorbed_insulin_count": 3, + } + found = decode_wizard(TestSaraBolus.bw_1_bytes) + if found != bw_ok_1: + print("FOUND:") + print(json.dumps(found, indent=2)) + print("EXPECTED:") + print(json.dumps(bw_ok_1, indent=2)) + found = decode_wizard(TestSaraBolus.bw_2_bytes) + if found != bw_ok_2: + print("FOUND:") + print(json.dumps(found, indent=2)) + print("EXPECTED:") + print(json.dumps(bw_ok_2, indent=2)) + +def decode_wizard(data): + """ + BYTE + 01: + 02: + 03: + 04: + 05: + 06: + 07: + 08: + 09: + 10: + 12: + 13: + 14: + 15: + 16: + 17: + 18: + 19: + 20: + 21: + 22: + """ + head = data[:2] + date = data[2:7] + datetime = parse_date(date) + body = data[7:] + bg = lib.BangInt([body[1] & 0x0F, head[1]]) + carb_input = int(body[0]) + carb_ratio = int(body[2]) + bg_target_low = int(body[5]) + bg_target_high = int(body[3]) + sensitivity = int(body[4]) + + print("BOLUS WIZARD", datetime.isoformat()) + wizard = { + "bg_input": bg, + "carb_input": carb_input, + "carb_ratio": carb_ratio, + "insulin_sensitivity": sensitivity, + "bg_target_low": bg_target_low, + "bg_target_high": bg_target_high, + } + return wizard -if __name__ == '__main__': - import doctest - doctest.testmod( ) +class BW722(BolusWizard): + def decode(self): + self.parse_time() + bg = lib.BangInt([self.body[1] & 0x0F, self.head[1]]) + carb_input = int(self.body[0]) + carb_ratio = int(self.body[2]) + bg_target_low = int(self.body[5]) + bg_target_high = int(self.body[3]) + sensitivity = int(self.body[12]) + + # XXX: Most likely incorrect. + correction = ( + twos_comp(self.body[7], 8) + twos_comp(self.body[5] & 0x0F, 8) + ) / 10.0 + wizard = { + "bg": bg, + "carb_input": carb_input, + "carb_ratio": carb_ratio, + "sensitivity": sensitivity, + "bg_target_low": bg_target_low, + "bg_target_high": bg_target_high, + #'bolus_estimate': int(self.body[6])/10.0, + #'food_estimate': int(self.body[13])/10.0, + #'unabsorbed_insulin_total': int(self.body[9])/10.0, + #'unabsorbed_insulin_count': self.body[11], + "correction_estimate": correction, + # '??': '??', + # 'unabsorbed_insulin_total': int(self.body[9])/10.0, + # 'food_estimate': int(self.body[0]), + } + return wizard + + +if __name__ == "__main__": + import doctest + + doctest.testmod() ##### # EOF diff --git a/decocare/records/times.py b/decocare/records/times.py index c237be6..b307652 100644 --- a/decocare/records/times.py +++ b/decocare/records/times.py @@ -1,311 +1,330 @@ - from datetime import datetime -class NotADate(Exception): pass +class NotADate(Exception): + pass + class Mask: - """ - Some useful bit masks. - """ - time = 0xC0 - invert = 0x3F - year = 0x7F - day = 0x1F + """ + Some useful bit masks. + """ + + time = 0xC0 + invert = 0x3F + year = 0x7F + day = 0x1F def quick_hex(bb): - return ' '.join( [ '%#04x' % x for x in bb ] ) + return " ".join(["%#04x" % x for x in bb]) + def parse_seconds(sec): - """ - simple mask - >>> parse_seconds(0x92) - 18 - """ - return sec & Mask.invert + """ + simple mask + >>> parse_seconds(0x92) + 18 + """ + return sec & Mask.invert + def parse_minutes(minutes): - """ - simple mask - >>> parse_minutes(0x9e) - 30 - """ - return minutes & Mask.invert + """ + simple mask + >>> parse_minutes(0x9e) + 30 + """ + return minutes & Mask.invert def parse_hours(hours): - """ - simple mask - >>> parse_hours(0x0b) - 11 + """ + simple mask + >>> parse_hours(0x0b) + 11 + + >>> parse_hours(0x28) + 8 - >>> parse_hours(0x28) - 8 + """ + return int(hours & 0x1F) - """ - return int(hours & 0x1f) def parse_day(day): - """ - simple mask - >>> parse_day( 0x01 ) - 1 - """ - return day & Mask.day + """ + simple mask + >>> parse_day( 0x01 ) + 1 + """ + return day & Mask.day + def parse_months(seconds, minutes): - """ - calculate from extra bits shaved off of seconds and minutes: + """ + calculate from extra bits shaved off of seconds and minutes: + + >>> parse_months( 0x92, 0x9e ) + 10 - >>> parse_months( 0x92, 0x9e ) - 10 + """ + high = (seconds & Mask.time) >> 4 + low = (minutes & Mask.time) >> 6 + return high | low - """ - high = (seconds & Mask.time) >> 4 - low = (minutes & Mask.time) >> 6 - return high | low def parse_years_lax(year): - """ - simple mask plus correction - """ - y = (year & Mask.year) + 2000 - return y + """ + simple mask plus correction + """ + y = (year & Mask.year) + 2000 + return y def extra_year_bits(year=0x86): - """ - practice getting some extra bits out of the year byte - >>> extra_year_bits( ) - [1] + """ + practice getting some extra bits out of the year byte + >>> extra_year_bits( ) + [1] - >>> extra_year_bits(0x06) - [0] + >>> extra_year_bits(0x06) + [0] - >>> extra_year_bits(0x86) - [1] + >>> extra_year_bits(0x86) + [1] - >>> extra_year_bits(0x46) - [0] + >>> extra_year_bits(0x46) + [0] - >>> extra_year_bits(0x26) - [0] + >>> extra_year_bits(0x26) + [0] - >>> extra_year_bits(0x16) - [0] + >>> extra_year_bits(0x16) + [0] - """ - # year = year[0] - masks = [ ( 0x80, 7) ] - nibbles = [ ] - for mask, shift in masks: - nibbles.append( ( (year & mask) >> shift ) ) - return nibbles + """ + # year = year[0] + masks = [(0x80, 7)] + nibbles = [] + for mask, shift in masks: + nibbles.append((year & mask) >> shift) + return nibbles -def extra_hour_bits(value): - """ - practice getting extra bits out of the hours bytes - >>> extra_hour_bits(0x28) - [0, 0, 1] +def extra_hour_bits(value): + """ + practice getting extra bits out of the hours bytes + + >>> extra_hour_bits(0x28) + [0, 0, 1] + + >>> extra_hour_bits(0x8) + [0, 0, 0] + """ + masks = [ + (0x80, 7), + (0x40, 6), + (0x20, 5), + ] + nibbles = [] + for mask, shift in masks: + nibbles.append((value & mask) >> shift) + return nibbles - >>> extra_hour_bits(0x8) - [0, 0, 0] - """ - masks = [ ( 0x80, 7), (0x40, 6), (0x20, 5), ] - nibbles = [ ] - for mask, shift in masks: - nibbles.append( ( (value & mask) >> shift ) ) - return nibbles def parse_years(year): - """ + """ simple mask plus correction >>> parse_years(0x06) 2006 - """ - # if year > 0x80: - # year = year - 0x80 - y = (year & Mask.year) + 2000 - # if y < 0 or y < 1999 or y > 2015: - # raise ValueError(y) - return y + """ + # if year > 0x80: + # year = year - 0x80 + y = (year & Mask.year) + 2000 + # if y < 0 or y < 1999 or y > 2015: + # raise ValueError(y) + return y + def encode_year(year): - pass + pass + def encode_monthbyte(sec=18, minute=30, month=10): - """ - >>> encode_monthbyte( ) == bytearray(b'\x92\x9e') - True + """ + >>> encode_monthbyte( ) == bytearray(bytes('\x92\x9e', 'raw_unicode_escape')) + True - >>> quick_hex(encode_monthbyte( )) - '0x92 0x9e' + >>> quick_hex(encode_monthbyte( )) + '0x92 0x9e' - >>> encode_monthbyte(sec=10) == bytearray(b'\x8a\x9e') - True + >>> encode_monthbyte(sec=10) == bytearray(bytes('\x8a\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(sec=35) == bytearray(b'\xa3\x9e') - True + >>> encode_monthbyte(sec=35) == bytearray(bytes('\xa3\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(sec=50) == bytearray(b'\xb2\x9e') - True + >>> encode_monthbyte(sec=50) == bytearray(bytes('\xb2\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=10) == bytearray(b'\x92\x8a') - True + >>> encode_monthbyte(minute=10) == bytearray(bytes('\x92\x8a', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=35) == bytearray(b'\x92\xa3') - True + >>> encode_monthbyte(minute=35) == bytearray(bytes('\x92\xa3', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=50) == bytearray(b'\x92\xb2') - True + >>> encode_monthbyte(minute=50) == bytearray(bytes('\x92\xb2', 'raw_unicode_escape')) + True - """ + """ - encoded = [ 0x00, 0x00 ] + encoded = [0x00, 0x00] + high = (month & (0x3 << 2)) >> 2 + low = month & (0x3) - high = (month & (0x3 << 2)) >> 2 - low = month & (0x3) + encoded[0] = sec | (high << 6) + encoded[1] = minute | (low << 6) + return bytearray(encoded) - encoded[0] = sec | (high << 6) - encoded[1] = minute | (low << 6) - return bytearray( encoded ) + # printf("0x%.2x 0x%.2x\n", buf[0], buf[1]) - printf("0x%.2x 0x%.2x\n", buf[0], buf[1]); def encode_minute(minute=30, month=10): - """ - >>> quick_hex(encode_minute( )) - '0x9e' + """ + >>> quick_hex(encode_minute( )) + '0x9e' + + """ + low = month & (0x3) + encoded = minute | (low << 6) + return bytearray([encoded]) - """ - low = month & (0x3) - encoded = minute | (low << 6) - return bytearray( [ encoded ] ) def encode_second(sec=18, month=10): - """ - >>> encode_second( ) == bytearray(b'\x92') - True + """ + >>> encode_second( ) == bytearray(bytes('\x92', 'raw_unicode_escape')) + True - >>> quick_hex(encode_second( )) - '0x92' + >>> quick_hex(encode_second( )) + '0x92' - """ - high = (month & (0x3 << 2)) >> 2 - encoded = sec | (high << 6) - return bytearray( [ encoded ] ) + """ + high = (month & (0x3 << 2)) >> 2 + encoded = sec | (high << 6) + return bytearray([encoded]) -def test_time_encoders( ): - """ - >>> test_time_encoders( ) - True - """ - one = bytearray().join([encode_second( ), encode_minute( )]) - two = encode_monthbyte( ) - return one == two +def test_time_encoders(): + """ + >>> test_time_encoders( ) + True + """ + + one = bytearray().join([encode_second(), encode_minute()]) + two = encode_monthbyte() + return one == two + def unmask_date(data): - """ - Extract date values from a series of bytes. - Always returns tuple given a bytearray of at least 5 bytes. + """ + Extract date values from a series of bytes. + Always returns tuple given a bytearray of at least 5 bytes. + + Returns 6-tuple of scalar values year, month, day, hours, minutes, + seconds. + >>> unmask_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) + (2006, 7, 1, 8, 23, 47) - Returns 6-tuple of scalar values year, month, day, hours, minutes, - seconds. - >>> unmask_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) - (2006, 7, 1, 8, 23, 47) + >>> unmask_date( bytearray( [ 0x00 ] * 5 )) + (2000, 0, 0, 0, 0, 0) - >>> unmask_date( bytearray( [ 0x00 ] * 5 )) - (2000, 0, 0, 0, 0, 0) + """ + data = data[:] + seconds = parse_seconds(data[0]) + minutes = parse_minutes(data[1]) - """ - data = data[:] - seconds = parse_seconds(data[0]) - minutes = parse_minutes(data[1]) + hours = parse_hours(data[2]) + day = parse_day(data[3]) + year = parse_years(data[4]) - hours = parse_hours(data[2]) - day = parse_day(data[3]) - year = parse_years(data[4]) + month = parse_months(data[0], data[1]) + return (year, month, day, hours, minutes, seconds) - month = parse_months( data[0], data[1] ) - return (year, month, day, hours, minutes, seconds) def parse_date(date, strict=False, loose=False): - """ - Choose whether or not to throw an error if you get a bad date. - """ - try: - return parse_date_strict(date) - except NotADate, e: - if strict: - raise - if loose: - return unmask_date(data) - - return None + """ + Choose whether or not to throw an error if you get a bad date. + """ + try: + return parse_date_strict(date) + except NotADate as e: + if strict: + raise + if loose: + return unmask_date(data) -def parse_date_strict(data): - """ - Apply strict datetime validation to extract a datetime from a series - of bytes. - Always throws an error for bad dates. + return None - >>> parse_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )).isoformat( ) - '2006-07-01T08:23:47' +def parse_date_strict(data): + """ + Apply strict datetime validation to extract a datetime from a series + of bytes. + Always throws an error for bad dates. - """ - (year, month, day, hours, minutes, seconds) = unmask_date(data) - try: - date = datetime(year, month, day, hours, minutes, seconds) - return date - except ValueError, e: - raise NotADate(e) + >>> parse_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )).isoformat( ) + '2006-07-01T08:23:47' -__test__ = { - 'unmask': ''' - These examples are fixed! Thanks ehwest, robg. - >>> unmask_date( bytearray( [ 0x93, 0xd4, 0x0e, 0x10, 0x0c ] )) - (2012, 11, 16, 14, 20, 19) + """ + (year, month, day, hours, minutes, seconds) = unmask_date(data) + try: + date = datetime(year, month, day, hours, minutes, seconds) + return date + except ValueError as e: + raise NotADate(e) - >>> unmask_date( bytearray( [ 0xa6, 0xeb, 0x0b, 0x10, 0x0c, ] )) - (2012, 11, 16, 11, 43, 38) - >>> unmask_date( bytearray( [ 0x95, 0xe8, 0x0e, 0x10, 0x0c ] )) - (2012, 11, 16, 14, 40, 21) +__test__ = { + "unmask": """ + These examples are fixed! Thanks ehwest, robg. + >>> unmask_date( bytearray( [ 0x93, 0xd4, 0x0e, 0x10, 0x0c ] )) + (2012, 11, 16, 14, 20, 19) + >>> unmask_date( bytearray( [ 0xa6, 0xeb, 0x0b, 0x10, 0x0c, ] )) + (2012, 11, 16, 11, 43, 38) - >>> unmask_date( bytearray( [ 0x80, 0xcf, 0x30, 0x10, 0x0c, ] )) - (2012, 11, 16, 16, 15, 0) + >>> unmask_date( bytearray( [ 0x95, 0xe8, 0x0e, 0x10, 0x0c ] )) + (2012, 11, 16, 14, 40, 21) - >>> unmask_date( bytearray( [ 0xa3, 0xcf, 0x30, 0x10, 0x0c, ] )) - (2012, 11, 16, 16, 15, 35) + >>> unmask_date( bytearray( [ 0x80, 0xcf, 0x30, 0x10, 0x0c, ] )) + (2012, 11, 16, 16, 15, 0) -''', - 'encode_monthbyte': ''' - >>> encode_monthbyte(month=1) == bytearray(b'\x12^') - True + >>> unmask_date( bytearray( [ 0xa3, 0xcf, 0x30, 0x10, 0x0c, ] )) + (2012, 11, 16, 16, 15, 35) - >>> encode_monthbyte(month=2) == bytearray(b'\x12\x9e') - True + """, + "encode_monthbyte": """ + >>> encode_monthbyte(month=1) == bytearray(bytes('\x12^', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=3) == bytearray(b'\x12\xde') - True + >>> encode_monthbyte(month=2) == bytearray(bytes('\x12\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=10, minute=0, sec=0) == bytearray(b'\x80\x80') - True + >>> encode_monthbyte(month=3) == bytearray(bytes('\x12\xde', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=10, minute=0, sec=24) == bytearray(b'\x98\x80') - True -''', + >>> encode_monthbyte(month=10, minute=0, sec=0) == bytearray(bytes('\x80\x80', 'raw_unicode_escape')) + True + >>> encode_monthbyte(month=10, minute=0, sec=24) == bytearray(bytes('\x98\x80', 'raw_unicode_escape')) + True + """, } -if __name__ == '__main__': - import doctest - doctest.testmod( ) +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/decocare/scan.py b/decocare/scan.py index 762023a..2217f29 100644 --- a/decocare/scan.py +++ b/decocare/scan.py @@ -1,22 +1,24 @@ - import glob + class ID: - VENDOR = 0x0a21 - PRODUCT = 0x8001 - @classmethod - def template (Klass, prefix): - postfix = '*' - usb_id = "%04x_%04x" % (ID.VENDOR, ID.PRODUCT) - candidate = ''.join([prefix, usb_id, postfix]) - return candidate + VENDOR = 0x0A21 + PRODUCT = 0x8001 + + @classmethod + def template(Klass, prefix): + postfix = "*" + usb_id = "{:04x}_{:04x}".format(ID.VENDOR, ID.PRODUCT) + candidate = "".join([prefix, usb_id, postfix]) + return candidate + -def scan (prefix='/dev/serial/by-id/*-', template=ID.template): - candidate = template(prefix) - results = glob.glob(candidate) - return (results[0:1] or ['']).pop( ) +def scan(prefix="/dev/serial/by-id/*-", template=ID.template): + candidate = template(prefix) + results = glob.glob(candidate) + return (results[0:1] or [""]).pop() -if __name__ == '__main__': - candidate = scan( ) - print candidate +if __name__ == "__main__": + candidate = scan() + print(candidate) diff --git a/decocare/session.py b/decocare/session.py index 0fe2abe..cb7cfa2 100644 --- a/decocare/session.py +++ b/decocare/session.py @@ -1,134 +1,144 @@ import logging import time -log = logging.getLogger( ).getChild(__name__) -import commands -import lib -import models -from errors import StickError, AckError, BadDeviceCommError - - -class Session(object): - def __init__(self, stick): - self.stick = stick - self.should_download = True - def init(self, skip_power_control=False): - stick = self.stick - log.info('test fetching product info %s' % stick) - log.info(pformat(stick.product_info( ))) - log.info('get signal strength of %s' % stick) - signal = 0 - while signal < 50: - signal = stick.signal_strength( ) - log.info('we seem to have found a nice signal strength of: %s' % signal) - def end(self): - stick = self.stick - log.info(pformat(stick.usb_stats( ))) - log.info(pformat(stick.radio_stats( ))) - - def execute(self, command): - self.command = command - for i in xrange(max(1, self.command.retries)): - log.info('execute attempt: %s' % (i + 1)) - try: - self.expectedLength = self.command.bytesPerRecord * self.command.maxRecords - self.transfer( ) - if self.should_download: - log.info('sleeping %s before download' % command.effectTime) - time.sleep(command.effectTime) - self.download( ) - log.info('finished executing:%s' % command) - if command.done( ): - return command - except BadDeviceCommError, e: - log.critical("ERROR: %s" % e) - # self.clearBuffers( ) - log.critical('this seems like a problem') - - def download(self): - errors = [ ] - if self.expectedLength > 0: - log.info('proceeding with download') - data = self.stick.download( ) - #self.command.data = data - self.command.respond(data) - return data - else: - log.info('no download required') - assert not errors, ("with errors:%s" %"\n".join( map(str, errors) )) - def transfer(self): - log.info('session transferring packet') - return self.stick.transmit_packet(self.command) +log = logging.getLogger().getChild(__name__) +from decocare import commands, lib, models +from decocare.errors import AckError, BadDeviceCommError, StickError + + +class Session: + def __init__(self, stick): + self.stick = stick + self.should_download = True + + def init(self, skip_power_control=False): + stick = self.stick + log.info("test fetching product info %s" % stick) + log.info(pformat(stick.product_info())) + log.info("get signal strength of %s" % stick) + signal = 0 + while signal < 50: + signal = stick.signal_strength() + log.info("we seem to have found a nice signal strength of: %s" % signal) + + def end(self): + stick = self.stick + log.info(pformat(stick.usb_stats())) + log.info(pformat(stick.radio_stats())) + + def execute(self, command): + self.command = command + for i in range(max(1, self.command.retries)): + log.info("execute attempt: %s" % (i + 1)) + try: + self.expectedLength = ( + self.command.bytesPerRecord * self.command.maxRecords + ) + self.transfer() + if self.should_download: + log.info("sleeping %s before download" % command.effectTime) + time.sleep(command.effectTime) + self.download() + log.info("finished executing:%s" % command) + if command.done(): + return command + except BadDeviceCommError as e: + log.critical("ERROR: %s" % e) + # self.clearBuffers( ) + log.critical("this seems like a problem") + + def download(self): + errors = [] + if self.expectedLength > 0: + log.info("proceeding with download") + data = self.stick.download() + # self.command.data = data + self.command.respond(data) + return data + else: + log.info("no download required") + assert not errors, "with errors:%s" % "\n".join(map(str, errors)) + + def transfer(self): + log.info("session transferring packet") + return self.stick.transmit_packet(self.command) + class Pump(Session): - def __init__(self, stick, serial='208850'): - super(type(self), self).__init__(stick) - self.serial = serial - log.info('setting up to talk with %s' % serial) - - def power_control(self, minutes=None): - log.info('BEGIN POWER CONTROL %s' % self.serial) - # print "PowerControl SERIAL", self.serial - response = self.query(commands.PowerControl, minutes=minutes) - power = self.command - log.info('manually download PowerControl serial %s' % self.serial) - data = self.stick.download( ) - log.info("ENDING manual download:\n%s" % lib.hexdump(data)) - return data - - def setModel (self, model=None, number=None): - if number: - self.model = models.lookup(number, self) - if model: - self.model = model - def read_model(self): - model = self.query(commands.ReadPumpModel) - self.modelNumber = model.getData( ) - self.setModel(number=self.modelNumber) - return model - - def read_history_0(self): - log.info("read HISTORY DATA") - comm = commands.ReadHistoryData(serial=self.serial, page=0) - self.execute(comm) - log.info('comm:READ history data page!!!:\n%s' % (comm.getData( ))) - - def execute(self, command): - command.serial = self.serial - return super(type(self), self).execute(command) - def query(self, Command, **kwds): - command = Command(serial=self.serial, **kwds) - self.execute(command) - return command - -if __name__ == '__main__': - import doctest - doctest.testmod( ) - - import sys - port = None - port = sys.argv[1:] and sys.argv[1] or False - serial_num = sys.argv[2:] and sys.argv[2] or False - if not port or not serial_num: - print "usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0] - sys.exit(1) - import link - import stick - from pprint import pformat - logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - log.info("howdy! I'm going to take a look at your pump.") - stick = stick.Stick(link.Link(port, timeout=.200)) - stick.open( ) - session = Pump(stick, serial_num) - log.info(pformat(stick.interface_stats( ))) - session.power_control( ) - log.info(pformat(stick.interface_stats( ))) - log.info('PUMP MODEL: %s' % session.read_model( )) - log.info(pformat(stick.interface_stats( ))) - log.info('PUMP READ PAGE 0: %s' % session.read_history_0( )) - log.info(pformat(stick.interface_stats( ))) - log.info("howdy! all done looking at pump") - # stick.open( ) + def __init__(self, stick, serial="208850"): + super(type(self), self).__init__(stick) + self.serial = serial + log.info("setting up to talk with %s" % serial) + + def power_control(self, minutes=None): + log.info("BEGIN POWER CONTROL %s" % self.serial) + # print("PowerControl SERIAL", self.serial) + response = self.query(commands.PowerControl, minutes=minutes) + power = self.command + log.info("manually download PowerControl serial %s" % self.serial) + data = self.stick.download() + log.info("ENDING manual download:\n%s" % lib.hexdump(data)) + return data + + def setModel(self, model=None, number=None): + if number: + self.model = models.lookup(number, self) + if model: + self.model = model + + def read_model(self): + model = self.query(commands.ReadPumpModel) + self.modelNumber = model.getData() + self.setModel(number=self.modelNumber) + return model + + def read_history_0(self): + log.info("read HISTORY DATA") + comm = commands.ReadHistoryData(serial=self.serial, page=0) + self.execute(comm) + log.info("comm:READ history data page!!!:\n%s" % (comm.getData())) + + def execute(self, command): + command.serial = self.serial + return super(type(self), self).execute(command) + + def query(self, Command, **kwds): + command = Command(serial=self.serial, **kwds) + self.execute(command) + return command + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + import sys + + port = None + port = sys.argv[1:] and sys.argv[1] or False + serial_num = sys.argv[2:] and sys.argv[2] or False + if not port or not serial_num: + print("usage:\n%s , eg /dev/ttyUSB0 208850" % sys.argv[0]) + sys.exit(1) + from pprint import pformat + + from decocare import link, stick + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + log.info("howdy! I'm going to take a look at your pump.") + stick = stick.Stick(link.Link(port, timeout=0.200)) + stick.open() + session = Pump(stick, serial_num) + log.info(pformat(stick.interface_stats())) + session.power_control() + log.info(pformat(stick.interface_stats())) + log.info("PUMP MODEL: %s" % session.read_model()) + log.info(pformat(stick.interface_stats())) + log.info("PUMP READ PAGE 0: %s" % session.read_history_0()) + log.info(pformat(stick.interface_stats())) + log.info("howdy! all done looking at pump") + # stick.open( ) ##### # EOF diff --git a/decocare/stick.py b/decocare/stick.py index d12731a..86004dc 100755 --- a/decocare/stick.py +++ b/decocare/stick.py @@ -1,8 +1,8 @@ - -import lib import logging import time +from decocare import lib + """ stick - implement a naive open source driver for Medtronic's Carelink USB stick. @@ -20,271 +20,304 @@ """ -log = logging.getLogger( ).getChild(__name__) +log = logging.getLogger().getChild(__name__) + +from decocare.errors import AckError, BadDeviceCommError, StickError + -from errors import StickError, AckError, BadDeviceCommError +class BadCRC(StickError): + pass + + +class UnresponsiveError(StickError): + pass -class BadCRC(StickError): pass -class UnresponsiveError (StickError): pass def CRC8(data): - return lib.CRC8.compute(data) - -class StickCommand(object): - """Basic stick command - - Each command is used to talk to the usb stick. - The usb stick interprets the opcode, and then performs the function - associated with the opcode. - Altogether, the suite of opcodes that the stick responds to allows - you to debug and track all packets you are sending/receiving plus - allows you to send recieve commands to the pump, by formatting your - message into payloads with opcodes, and then letting the stick work - on what you've given it. It's kind of like a modem with this funky - binary interface and 64 byte payloads. - """ - code = [ 0x00 ] - label = 'example stick command' - delay = .001 - size = 64 - - def __str__(self): - code = ' '.join([ '%#04x' % op for op in self.code ]) - return '{0}:{1}'.format(self.__class__.__name__, code) - def __repr__(self): - return '<{0:s}:size({1})>'.format(self, self.size) - def format(self): - return self.format_cl2(*self.code) - - def format_cl2(self, msg, a2=0x00, a3=0x00): - # generally commands are 3 bytes, most often CMD, 0x00, 0x00 - msg = bytearray([ msg, a2, a3 ]) - return msg - - def parse(self, data): - self.data = data - - def respond(self, raw): - if len(raw) == 0: - log.error("ACK is zero bytes!") - # return False - raise AckError("ACK is 0 bytes:\n%s" % lib.hexdump(raw)) - commStatus = raw[0] - # usable response - assert commStatus == 1, ('commStatus: %02x expected 0x1' % commStatus) - status = raw[1] - # status == 102 'f' NAK, look up NAK - if status == 85: # 'U' - return raw[:3], raw[3:] - assert False, ("NAK!!\n%s" % lib.hexdump(raw[:3])) - + return lib.CRC8.compute(data) + + +class StickCommand: + """Basic stick command + + Each command is used to talk to the usb stick. + The usb stick interprets the opcode, and then performs the function + associated with the opcode. + Altogether, the suite of opcodes that the stick responds to allows + you to debug and track all packets you are sending/receiving plus + allows you to send recieve commands to the pump, by formatting your + message into payloads with opcodes, and then letting the stick work + on what you've given it. It's kind of like a modem with this funky + binary interface and 64 byte payloads. + """ + + code = [0x00] + label = "example stick command" + delay = 0.001 + size = 64 + + def __str__(self): + code = " ".join(["%#04x" % op for op in self.code]) + return "{}:{}".format(self.__class__.__name__, code) + + def __repr__(self): + return "<{:s}:size({})>".format(self, self.size) + + def format(self): + return self.format_cl2(*self.code) + + def format_cl2(self, msg, a2=0x00, a3=0x00): + # generally commands are 3 bytes, most often CMD, 0x00, 0x00 + msg = bytearray([msg, a2, a3]) + return msg + + def parse(self, data): + self.data = data + + def respond(self, raw): + if len(raw) == 0: + log.error("ACK is zero bytes!") + # return False + raise AckError("ACK is 0 bytes:\n%s" % lib.hexdump(raw)) + commStatus = raw[0] + # usable response + assert commStatus == 1, "commStatus: %02x expected 0x1" % commStatus + status = raw[1] + # status == 102 'f' NAK, look up NAK + if status == 85: # 'U' + return raw[:3], raw[3:] + assert False, "NAK!!\n%s" % lib.hexdump(raw[:3]) + class ProductInfo(StickCommand): - """Get product info from the usb device. - - Useful for identifying - what kind of usb stick you've got; there are a few different kinds. - Eg, European vs US regulatory domains require different frequencies for compliance. - - """ - code = [ 4 ] - SW_VER = 16 - label = 'usb.productInfo' - rf_table = { 001: '868.35Mhz' , - 000: '916.5Mhz' , - 255: '916.5Mhz' } - iface_key = { 3: 'USB', - 1: 'Paradigm RF' } - - @classmethod - def decodeInterfaces( klass, L ): - n, tail = L[ 0 ], L[ 1: ] - interfaces = [ ] - for x in xrange( n ): - i = x*2 - k, v = tail[i], tail[i+1] - interfaces.append( ( k, klass.iface_key.get( v, 'UNKNOWN' ) ) ) - return interfaces - - @classmethod - def decode( klass, data ): - return { - 'rf.freq' : klass.rf_table.get( data[ 5 ], 'UNKNOWN' ) - , 'serial' : str( data[ 0:3 ]).encode( 'hex' ) - , 'product.version' : '{0}.{1}'.format( *data[ 3:5 ] ) - , 'description' : str( data[ 06:16 ] ) - , 'software.version' : '{0}.{1}'.format( *data[ 16:18 ] ) - , 'interfaces' : klass.decodeInterfaces( data[ 18: ] ) - } - - _test_ok = bytearray( [ - ] ) - def parse(self, data): + """Get product info from the usb device. + + Useful for identifying + what kind of usb stick you've got; there are a few different kinds. + Eg, European vs US regulatory domains require different frequencies for compliance. + """ + + code = [4] + SW_VER = 16 + label = "usb.productInfo" + rf_table = {1: "868.35Mhz", 0: "916.5Mhz", 255: "916.5Mhz"} + iface_key = {3: "USB", 1: "Paradigm RF"} + + @classmethod + def decodeInterfaces(klass, L): + n, tail = L[0], L[1:] + interfaces = [] + for x in range(n): + i = x * 2 + k, v = tail[i], tail[i + 1] + interfaces.append((k, klass.iface_key.get(v, "UNKNOWN"))) + return interfaces + + @classmethod + def decode(klass, data): + return { + "rf.freq": klass.rf_table.get(data[5], "UNKNOWN"), + "serial": str(data[0:3]).encode("hex"), + "product.version": "{}.{}".format(*data[3:5]), + "description": str(data[6:16]), + "software.version": "{}.{}".format(*data[16:18]), + "interfaces": klass.decodeInterfaces(data[18:]), + } + + _test_ok = bytearray([]) + + def parse(self, data): + """ #>>> """ - return self.decode(data) + return self.decode(data) + class InterfaceStats(StickCommand): - """Abstract stats decoder. - """ - code = [ 5 ] - INTERFACE_IDX = 19 - label = 'usb.interfaceStats' - @classmethod - def decode( klass, data): + """Abstract stats decoder. """ + + code = [5] + INTERFACE_IDX = 19 + label = "usb.interfaceStats" + + @classmethod + def decode(klass, data): + """ Decode interface stats. The stick exposes 6 counters to monitor errors, crcs, naks, timeouts, rx, and tx packets. Very useful for debugging. """ - return { - 'errors.crc' : data[ 0 ] - , 'errors.sequence' : data[ 1 ] - , 'errors.naks' : data[ 2 ] - , 'errors.timeouts' : data[ 3 ] - , 'packets.received': lib.BangLong( data[ 4: 8 ] ) - , 'packets.transmit': lib.BangLong( data[ 8:12 ] ) - } - def parse(self, data): - """ + return { + "errors.crc": data[0], + "errors.sequence": data[1], + "errors.naks": data[2], + "errors.timeouts": data[3], + "packets.received": lib.BangLong(data[4:8]), + "packets.transmit": lib.BangLong(data[8:12]), + } + + def parse(self, data): + """ #>>> """ - return self.decode(data) + return self.decode(data) class UsbStats(InterfaceStats): - """Count of packets and stats on the usb side of the stick.""" - code = [ 5, 1 ] + """Count of packets and stats on the usb side of the stick.""" + + code = [5, 1] + class RadioStats(InterfaceStats): - """Count of packets and stats on the radio side of the stick.""" - code = [ 5, 0 ] + """Count of packets and stats on the radio side of the stick.""" + + code = [5, 0] + class SignalStrength(StickCommand): - """This seems to be required to initialize communications with the - usb stick. Also, you should wait until a minimum threshold is - reached. - """ - code = [ 6, 0 ] - def parse(self, data): + """This seems to be required to initialize communications with the + usb stick. Also, you should wait until a minimum threshold is + reached. """ + + code = [6, 0] + + def parse(self, data): + """ #>>> """ - # result[0] is signal strength - self.value = int(data[0]) - log.info('%r:readSignalStrength:%s' % (self, int(data[0]))) - return int(data[0]) + # result[0] is signal strength + self.value = int(data[0]) + log.info("{!r}:readSignalStrength:{}".format(self, int(data[0]))) + return int(data[0]) + class LinkStatus(StickCommand): - """Basic ACK type of command. - Used to poll the modem's radio buffer. When the radio buffer is - full, we can download a packet from the buffer. Otherwise, we need - to be mindful of the state the radio is in. This opcode tells you - the current state of the radio/stick. - """ - code = [ 0x03 ] - reasons = ['OK'] - - def __str__(self): - extra = '' - size = getattr(self, 'size', None) or '??' - extra += "size=%s" % size - if getattr(self, 'error', False): - extra += '{0}:error:{1}:reason:{2}'.format(self.__class__.__name__, self.error, str(self.reasons)) - base = super(type(self), self).__str__( ) - return '{0}:status:{1}'.format(base, extra) - - def record_error(self, result): - self.error = True - self.ack = result[0] # 0 indicates success - # believe success = result[1] # 'U' or 'f' - self.status = result[2] - lb, hb = result[3], result[4] - self.size = lib.BangInt((lb, hb)) - - if self.ack == 0 and (self.status & 0x1) > 0: - self.error = False - self.set_reason(self.ack) - - def set_reason(self, status): - reasons = [ ] - if (status & 0x2) > 0: - reasons.append('STATUS: receive in progress!') - if (status & 0x4) > 0: - reasons.append('STATUS: transmit in progress!') - if (status & 0x8) > 0: - reasons.append('STATUS: interface error!') - if (status & 0x10) > 0: - reasons.append('STATUS: receive overflow!') - if (status & 0x20) > 0: - reasons.append('STATUS: transmit overflow!') - if (status & 0x1) > 0: - reasons.append('STATUS: OK') - msg = '\n'.join(map(str, [ self, '|'.join(reasons) ])) - log.info(msg) - self.reasons = reasons - - def parse(self, result): + """Basic ACK type of command. + Used to poll the modem's radio buffer. When the radio buffer is + full, we can download a packet from the buffer. Otherwise, we need + to be mindful of the state the radio is in. This opcode tells you + the current state of the radio/stick. """ + + code = [0x03] + reasons = ["OK"] + + def __str__(self): + extra = "" + size = getattr(self, "size", None) or "??" + extra += "size=%s" % size + if getattr(self, "error", False): + extra += "{}:error:{}:reason:{}".format( + self.__class__.__name__, self.error, str(self.reasons) + ) + base = super(type(self), self).__str__() + return "{}:status:{}".format(base, extra) + + def record_error(self, result): + self.error = True + self.ack = result[0] # 0 indicates success + # believe success = result[1] # 'U' or 'f' + self.status = result[2] + lb, hb = result[3], result[4] + self.size = lib.BangInt((lb, hb)) + + if self.ack == 0 and (self.status & 0x1) > 0: + self.error = False + self.set_reason(self.ack) + + def set_reason(self, status): + reasons = [] + if (status & 0x2) > 0: + reasons.append("STATUS: receive in progress!") + if (status & 0x4) > 0: + reasons.append("STATUS: transmit in progress!") + if (status & 0x8) > 0: + reasons.append("STATUS: interface error!") + if (status & 0x10) > 0: + reasons.append("STATUS: receive overflow!") + if (status & 0x20) > 0: + reasons.append("STATUS: transmit overflow!") + if (status & 0x1) > 0: + reasons.append("STATUS: OK") + msg = "\n".join(map(str, [self, "|".join(reasons)])) + log.info(msg) + self.reasons = reasons + + def parse(self, result): + """ #>>> """ - self.record_error(result) - if self.ack != 0: - log.error("readStatus: non-zero status: %02x" % self.ack) - # should this trigger a retry, and if so where? - # should the other usb commands also trigger these retries? and at the - # same points? - raise AckError("readStatus: non-zero status: %02x" % self.ack) - if self.error is not True: - return self.size - return 0 + self.record_error(result) + if self.ack != 0: + log.error("readStatus: non-zero status: %02x" % self.ack) + # should this trigger a retry, and if so where? + # should the other usb commands also trigger these retries? and at the + # same points? + raise AckError("readStatus: non-zero status: %02x" % self.ack) + if self.error is not True: + return self.size + return 0 + class ReadRadio(StickCommand): - """ - Read buffer from the radio. - - Downloads a packet from the radio buffer. - - """ - code = [ 0x0C, 0x00 ] - dl_size = 0 - size = 64 - def __init__(self, size): - self.size = size - self.dl_size = size - packet = [12, 0, lib.HighByte(size), lib.LowByte(size)] - if size < 64 and size != 15: - log.error('size (%s) is less than 64 and not 15, which may cause an error.' % size) - self.size = 64 - self.code = packet + [ CRC8(packet) ] - - def __str__(self): - return '{0}:size:{1}'.format(self.__class__.__name__, self.dl_size) - def __repr__(self): - return '<{0:s}>'.format(self) - def format(self): - msg = bytearray(self.code) - return msg - - def respond(self, raw): - if len(raw) == 0: - log.error("ReadRadio ACK is zero bytes!") - # return False - raise AckError("ACK is 0 bytes: %s" % lib.hexdump(raw)) - log.info('readData validating remote raw[ack]: %02x' % raw[0]) - log.info('readData; foreign raw should be at least 14 bytes? %s %s' % (len(raw), len(raw) > 14)) - log.info('readData; raw[retries] %s' % int(raw[3])) - dl_status = int(raw[0]) - if dl_status != 0x02: # this differs from the others? - raise BadDeviceCommError("bad dl raw! %r" % raw) - assert (int(raw[0]) == 2), repr(raw) - return raw[:1], raw - - def parse(self, raw): """ - Detect BadCRC here. Also, look for eod set. + Read buffer from the radio. + + Downloads a packet from the radio buffer. + """ + + code = [0x0C, 0x00] + dl_size = 0 + size = 64 + + def __init__(self, size): + self.size = size + self.dl_size = size + packet = [12, 0, lib.HighByte(size), lib.LowByte(size)] + if size < 64 and size != 15: + log.error( + "size (%s) is less than 64 and not 15, which may cause an error." % size + ) + self.size = 64 + self.code = packet + [CRC8(packet)] + + def __str__(self): + return "{}:size:{}".format(self.__class__.__name__, self.dl_size) + + def __repr__(self): + return "<{:s}>".format(self) + + def format(self): + msg = bytearray(self.code) + return msg + + def respond(self, raw): + if len(raw) == 0: + log.error("ReadRadio ACK is zero bytes!") + # return False + raise AckError("ACK is 0 bytes: %s" % lib.hexdump(raw)) + log.info("readData validating remote raw[ack]: %02x" % raw[0]) + log.info( + "readData; foreign raw should be at least 14 bytes? {} {}".format( + len(raw), len(raw) > 14 + ) + ) + log.info("readData; raw[retries] %s" % int(raw[3])) + dl_status = int(raw[0]) + if dl_status != 0x02: # this differs from the others? + raise BadDeviceCommError("bad dl raw! %r" % raw) + assert int(raw[0]) == 2, repr(raw) + return raw[:1], raw + + def parse(self, raw): + """ + Detect BadCRC here. Also, look for eod set. """ + """ log.info('readData validating remote raw[ack]: %02x' % raw[0]) log.info('readData; foreign raw should be at least 14 bytes? %s %s' % (len(raw), len(raw) > 14)) log.info('readData; raw[retries] %s' % int(raw[3])) @@ -300,174 +333,188 @@ def parse(self, raw): lb, hb = raw[5] & 0x7F, raw[6] self.eod = (raw[5] & 0x80) > 0 """ - lb, hb = raw[5] & 0x7F, raw[6] - self.eod = (raw[5] & 0x80) > 0 - resLength = lib.BangInt((lb, hb)) - # we don't really care about the length - #assert resLength < 64, ("cmd low byte count:\n%s" % lib.hexdump(raw)) - - data = raw[13:13+resLength] - self.packet = data - log.info('%s:eod:found eod (%s)' % (self, self.eod)) - log.info('found packet len(%s), link expects(%s)' % (len(self.packet), resLength)) - assert len(data) == resLength - head = raw[13:] - crc = raw[-1] - # crc check - if crc == 0 and len(data) > 1: - log.warn('bad zero CRC?') - expected_crc = CRC8(data) - if crc != expected_crc: - msg = ':'.join( [ 'ReadRadio:BAD ACK:found raw[crc]: %#04x' % (crc), - 'expected_crc(data): %#04x' % (expected_crc), - 'raw:\n%s\n' % (lib.hexdump(raw)), - 'head:\n%s\n' % (lib.hexdump(head)), - 'data:\n%s\n' % (lib.hexdump(data)) ] ) - log.info(msg) - log.info("XXX:IGNORE:BadCRC:returning empty message, sleep .100, avoid errors.") - time.sleep(.100) - return bytearray( ) - raise BadCRC(msg) - assert crc == expected_crc - return data + lb, hb = raw[5] & 0x7F, raw[6] + self.eod = (raw[5] & 0x80) > 0 + resLength = lib.BangInt((lb, hb)) + # we don't really care about the length + # assert resLength < 64, ("cmd low byte count:\n%s" % lib.hexdump(raw)) + + data = raw[13 : 13 + resLength] + self.packet = data + log.info("{}:eod:found eod ({})".format(self, self.eod)) + log.info( + "found packet len({}), link expects({})".format(len(self.packet), resLength) + ) + assert len(data) == resLength + head = raw[13:] + crc = raw[-1] + # crc check + if crc == 0 and len(data) > 1: + log.warn("bad zero CRC?") + expected_crc = CRC8(data) + if crc != expected_crc: + msg = ":".join( + [ + "ReadRadio:BAD ACK:found raw[crc]: %#04x" % (crc), + "expected_crc(data): %#04x" % (expected_crc), + "raw:\n%s\n" % (lib.hexdump(raw)), + "head:\n%s\n" % (lib.hexdump(head)), + "data:\n%s\n" % (lib.hexdump(data)), + ] + ) + log.info(msg) + log.info( + "XXX:IGNORE:BadCRC:returning empty message, sleep .100, avoid errors." + ) + time.sleep(0.100) + return bytearray() + raise BadCRC(msg) + assert crc == expected_crc + return data + class TransmitPacket(StickCommand): - """Format a packet to send on the radio. - - This commands formats a packet from usb, and shoves it into the - radio buffer. - The radio buffer is broadcast "over the air" so that any device - sensitive to the packets you sent will respond accordingly - (probably sending data back). - For this reason, the serial number of the device you'd like to talk - to is formatted into the packet. - - """ - code = [ 1, 0, 167, 1 ] - head = [ 1, 0, 167, 1 ] - # wraps pump commands - def __init__(self, command): - self.command = command - self.params = command.params - self.code = command.code - self.retries = command.retries - self.serial = command.serial - # self.delay = command.effectTime - - def __str__(self): - if getattr(self, 'command', False): - return '{0}:{1:s}'.format(self.__class__.__name__, self.command) - code = ' '.join([ '%#04x' % op for op in self.head ]) - return '{0}:{1}'.format(self.__class__.__name__, code) - def __repr__(self): - return '<{0:s}>'.format(self) - - - def calcRecordsRequired(self): - return self.command.calcRecordsRequired( ) - - def format(self): + """Format a packet to send on the radio. + + This commands formats a packet from usb, and shoves it into the + radio buffer. + The radio buffer is broadcast "over the air" so that any device + sensitive to the packets you sent will respond accordingly + (probably sending data back). + For this reason, the serial number of the device you'd like to talk + to is formatted into the packet. + """ + + code = [1, 0, 167, 1] + head = [1, 0, 167, 1] + # wraps pump commands + def __init__(self, command): + self.command = command + self.params = command.params + self.code = command.code + self.retries = command.retries + self.serial = command.serial + # self.delay = command.effectTime + + def __str__(self): + if getattr(self, "command", False): + return "{}:{:s}".format(self.__class__.__name__, self.command) + code = " ".join(["%#04x" % op for op in self.head]) + return "{}:{}".format(self.__class__.__name__, code) + + def __repr__(self): + return "<{:s}>".format(self) + + def calcRecordsRequired(self): + return self.command.calcRecordsRequired() + + def format(self): + """ Formatting of the packet to be sent gets done here. """ - params = self.params - code = self.code - maxRetries = self.retries - serial = list(bytearray(self.serial.decode('hex'))) - paramsCount = len(params) - head = [ 1, 0, 167, 1 ] - # serial - packet = head + serial - # paramCount 2 bytes - packet.extend( [ (0x80 | lib.HighByte(paramsCount)), - lib.LowByte(paramsCount) ] ) - # not sure what this byte means - button = 0 - # special case command 93 - if code == 93: - button = 85 - packet.append(button) - packet.append(maxRetries) - # how many packets/frames/pages/flows will this take? - responseSize = self.calcRecordsRequired() - # really only 1 or 2? - pages = responseSize - if responseSize > 1: - pages = 2 - packet.append(pages) - packet.append(0) - # command code goes here - packet.append(code) - packet.append(CRC8(packet)) - packet.extend(params) - packet.append(CRC8(params)) - log.debug(packet) - return bytearray(packet) - - def respond(self, raw): - code = self.command.code - params = self.params - if code != 93 or params[0] != 0: - ack, body = super(type(self), self).respond(raw) - return ack, body - - return (bytearray(raw), bytearray(raw)) - def parse(self, results): - return results - #self.checkAck(results) - - - -class Stick(object): - """ - The carelink usb stick acts like a buffer. - - It has a variety of commands providing synchronous IO, eg, you may - generally perform a read immediately after writing to it, and expect a - response. - - The commands operate on a local buffer used to facilitate exchanging - messages over RF with the pump. RF communication with the pump - happens asynchronously, requiring us to go through 3 separate - phases for each message we'd like to exchange with the pumps: - * transmit - send commmand - * poll_size - loop - * download - loop - - Each command is usually only 3 bytes. - - The protocol offers some facility for detecting and recovering - from inconsistencies in the underlying transport of data, however, - we are somwhat ignorant of them. The tricky bits are exactly how - to recover from, eg CRC, errors that can occur. - The "shape" and timing of these loops seem to mostly get the job - done. - - The Stick object provides a bunch of useful methods, that given a link, - will represent the state of one active usb stick. - - """ - link = None - def __init__(self, link): - self.link = link - self.command = None - self._download_i = False - - def __str__(self): - s = [ self.__class__.__name__, - 'transmit[{}]' .format(str(getattr(self, 'transmit', None))), - 'reader[{}]' .format(str(getattr(self, 'reader', None))), - 'download_i[{}]'.format(str(getattr(self, '_download_i', None))), - 'status[{}]' .format(repr(getattr(self, 'last_status', None))), - 'poll_size[{}]' .format(str(getattr(self, '_poll_size', None))), - 'poll_i[{}]' .format(str(getattr(self, '_poll_i', None))), - 'command[{}]' .format(repr(getattr(self, 'command', None))), - ] - return ' '.join(s) - def __repr__(self): - return '<{0}>'.format(str(self)) - def process(self): + params = self.params + code = self.code + maxRetries = self.retries + serial = list(bytearray(self.serial.decode("hex"))) + paramsCount = len(params) + head = [1, 0, 167, 1] + # serial + packet = head + serial + # paramCount 2 bytes + packet.extend([(0x80 | lib.HighByte(paramsCount)), lib.LowByte(paramsCount)]) + # not sure what this byte means + button = 0 + # special case command 93 + if code == 93: + button = 85 + packet.append(button) + packet.append(maxRetries) + # how many packets/frames/pages/flows will this take? + responseSize = self.calcRecordsRequired() + # really only 1 or 2? + pages = responseSize + if responseSize > 1: + pages = 2 + packet.append(pages) + packet.append(0) + # command code goes here + packet.append(code) + packet.append(CRC8(packet)) + packet.extend(params) + packet.append(CRC8(params)) + log.debug(packet) + return bytearray(packet) + + def respond(self, raw): + code = self.command.code + params = self.params + if code != 93 or params[0] != 0: + ack, body = super(type(self), self).respond(raw) + return ack, body + + return (bytearray(raw), bytearray(raw)) + + def parse(self, results): + return results + # self.checkAck(results) + + +class Stick: + """ + The carelink usb stick acts like a buffer. + + It has a variety of commands providing synchronous IO, eg, you may + generally perform a read immediately after writing to it, and expect a + response. + + The commands operate on a local buffer used to facilitate exchanging + messages over RF with the pump. RF communication with the pump + happens asynchronously, requiring us to go through 3 separate + phases for each message we'd like to exchange with the pumps: + * transmit - send commmand + * poll_size - loop + * download - loop + + Each command is usually only 3 bytes. + + The protocol offers some facility for detecting and recovering + from inconsistencies in the underlying transport of data, however, + we are somwhat ignorant of them. The tricky bits are exactly how + to recover from, eg CRC, errors that can occur. + The "shape" and timing of these loops seem to mostly get the job + done. + + The Stick object provides a bunch of useful methods, that given a link, + will represent the state of one active usb stick. + """ + + link = None + + def __init__(self, link): + self.link = link + self.command = None + self._download_i = False + + def __str__(self): + s = [ + self.__class__.__name__, + "transmit[{}]".format(str(getattr(self, "transmit", None))), + "reader[{}]".format(str(getattr(self, "reader", None))), + "download_i[{}]".format(str(getattr(self, "_download_i", None))), + "status[{}]".format(repr(getattr(self, "last_status", None))), + "poll_size[{}]".format(str(getattr(self, "_poll_size", None))), + "poll_i[{}]".format(str(getattr(self, "_poll_i", None))), + "command[{}]".format(repr(getattr(self, "command", None))), + ] + return " ".join(s) + + def __repr__(self): + return "<{}>".format(str(self)) + + def process(self): + """ Working with the usb stick typically follows a pretty routine process: 1. send our opcode, get a response 2. use some custom logic, per opcode to respond to the stick's reponse @@ -475,11 +522,12 @@ def process(self): This has to be done for each opcode. """ - msg = ':'.join(['PROCESS', 'START' - ] + map(str, [ self.timer.millis( ), self.command])) - log.info(msg) - log.info('link %s processing %s)' % ( self, self.command )) - """ + msg = ":".join( + ["PROCESS", "START"] + list(map(str, [self.timer.millis(), self.command])) + ) + log.info(msg) + log.info("link {} processing {})".format(self, self.command)) + """ self.link.write(self.command.format( )) log.debug('sleeping %s' % self.command.delay) time.sleep(self.command.delay) @@ -487,166 +535,189 @@ def process(self): raw = bytearray(self.link.read(size)) """ - raw = self.send_force_read( ) - if not raw or len(raw) == 0: - log.info('process zero length READ, try once more sleep .010') - time.sleep(.010) - raw = bytearray(self.link.read(self.command.size)) + raw = self.send_force_read() + if not raw or len(raw) == 0: + log.info("process zero length READ, try once more sleep .010") + time.sleep(0.010) + raw = bytearray(self.link.read(self.command.size)) + + ack, response = self.command.respond(raw) + info = self.command.parse(response) + log.info("finished processing {}, {}".format(self.command, repr(info))) + msg = ":".join( + ["PROCESS", "END"] + list(map(str, [self.timer.millis(), self.command])) + ) + log.info(msg) + return info - ack, response = self.command.respond(raw) - info = self.command.parse(response) - log.info('finished processing {0}, {1}'.format(self.command, repr(info))) - msg = ':'.join(['PROCESS', 'END' - ] + map(str, [ self.timer.millis( ), self.command])) - log.info(msg) - return info - - def query(self, Command): - """ + def query(self, Command): + """ query - simplify the process of working with the stick, pass your command, get the result """ - self.command = command = Command( ) - return self.process( ) + self.command = command = Command() + return self.process() - def product_info(self): - """ + def product_info(self): + """ Get the product info from the connected stick. """ - return self.query(ProductInfo) + return self.query(ProductInfo) - def interface_stats(self): - """ + def interface_stats(self): + """ debug both sets of interface stats. """ - return {'usb': self.usb_stats( ), 'radio': self.radio_stats( ) } - - def usb_stats(self): - """ + return {"usb": self.usb_stats(), "radio": self.radio_stats()} + + def usb_stats(self): + """ just get usb stats. """ - return self.query(UsbStats) + return self.query(UsbStats) - def radio_stats(self): - """ + def radio_stats(self): + """ just get radio stats. """ - return self.query(RadioStats) + return self.query(RadioStats) - def signal_strength(self): - """ + def signal_strength(self): + """ just get signal strength from connected stick """ - return self.query(SignalStrength) + return self.query(SignalStrength) - def poll_size(self): - """ + def poll_size(self): + """ query how many bytes are waiting in the radio buffer, ready to be downloaded There seem to be a few sweet spots, where you want to download the data. """ - size = 0 - start = time.time() - i = 0 - log.debug('%r:STARTING POLL PHASE:attempt:%s' % (self, i)) - #while size == 0 and size < 64 and time.time() - start < 1: - while size == 0 and time.time() - start < 1: - self._poll_i = i - self._poll_size = size - log.debug('%r:poll:attempt:%s' % (self, i)) - size = self.read_status( ) - self._poll_size = size - if size == 0: - log.debug('poll zero, sleeping in POLL, .100') - time.sleep(.100) - i += 1 - log.info('%s:STOP POLL after %s attempts:size:%s' % (self, i, size)) - self._poll_size = size - self._poll_i = False - return size - - def read_status(self): - """ + size = 0 + start = time.time() + i = 0 + log.debug("{!r}:STARTING POLL PHASE:attempt:{}".format(self, i)) + # while size == 0 and size < 64 and time.time() - start < 1: + while size == 0 and time.time() - start < 1: + self._poll_i = i + self._poll_size = size + log.debug("{!r}:poll:attempt:{}".format(self, i)) + size = self.read_status() + self._poll_size = size + if size == 0: + log.debug("poll zero, sleeping in POLL, .100") + time.sleep(0.100) + i += 1 + log.info("{}:STOP POLL after {} attempts:size:{}".format(self, i, size)) + self._poll_size = size + self._poll_i = False + return size + + def read_status(self): + """ Get current link status. """ - # log.debug('read_status') - result = self.query(LinkStatus) - self.last_status = self.command - return result + # log.debug('read_status') + result = self.query(LinkStatus) + self.last_status = self.command + return result - def old_download_packet(self, size): - """ + def old_download_packet(self, size): + """ Naive version of downloading a packet. Didn't quite work right. """ - log.info("download_packet:%s" % (size)) - self.command = ReadRadio(size) - packet = self.process( ) - return packet + log.info("download_packet:%s" % (size)) + self.command = ReadRadio(size) + packet = self.process() + return packet - def send_force_read(self, retries=1, timeout=1): - """ + def send_force_read(self, retries=1, timeout=1): + """ Pretty simple, try really hard to ensure that we've sent our bytes, and we get a response. This is probably overkill, but seems to get the job done. """ - # - # so the behavior of a read_radio should probably be similar to - # poll_size?? - reader = self.command - read_size = 64 - size = reader.size - start = time.time( ) - raw = bytearray( ) - for attempt in xrange(retries): - log.info(' '.join([ - 'send_force_read: attempt {0}/{1}'.format(attempt, retries), - 'send command,', - 'read until we get something within some timeout'])) - log.info('link %s sending %s)' % ( self, reader )) - self.link.write(reader.format( )) - log.debug('sleeping %s' % reader.delay) - time.sleep(reader.delay) - raw = bytearray(self.link.read(size)) - if len(raw) == 0: - log.info('zero length READ, try once more sleep .250') - time.sleep(.250) - raw = bytearray(self.link.read(self.command.size)) - - if len(raw) != 0: - log.info(' '.join(['quit send_force_read,', - 'found len:', str(len(raw)), - 'expected', str(size), - 'after', str(attempt), 'attempts'])) - return raw - log.critical(' '.join([ "FAILED TO DOWNLOAD ANYTHING,", - "after %s " % (attempt), - "expected:%s" % (size) ])) - assert not raw - - def download_packet(self, size): - """ + # + # so the behavior of a read_radio should probably be similar to + # poll_size?? + reader = self.command + read_size = 64 + size = reader.size + start = time.time() + raw = bytearray() + for attempt in range(retries): + log.info( + " ".join( + [ + "send_force_read: attempt {}/{}".format(attempt, retries), + "send command,", + "read until we get something within some timeout", + ] + ) + ) + log.info("link {} sending {})".format(self, reader)) + self.link.write(reader.format()) + log.debug("sleeping %s" % reader.delay) + time.sleep(reader.delay) + raw = bytearray(self.link.read(size)) + if len(raw) == 0: + log.info("zero length READ, try once more sleep .250") + time.sleep(0.250) + raw = bytearray(self.link.read(self.command.size)) + + if len(raw) != 0: + log.info( + " ".join( + [ + "quit send_force_read,", + "found len:", + str(len(raw)), + "expected", + str(size), + "after", + str(attempt), + "attempts", + ] + ) + ) + return raw + log.critical( + " ".join( + [ + "FAILED TO DOWNLOAD ANYTHING,", + "after %s " % (attempt), + "expected:%s" % (size), + ] + ) + ) + assert not raw + + def download_packet(self, size): + """ This is the tricky bit, where we stroke the radio and hope it gives us a buffer full of data. """ - log.info("%s:download_packet:%s" % (self, size)) - # XXX: this is the tricky bit - original_size = size - self.command = reader = ReadRadio(size) - self.reader = reader - msg = ':'.join(['PROCESS', 'START' - ] + map(str, [ self.timer.millis( ), self.command])) - log.info(msg) - if size == 0: - log.info('Download Size is ZERO, returning nothing') - return bytearray( ) - - raw = self.send_force_read( ) - # return - # packet = self.process( ) - # return packet - - # copy pasted from process - """ + log.info("{}:download_packet:{}".format(self, size)) + # XXX: this is the tricky bit + original_size = size + self.command = reader = ReadRadio(size) + self.reader = reader + msg = ":".join( + ["PROCESS", "START"] + list(map(str, [self.timer.millis(), self.command])) + ) + log.info(msg) + if size == 0: + log.info("Download Size is ZERO, returning nothing") + return bytearray() + + raw = self.send_force_read() + # return + # packet = self.process( ) + # return packet + + # copy pasted from process + """ log.info('link %s processing %s)' % ( self, self.command )) # self.link.process(command) self.link.write(self.command.format( )) @@ -656,86 +727,91 @@ def download_packet(self, size): raw = bytearray(self.link.read(size)) """ - # if len(raw) == 0: - if not raw: - log.info('zero length READ, try once more sleep .500') - time.sleep(.500) - raw = bytearray(self.link.read(self.command.size)) - - try: - ack, response = self.command.respond(raw) - info = self.command.parse(response) - msg = ':'.join(['PROCESS', 'END' - ] + map(str, [ self.timer.millis( ), self.command])) - log.info(msg) - return info - except BadDeviceCommError, e: - log.critical("download_packet:%s:ERROR:%s:ACK!?" % (self, e)) - log.info("we failed to pass %s ACK!?" % (self.command)) - log.info('expected size was: %s' % original_size) - status = LinkStatus( ) - if original_size < 64: - #size = self.read_status( ) - #size = self.poll_size( ) - log.info('XXX:JUST a bit more READ new size: %s, sleep .100' % original_size) - self.link.write(status.format( )) - time.sleep(.100) - raw = bytearray(self.link.read(64)) - ack, response = reader.respond(raw) - info = reader.parse(response) - return info - - ack, body = status.respond(raw) - size = status.parse(body) - log.info('attempt another read') - info = None - - raw = bytearray(self.link.read(size)) - if len(raw) == 0: - log.info('NESTED zero length READ, try once more sleep .100') - time.sleep(.100) - raw = bytearray(self.link.read(self.command.size)) - - ack, body = status.respond(raw) - info = self.command.parse(body) - + # if len(raw) == 0: + if not raw: + log.info("zero length READ, try once more sleep .500") + time.sleep(0.500) + raw = bytearray(self.link.read(self.command.size)) + try: + ack, response = self.command.respond(raw) + info = self.command.parse(response) + msg = ":".join( + ["PROCESS", "END"] + list(map(str, [self.timer.millis(), self.command])) + ) + log.info(msg) + return info + except BadDeviceCommError as e: + log.critical("download_packet:{}:ERROR:{}:ACK!?".format(self, e)) + log.info("we failed to pass %s ACK!?" % (self.command)) + log.info("expected size was: %s" % original_size) + status = LinkStatus() + if original_size < 64: + # size = self.read_status( ) + # size = self.poll_size( ) + log.info( + "XXX:JUST a bit more READ new size: %s, sleep .100" % original_size + ) + self.link.write(status.format()) + time.sleep(0.100) + raw = bytearray(self.link.read(64)) + ack, response = reader.respond(raw) + info = reader.parse(response) + return info + + ack, body = status.respond(raw) + size = status.parse(body) + log.info("attempt another read") + info = None + + raw = bytearray(self.link.read(size)) + if len(raw) == 0: + log.info("NESTED zero length READ, try once more sleep .100") + time.sleep(0.100) + raw = bytearray(self.link.read(self.command.size)) + + ack, body = status.respond(raw) + info = self.command.parse(body) + + log.info("finished processing {}, {}".format(self.command, repr(info))) + return info - log.info('finished processing {0}, {1}'.format(self.command, repr(info))) - return info - - def download(self, size=None): - """ + def download(self, size=None): + """ Theory is to download anything and everything available off the radio buffer, and to wait if necessary. """ - eod = False - results = bytearray( ) - ailing = 0 - i = 0 - log_head = 'download(attempts[{}])' - expecting = 'download(attempts[{}],expect[{}])' - stats = '{}:download(attempts[{}],expect[{}],results[{}]:data[{}])' - expect_eod = False - log.info('download:start:%s' % i) - data = bytearray( ) - while not eod: - i += 1 - self._download_i = i - data = bytearray( ) - if size is None: - log.info("%s:begin first poll first sleep .250" % (stats.format(self, i, 0, - len(results), len(data)))) - time.sleep(.250) - size = self.poll_size( ) - log.info("%s:end first poll" % (stats.format(self, i, size, - len(results), len(data)))) - if size == 0: - if i % 3 == 0: - time.sleep(1.5) - #time.sleep(1.5) - size = self.poll_size( ) - """ + eod = False + results = bytearray() + ailing = 0 + i = 0 + log_head = "download(attempts[{}])" + expecting = "download(attempts[{}],expect[{}])" + stats = "{}:download(attempts[{}],expect[{}],results[{}]:data[{}])" + expect_eod = False + log.info("download:start:%s" % i) + data = bytearray() + while not eod: + i += 1 + self._download_i = i + data = bytearray() + if size is None: + log.info( + "%s:begin first poll first sleep .250" + % (stats.format(self, i, 0, len(results), len(data))) + ) + time.sleep(0.250) + size = self.poll_size() + log.info( + "%s:end first poll" + % (stats.format(self, i, size, len(results), len(data))) + ) + if size == 0: + if i % 3 == 0: + time.sleep(1.5) + # time.sleep(1.5) + size = self.poll_size() + """ if size == 0: # if size == 0 and i > 1: log.info("%s:zero poll size, sleep .500 try again" % ( \ @@ -743,202 +819,250 @@ def download(self, size=None): size = self.poll_size( ) time.sleep(.500) """ - if size == 0 and i > 1: - log.warn("%s:BAD AILING" % (stats.format(self, i, size, - len(results), len(data)))) - ailing = ailing + 1 - if ailing > 1: - break - continue - # break - elif ailing > 0: - ailing = ailing - 1 - - log.info("%s:proceed to download packet" % (stats.format(self, i, size, - len(results), len(data)))) - #time.sleep(.100) - data = self.download_packet(size) - expect_eod = False - if data: - results.extend(data) - expect_eod = self.command.eod - log.info("%s:adding segment" % (stats.format(self, i, size, - len(results), len(data)))) - else: - log.info("%s:no data, try again sleep .400" % (stats.format(self, i, size, - len(results), len(data)))) - time.sleep(.400) - # eod = expect_eod and size < 15 - eod = expect_eod - # or size < 15 - if not eod: - log.info("%s:no eod, sleep .200 try again" % (stats.format(self, i, size, - len(results), len(data)))) - time.sleep(.200) - size = self.poll_size( ) - - log.info("%s:DONE" % (stats.format(self, i, size, - len(results), len(data)))) - self._download_i = False - # self.reader = None - return results - - def clear_buffer(self): - """ + if size == 0 and i > 1: + log.warn( + "%s:BAD AILING" + % (stats.format(self, i, size, len(results), len(data))) + ) + ailing = ailing + 1 + if ailing > 1: + break + continue + # break + elif ailing > 0: + ailing = ailing - 1 + + log.info( + "%s:proceed to download packet" + % (stats.format(self, i, size, len(results), len(data))) + ) + # time.sleep(.100) + data = self.download_packet(size) + expect_eod = False + if data: + results.extend(data) + expect_eod = self.command.eod + log.info( + "%s:adding segment" + % (stats.format(self, i, size, len(results), len(data))) + ) + else: + log.info( + "%s:no data, try again sleep .400" + % (stats.format(self, i, size, len(results), len(data))) + ) + time.sleep(0.400) + # eod = expect_eod and size < 15 + eod = expect_eod + # or size < 15 + if not eod: + log.info( + "%s:no eod, sleep .200 try again" + % (stats.format(self, i, size, len(results), len(data))) + ) + time.sleep(0.200) + size = self.poll_size() + + log.info("%s:DONE" % (stats.format(self, i, size, len(results), len(data)))) + self._download_i = False + # self.reader = None + return results + + def clear_buffer(self): + """ An alternative download solution. This can be helpful in scenarios where a prior run seems crashed your process, but the radio is still transmitting and receiving data. Running this method collects data from the radio until it's done receiving, more or less, at which point you should be free to try again. """ - bad = bytearray( ) - raw = bytearray( ) - for attempt in xrange( 3 ): - segments = [ ] - segs_vs_raw = 'segments[{0}],total_segments[{1}]:raw[{2}]' - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log_head = "XXX:clear_buffer[attempt][%s]" % (attempt) - log.debug('INTERFACE STATS:\n%s' % lib.pformat(self.interface_stats( ))) - log.info(":".join([ log_head, log_detail, "BEGIN ", "first poll" ])) - size = self.poll_size( ) - end_poll = ':'.join( [ log_head, log_detail, - "END first poll %s" % (size), - "SHOULD DOWNLOAD ", str(size != 0) ] ) - log.info(end_poll) - if size == 0: - break - - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log.info("%s:download the size? %s:%s" % (log_head, size, log_detail)) - - while size > 14: - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log_head = "XXX:clear_buffer[attempt][%s]" % (attempt) - log.info( ':'.join([ "%s size:%s" % (log_head, size), - log_detail, - "clear_buffer BUFFER self.download( )" ])) - try: - segment = self.download( ) - raw.extend(segment) - segments.append(segment) - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log.info(":".join([ "%s:tx:found" % (log_head), - log_detail, - 'len(raw)', str(len(raw)), - 'expected', str(size), - 'len(segment)', str(len(segment)) ])) - except BadCRC, e: - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log.critical('%s:IGNORING:%s:%s' % (log_head, log_detail, e)) - - seg_stats = ( len(segments), sum(map(len, segments)), len(raw) ) - log_detail = segs_vs_raw.format(*seg_stats) - log.info(':'.join([ "%s downloaded %s segment" % (log_head, len(raw)), + bad = bytearray() + raw = bytearray() + for attempt in range(3): + segments = [] + segs_vs_raw = "segments[{0}],total_segments[{1}]:raw[{2}]" + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log_head = "XXX:clear_buffer[attempt][%s]" % (attempt) + log.debug("INTERFACE STATS:\n%s" % lib.pformat(self.interface_stats())) + log.info(":".join([log_head, log_detail, "BEGIN ", "first poll"])) + size = self.poll_size() + end_poll = ":".join( + [ + log_head, + log_detail, + "END first poll %s" % (size), + "SHOULD DOWNLOAD ", + str(size != 0), + ] + ) + log.info(end_poll) + if size == 0: + break + + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log.info("{}:download the size? {}:{}".format(log_head, size, log_detail)) + + while size > 14: + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log_head = "XXX:clear_buffer[attempt][%s]" % (attempt) + log.info( + ":".join( + [ + "{} size:{}".format(log_head, size), log_detail, - "RAW:\n%s" % lib.hexdump(raw) ])) - size = self.poll_size( ) - - log.debug("INTERFACE STATS:\n%s" % lib.pformat(self.interface_stats( ))) - if raw: - return raw - if size == 0: - log.info("\n".join([ "%s:END:no data:INTERFACE STATS" % (log_head), - lib.pformat(self.interface_stats( )) ])) - - def transmit_packet(self, command): - """ + "clear_buffer BUFFER self.download( )", + ] + ) + ) + try: + segment = self.download() + raw.extend(segment) + segments.append(segment) + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log.info( + ":".join( + [ + "%s:tx:found" % (log_head), + log_detail, + "len(raw)", + str(len(raw)), + "expected", + str(size), + "len(segment)", + str(len(segment)), + ] + ) + ) + except BadCRC as e: + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log.critical("{}:IGNORING:{}:{}".format(log_head, log_detail, e)) + + seg_stats = (len(segments), sum(map(len, segments)), len(raw)) + log_detail = segs_vs_raw.format(*seg_stats) + log.info( + ":".join( + [ + "{} downloaded {} segment".format(log_head, len(raw)), + log_detail, + "RAW:\n%s" % lib.hexdump(raw), + ] + ) + ) + size = self.poll_size() + + log.debug("INTERFACE STATS:\n%s" % lib.pformat(self.interface_stats())) + if raw: + return raw + if size == 0: + log.info( + "\n".join( + [ + "%s:END:no data:INTERFACE STATS" % (log_head), + lib.pformat(self.interface_stats()), + ] + ) + ) + + def transmit_packet(self, command): + """ Address a pump with a request. """ - packet = TransmitPacket(command) - self.command = packet - self.transmit = packet - log.info('transmit_packet:write:%r' % (self.command)) - result = self.process( ) - return result - - def open(self): - """ + packet = TransmitPacket(command) + self.command = packet + self.transmit = packet + log.info("transmit_packet:write:%r" % (self.command)) + result = self.process() + return result + + def open(self): + """ Open and get signal strength so everything is ready to go. """ - self.link.baudrate = 9600 - self.timer = lib.Timer( ) - for attempt in xrange( 1 ): - try: - msg = ':'.join(['PROCESS', 'OPEN', str(self.timer.millis( ))] ) - log.info(msg) - log.info('%s' % self.product_info( )) - log.info('%s' % self.product_info( )) - log.info('get signal strength of %s' % self) - signal = 0 - while signal < 50: - signal = self.signal_strength( ) - log.info('we seem to have found a nice signal strength of: %s' % signal) - return True - except AckError, e: - log.info('failed:(%s):\n%s' % (attempt, e)) - raise - - def close (self): - self.link.close( ) - - @staticmethod - def decode_hex (msg, Candidate): - candidate = Candidate( ) - raw = lib.hexbytes(msg) - ack, resp = candidate.respond(raw) - result = candidate.parse(resp) - return result - -if __name__ == '__main__': - import doctest - doctest.testmod( ) - - import sys - port = None - port = sys.argv[1:] and sys.argv[1] or False - if not port: - print "usage:\n%s /dev/ttyUSB0" % sys.argv[0] - sys.exit(1) - import link - from pprint import pformat - logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) - log.info("howdy! I'm going to take a look at your carelink usb stick.") - stick = Stick(link.Link(port)) - stick.open( ) - log.info('test fetching product info %s' % stick) - log.info(pformat(stick.product_info( ))) - log.info('get signal strength of %s' % stick) - signal = 0 - while signal < 50: - signal = stick.signal_strength( ) - log.info('we seem to have found a nice signal strength of: %s' % signal) - log.info(""" - at this point, we could issue remote commands to a medical - device, let's inspect the interfaces""".strip( )) - #log.info(pformat(stick.usb_stats( ))) - #log.info(pformat(stick.radio_stats( ))) - log.info(pformat(stick.interface_stats( ))) - """ - size = stick.poll_size( ) - log.info("can we poll the size? %s" % (size)) - if size > 14: - log.info("DOWNLOADING %s TO CLEAR BUFFER" % size) - log.info('\n'.join(["can we download ?", lib.hexdump(stick.download( ))])) - """ - log.info("CLEAR BUFFERS") - extra = stick.clear_buffer( ) - if extra: - log.info(lib.hexdump(extra)) - else: - log.info("NO PENDING BUFFER") - log.info("DONE CLEARING BUFFERS") - log.info("INTERFACE STATS:\n%s" % pformat(stick.interface_stats( ))) - log.info("howdy! all done looking at the stick") + self.link.baudrate = 9600 + self.timer = lib.Timer() + for attempt in range(1): + try: + msg = ":".join(["PROCESS", "OPEN", str(self.timer.millis())]) + log.info(msg) + log.info("%s" % self.product_info()) + log.info("%s" % self.product_info()) + log.info("get signal strength of %s" % self) + signal = 0 + while signal < 50: + signal = self.signal_strength() + log.info("we seem to have found a nice signal strength of: %s" % signal) + return True + except AckError as e: + log.info("failed:({}):\n{}".format(attempt, e)) + raise + + def close(self): + self.link.close() + + @staticmethod + def decode_hex(msg, Candidate): + candidate = Candidate() + raw = lib.hexbytes(msg) + ack, resp = candidate.respond(raw) + result = candidate.parse(resp) + return result + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + import sys + + port = None + port = sys.argv[1:] and sys.argv[1] or False + if not port: + print("usage:\n%s /dev/ttyUSB0" % sys.argv[0]) + sys.exit(1) + from pprint import pformat + + from decocare import link + + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + log.info("howdy! I'm going to take a look at your carelink usb stick.") + stick = Stick(link.Link(port)) + stick.open() + log.info("test fetching product info %s" % stick) + log.info(pformat(stick.product_info())) + log.info("get signal strength of %s" % stick) + signal = 0 + while signal < 50: + signal = stick.signal_strength() + log.info("we seem to have found a nice signal strength of: %s" % signal) + log.info( + "at this point, we could issue remote commands to a medical" + " device, let's inspect the interfaces" + ) + # log.info(pformat(stick.usb_stats( ))) + # log.info(pformat(stick.radio_stats( ))) + log.info(pformat(stick.interface_stats())) + """ + size = stick.poll_size( ) + log.info("can we poll the size? %s" % (size)) + if size > 14: + log.info("DOWNLOADING %s TO CLEAR BUFFER" % size) + log.info('\n'.join(["can we download ?", lib.hexdump(stick.download( ))])) + """ + log.info("CLEAR BUFFERS") + extra = stick.clear_buffer() + if extra: + log.info(lib.hexdump(extra)) + else: + log.info("NO PENDING BUFFER") + log.info("DONE CLEARING BUFFERS") + log.info("INTERFACE STATS:\n%s" % pformat(stick.interface_stats())) + log.info("howdy! all done looking at the stick") ##### # EOF diff --git a/doc/conf.py b/doc/conf.py index ec2151a..ff9dee7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # insulaudit:decoding-carelink documentation build configuration file, created by # sphinx-quickstart on Tue Jun 18 11:59:09 2013. @@ -10,206 +9,214 @@ # # All configuration values have a default; values that are commented out # serve to show the default. - -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, os.path.abspath('../')) +# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath("../")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', - 'sphinx.ext.mathjax', 'sphinx.ext.viewcode' - ] + ['sphinxcontrib.seqdiag'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.pngmath", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", +] + ["sphinxcontrib.seqdiag"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'insulaudit:decoding-carelink' -copyright = u'2013, Insulaudit contributors' +project = "insulaudit:decoding-carelink" +copyright = "2013, Insulaudit contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.0.3' +version = "0.0.3" # The full version, including alpha/beta/rc tags. -release = '0.0.3' +release = "0.0.3" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'decocare' +htmlhelp_basename = "decocare" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'insulauditdecoding-carelink.tex', u'insulaudit:decoding-carelink Documentation', - u'Insulaudit contributors', 'manual'), + ( + "index", + "insulauditdecoding-carelink.tex", + "insulaudit:decoding-carelink Documentation", + "Insulaudit contributors", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -217,12 +224,17 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'insulauditdecoding-carelink', u'insulaudit:decoding-carelink Documentation', - [u'Insulaudit contributors'], 1) + ( + "index", + "insulauditdecoding-carelink", + "insulaudit:decoding-carelink Documentation", + ["Insulaudit contributors"], + 1, + ) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -231,16 +243,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'insulauditdecoding-carelink', u'insulaudit:decoding-carelink Documentation', - u'Insulaudit contributors', 'insulauditdecoding-carelink', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "insulauditdecoding-carelink", + "insulaudit:decoding-carelink Documentation", + "Insulaudit contributors", + "insulauditdecoding-carelink", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/ez_setup.py b/ez_setup.py index f2ba870..d2ef6b2 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -15,15 +15,14 @@ This file can also be run as a script to install or upgrade setuptools. """ +import optparse import os +import platform import shutil +import subprocess import sys -import tempfile import tarfile -import optparse -import subprocess -import platform - +import tempfile from distutils import log try: @@ -34,23 +33,30 @@ DEFAULT_VERSION = "1.0" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" + def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 + def _check_call_py24(cmd, *args, **kwargs): res = subprocess.call(cmd, *args, **kwargs) + class CalledProcessError(Exception): pass + if not res == 0: msg = "Command '%s' return non-zero exit status %d" % (cmd, res) raise CalledProcessError(msg) -vars(subprocess).setdefault('check_call', _check_call_py24) + + +vars(subprocess).setdefault("check_call", _check_call_py24) + def _install(tarball, install_args=()): # extracting the tarball tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) + log.warn("Extracting in %s", tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) @@ -61,13 +67,13 @@ def _install(tarball, install_args=()): # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) - log.warn('Now working in %s', subdir) + log.warn("Now working in %s", subdir) # installing - log.warn('Installing Setuptools') - if not _python_cmd('setup.py', 'install', *install_args): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') + log.warn("Installing Setuptools") + if not _python_cmd("setup.py", "install", *install_args): + log.warn("Something went wrong during the installation.") + log.warn("See the error message above.") # exitcode will be 2 return 2 finally: @@ -78,7 +84,7 @@ def _install(tarball, install_args=()): def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) + log.warn("Extracting in %s", tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) @@ -89,11 +95,11 @@ def _build_egg(egg, tarball, to_dir): # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) - log.warn('Now working in %s', subdir) + log.warn("Now working in %s", subdir) # building an egg - log.warn('Building a Setuptools egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) + log.warn("Building a Setuptools egg in %s", to_dir) + _python_cmd("setup.py", "-q", "bdist_egg", "--dist-dir", to_dir) finally: os.chdir(old_wd) @@ -101,33 +107,39 @@ def _build_egg(egg, tarball, to_dir): # returning the result log.warn(egg) if not os.path.exists(egg): - raise IOError('Could not build the egg.') + raise OSError("Could not build the egg.") def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) + egg = os.path.join( + to_dir, + "setuptools-%s-py%d.%d.egg" + % (version, sys.version_info[0], sys.version_info[1]), + ) if not os.path.exists(egg): - tarball = download_setuptools(version, download_base, - to_dir, download_delay) + tarball = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) # Remove previously-imported pkg_resources if present (see # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). - if 'pkg_resources' in sys.modules: - del sys.modules['pkg_resources'] + if "pkg_resources" in sys.modules: + del sys.modules["pkg_resources"] import setuptools + setuptools.bootstrap_install_from = egg -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15): +def use_setuptools( + version=DEFAULT_VERSION, + download_base=DEFAULT_URL, + to_dir=os.curdir, + download_delay=15, +): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) - was_imported = 'pkg_resources' in sys.modules or \ - 'setuptools' in sys.modules + was_imported = "pkg_resources" in sys.modules or "setuptools" in sys.modules try: import pkg_resources except ImportError: @@ -139,19 +151,19 @@ def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, e = sys.exc_info()[1] if was_imported: sys.stderr.write( - "The required version of setuptools (>=%s) is not available,\n" - "and can't be installed while this script is running. Please\n" - "install a more recent version first, using\n" - "'easy_install -U setuptools'." - "\n\n(Currently using %r)\n" % (version, e.args[0])) + "The required version of setuptools (>=%s) is not available,\n" + "and can't be installed while this script is running. Please\n" + "install a more recent version first, using\n" + "'easy_install -U setuptools'." + "\n\n(Currently using %r)\n" % (version, e.args[0]) + ) sys.exit(2) else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return _do_download(version, download_base, to_dir, - download_delay) + del pkg_resources, sys.modules["pkg_resources"] # reload ok + return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, - download_delay) + return _do_download(version, download_base, to_dir, download_delay) + def download_file_powershell(url, target): """ @@ -160,17 +172,18 @@ def download_file_powershell(url, target): """ target = os.path.abspath(target) cmd = [ - 'powershell', - '-Command', + "powershell", + "-Command", "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), ] subprocess.check_call(cmd) + def has_powershell(): - if platform.system() != 'Windows': + if platform.system() != "Windows": return False - cmd = ['powershell', '-Command', 'echo test'] - devnull = open(os.path.devnull, 'wb') + cmd = ["powershell", "-Command", "echo test"] + devnull = open(os.path.devnull, "wb") try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: @@ -179,15 +192,18 @@ def has_powershell(): devnull.close() return True + download_file_powershell.viable = has_powershell + def download_file_curl(url, target): - cmd = ['curl', url, '--silent', '--output', target] + cmd = ["curl", url, "--silent", "--output", target] subprocess.check_call(cmd) + def has_curl(): - cmd = ['curl', '--version'] - devnull = open(os.path.devnull, 'wb') + cmd = ["curl", "--version"] + devnull = open(os.path.devnull, "wb") try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: @@ -196,15 +212,18 @@ def has_curl(): devnull.close() return True + download_file_curl.viable = has_curl + def download_file_wget(url, target): - cmd = ['wget', url, '--quiet', '--output-document', target] + cmd = ["wget", url, "--quiet", "--output-document", target] subprocess.check_call(cmd) + def has_wget(): - cmd = ['wget', '--version'] - devnull = open(os.path.devnull, 'wb') + cmd = ["wget", "--version"] + devnull = open(os.path.devnull, "wb") try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: @@ -213,8 +232,10 @@ def has_wget(): devnull.close() return True + download_file_wget.viable = has_wget + def download_file_insecure(url, target): """ Use Python to download the file, even though it cannot authenticate the @@ -223,7 +244,7 @@ def download_file_insecure(url, target): try: from urllib.request import urlopen except ImportError: - from urllib2 import urlopen + from urllib.request import urlopen src = dst = None try: src = urlopen(url) @@ -238,8 +259,10 @@ def download_file_insecure(url, target): if dst: dst.close() + download_file_insecure.viable = lambda: True + def get_best_downloader(): downloaders = [ download_file_powershell, @@ -252,8 +275,10 @@ def get_best_downloader(): if dl.viable(): return dl -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15): + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15 +): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available @@ -284,6 +309,7 @@ def _extractall(self, path=".", members=None): import copy import operator from tarfile import ExtractError + directories = [] if members is None: @@ -299,12 +325,14 @@ def _extractall(self, path=".", members=None): # Reverse sort directories. if sys.version_info < (2, 4): + def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) + directories.sort(sorter) directories.reverse() else: - directories.sort(key=operator.attrgetter('name'), reverse=True) + directories.sort(key=operator.attrgetter("name"), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: @@ -323,37 +351,47 @@ def sorter(dir1, dir2): def _build_install_args(options): """ - Build the arguments to 'python setup.py install' on the setuptools package + Build the arguments to 'python3 setup.py install' on the setuptools package """ install_args = [] if options.user_install: if sys.version_info < (2, 6): log.warn("--user requires Python 2.6 or later") raise SystemExit(1) - install_args.append('--user') + install_args.append("--user") return install_args + def _parse_args(): """ Parse the command line for options """ parser = optparse.OptionParser() parser.add_option( - '--user', dest='user_install', action='store_true', default=False, - help='install in user site package (requires Python 2.6 or later)') + "--user", + dest="user_install", + action="store_true", + default=False, + help="install in user site package (requires Python 2.6 or later)", + ) parser.add_option( - '--download-base', dest='download_base', metavar="URL", + "--download-base", + dest="download_base", + metavar="URL", default=DEFAULT_URL, - help='alternative URL from where to download the setuptools package') + help="alternative URL from where to download the setuptools package", + ) options, args = parser.parse_args() # positional arguments are ignored return options + def main(version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" options = _parse_args() tarball = download_setuptools(download_base=options.download_base) return _install(tarball, _build_install_args(options)) -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(main()) diff --git a/find_data_position.py b/find_data_position.py index ae3ff91..f9bc11f 100644 --- a/find_data_position.py +++ b/find_data_position.py @@ -12,22 +12,22 @@ # argv[3] = package length ############################################################################## -import sys import getopt - +import sys file_in_name = sys.argv[1] data_to_find = sys.argv[2] +pack_len = sys.argv[3] # uncomment lines below to run as a test file_in_name = "logs/analyze/20140421_030133-ReadGlucoseHistory-page-16.data" data_to_find = "" -print_data_before = false # if true will print data before , if false will print after +print_data_before = False # if true will print data before , if false will print after myBytes = bytearray() -with open(fileInName, 'rb') as file: - while 1: +with open(file_in_name, "rb") as file: + while True: byte = file.read(1) if not byte: break @@ -37,13 +37,22 @@ myHexBytes = [] myBinBytes = [] for i in range(0, len(myBytes)): - myHexBytes.append('{0:02x}'.format(myBytes[i])) - myBinBytes.append('{0:08b}'.format(myBytes[i])) - + myHexBytes.append("{:02x}".format(myBytes[i])) + myBinBytes.append("{:08b}".format(myBytes[i])) + for i in range(0, len(myBytes)): - if myHexBytes[i] == op_code: - pack_start = i+1 - hex_str = '-'.join(myHexBytes[pack_start:pack_start+pack_len]) - bin_str = '-'.join(myBinBytes[pack_start:pack_start+pack_len]) - print "At "+str(i)+" found : ["+myHexBytes[i]+"] ["+hex_str+"] ["+bin_str+"]" - + if myHexBytes[i] == data_to_find: + pack_start = i + 1 + hex_str = "-".join(myHexBytes[pack_start : pack_start + pack_len]) + bin_str = "-".join(myBinBytes[pack_start : pack_start + pack_len]) + print( + "At " + + str(i) + + " found : [" + + myHexBytes[i] + + "] [" + + hex_str + + "] [" + + bin_str + + "]" + ) diff --git a/find_dates.py b/find_dates.py index f0789d2..7109c8f 100644 --- a/find_dates.py +++ b/find_dates.py @@ -1,144 +1,159 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -import sys import argparse +import sys import textwrap - -from pprint import pprint, pformat from binascii import hexlify from datetime import datetime +from pprint import pformat, pprint -from datetime import datetime from decocare import lib from decocare.records import times -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - return parser +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + return parser + -def parse_minutes (one): - minute = (one & 0x7F ) - return minute +def parse_minutes(one): + minute = one & 0x7F + return minute -def parse_hours (one): - return (one & 0x1F ) -def parse_day (one): - return one & 0x1F +def parse_hours(one): + return one & 0x1F + + +def parse_day(one): + return one & 0x1F + + +def parse_months(one): + return one >> 4 -def parse_months (one): - return one >> 4 import binascii -def dehex (hexstr): - return bytearray(binascii.unhexlify(hexstr.replace(' ', ''))) -def cgm_timestamp (data): - """ +def dehex(hexstr): + return bytearray(binascii.unhexlify(hexstr.replace(" ", ""))) + + +def cgm_timestamp(data): + """ # >>> cgm_timestamp(dehex('')) # >>> cgm_timestamp(dehex('')) - """ - result = parse_date(data) - if result is not None: - return result.isoformat( ) - return result - -def parse_date (data): - """ - """ - data = data[:] - seconds = 0 - minutes = 0 - hours = 0 - - year = times.parse_years(data[0]) - day = parse_day(data[1]) - minutes = parse_minutes(data[2]) - - hours = parse_hours(data[3]) - - month = parse_months(data[3]) - - try: - date = datetime(year, month, day, hours, minutes, seconds) - return date - except ValueError, e: - pass - return None - -def dump_one (byte): - template = "{0:#04x} {0:08b} {0:d}" - return template.format(byte) - -def dump_four (byte, indent=0, newline="\n"): - lines = [ ] - spaces = ''.join([' '] * indent) - for x in range(4): - lines.append(spaces + dump_one(byte[x])) - return newline.join(lines) - -class TimeExperiment (object): - def find_dates(self, stream): - records = [ ] - bolus = bytearray(stream.read(4)) - dates = [ ] - extra = bytearray( ) - everything = bolus - SIZE = 4 - opcode = '' - last = 0 - for B in iter(lambda: stream.read(1), ""): - h, t = B[:1], B[1:] - bolus.append(h) - bolus.extend(t) - everything.extend(B) - if len(everything) < SIZE: - continue - candidate = everything[-SIZE:] - date = parse_date(candidate) - if date is not None: - last = stream.tell( ) - # last = len(everything) - start = last - SIZE - print "### FOUND ", date.isoformat( ), ' @ ', start, "%#08x" % start - print "#### previous" - print lib.hexdump(bolus, indent=4) - print "#### datetime" - print lib.hexdump(candidate, indent=4) - print "" - found = dict(timestamp=date, blob=bolus) - print dump_four(candidate, indent=4) - # print lib.hexdump(bolus) - records.append(found) - bolus = bytearray( ) - return records - - def main(self): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - tw_opts = { - 'width': 50, - 'subsequent_indent': ' ', - 'initial_indent': ' ', - } - self.wrapper = textwrap.TextWrapper(**tw_opts) - for stream in opts.infile: - self.find_dates(stream) - -if __name__ == '__main__': - app = TimeExperiment( ) - app.main( ) + """ + result = parse_date(data) + if result is not None: + return result.isoformat() + return result + + +def parse_date(data): + """ + """ + data = data[:] + seconds = 0 + minutes = 0 + hours = 0 + + year = times.parse_years(data[0]) + day = parse_day(data[1]) + minutes = parse_minutes(data[2]) + + hours = parse_hours(data[3]) + + month = parse_months(data[3]) + + try: + date = datetime(year, month, day, hours, minutes, seconds) + return date + except ValueError as e: + pass + return None + + +def dump_one(byte): + template = "{0:#04x} {0:08b} {0:d}" + return template.format(byte) + + +def dump_four(byte, indent=0, newline="\n"): + lines = [] + spaces = "".join([" "] * indent) + for x in range(4): + lines.append(spaces + dump_one(byte[x])) + return newline.join(lines) + + +class TimeExperiment: + def find_dates(self, stream): + records = [] + bolus = bytearray(stream.read(4)) + dates = [] + extra = bytearray() + everything = bolus + SIZE = 4 + opcode = "" + last = 0 + for B in iter(lambda: stream.read(1), ""): + h, t = B[:1], B[1:] + bolus.append(h) + bolus.extend(t) + everything.extend(B) + if len(everything) < SIZE: + continue + candidate = everything[-SIZE:] + date = parse_date(candidate) + if date is not None: + last = stream.tell() + # last = len(everything) + start = last - SIZE + print("### FOUND ", date.isoformat(), " @ ", start, "%#08x" % start) + print("#### previous") + print(lib.hexdump(bolus, indent=4)) + print("#### datetime") + print(lib.hexdump(candidate, indent=4)) + print("") + found = dict(timestamp=date, blob=bolus) + print(dump_four(candidate, indent=4)) + # print(lib.hexdump(bolus)) + records.append(found) + bolus = bytearray() + return records + + def main(self): + parser = get_opt_parser() + opts = parser.parse_args() + tw_opts = { + "width": 50, + "subsequent_indent": " ", + "initial_indent": " ", + } + self.wrapper = textwrap.TextWrapper(**tw_opts) + for stream in opts.infile: + self.find_dates(stream) + + +if __name__ == "__main__": + app = TimeExperiment() + app.main() ##### # EOF diff --git a/find_print_op_codes.py b/find_print_op_codes.py index 16e3b1c..e0b26ff 100644 --- a/find_print_op_codes.py +++ b/find_print_op_codes.py @@ -12,89 +12,113 @@ # argv[3] = package length ############################################################################## -import sys import getopt - +import sys # uncomment lines below to run as a test -#fileInName = "logs/analyze/20140421_030133-ReadGlucoseHistory-page-16.data" -#op_code = '08' -#pack_len = 6 -#reverse = True +# fileInName = "logs/analyze/20140421_030133-ReadGlucoseHistory-page-16.data" +# op_code = '08' +# pack_len = 6 +# reverse = True + class OpCodeFinder: - bytes = None - bin_bytes = None - hex_bytes = None - found = [] - - def __init__(self, fileInName, op_code, pack_len, reverse): - self.fileInName = fileInName - self.op_code = op_code - self.pack_len = pack_len - self.reverse = reverse - - def main(self): - self.find() - self.pretty_print() - - # run without printing - def find(self): - self.file_to_bytes() - if self.reverse: - self.bytes.reverse() - self.create_hex_bin_bytes() - self.search_bytes() - return self.found - - def create_hex_bin_bytes(self): - self.hex_bytes = [] - self.bin_bytes = [] - for i in range(0, len(self.bytes)): - self.hex_bytes.append('{0:02x}'.format(self.bytes[i])) - self.bin_bytes.append('{0:08b}'.format(self.bytes[i])) - - def file_to_bytes(self): - myBytes = bytearray() - with open(self.fileInName, 'rb') as file: - while 1: - byte = file.read(1) - if not byte: - break - myBytes.append(byte) - self.bytes = myBytes + bytes = None + bin_bytes = None + hex_bytes = None + found = [] + + def __init__(self, fileInName, op_code, pack_len, reverse): + self.fileInName = fileInName + self.op_code = op_code + self.pack_len = pack_len + self.reverse = reverse + + def main(self): + self.find() + self.pretty_print() + + # run without printing + def find(self): + self.file_to_bytes() + if self.reverse: + self.bytes.reverse() + self.create_hex_bin_bytes() + self.search_bytes() + return self.found + + def create_hex_bin_bytes(self): + self.hex_bytes = [] + self.bin_bytes = [] + for i in range(0, len(self.bytes)): + self.hex_bytes.append("{:02x}".format(self.bytes[i])) + self.bin_bytes.append("{:08b}".format(self.bytes[i])) + + def file_to_bytes(self): + myBytes = bytearray() + with open(self.fileInName, "rb") as file: + while True: + byte = file.read(1) + if not byte: + break + myBytes.append(byte) + self.bytes = myBytes + + def search_bytes(self): + for i in range(0, len(self.bytes)): + if self.hex_bytes[i] == self.op_code: + packet = {} + packet["start_point"] = i + 1 + packet["hex_str"] = "-".join( + self.hex_bytes[i + 1 : i + 1 + self.pack_len] + ) + packet["bin_str"] = "-".join( + self.bin_bytes[i + 1 : i + 1 + self.pack_len] + ) + packet["hex_str_raw"] = "".join( + self.hex_bytes[i + 1 : i + 1 + self.pack_len] + ) + packet["bin_str_raw"] = "".join( + self.bin_bytes[i + 1 : i + 1 + self.pack_len] + ) + packet["op_code"] = self.op_code + packet["pack_len"] = self.pack_len + self.found.append(packet) + + def pretty_print(self): + print("****START****\n\n") + print( + "Searched (" + + self.fileInName + + ") for '" + + self.op_code + + "' of length (" + + str(self.pack_len) + + ")" + ) + for i in range(0, len(self.found)): + packet = self.found[i] + print( + "At " + + str(packet["start_point"]) + + " found : [" + + packet["hex_str"] + + "] [" + + packet["bin_str"] + + "]" + ) + print("\n\n****END*****") - def search_bytes(self): - for i in range(0, len(self.bytes)): - if self.hex_bytes[i] == self.op_code: - packet = {} - packet['start_point'] = i+1 - packet['hex_str'] = '-'.join(self.hex_bytes[i+1:i+1+self.pack_len]) - packet['bin_str'] = '-'.join(self.bin_bytes[i+1:i+1+self.pack_len]) - packet['hex_str_raw'] = ''.join(self.hex_bytes[i+1:i+1+self.pack_len]) - packet['bin_str_raw'] = ''.join(self.bin_bytes[i+1:i+1+self.pack_len]) - packet['op_code'] = self.op_code - packet['pack_len'] = self.pack_len - self.found.append(packet) - - def pretty_print(self): - print "****START****\n\n" - print "Searched (" + self.fileInName + ") for '" + self.op_code + "' of length (" + str(self.pack_len) + ")" - for i in range(0, len(self.found)): - packet = self.found[i] - print "At "+str(packet['start_point'])+" found : ["+packet['hex_str']+"] ["+packet['bin_str']+"]" - print "\n\n****END*****" +if __name__ == "__main__": + fileInName = sys.argv[1] + op_code = sys.argv[2] + pack_len = int(sys.argv[3]) + reverse = False + if sys.argv[4] == "True": + reverse = True + op_code_finder = OpCodeFinder(fileInName, op_code, pack_len, reverse) + op_code_finder.main() -if __name__ == '__main__': - fileInName = sys.argv[1] - op_code = sys.argv[2] - pack_len = int(sys.argv[3]) - reverse = False - if sys.argv[4] == "True": - reverse = True - op_code_finder = OpCodeFinder(fileInName, op_code, pack_len, reverse) - op_code_finder.main() - ##### -# EOF \ No newline at end of file +# EOF diff --git a/list_cgm.py b/list_cgm.py index 9a1d1d1..5ab8583 100644 --- a/list_cgm.py +++ b/list_cgm.py @@ -1,48 +1,57 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -import sys import argparse +import sys # TODO: should probably be able to remove this stuff -from pprint import pprint, pformat +from pprint import pformat, pprint + from decocare import cgm + + # this stuff will stay -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") - - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - parser.add_argument('--larger', - dest='larger', action='store_true') - parser.add_argument('--no-larger', - dest='larger', action='store_false') - - return parser +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + parser.add_argument("--larger", dest="larger", action="store_true") + parser.add_argument("--no-larger", dest="larger", action="store_false") + + return parser + import json -class ListCGM (object): - - def print_records (self, records, opts={}): - print json.dumps(records, indent=2) - def main(self): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - self.records = [ ] - for stream in opts.infile: - page = cgm.PagedData(stream, larger=opts.larger) - self.records.extend(page.decode( )) - - self.print_records(self.records) - -if __name__ == '__main__': - app = ListCGM( ) - app.main( ) + + +class ListCGM: + def print_records(self, records, opts={}): + print(json.dumps(records, indent=2)) + + def main(self): + parser = get_opt_parser() + opts = parser.parse_args() + self.records = [] + for stream in opts.infile: + page = cgm.PagedData(stream, larger=opts.larger) + self.records.extend(page.decode()) + + self.print_records(self.records) + + +if __name__ == "__main__": + app = ListCGM() + app.main() ##### EOF diff --git a/list_dates.py b/list_dates.py index f06becd..927e7ca 100644 --- a/list_dates.py +++ b/list_dates.py @@ -1,166 +1,175 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -import sys import argparse +import sys import textwrap - -from pprint import pprint, pformat from binascii import hexlify from datetime import datetime +from pprint import pformat, pprint from decocare import lib - from decocare.history import NotADate, parse_date -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - return parser +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + return parser def opcode_min_read(opcode): - TABLE = { - 0x07: 29, - #0x1f: 8, - } - offset = TABLE.get(opcode, 1) - return offset + TABLE = { + 0x07: 29, + # 0x1f: 8, + } + offset = TABLE.get(opcode, 1) + return offset def opcode_read_ahead(opcode, fd=None): - TABLE = { - 0x5b: 22, - #0x64: 4, - 0x03: 4, - #0x6b: 33, - 0x6b: 27, - 0x45: 6, - 0x07: -1, - #0x1f: 22, - #0x1f: 8, - } - TABLE = { } - return TABLE.get(opcode, 0) - if TABLE.get(opcode) is not None: - #print "special opcode %#04x, read:%s" % (opcode, TABLE[opcode]) - return bytearray(fd.read(TABLE[opcode])) - return bytearray( ) + TABLE = { + 0x5B: 22, + # 0x64: 4, + 0x03: 4, + # 0x6b: 33, + 0x6B: 27, + 0x45: 6, + 0x07: -1, + # 0x1f: 22, + # 0x1f: 8, + } + TABLE = {} + return TABLE.get(opcode, 0) + if TABLE.get(opcode) is not None: + # print("special opcode %#04x, read:%s" % (opcode, TABLE[opcode])) + return bytearray(fd.read(TABLE[opcode])) + return bytearray() + def sniff_invalid_opcode(opcode): - if opcode == 0x00: - raise NotADate("invalid opcode") + if opcode == 0x00: + raise NotADate("invalid opcode") + def eat_nulls(fd): - nulls = bytearray( ) - for B in iter(lambda: fd.read(1), ""): - if B == 0x00: - nulls.append(B) - else: - fd.seek(fd.tell( ) - 1) - break - print "found %s nulls" % len(nulls) - return nulls + nulls = bytearray() + for B in iter(lambda: fd.read(1), ""): + if B == 0x00: + nulls.append(B) + else: + fd.seek(fd.tell() - 1) + break + print("found %s nulls" % len(nulls)) + return nulls + def find_dates(stream): - records = [ ] - bolus = bytearray(stream.read(4)) - extra = bytearray( ) - opcode = '' - for B in iter(lambda: stream.read(1), ""): - h, t = B[:1], B[1:] - bolus.append(h) - bolus.extend(t) - if len(bolus) < 6: - bolus.extend(stream.read(opcode_min_read(bolus[0]))) - try: - date = parse_date(bolus[-5:]) - opcode = bolus[0] - if len(bolus) <= 5: - raise NotADate('too short of a record') - #sniff_invalid_opcode( bolus[0] ) - extra_len = opcode_read_ahead(bolus[0]) - if extra_len > 0: - extra.extend( bytearray(stream.read(extra_len)) ) - records.append( (date, bolus[:-5], bolus[-5:], extra) ) - bolus = bytearray(stream.read(4)) - extra = bytearray( ) - opcode = '' - except NotADate, e: - opcode = bolus[0] - pass - return records + records = [] + bolus = bytearray(stream.read(4)) + extra = bytearray() + opcode = "" + for B in iter(lambda: stream.read(1), ""): + h, t = B[:1], B[1:] + bolus.append(h) + bolus.extend(t) + if len(bolus) < 6: + bolus.extend(stream.read(opcode_min_read(bolus[0]))) + try: + date = parse_date(bolus[-5:]) + opcode = bolus[0] + if len(bolus) <= 5: + raise NotADate("too short of a record") + # sniff_invalid_opcode( bolus[0] ) + extra_len = opcode_read_ahead(bolus[0]) + if extra_len > 0: + extra.extend(bytearray(stream.read(extra_len))) + records.append((date, bolus[:-5], bolus[-5:], extra)) + bolus = bytearray(stream.read(4)) + extra = bytearray() + opcode = "" + except NotADate as e: + opcode = bolus[0] + pass + return records + def int_dump(stream, indent=0): - """ - >>> int_dump(bytearray([0x01, 0x02])) - ' 1 2' - - - """ - cells = [ '%#04s' % (x) for x in stream ] - lines = [ ] - indent = ''.join( [ ' ' ] * indent ) - while cells: - octet = cells[:8] - line = ' '.join(octet) - lines.append(indent + line) - cells = cells[8:] - - out = ('\n').join([ line for line in lines ]) - return out - -def main( ): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - tw_opts = { - 'width': 50, - 'subsequent_indent': ' ', - 'initial_indent': ' ', - } - wrapper = textwrap.TextWrapper(**tw_opts) - for stream in opts.infile: - records = find_dates(stream) - print "%s: %s records" % (stream.name, len(records)) - i = 0 - for rec in records: - date, datum, tail, extra = rec - opcode = datum[0] - stats = "hex({}), extra({})".format(len(datum), len(extra)) - date_str = str(date) - if date is not None: - date_str = date.isoformat( ) - - print "#### RECORD %s: %s %#04x %s" % (i, date_str, opcode, stats) - print " hex (%s)" % len(datum) - print lib.hexdump(datum, indent=4) - print " decimal" - print int_dump(datum, indent=11) - print " datetime\n%s" % (lib.hexdump(tail, indent=4)) - print " extra(%s)" % len(extra), - if len(extra) > 0: - print "\n%s" % (lib.hexdump(extra, indent=4)) - print int_dump(extra, indent=11) - else: - print "%s" % (None) - print "" - i += 1 - stream.close( ) - -if __name__ == '__main__': - import doctest - failures, tests = doctest.testmod( ) - if failures > 0: - print "REFUSING TO RUN DUE TO FAILED TESTS" - sys.exit(1) - main( ) + """ + >>> int_dump(bytearray([0x01, 0x02])) + ' 1 2' + + """ + cells = ["%#04s" % (x) for x in stream] + lines = [] + indent = "".join([" "] * indent) + while cells: + octet = cells[:8] + line = " ".join(octet) + lines.append(indent + line) + cells = cells[8:] + + out = ("\n").join([line for line in lines]) + return out + + +def main(): + parser = get_opt_parser() + opts = parser.parse_args() + tw_opts = { + "width": 50, + "subsequent_indent": " ", + "initial_indent": " ", + } + wrapper = textwrap.TextWrapper(**tw_opts) + for stream in opts.infile: + records = find_dates(stream) + print("{}: {} records".format(stream.name, len(records))) + i = 0 + for rec in records: + date, datum, tail, extra = rec + opcode = datum[0] + stats = "hex({}), extra({})".format(len(datum), len(extra)) + date_str = str(date) + if date is not None: + date_str = date.isoformat() + + print("#### RECORD {}: {} {:#04x} {}".format(i, date_str, opcode, stats)) + print(" hex (%s)" % len(datum)) + print(lib.hexdump(datum, indent=4)) + print(" decimal") + print(int_dump(datum, indent=11)) + print(" datetime\n%s" % (lib.hexdump(tail, indent=4))) + print(" extra(%s)" % len(extra), end=" ") + if len(extra) > 0: + print("\n%s" % (lib.hexdump(extra, indent=4))) + print(int_dump(extra, indent=11)) + else: + print("%s" % (None)) + print("") + i += 1 + stream.close() + + +if __name__ == "__main__": + import doctest + + failures, tests = doctest.testmod() + if failures > 0: + print("REFUSING TO RUN DUE TO FAILED TESTS") + sys.exit(1) + main() ##### # EOF diff --git a/list_history.py b/list_history.py index f6b4908..9383e26 100755 --- a/list_history.py +++ b/list_history.py @@ -1,48 +1,50 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -import sys import argparse -import textwrap -from pprint import pprint, pformat -from binascii import hexlify # from datetime import datetime # from scapy.all import * import json +import sys +import textwrap +from binascii import hexlify +from pprint import pformat, pprint + +from decocare import history, lib, models +from decocare.history import HistoryPage, parse_record -from decocare import lib, history, models -from decocare.history import parse_record, HistoryPage +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") + parser.add_argument("--collate", dest="collate", default=False, action="store_true") - parser.add_argument('--collate', - dest='collate', - default=False, - action='store_true') + parser.add_argument("--larger", dest="larger", action="store_true") - parser.add_argument('--larger', - dest='larger', action='store_true') - - parser.add_argument('--no-larger', - dest='larger', action='store_false') + parser.add_argument("--no-larger", dest="larger", action="store_false") - parser.add_argument('--model', - # type=get_model, - choices=models.known.keys( )) + parser.add_argument( + "--model", + # type=get_model, + choices=list(models.known.keys()), + ) + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + parser.set_defaults(larger=False) + return parser - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - parser.set_defaults(larger=False) - return parser ## # move to history.py @@ -50,81 +52,85 @@ def get_opt_parser( ): def eat_nulls(fd): - nulls = bytearray( ) - for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): - if B[0] == 0x00: - nulls.extend(B) - else: - fd.seek(fd.tell( ) - 1) - break - print "found %s nulls" % len(nulls) - return nulls + nulls = bytearray() + for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): + if B[0] == 0x00: + nulls.extend(B) + else: + fd.seek(fd.tell() - 1) + break + print("found %s nulls" % len(nulls)) + return nulls + def find_records(stream, opts): - records = [ ] - errors = [ ] - bolus = bytearray( ) - extra = bytearray( ) - opcode = '' - - for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): - - if B == bytearray( [ 0x00, 0x00 ] ): - print ("#### STOPPING DOUBLE NULLS @ %s," % stream.tell( )), - nulls = eat_nulls(stream) - print "reading more to debug %#04x" % B[0] - print lib.hexdump(B, indent=4) - print lib.int_dump(B, indent=11) - - extra = bytearray(stream.read(32)) - print "##### DEBUG HEX" - print lib.hexdump(extra, indent=4) - print "##### DEBUG DECIMAL" - print lib.int_dump(extra, indent=11) - # print "XXX:???:XXX", history.parse_date(bolus).isoformat( ) - break - record = parse_record( stream, B, larger=opts.larger ) - records.append(record) - - return records - -def main( ): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - tw_opts = { - 'width': 50, - 'subsequent_indent': ' ', - 'initial_indent': ' ', - } - wrapper = textwrap.TextWrapper(**tw_opts) - for stream in opts.infile: - print "## START %s" % (stream.name) - records = [ ] - if opts.collate: - page = HistoryPage(bytearray(stream.read( )), opts.model) - records.extend(page.decode(larger=opts.larger )) - else: - records = find_records(stream, opts) - i = 0 - for record in records: - - prefix = '#### RECORD {} {}'.format(i, str(record)) - if getattr(record, 'pformat', None): - print record.pformat(prefix) - else: - json.dumps(record, indent=2) - i += 1 - print "`end %s: %s records`" % (stream.name, len(records)) - if opts.collate: - opts.out.write(json.dumps(records, indent=2)) - stream.close( ) - -if __name__ == '__main__': - import doctest - failures, tests = doctest.testmod( ) - if failures > 0: - print "REFUSING TO RUN DUE TO FAILED TESTS" - sys.exit(1) - main( ) + records = [] + errors = [] + bolus = bytearray() + extra = bytearray() + opcode = "" + + for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): + + if B == bytearray([0x00, 0x00]): + print(("#### STOPPING DOUBLE NULLS @ %s," % stream.tell()), end=" ") + nulls = eat_nulls(stream) + print("reading more to debug %#04x" % B[0]) + print(lib.hexdump(B, indent=4)) + print(lib.int_dump(B, indent=11)) + + extra = bytearray(stream.read(32)) + print("##### DEBUG HEX") + print(lib.hexdump(extra, indent=4)) + print("##### DEBUG DECIMAL") + print(lib.int_dump(extra, indent=11)) + # print("XXX:???:XXX", history.parse_date(bolus).isoformat( )) + break + record = parse_record(stream, B, larger=opts.larger) + records.append(record) + + return records + + +def main(): + parser = get_opt_parser() + opts = parser.parse_args() + tw_opts = { + "width": 50, + "subsequent_indent": " ", + "initial_indent": " ", + } + wrapper = textwrap.TextWrapper(**tw_opts) + for stream in opts.infile: + print("## START %s" % (stream.name)) + records = [] + if opts.collate: + page = HistoryPage(bytearray(stream.read()), opts.model) + records.extend(page.decode(larger=opts.larger)) + else: + records = find_records(stream, opts) + i = 0 + for record in records: + + prefix = "#### RECORD {} {}".format(i, str(record)) + if getattr(record, "pformat", None): + print(record.pformat(prefix)) + else: + json.dumps(record, indent=2) + i += 1 + print("`end {}: {} records`".format(stream.name, len(records))) + if opts.collate: + opts.out.write(json.dumps(records, indent=2)) + stream.close() + + +if __name__ == "__main__": + import doctest + + failures, tests = doctest.testmod() + if failures > 0: + print("REFUSING TO RUN DUE TO FAILED TESTS") + sys.exit(1) + main() ##### # EOF diff --git a/list_opcodes.py b/list_opcodes.py index 384911f..0763e94 100644 --- a/list_opcodes.py +++ b/list_opcodes.py @@ -1,399 +1,411 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -import sys import argparse +import sys import textwrap - -from pprint import pprint, pformat from binascii import hexlify +from pprint import pformat, pprint + +from decocare import history, lib +from decocare.history import NotADate + # from datetime import datetime # from scapy.all import * -from decocare import lib, history -from decocare.history import NotADate -def get_opt_parser( ): - parser = argparse.ArgumentParser( ) - parser.add_argument('infile', nargs="+", - default=sys.stdin, - type=argparse.FileType('r'), - help="Find dates in this file.") - parser.add_argument('--out', - default=sys.stdout, - type=argparse.FileType('w'), - help="Write records here.") - return parser +def get_opt_parser(): + parser = argparse.ArgumentParser() + parser.add_argument( + "infile", + nargs="+", + default=sys.stdin, + type=argparse.FileType("r"), + help="Find dates in this file.", + ) + + parser.add_argument( + "--out", + default=sys.stdout, + type=argparse.FileType("w"), + help="Write records here.", + ) + return parser + def parse_date(date): - try: - return history.parse_date(date) - except NotADate, e: pass - - return None - -class Record(object): - _names = { - 0x01: 'Bolus', - 0x03: 'Prime', - 0x06: 'NoDelivery', - 0x07: 'ResultTotals', - 0x08: 'ChangeBasalProfile', - 0x0C: 'ClearAlarm', - 0x14: 'SelectBasalProfile', - 0x16: 'TempBasal[eof]', - 0x17: 'ChangeTime', - 0x18: 'NewTimeSet', - 0x19: 'LowBattery', - 0x1a: 'Battery', - 0x1e: 'PumpSuspend', - 0x1f: 'PumpResume', - 0x0a: 'CalForBG', - 0x21: 'Rewind', - 0x26: 'EnableDisableRemote', - 0x27: 'ChangeRemoteID', - 0x33: 'TempBasal', - 0x34: 'LowReservoir', - 0x5b: 'BolusWizard', - 0x5c: 'BolusGiven?', - 0x63: 'ChangeUtility?', - 0x64: 'ChangeTimeDisplay', - - } - - _head = { - 0x01: 4, - 0x03: 5, - - 0x06: 4, - 0x07: 5, - - 0x28: 7, - 0x45: 7, - - # observed on bewest-pump - # 0x2e: 24, - # 0x5c: 12, - 0x5b: 2, - 0x5c: 2, - - # 0x0c: 22, - 0x6d: 46, - # 0x6d: 46 - 5, - - # hacks - - } - _date = 5 - _body = { - #0x5b: 15, - # 0x5b: 22, - 0x5b: 13, - 0x45: 3, - #0x06: 7, - 0x07: 38 + 3, - 0x08: 42, - - 0x26: 14, - 0x33: 1, - 0x34: 0, - - # 0x6b: 15, - # 0x18: 6, - # 0x21: 23, - - #0x6c: 32, - - # observed on bewest-pump - - - # hacks - #0x0a: 0, - #0x6c: 3, - } - - def __init__(self, head=bytearray( ), date=bytearray( ), body=bytearray( ) ): - self.head = head - self.date = date - self.opcode = None - if head[:1]: - self.opcode = head[0] - self.datetime = parse_date(date) - self.body = body - - @classmethod - def lookup_head(cls, opcode): - return cls._head.get(opcode, 2) - - @classmethod - def lookup_date(cls, opcode): - return cls._date - - @classmethod - def has_variable_head(cls, opcode): - if opcode == 0x5c: - return True - return False - - @classmethod - def variable_read(cls, opcode, body): - if opcode == 0x5c and body[1:]: - #print "XXX: VARIABLE READ: %#04x" % body[1] - return body[1] - return 0 - @classmethod - def seeks_null(cls, opcode, body): - return False - if opcode == 0x5c: - return True - if False and opcode == 0x5b: - # print 'XXX: %#04x' % body[13] - if body[13:] and body[13] == 0x5c: - return True - return False - - @classmethod - def is_midnight(cls, head): - if head[1:] and head[0] == 0x07: - if head[1:] and head[1] == 0x00: - return True - return False - - @classmethod - def lookup_body(cls, opcode): - # print "lookup body for opcode %#04x" % (opcode) - return cls._body.get(opcode, 0) - - def __str__(self): - name = self._names.get(self.opcode, self.__class__.__name__) - lengths = 'head[{}], body[{}]'.format(len(self.head), len(self.body)) - # opcodes = ' '.join(['%#04x' % x for x in self.head[:1]]) - opcodes = '%#04x' % self.opcode - return ' '.join([name, self.date_str( ), lengths, opcodes ]) - - def date_str(self): - result = 'unknown' - if self.datetime is not None: - result = self.datetime.isoformat( ) - else: - if self.is_midnight(self.head): - result = "MIDNIGHT!?: {}".format(history.unmask_date(self.date)) - return result - - def pformat(self, prefix=''): - head = '\n'.join([ " op hex (%s)" % len(self.head), lib.hexdump(self.head, indent=4), - " decimal", int_dump(self.head, indent=11) ]) - date = '\n'.join([ " datetime (%s)" % self.date_str( ), - lib.hexdump(self.date, indent=4) ]) - - body = " body (%s)" % len(self.body) - if len(self.body) > 0: - body = '\n'.join([ body, - " hex", lib.hexdump(self.body, indent=4), - " decimal", int_dump(self.body, indent=11) ]) - extra = [ ] - hour_bits = history.extra_hour_bits(self.date[1]) - year_bits = history.extra_year_bits(self.date[4]) - day_bits = history.extra_hour_bits(self.date[3]) - if 1 in hour_bits: - extra.append("HOUR BITS: {}".format(str(hour_bits))) - if 1 in day_bits: - extra.append("DAY BITS: {}".format(str(day_bits))) - if 1 in year_bits: - extra.append("YEAR BITS: {}".format(str(year_bits))) - extra = ' ' + ' '.join(extra) - return '\n'.join([ prefix, head, date, body, extra ]) + try: + return history.parse_date(date) + except NotADate as e: + pass + + return None + + +class Record: + _names = { + 0x01: "Bolus", + 0x03: "Prime", + 0x06: "NoDelivery", + 0x07: "ResultTotals", + 0x08: "ChangeBasalProfile", + 0x0C: "ClearAlarm", + 0x14: "SelectBasalProfile", + 0x16: "TempBasal[eof]", + 0x17: "ChangeTime", + 0x18: "NewTimeSet", + 0x19: "LowBattery", + 0x1A: "Battery", + 0x1E: "PumpSuspend", + 0x1F: "PumpResume", + 0x0A: "CalForBG", + 0x21: "Rewind", + 0x26: "EnableDisableRemote", + 0x27: "ChangeRemoteID", + 0x33: "TempBasal", + 0x34: "LowReservoir", + 0x5B: "BolusWizard", + 0x5C: "BolusGiven?", + 0x63: "ChangeUtility?", + 0x64: "ChangeTimeDisplay", + } + + _head = { + 0x01: 4, + 0x03: 5, + 0x06: 4, + 0x07: 5, + 0x28: 7, + 0x45: 7, + # observed on bewest-pump + # 0x2e: 24, + # 0x5c: 12, + 0x5B: 2, + 0x5C: 2, + # 0x0c: 22, + 0x6D: 46, + # 0x6d: 46 - 5, + # hacks + } + _date = 5 + _body = { + # 0x5b: 15, + # 0x5b: 22, + 0x5B: 13, + 0x45: 3, + # 0x06: 7, + 0x07: 38 + 3, + 0x08: 42, + 0x26: 14, + 0x33: 1, + 0x34: 0, + # 0x6b: 15, + # 0x18: 6, + # 0x21: 23, + # 0x6c: 32, + # observed on bewest-pump + # hacks + # 0x0a: 0, + # 0x6c: 3, + } + + def __init__(self, head=bytearray(), date=bytearray(), body=bytearray()): + self.head = head + self.date = date + self.opcode = None + if head[:1]: + self.opcode = head[0] + self.datetime = parse_date(date) + self.body = body + + @classmethod + def lookup_head(cls, opcode): + return cls._head.get(opcode, 2) + + @classmethod + def lookup_date(cls, opcode): + return cls._date + + @classmethod + def has_variable_head(cls, opcode): + if opcode == 0x5C: + return True + return False + + @classmethod + def variable_read(cls, opcode, body): + if opcode == 0x5C and body[1:]: + # print("XXX: VARIABLE READ: %#04x" % body[1]) + return body[1] + return 0 + + @classmethod + def seeks_null(cls, opcode, body): + return False + if opcode == 0x5C: + return True + if False and opcode == 0x5B: + # print('XXX: %#04x' % body[13]) + if body[13:] and body[13] == 0x5C: + return True + return False + + @classmethod + def is_midnight(cls, head): + if head[1:] and head[0] == 0x07: + if head[1:] and head[1] == 0x00: + return True + return False + + @classmethod + def lookup_body(cls, opcode): + # print("lookup body for opcode %#04x" % (opcode)) + return cls._body.get(opcode, 0) + + def __str__(self): + name = self._names.get(self.opcode, self.__class__.__name__) + lengths = "head[{}], body[{}]".format(len(self.head), len(self.body)) + # opcodes = ' '.join(['%#04x' % x for x in self.head[:1]]) + opcodes = "%#04x" % self.opcode + return " ".join([name, self.date_str(), lengths, opcodes]) + + def date_str(self): + result = "unknown" + if self.datetime is not None: + result = self.datetime.isoformat() + else: + if self.is_midnight(self.head): + result = "MIDNIGHT!?: {}".format(history.unmask_date(self.date)) + return result + + def pformat(self, prefix=""): + head = "\n".join( + [ + " op hex (%s)" % len(self.head), + lib.hexdump(self.head, indent=4), + " decimal", + int_dump(self.head, indent=11), + ] + ) + date = "\n".join( + [" datetime (%s)" % self.date_str(), lib.hexdump(self.date, indent=4)] + ) + + body = " body (%s)" % len(self.body) + if len(self.body) > 0: + body = "\n".join( + [ + body, + " hex", + lib.hexdump(self.body, indent=4), + " decimal", + int_dump(self.body, indent=11), + ] + ) + extra = [] + hour_bits = history.extra_hour_bits(self.date[1]) + year_bits = history.extra_year_bits(self.date[4]) + day_bits = history.extra_hour_bits(self.date[3]) + if 1 in hour_bits: + extra.append("HOUR BITS: {}".format(str(hour_bits))) + if 1 in day_bits: + extra.append("DAY BITS: {}".format(str(day_bits))) + if 1 in year_bits: + extra.append("YEAR BITS: {}".format(str(year_bits))) + extra = " " + " ".join(extra) + return "\n".join([prefix, head, date, body, extra]) def eat_nulls(fd): - nulls = bytearray( ) - for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): - if B[0] == 0x00: - nulls.extend(B) - else: - fd.seek(fd.tell( ) - 1) - break - print "found %s nulls" % len(nulls) - return nulls + nulls = bytearray() + for B in iter(lambda: bytearray(fd.read(1)), bytearray("")): + if B[0] == 0x00: + nulls.extend(B) + else: + fd.seek(fd.tell() - 1) + break + print("found %s nulls" % len(nulls)) + return nulls + def seek_null(fd): - bolus = bytearray( ) - for B in iter(lambda: fd.read(1), ""): - bolus.append(B) - # print lib.hexdump(bolus) - if B == bytearray([ 0x00 ]): - return bolus - return bolus + bolus = bytearray() + for B in iter(lambda: fd.read(1), ""): + bolus.append(B) + # print(lib.hexdump(bolus)) + if B == bytearray([0x00]): + return bolus + return bolus + def find_dates(stream): - records = [ ] - errors = [ ] - bolus = bytearray( ) - extra = bytearray( ) - opcode = '' - for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): - - opcode = B[0] - #bolus.append(B) - bolus.extend(B) - - head_length = Record.lookup_head(opcode) - body_length = Record.lookup_body(opcode) - date_length = Record.lookup_date(opcode) - total = len(bolus) - - - if total < head_length: - bolus.extend(bytearray(stream.read(head_length-total))) - variable_read = Record.variable_read(opcode, bolus) - - if variable_read > 2: - - #print "super special" - bolus.extend(bytearray(stream.read(variable_read))) - #print lib.hexdump( bolus ) - opcode = bolus[variable_read] - #print "NEW OPCODE %#04x" % opcode - head_length = Record.lookup_head(opcode) - body_length = Record.lookup_body(opcode) - total = len(bolus) - variable_read - - if total < head_length: - bolus.extend( stream.read(head_length-total) ) - total = len(bolus) - - #body_length = Record.lookup_body(opcode) - #date_length = Record.lookup_date(opcode) - #total = len(bolus) - - total = len(bolus) - head_length = total - - - head = bolus[:max(total, 1)] - - bolus.extend(bytearray(stream.read(date_length))) - date = bolus[head_length:head_length+date_length] - total = len(bolus) - # print repr(bolus), date_length, repr(date) - if len(date) < 5: - print "DATE LESS THAN 5!", stream.tell( ) - print lib.hexdump(bolus) - break - records[-1].body.extend(bolus) - datetime = parse_date(date) - if bytearray( [0x00] * max(total-2, 5) ) in bolus: - nulls = bytearray(eat_nulls(stream)) - pos = stream.tell( ) - if pos > 1021: - bolus = bolus + nulls + bytearray(stream.read(-1)) - crc = bolus[-2:] - nulls = bolus[:-2] - print "EOF {} nulls, CRC:".format(len(nulls)) - print lib.hexdump(crc) - else: - print total, ' ', max(total-2, 5) - print lib.hexdump( [0x00] * min(total, 5) ) - print - print "TOO MANY NULLS, BAILING ON STREAM at %s " % stream.tell( ) - print "bolus" - print lib.hexdump(bolus) - print "nulls" - print lib.hexdump(nulls) - print "MISSING: ARE THERE 32 more bytes?" - print lib.hexdump(bytearray(stream.read(32))) - # records[-1].body.extend(nulls) - break - - - if not Record.is_midnight(head): - if datetime is None: - print ("#### MISSING DATETIME @ %s," % stream.tell( )), - print "reading more to debug %#04x" % opcode - print lib.hexdump(bolus, indent=4) - print int_dump(bolus, indent=11) - - extra = bytearray(stream.read(32)) - print "##### DEBUG HEX" - print lib.hexdump(extra, indent=4) - print "##### DEBUG DECIMAL" - print int_dump(extra, indent=11) - if history.parse_date(bolus): - print "XXX:???:XXX", history.parse_date(bolus).isoformat( ) - break - - if datetime is not None or Record.is_midnight(head): - - body = bytearray(stream.read(body_length)) - bolus.extend(body) - if False or Record.seeks_null(opcode, body): - print "should eat up to null, second %s" % repr(body[1:]) - if body[1:]: - if body[-1] != 0x00: - extra = seek_null(stream) - print "found %s extra" % len(extra) - body.extend(extra) - bolus.extend(extra) - epi = bytearray(stream.read(date_length)) - finished = parse_date(epi) - record = Record(head, date, body) - prefix = "#### RECORD %s %s" % (len(records), str(record) ) - print record.pformat(prefix) - print "" - records.append(record) - bolus = bytearray( ) - opcode = '' - - return records + records = [] + errors = [] + bolus = bytearray() + extra = bytearray() + opcode = "" + for B in iter(lambda: bytearray(stream.read(2)), bytearray("")): + + opcode = B[0] + # bolus.append(B) + bolus.extend(B) + + head_length = Record.lookup_head(opcode) + body_length = Record.lookup_body(opcode) + date_length = Record.lookup_date(opcode) + total = len(bolus) + + if total < head_length: + bolus.extend(bytearray(stream.read(head_length - total))) + variable_read = Record.variable_read(opcode, bolus) + + if variable_read > 2: + + # print("super special") + bolus.extend(bytearray(stream.read(variable_read))) + # print(lib.hexdump( bolus )) + opcode = bolus[variable_read] + # print("NEW OPCODE %#04x" % opcode) + head_length = Record.lookup_head(opcode) + body_length = Record.lookup_body(opcode) + total = len(bolus) - variable_read + + if total < head_length: + bolus.extend(stream.read(head_length - total)) + total = len(bolus) + + # body_length = Record.lookup_body(opcode) + # date_length = Record.lookup_date(opcode) + # total = len(bolus) + + total = len(bolus) + head_length = total + + head = bolus[: max(total, 1)] + + bolus.extend(bytearray(stream.read(date_length))) + date = bolus[head_length : head_length + date_length] + total = len(bolus) + # print(repr(bolus), date_length, repr(date)) + if len(date) < 5: + print("DATE LESS THAN 5!", stream.tell()) + print(lib.hexdump(bolus)) + break + records[-1].body.extend(bolus) + datetime = parse_date(date) + if bytearray([0x00] * max(total - 2, 5)) in bolus: + nulls = bytearray(eat_nulls(stream)) + pos = stream.tell() + if pos > 1021: + bolus = bolus + nulls + bytearray(stream.read(-1)) + crc = bolus[-2:] + nulls = bolus[:-2] + print("EOF {} nulls, CRC:".format(len(nulls))) + print(lib.hexdump(crc)) + else: + print(total, " ", max(total - 2, 5)) + print(lib.hexdump([0x00] * min(total, 5))) + print() + print("TOO MANY NULLS, BAILING ON STREAM at %s " % stream.tell()) + print("bolus") + print(lib.hexdump(bolus)) + print("nulls") + print(lib.hexdump(nulls)) + print("MISSING: ARE THERE 32 more bytes?") + print(lib.hexdump(bytearray(stream.read(32)))) + # records[-1].body.extend(nulls) + break + + if not Record.is_midnight(head): + if datetime is None: + print(("#### MISSING DATETIME @ %s," % stream.tell()), end=" ") + print("reading more to debug %#04x" % opcode) + print(lib.hexdump(bolus, indent=4)) + print(int_dump(bolus, indent=11)) + + extra = bytearray(stream.read(32)) + print("##### DEBUG HEX") + print(lib.hexdump(extra, indent=4)) + print("##### DEBUG DECIMAL") + print(int_dump(extra, indent=11)) + if history.parse_date(bolus): + print("XXX:???:XXX", history.parse_date(bolus).isoformat()) + break + + if datetime is not None or Record.is_midnight(head): + + body = bytearray(stream.read(body_length)) + bolus.extend(body) + if False or Record.seeks_null(opcode, body): + print("should eat up to null, second %s" % repr(body[1:])) + if body[1:]: + if body[-1] != 0x00: + extra = seek_null(stream) + print("found %s extra" % len(extra)) + body.extend(extra) + bolus.extend(extra) + epi = bytearray(stream.read(date_length)) + finished = parse_date(epi) + record = Record(head, date, body) + prefix = "#### RECORD {} {}".format(len(records), str(record)) + print(record.pformat(prefix)) + print("") + records.append(record) + bolus = bytearray() + opcode = "" + + return records + def int_dump(stream, indent=0): - """ - >>> int_dump(bytearray([0x01, 0x02])) - ' 1 2' - - - """ - cells = [ '%#04s' % (x) for x in stream ] - lines = [ ] - indent = ''.join( [ ' ' ] * indent ) - while cells: - octet = cells[:8] - line = ' '.join(octet) - lines.append(indent + line) - cells = cells[8:] - - out = ('\n').join([ line for line in lines ]) - return out - -def main( ): - parser = get_opt_parser( ) - opts = parser.parse_args( ) - tw_opts = { - 'width': 50, - 'subsequent_indent': ' ', - 'initial_indent': ' ', - } - wrapper = textwrap.TextWrapper(**tw_opts) - for stream in opts.infile: - print "## START %s" % (stream.name) - records = find_dates(stream) - i = 0 - for record in records: - - prefix = '#### RECORD {} {}'.format(i, str(record)) - # record.pformat(prefix) - i += 1 - print "`end %s: %s records`" % (stream.name, len(records)) - stream.close( ) - -if __name__ == '__main__': - import doctest - failures, tests = doctest.testmod( ) - if failures > 0: - print "REFUSING TO RUN DUE TO FAILED TESTS" - sys.exit(1) - main( ) + """ + >>> int_dump(bytearray([0x01, 0x02])) + ' 1 2' + + """ + cells = ["%#04s" % (x) for x in stream] + lines = [] + indent = "".join([" "] * indent) + while cells: + octet = cells[:8] + line = " ".join(octet) + lines.append(indent + line) + cells = cells[8:] + + out = ("\n").join([line for line in lines]) + return out + + +def main(): + parser = get_opt_parser() + opts = parser.parse_args() + tw_opts = { + "width": 50, + "subsequent_indent": " ", + "initial_indent": " ", + } + wrapper = textwrap.TextWrapper(**tw_opts) + for stream in opts.infile: + print("## START %s" % (stream.name)) + records = find_dates(stream) + i = 0 + for record in records: + + prefix = "#### RECORD {} {}".format(i, str(record)) + # record.pformat(prefix) + i += 1 + print("`end {}: {} records`".format(stream.name, len(records))) + stream.close() + + +if __name__ == "__main__": + import doctest + + failures, tests = doctest.testmod() + if failures > 0: + print("REFUSING TO RUN DUE TO FAILED TESTS") + sys.exit(1) + main() ##### # EOF diff --git a/logs/analyze/byte_to_bin.py b/logs/analyze/byte_to_bin.py index 61212bf..82c72d7 100644 --- a/logs/analyze/byte_to_bin.py +++ b/logs/analyze/byte_to_bin.py @@ -1,4 +1,3 @@ - ############################################################################## # Edward Robinson # @@ -9,17 +8,15 @@ # argv[2] = output type (bin=pure binary, hex=hexidecimal) # argv[3] = with new line characters (true or false) ############################################################################## - -import sys import getopt - +import sys lineSize = 10 -#fileInName = sys.argv[1] -#outType = sys.argv[2] -#if sys.argv[3] == "true": +# fileInName = sys.argv[1] +# outType = sys.argv[2] +# if sys.argv[3] == "true": # withNewLine = True -#else: +# else: # withNewLine = False outType = "hex" fileInName = "ReadHistoryData-page-0.data" @@ -30,34 +27,34 @@ fileOutName = fileInName + ".hex" myBytes = bytearray() -with open(fileInName, 'rb') as file: - while 1: +with open(fileInName, "rb") as file: + while True: byte = file.read(1) if not byte: break myBytes.append(byte) -#j = len(myBytes) -#print j -#j = j - 1 -#print myBytes[j] -#print '{0:08b}'.format(myBytes[j]) -#print hex(myBytes[j]) +# j = len(myBytes) +# print(j) +# j = j - 1 +# print(myBytes[j]) +# print('{0:08b}'.format(myBytes[j])) +# print(hex(myBytes[j])) # myBytes=myBytes.replace("\n","") # myBytes=myBytes.replace("\t","") j = 0 -fileOut = open(fileOutName, 'w') +fileOut = open(fileOutName, "w") # only do line by line if there will be multiple lines if len(myBytes) > lineSize: for i in range(0, len(myBytes) - lineSize, lineSize): for j in range(0, lineSize): # convert byte to appropriate format if outType == "bin": - out = '{0:08b}'.format(myBytes[j + i]) + out = "{:08b}".format(myBytes[j + i]) else: - out = '{0:02x}'.format(myBytes[j + i]) + out = "{:02x}".format(myBytes[j + i]) fileOut.write(out) if withNewLine: fileOut.write("\n") @@ -68,13 +65,13 @@ for i in range(0, len(myBytes) - j): # convert to hex or binary and print if outType == "bin": - out = '{0:08b}'.format(myBytes[i]) + out = "{:08b}".format(myBytes[i]) else: - out = '{0:02x}'.format(myBytes[i]) + out = "{:02x}".format(myBytes[i]) fileOut.write(out) if withNewLine: fileOut.write("\n") # info stuff -# print "bytes raw : " + myBytes -# print "bytes bin : " + '{0:08b}'.format(myBytes) -# print "bytes hex : " + hex(myBytes) +# print("bytes raw : " + myBytes) +# print("bytes bin : " + '{0:08b}'.format(myBytes)) +# print("bytes hex : " + hex(myBytes)) diff --git a/logs/analyze/get_last_sg_from_data.py b/logs/analyze/get_last_sg_from_data.py index 218d558..b8c09fb 100644 --- a/logs/analyze/get_last_sg_from_data.py +++ b/logs/analyze/get_last_sg_from_data.py @@ -1,4 +1,3 @@ - ############################################################################## # Edward Robinson # @@ -7,76 +6,74 @@ # # inputs : # argv[1] = file input name -# if no file name is given then the file with the greatest date in -# the logs directory is used +# if no file name is given then the file with the greatest date in +# the logs directory is used # ############################################################################## - -import sys import getopt +import sys - -#lineSize = 10 -#fileInName = sys.argv[1] -#outType = sys.argv[2] -#if sys.argv[3] == "true": +# lineSize = 10 +# fileInName = sys.argv[1] +# outType = sys.argv[2] +# if sys.argv[3] == "true": # withNewLine = True -#else: +# else: # withNewLine = False -#outType = "hex" +# outType = "hex" fileInName = "20140421_030133-ReadGlucoseHistory-page-16.data" fileInName = "20140421_042530-ReadGlucoseHistory-page-0.data" fileOutName = "latest-sg.xml" -#fileOutName = fileInName + ".values" -#withNewLine = False -#if outType == "bin": +# fileOutName = fileInName + ".values" +# withNewLine = False +# if outType == "bin": # fileOutName = fileInName + ".bin" -#else: +# else: # fileOutName = fileInName + ".hex" myBytes = bytearray() -with open(fileInName, 'rb') as file: - while 1: +with open(fileInName, "rb") as file: + while True: byte = file.read(1) if not byte: break myBytes.append(byte) -#j = len(myBytes) -#print j -#j = j - 1 -#print myBytes[j] -#print '{0:08b}'.format(myBytes[j]) -#print hex(myBytes[j]) +# j = len(myBytes) +# print(j) +# j = j - 1 +# print(myBytes[j]) +# print('{0:08b}'.format(myBytes[j])) +# print(hex(myBytes[j])) # myBytes=myBytes.replace("\n","") # myBytes=myBytes.replace("\t","") j = 0 -fileOut = open(fileOutName, 'w') +fileOut = open(fileOutName, "w") numZerosCounter = 0 latest_sg = 0 # for each byte: convert it to a decimal, double it # check that it is a valid sg and then mark it as the latest for i in range(0, len(myBytes)): - bin = '{0:08b}'.format(myBytes[i]) - hex = '{0:02x}'.format(myBytes[i]) - dec = int(hex, 16) - sg = dec * 2 - if sg == 0: - numZerosCounter = numZerosCounter + 1 - else: - numZerosCounter = 0 - if numZerosCounter > 20: - break - if sg > 40 and sg < 400: - latest_sg = sg + bin = "{:08b}".format(myBytes[i]) + hex = "{:02x}".format(myBytes[i]) + dec = int(hex, 16) + sg = dec * 2 + if sg == 0: + numZerosCounter = numZerosCounter + 1 + else: + numZerosCounter = 0 + if numZerosCounter > 20: + break + if sg > 40 and sg < 400: + latest_sg = sg -fileOut.write(""+str(latest_sg)+"") +fileOut.write("" + str(latest_sg) + "") ####### old code ###### # only do line by line if there will be multiple lines -#if len(myBytes) > lineSize: +# if len(myBytes) > lineSize: # for i in range(0, len(myBytes) - lineSize, lineSize): # for j in range(0, lineSize): # # convert byte to appropriate format @@ -90,7 +87,7 @@ # j = i + lineSize # # if the last few bytes don't fill a line from the loop above print them here -#if j < len(myBytes): +# if j < len(myBytes): # for i in range(0, len(myBytes) - j): # # convert to hex or binary and print # if outType == "bin": @@ -101,6 +98,6 @@ # if withNewLine: # fileOut.write("\n") # info stuff -# print "bytes raw : " + myBytes -# print "bytes bin : " + '{0:08b}'.format(myBytes) -# print "bytes hex : " + hex(myBytes) +# print("bytes raw : " + myBytes) +# print("bytes bin : " + '{0:08b}'.format(myBytes)) +# print("bytes hex : " + hex(myBytes)) diff --git a/logs/analyze/op_code_finder.py b/logs/analyze/op_code_finder.py index 4486465..e3ecc9a 100644 --- a/logs/analyze/op_code_finder.py +++ b/logs/analyze/op_code_finder.py @@ -1,155 +1,161 @@ - from datetime import * -"""def str_to_int(string): - return - -def int_to_hex(): - -def int_to_hexstr(): - # convert integer to string - return '{0:02x}'.format(op_code) - -def hex_to_int(): - # convert hex char(s) to int - '{0:08b}'.format() -""" -# -*- coding: utf-8 *-* class op_code_stats: -# op_code = 0 -# op_code_str = '{0:02x}'.format(op_code) -# num_records = 0 -# record_lens = Counter() - - def __init__(self, op_code, data): - self.op_code = op_code - self.op_code_str = '{0:02x}'.format(op_code) - records = data.split(self.op_code_str) - self.num_records = len(records) - from collections import Counter - self.record_lens = Counter() - for record in records: - record_len = len(record) - if record_len > 1 and record_len < 500: - self.record_lens[str(record_len)] += 1 - - def is_likely_op(self): - if self.num_records < 5: - return False - most_common = self.record_lens.most_common(1) - if not most_common: - return False - if int(most_common[0][0]) < 5 or int(most_common[0][0]) > 20: - return False - if most_common[0][1] * 4 < self.num_records: - return False - return True - - def str(self): - string = "Data "+self.op_code_str+" ("+str(self.num_records)+"): " - most_common = self.record_lens.most_common(7) - for record_size in most_common: - string += str(record_size[0]) +" ("+ str(record_size[1]) + ")\t" - return string - #for record in self.record_lens: - # string += record+"["+self.record_lens[record]+"]\t" - - -class record(): - - def __init__(self, op_code, raw): - self.is_valid_record = True - self.op_code = op_code - self.raw = raw - self.hex_bytes = array.array('B', raw.decode("hex")) - self.binary = bin(int(raw, 16))[2:].zfill(56) - # parse_date seems to not be quite correct -# self.bwdatetime = parse_date(self.hex_bytes[-5:]) -# print str(self.bwdatetime) - self.mydatetime = self.mydate(raw) - - - def mydate(self, raw): - year = int(self.binary[-8:], 2) - # month - month = int(self.binary[-24:-22]+self.binary[-16:-14], 2) -# day_offset possibilities: -24, -57 - doesn't seem quite right - day_offset = -24 - # if day is split possibilities (size 2): - day = int(self.binary[day_offset:day_offset+5], 2) - # hour offset options: -21 -33 -38 => -21 looks almost perfect - hour_offset = -21 - hour = int(self.binary[hour_offset:hour_offset+5], 2) -# hour = int(self.binary[-21:-17], 2) - # minute offset options: -27 -28 => -28 looks to be the best but not perfect - min_offset = -28 - minute = int(self.binary[min_offset:min_offset+6], 2) -# print_offset = hour_offset -# print str(month)+"/"+str(day) + "/"+str(year)+\ -# " "+str(hour).zfill(2)+":"+str(minute).zfill(2)\ -# +" \t"+self.binary[print_offset:print_offset+5]+" "\ -# +self.bin_str() - if year != 14 or month > 12 or month < 1 or day > 28 or day < 1 or\ - hour >= 24 or minute >= 60: - self.is_valid_record = False - return - return datetime(year, month, day, hour, minute) - - def bin_str(self): - string = "" - for i in range(0, 7): - string += self.binary[i*8:(i+1)*8] + "-" - return string - - def str(self): - if self.is_valid_record: - return "Record "+self.op_code+": "+str(self.mydatetime)+" - "\ - +self.raw+" - "+self.binary - # +" - "+str(self.bwdatetime) - else: - return "This is an invalid record: "+self.binary + # op_code = 0 + # op_code_str = '{0:02x}'.format(op_code) + # num_records = 0 + # record_lens = Counter() + + def __init__(self, op_code, data): + self.op_code = op_code + self.op_code_str = "{:02x}".format(op_code) + records = data.split(self.op_code_str) + self.num_records = len(records) + from collections import Counter + + self.record_lens = Counter() + for record in records: + record_len = len(record) + if record_len > 1 and record_len < 500: + self.record_lens[str(record_len)] += 1 + + def is_likely_op(self): + if self.num_records < 5: + return False + most_common = self.record_lens.most_common(1) + if not most_common: + return False + if int(most_common[0][0]) < 5 or int(most_common[0][0]) > 20: + return False + if most_common[0][1] * 4 < self.num_records: + return False + return True + + def str(self): + string = "Data " + self.op_code_str + " (" + str(self.num_records) + "): " + most_common = self.record_lens.most_common(7) + for record_size in most_common: + string += str(record_size[0]) + " (" + str(record_size[1]) + ")\t" + return string + # for record in self.record_lens: + # string += record+"["+self.record_lens[record]+"]\t" + + +class record: + def __init__(self, op_code, raw): + self.is_valid_record = True + self.op_code = op_code + self.raw = raw + self.hex_bytes = array.array("B", raw.decode("hex")) + self.binary = bin(int(raw, 16))[2:].zfill(56) + # parse_date seems to not be quite correct + # self.bwdatetime = parse_date(self.hex_bytes[-5:]) + # print str(self.bwdatetime) + self.mydatetime = self.mydate(raw) + + def mydate(self, raw): + year = int(self.binary[-8:], 2) + # month + month = int(self.binary[-24:-22] + self.binary[-16:-14], 2) + # day_offset possibilities: -24, -57 - doesn't seem quite right + day_offset = -24 + # if day is split possibilities (size 2): + day = int(self.binary[day_offset : day_offset + 5], 2) + # hour offset options: -21 -33 -38 => -21 looks almost perfect + hour_offset = -21 + hour = int(self.binary[hour_offset : hour_offset + 5], 2) + # hour = int(self.binary[-21:-17], 2) + # minute offset options: -27 -28 => -28 looks to be the best but not perfect + min_offset = -28 + minute = int(self.binary[min_offset : min_offset + 6], 2) + # print_offset = hour_offset + # print str(month)+"/"+str(day) + "/"+str(year)+\ + # " "+str(hour).zfill(2)+":"+str(minute).zfill(2)\ + # +" \t"+self.binary[print_offset:print_offset+5]+" "\ + # +self.bin_str() + if ( + year != 14 + or month > 12 + or month < 1 + or day > 28 + or day < 1 + or hour >= 24 + or minute >= 60 + ): + self.is_valid_record = False + return + return datetime(year, month, day, hour, minute) + + def bin_str(self): + string = "" + for i in range(0, 7): + string += self.binary[i * 8 : (i + 1) * 8] + "-" + return string + + def str(self): + if self.is_valid_record: + return ( + "Record " + + self.op_code + + ": " + + str(self.mydatetime) + + " - " + + self.raw + + " - " + + self.binary + ) + # +" - "+str(self.bwdatetime) + else: + return "This is an invalid record: " + self.binary ############# Main ################# fileInName = "ReadHistoryData-page-0.data.hex" -with open (fileInName, "r") as myfile: - data=myfile.read().replace('\n', '') +with open(fileInName) as myfile: + data = myfile.read().replace("\n", "") op_codes = [] for i in range(256): - op_code = i #= int(data[i:i+2], 16) - op_codes.append(op_code_stats(op_code, data)) + op_code = i # = int(data[i:i+2], 16) + op_codes.append(op_code_stats(op_code, data)) op_codes = sorted(op_codes, key=lambda op_code_stats: op_code_stats.num_records) -#for op_code in op_codes: +# for op_code in op_codes: # if op_code.is_likely_op(): # print op_code.str() import array -from times import * from datetime import * -op_code = 11 # 11=0b -#op_code = 10 # 10=0a -print "\n\n" -records = data.split('{0:02x}'.format(op_code)) + +from times import * + +op_code = 11 # 11=0b +# op_code = 10 # 10=0a +print("\n\n") +records = data.split("{:02x}".format(op_code)) parsed_records = [] for package in records: - record_len = len(package) - if len(package) == 14: - parsed_records.append(record("0b", package)) + record_len = len(package) + if len(package) == 14: + parsed_records.append(record("0b", package)) valid_records = [] for record in parsed_records: - if record.is_valid_record: - valid_records.append(record) + if record.is_valid_record: + valid_records.append(record) -print "should match about 284 parsed("+ str(len(parsed_records))+") valid("+\ - str(len(valid_records))+")" +print( + "should match about 284 parsed(" + + str(len(parsed_records)) + + ") valid(" + + str(len(valid_records)) + + ")" +) -valid_records.sort(key=lambda r:r.mydatetime) +valid_records.sort(key=lambda r: r.mydatetime) for record in valid_records: - print record.str() + print(record.str()) diff --git a/logs/analyze/sg_data_to_values.py b/logs/analyze/sg_data_to_values.py index 0793351..3f6e1b6 100755 --- a/logs/analyze/sg_data_to_values.py +++ b/logs/analyze/sg_data_to_values.py @@ -1,4 +1,3 @@ - ############################################################################## # Edward Robinson # @@ -7,72 +6,69 @@ # # inputs : # argv[1] = file input name -# if no file name is given then the file with the greatest date in -# the logs directory is used +# if no file name is given then the file with the greatest date in +# the logs directory is used # ############################################################################## - -import sys import getopt +import sys - -#lineSize = 10 -#fileInName = sys.argv[1] -#outType = sys.argv[2] -#if sys.argv[3] == "true": +# lineSize = 10 +# fileInName = sys.argv[1] +# outType = sys.argv[2] +# if sys.argv[3] == "true": # withNewLine = True -#else: +# else: # withNewLine = False -#outType = "hex" +# outType = "hex" fileInName = "20140421_030133-ReadGlucoseHistory-page-16.data" fileOutName = fileInName + ".values" -#withNewLine = False -#if outType == "bin": +# withNewLine = False +# if outType == "bin": # fileOutName = fileInName + ".bin" -#else: +# else: # fileOutName = fileInName + ".hex" myBytes = bytearray() -with open(fileInName, 'rb') as file: - while 1: +with open(fileInName, "rb") as file: + while True: byte = file.read(1) if not byte: break myBytes.append(byte) -#j = len(myBytes) -#print j -#j = j - 1 -#print myBytes[j] -#print '{0:08b}'.format(myBytes[j]) -#print hex(myBytes[j]) +# j = len(myBytes) +# print(j) +# j = j - 1 +# print(myBytes[j]) +# print('{0:08b}'.format(myBytes[j])) +# print(hex(myBytes[j])) # myBytes=myBytes.replace("\n","") # myBytes=myBytes.replace("\t","") j = 0 -fileOut = open(fileOutName, 'w') +fileOut = open(fileOutName, "w") numZerosCounter = 0 -# for each byte: convert it to a decimal, double it, +# for each byte: convert it to a decimal, double it, # write it to a new line in the output file for i in range(0, len(myBytes)): - bin = '{0:08b}'.format(myBytes[i]) - hex = '{0:02x}'.format(myBytes[i]) - dec = int(hex, 16) - sg = dec * 2 - if sg == 0: - numZerosCounter = numZerosCounter + 1 - else: - numZerosCounter = 0 - if numZerosCounter > 20: - break - fileOut.write("value : "+str(sg)+"\t"+bin+"\t"+hex+"\n") - + bin = "{:08b}".format(myBytes[i]) + hex = "{:02x}".format(myBytes[i]) + dec = int(hex, 16) + sg = dec * 2 + if sg == 0: + numZerosCounter = numZerosCounter + 1 + else: + numZerosCounter = 0 + if numZerosCounter > 20: + break + fileOut.write("value : " + str(sg) + "\t" + bin + "\t" + hex + "\n") ####### old code ###### # only do line by line if there will be multiple lines -#if len(myBytes) > lineSize: +# if len(myBytes) > lineSize: # for i in range(0, len(myBytes) - lineSize, lineSize): # for j in range(0, lineSize): # # convert byte to appropriate format @@ -86,7 +82,7 @@ # j = i + lineSize # # if the last few bytes don't fill a line from the loop above print them here -#if j < len(myBytes): +# if j < len(myBytes): # for i in range(0, len(myBytes) - j): # # convert to hex or binary and print # if outType == "bin": @@ -97,6 +93,6 @@ # if withNewLine: # fileOut.write("\n") # info stuff -# print "bytes raw : " + myBytes -# print "bytes bin : " + '{0:08b}'.format(myBytes) -# print "bytes hex : " + hex(myBytes) +# print("bytes raw : " + myBytes) +# print("bytes bin : " + '{0:08b}'.format(myBytes)) +# print("bytes hex : " + hex(myBytes)) diff --git a/logs/analyze/times.py b/logs/analyze/times.py index bae4803..e17310d 100644 --- a/logs/analyze/times.py +++ b/logs/analyze/times.py @@ -1,311 +1,330 @@ - from datetime import datetime -class NotADate(Exception): pass +class NotADate(Exception): + pass + class Mask: - """ - Some useful bit masks. - """ - time = 0xC0 - invert = 0x3F - year = 0x0F - day = 0x1F + """ + Some useful bit masks. + """ + + time = 0xC0 + invert = 0x3F + year = 0x0F + day = 0x1F def quick_hex(bb): - return ' '.join( [ '%#04x' % x for x in bb ] ) + return " ".join(["%#04x" % x for x in bb]) + def parse_seconds(sec): - """ - simple mask - >>> parse_seconds(0x92) - 18 - """ - return sec & Mask.invert + """ + simple mask + >>> parse_seconds(0x92) + 18 + """ + return sec & Mask.invert + def parse_minutes(minutes): - """ - simple mask - >>> parse_minutes(0x9e) - 30 - """ - return minutes & Mask.invert + """ + simple mask + >>> parse_minutes(0x9e) + 30 + """ + return minutes & Mask.invert def parse_hours(hours): - """ - simple mask - >>> parse_hours(0x0b) - 11 + """ + simple mask + >>> parse_hours(0x0b) + 11 + + >>> parse_hours(0x28) + 8 - >>> parse_hours(0x28) - 8 + """ + return int(hours & 0x1F) - """ - return int(hours & 0x1f) def parse_day(day): - """ - simple mask - >>> parse_day( 0x01 ) - 1 - """ - return day & Mask.day + """ + simple mask + >>> parse_day( 0x01 ) + 1 + """ + return day & Mask.day + def parse_months(seconds, minutes): - """ - calculate from extra bits shaved off of seconds and minutes: + """ + calculate from extra bits shaved off of seconds and minutes: + + >>> parse_months( 0x92, 0x9e ) + 10 - >>> parse_months( 0x92, 0x9e ) - 10 + """ + high = (seconds & Mask.time) >> 4 + low = (minutes & Mask.time) >> 6 + return high | low - """ - high = (seconds & Mask.time) >> 4 - low = (minutes & Mask.time) >> 6 - return high | low def parse_years_lax(year): - """ - simple mask plus correction - """ - y = (year & Mask.year) + 2000 - return y + """ + simple mask plus correction + """ + y = (year & Mask.year) + 2000 + return y def extra_year_bits(year=0x86): - """ - practice getting some extra bits out of the year byte - >>> extra_year_bits( ) - [1, 0, 0, 0] - - >>> extra_year_bits(0x06) - [0, 0, 0, 0] - - >>> extra_year_bits(0x86) - [1, 0, 0, 0] - - >>> extra_year_bits(0x46) - [0, 1, 0, 0] - - >>> extra_year_bits(0x26) - [0, 0, 1, 0] - - >>> extra_year_bits(0x16) - [0, 0, 0, 1] - - """ - # year = year[0] - masks = [ ( 0x80, 7), (0x40, 6), (0x20, 5), (0x10, 4) ] - nibbles = [ ] - for mask, shift in masks: - nibbles.append( ( (year & mask) >> shift ) ) - return nibbles - + """ + practice getting some extra bits out of the year byte + >>> extra_year_bits( ) + [1, 0, 0, 0] + + >>> extra_year_bits(0x06) + [0, 0, 0, 0] + + >>> extra_year_bits(0x86) + [1, 0, 0, 0] + + >>> extra_year_bits(0x46) + [0, 1, 0, 0] + + >>> extra_year_bits(0x26) + [0, 0, 1, 0] + + >>> extra_year_bits(0x16) + [0, 0, 0, 1] + + """ + # year = year[0] + masks = [(0x80, 7), (0x40, 6), (0x20, 5), (0x10, 4)] + nibbles = [] + for mask, shift in masks: + nibbles.append((year & mask) >> shift) + return nibbles + + def extra_hour_bits(value): - """ - practice getting extra bits out of the hours bytes - - >>> extra_hour_bits(0x28) - [0, 0, 1] - - >>> extra_hour_bits(0x8) - [0, 0, 0] - """ - masks = [ ( 0x80, 7), (0x40, 6), (0x20, 5), ] - nibbles = [ ] - for mask, shift in masks: - nibbles.append( ( (value & mask) >> shift ) ) - return nibbles - + """ + practice getting extra bits out of the hours bytes + + >>> extra_hour_bits(0x28) + [0, 0, 1] + + >>> extra_hour_bits(0x8) + [0, 0, 0] + """ + masks = [ + (0x80, 7), + (0x40, 6), + (0x20, 5), + ] + nibbles = [] + for mask, shift in masks: + nibbles.append((value & mask) >> shift) + return nibbles + + def parse_years(year): - """ + """ simple mask plus correction >>> parse_years(0x06) 2006 - """ - # if year > 0x80: - # year = year - 0x80 - y = (year & Mask.year) + 2000 - # if y < 0 or y < 1999 or y > 2015: - # raise ValueError(y) - return y + """ + # if year > 0x80: + # year = year - 0x80 + y = (year & Mask.year) + 2000 + # if y < 0 or y < 1999 or y > 2015: + # raise ValueError(y) + return y + def encode_year(year): - pass + pass + def encode_monthbyte(sec=18, minute=30, month=10): - """ - >>> encode_monthbyte( ) == bytearray(b'\x92\x9e') - True + """ + >>> encode_monthbyte( ) == bytearray(bytes('\x92\x9e', 'raw_unicode_escape')) + True - >>> quick_hex(encode_monthbyte( )) - '0x92 0x9e' + >>> quick_hex(encode_monthbyte( )) + '0x92 0x9e' - >>> encode_monthbyte(sec=10) == bytearray(b'\x8a\x9e') - True + >>> encode_monthbyte(sec=10) == bytearray(bytes('\x8a\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(sec=35) == bytearray(b'\xa3\x9e') - True + >>> encode_monthbyte(sec=35) == bytearray(bytes('\xa3\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(sec=50) == bytearray(b'\xb2\x9e') - True + >>> encode_monthbyte(sec=50) == bytearray(bytes('\xb2\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=10) == bytearray(b'\x92\x8a') - True + >>> encode_monthbyte(minute=10) == bytearray(bytes('\x92\x8a', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=35) == bytearray(b'\x92\xa3') - True + >>> encode_monthbyte(minute=35) == bytearray(bytes('\x92\xa3', 'raw_unicode_escape')) + True - >>> encode_monthbyte(minute=50) == bytearray(b'\x92\xb2') - True + >>> encode_monthbyte(minute=50) == bytearray(bytes('\x92\xb2', 'raw_unicode_escape')) + True - """ + """ - encoded = [ 0x00, 0x00 ] + encoded = [0x00, 0x00] + high = (month & (0x3 << 2)) >> 2 + low = month & (0x3) - high = (month & (0x3 << 2)) >> 2 - low = month & (0x3) + encoded[0] = sec | (high << 6) + encoded[1] = minute | (low << 6) + return bytearray(encoded) - encoded[0] = sec | (high << 6) - encoded[1] = minute | (low << 6) - return bytearray( encoded ) + printf("0x%.2x 0x%.2x\n", buf[0], buf[1]) - printf("0x%.2x 0x%.2x\n", buf[0], buf[1]); def encode_minute(minute=30, month=10): - """ - >>> quick_hex(encode_minute( )) - '0x9e' + """ + >>> quick_hex(encode_minute( )) + '0x9e' + + """ + low = month & (0x3) + encoded = minute | (low << 6) + return bytearray([encoded]) - """ - low = month & (0x3) - encoded = minute | (low << 6) - return bytearray( [ encoded ] ) def encode_second(sec=18, month=10): - """ - >>> encode_second( ) == bytearray(b'\x92') - True + """ + >>> encode_second( ) == bytearray(bytes('\x92', 'raw_unicode_escape')) + True + + >>> quick_hex(encode_second( )) + '0x92' - >>> quick_hex(encode_second( )) - '0x92' + """ + high = (month & (0x3 << 2)) >> 2 + encoded = sec | (high << 6) + return bytearray([encoded]) - """ - high = (month & (0x3 << 2)) >> 2 - encoded = sec | (high << 6) - return bytearray( [ encoded ] ) -def test_time_encoders( ): - """ - >>> test_time_encoders( ) - True - """ +def test_time_encoders(): + """ + >>> test_time_encoders( ) + True + """ + + one = bytearray().join([encode_second(), encode_minute()]) + two = encode_monthbyte() + return one == two - one = bytearray().join([encode_second( ), encode_minute( )]) - two = encode_monthbyte( ) - return one == two def unmask_date(data): - """ - Extract date values from a series of bytes. - Always returns tuple given a bytearray of at least 5 bytes. + """ + Extract date values from a series of bytes. + Always returns tuple given a bytearray of at least 5 bytes. + + Returns 6-tuple of scalar values year, month, day, hours, minutes, + seconds. + >>> unmask_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) + (2006, 7, 1, 8, 23, 47) - Returns 6-tuple of scalar values year, month, day, hours, minutes, - seconds. - >>> unmask_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )) - (2006, 7, 1, 8, 23, 47) + >>> unmask_date( bytearray( [ 0x00 ] * 5 )) + (2000, 0, 0, 0, 0, 0) - >>> unmask_date( bytearray( [ 0x00 ] * 5 )) - (2000, 0, 0, 0, 0, 0) + """ + data = data[:] + seconds = parse_seconds(data[0]) + minutes = parse_minutes(data[1]) - """ - data = data[:] - seconds = parse_seconds(data[0]) - minutes = parse_minutes(data[1]) + hours = parse_hours(data[2]) + day = parse_day(data[3]) + year = parse_years(data[4]) - hours = parse_hours(data[2]) - day = parse_day(data[3]) - year = parse_years(data[4]) + month = parse_months(data[0], data[1]) + return (year, month, day, hours, minutes, seconds) - month = parse_months( data[0], data[1] ) - return (year, month, day, hours, minutes, seconds) def parse_date(date, strict=False, loose=False): - """ - Choose whether or not to throw an error if you get a bad date. - """ - try: - return parse_date_strict(date) - except NotADate, e: - if strict: - raise - if loose: - return unmask_date(data) - - return None + """ + Choose whether or not to throw an error if you get a bad date. + """ + try: + return parse_date_strict(date) + except NotADate as e: + if strict: + raise + if loose: + return unmask_date(data) -def parse_date_strict(data): - """ - Apply strict datetime validation to extract a datetime from a series - of bytes. - Always throws an error for bad dates. + return None - >>> parse_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )).isoformat( ) - '2006-07-01T08:23:47' +def parse_date_strict(data): + """ + Apply strict datetime validation to extract a datetime from a series + of bytes. + Always throws an error for bad dates. - """ - (year, month, day, hours, minutes, seconds) = unmask_date(data) - try: - date = datetime(year, month, day, hours, minutes, seconds) - return date - except ValueError, e: - raise NotADate(e) + >>> parse_date(bytearray( [ 0x6f, 0xd7, 0x08, 0x01, 0x06 ] )).isoformat( ) + '2006-07-01T08:23:47' -__test__ = { - 'unmask': ''' - These examples are fixed! Thanks ehwest, robg. - >>> unmask_date( bytearray( [ 0x93, 0xd4, 0x0e, 0x10, 0x0c ] )) - (2012, 11, 16, 14, 20, 19) + """ + (year, month, day, hours, minutes, seconds) = unmask_date(data) + try: + date = datetime(year, month, day, hours, minutes, seconds) + return date + except ValueError as e: + raise NotADate(e) - >>> unmask_date( bytearray( [ 0xa6, 0xeb, 0x0b, 0x10, 0x0c, ] )) - (2012, 11, 16, 11, 43, 38) - >>> unmask_date( bytearray( [ 0x95, 0xe8, 0x0e, 0x10, 0x0c ] )) - (2012, 11, 16, 14, 40, 21) +__test__ = { + "unmask": """ + These examples are fixed! Thanks ehwest, robg. + >>> unmask_date( bytearray( [ 0x93, 0xd4, 0x0e, 0x10, 0x0c ] )) + (2012, 11, 16, 14, 20, 19) + >>> unmask_date( bytearray( [ 0xa6, 0xeb, 0x0b, 0x10, 0x0c, ] )) + (2012, 11, 16, 11, 43, 38) - >>> unmask_date( bytearray( [ 0x80, 0xcf, 0x30, 0x10, 0x0c, ] )) - (2012, 11, 16, 16, 15, 0) + >>> unmask_date( bytearray( [ 0x95, 0xe8, 0x0e, 0x10, 0x0c ] )) + (2012, 11, 16, 14, 40, 21) - >>> unmask_date( bytearray( [ 0xa3, 0xcf, 0x30, 0x10, 0x0c, ] )) - (2012, 11, 16, 16, 15, 35) + >>> unmask_date( bytearray( [ 0x80, 0xcf, 0x30, 0x10, 0x0c, ] )) + (2012, 11, 16, 16, 15, 0) -''', - 'encode_monthbyte': ''' - >>> encode_monthbyte(month=1) == bytearray(b'\x12^') - True + >>> unmask_date( bytearray( [ 0xa3, 0xcf, 0x30, 0x10, 0x0c, ] )) + (2012, 11, 16, 16, 15, 35) - >>> encode_monthbyte(month=2) == bytearray(b'\x12\x9e') - True + """, + "encode_monthbyte": """ + >>> encode_monthbyte(month=1) == bytearray(bytes('\x12^', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=3) == bytearray(b'\x12\xde') - True + >>> encode_monthbyte(month=2) == bytearray(bytes('\x12\x9e', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=10, minute=0, sec=0) == bytearray(b'\x80\x80') - True + >>> encode_monthbyte(month=3) == bytearray(bytes('\x12\xde', 'raw_unicode_escape')) + True - >>> encode_monthbyte(month=10, minute=0, sec=24) == bytearray(b'\x98\x80') - True -''', + >>> encode_monthbyte(month=10, minute=0, sec=0) == bytearray(bytes('\x80\x80', 'raw_unicode_escape')) + True + >>> encode_monthbyte(month=10, minute=0, sec=24) == bytearray(bytes('\x98\x80', 'raw_unicode_escape')) + True + """, } -if __name__ == '__main__': - import doctest - doctest.testmod( ) +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/logs/cgm/README.markdown b/logs/cgm/README.markdown index 174fbbd..e677641 100644 --- a/logs/cgm/README.markdown +++ b/logs/cgm/README.markdown @@ -137,13 +137,13 @@ Is it possible that this is: ``` ```python ->>> print "{0:#04x} {0:08b} {0:d}".format( 0x16 ) +>>> print("{0:#04x} {0:08b} {0:d}".format( 0x16 )) 0x16 00010110 22 ->>> print "{0:#04x} {0:08b} {0:d}".format( 0x10 ) +>>> print("{0:#04x} {0:08b} {0:d}".format( 0x10 )) 0x10 00010000 16 ->>> print "{0:#04x} {0:08b} {0:d}".format( 0x4a ) +>>> print("{0:#04x} {0:08b} {0:d}".format( 0x4a )) 0x4a 01001010 74 ->>> print "{0:#04x} {0:08b} {0:d}".format( 0x0e ) +>>> print("{0:#04x} {0:08b} {0:d}".format( 0x0e )) 0x0e 00001110 14 ``` diff --git a/parser_tester.py b/parser_tester.py index 1501c45..cfbb432 100644 --- a/parser_tester.py +++ b/parser_tester.py @@ -9,50 +9,59 @@ # ################################################################################ -# inputs file name, op_code, pack_len, bin to interpret, +# inputs file name, op_code, pack_len, bin to interpret, # get the packets from the file # for each packet convert the requested bits to an integer -# +# -import sys import getopt +import sys + from find_print_op_codes import OpCodeFinder + class ParserTest: - - def main(self, packets, bin_to_read_start, bin_to_read_end, reverse): - self.packets = packets - for i in range(0, len(self.packets)): - if reverse: - packet = self.packets[len(self.packets) - i - 1] - else: - packet = self.packets[i] - bin = packet['bin_str_raw'][bin_to_read_start:bin_to_read_end] - dec = int(bin, 2) - print_str = "At "+str(packet['start_point'])+": " - print_str += "bin[" + str(bin_to_read_start) + ":" + str(bin_to_read_end) + "] " - print_str += "= " + str(dec) + " ["+packet['hex_str']+"] ["+packet['bin_str']+"]" - print print_str - - - - -if __name__ == '__main__': - fileInName = sys.argv[1] - op_code = sys.argv[2] - pack_len = int(sys.argv[3]) - reverse = False - if sys.argv[4] == "True": - reverse = True - - bin_to_read_start = int(sys.argv[5]) - bin_to_read_end = int(sys.argv[6]) - - op_code_finder = OpCodeFinder(fileInName, op_code, pack_len, reverse) - packets = op_code_finder.find() - parser = ParserTest() - parser.main(packets, bin_to_read_start, bin_to_read_end, reverse) - - + def main(self, packets, bin_to_read_start, bin_to_read_end, reverse): + self.packets = packets + for i in range(0, len(self.packets)): + if reverse: + packet = self.packets[len(self.packets) - i - 1] + else: + packet = self.packets[i] + bin = packet["bin_str_raw"][bin_to_read_start:bin_to_read_end] + dec = int(bin, 2) + print_str = "At " + str(packet["start_point"]) + ": " + print_str += ( + "bin[" + str(bin_to_read_start) + ":" + str(bin_to_read_end) + "] " + ) + print_str += ( + "= " + + str(dec) + + " [" + + packet["hex_str"] + + "] [" + + packet["bin_str"] + + "]" + ) + print(print_str) + + +if __name__ == "__main__": + fileInName = sys.argv[1] + op_code = sys.argv[2] + pack_len = int(sys.argv[3]) + reverse = False + if sys.argv[4] == "True": + reverse = True + + bin_to_read_start = int(sys.argv[5]) + bin_to_read_end = int(sys.argv[6]) + + op_code_finder = OpCodeFinder(fileInName, op_code, pack_len, reverse) + packets = op_code_finder.find() + parser = ParserTest() + parser.main(packets, bin_to_read_start, bin_to_read_end, reverse) + + ##### -# EOF \ No newline at end of file +# EOF diff --git a/set-temp.sh b/set-temp.sh index 68e3e51..8dac392 100644 --- a/set-temp.sh +++ b/set-temp.sh @@ -1,2 +1,2 @@ -sudo python bin/mm-temp-basals.py --serial 584923 --duration 30 --rate 0 --port /dev/ttyUSB0 set -#sudo python bin/mm-temp-basals.py --serial 584923 --port /dev/ttyUSB0 --duration 30 --rate 1.0 set +sudo python3 bin/mm-temp-basals.py --serial 584923 --duration 30 --rate 0 --port /dev/ttyUSB0 set +#sudo python3 bin/mm-temp-basals.py --serial 584923 --port /dev/ttyUSB0 --duration 30 --rate 1.0 set diff --git a/setup.py b/setup.py index 4ce5a58..34088dc 100644 --- a/setup.py +++ b/setup.py @@ -1,56 +1,60 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK - -from setuptools import setup, find_packages import platform +from setuptools import find_packages, setup + import decocare + def readme(): with open("README.markdown") as f: return f.read() -setup(name='decocare', - version='0.0.31', # http://semver.org/ - description='Audit, inspect, and command MM insulin pumps.', + +setup( + name="decocare", + version="0.1.0", # http://semver.org/ + description="Audit, inspect, and command MM insulin pumps.", long_description=readme(), + long_description_content_type="text/markdown", author="Ben West", author_email="bewest+insulaudit@gmail.com", url="https://github.com/openaps/decocare", - #namespace_packages = ['insulaudit'], - packages=find_packages( ), - install_requires = [ - 'pyserial', 'python-dateutil', 'argcomplete' - ], - scripts = [ - 'bin/mm-press-key.py', - 'bin/mm-send-comm.py', - 'bin/mm-set-suspend.py', - 'bin/mm-temp-basals.py', - 'bin/mm-decode-history-page.py', - 'bin/mm-latest.py', - 'bin/mm-bolus.py', - 'bin/mm-set-rtc.py', - 'bin/mm-pretty-csv', + # namespace_packages = ['insulaudit'], + packages=find_packages(), + install_requires=["pyserial", "python-dateutil", "argcomplete"], + scripts=[ + "bin/mm-press-key.py", + "bin/mm-send-comm.py", + "bin/mm-set-suspend.py", + "bin/mm-temp-basals.py", + "bin/mm-decode-history-page.py", + "bin/mm-latest.py", + "bin/mm-bolus.py", + "bin/mm-set-rtc.py", + "bin/mm-pretty-csv", ], - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering', - 'Topic :: Software Development :: Libraries' + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries", ], include_package_data=True, - package_data = { - 'decocare': ['etc/*', '*.rules' ], - # 'decocare.etc': ['*.rules' ], + package_data={ + "decocare": ["etc/*", "*.rules"], + # 'decocare.etc': ['*.rules' ], }, - data_files = [ - # commenting out for readthedocs and virtualenv users. - # ('/etc/udev/rules.d', ['decocare/etc/80-medtronic-carelink.rules'] ) + data_files=[ + # commenting out for readthedocs and virtualenv users. + # ('/etc/udev/rules.d', ['decocare/etc/80-medtronic-carelink.rules'] ) ], - zip_safe=False + zip_safe=False, ) #####