├── .gitignore
├── 80-dexcom.rules
├── Dockerfile
├── README.md
├── dexcom_receiver.py
├── dexcom_share.py
├── dexpy.json
├── dexpy.py
├── docker-compose.yml
├── docker-entrypoint.sh
├── glucose.py
├── install.sh
├── requirements.txt
└── usbreceiver
├── __init__.py
├── constants.py
├── crc16.py
├── database_records.py
├── packetwriter.py
├── readdata.py
└── util.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | .vscode/
3 | .idea/*
4 |
--------------------------------------------------------------------------------
/80-dexcom.rules:
--------------------------------------------------------------------------------
1 | #########################################################################
2 | ## This source file is from the openaps/dexcom_reader project.
3 | ##
4 | ## https://github.com/openaps/dexcom_reader
5 | ##
6 | ## It is under an MIT licence described in the 3 paragraphs below:
7 | ##
8 | ##########################################################################
9 | ##
10 | ## Permission is hereby granted, free of charge, to any person obtaining a
11 | ## copy of this software and associated documentation files (the "Software"),
12 | ## to deal in the Software without restriction, including without limitation
13 | ## the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | ## and/or sell copies of the Software, and to permit persons to whom the
15 | ## Software is furnished to do so, subject to the following conditions:
16 | ##
17 | ## The above copyright notice and this permission notice shall be included
18 | ## in all copies or substantial portions of the Software.
19 | ##
20 | ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | ## OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | ## THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | ## OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | ## ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | ## OTHER DEALINGS IN THE SOFTWARE.
27 | ##
28 | ##########################################################################
29 | #
30 | #
31 | #
32 | ## udev can only match one entry at a time, but this triggers additional add entries.
33 | # 22a3:0047
34 | # 22a3 0047
35 | ATTRS{idVendor}=="22a3", ATTRS{idProduct}=="0047", \
36 | ENV{ID_MM_DEVICE_IGNORE}="1", \
37 | MODE="0664", GROUP="plugdev"
38 |
39 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | RUN apt update -y && apt upgrade -y
4 | RUN apt install -y udev ntp
5 |
6 | WORKDIR /usr/src/app
7 | RUN python3 -m pip install pip setuptools --upgrade
8 | RUN python3 -m pip install --no-cache-dir -r requirements.txt
9 |
10 | COPY . .
11 | RUN mkdir -p /etc/udev/rules.d
12 | RUN cp /usr/src/app/80-dexcom.rules /etc/udev/rules.d/
13 | RUN chmod 755 /usr/src/app/entrypoint.sh
14 |
15 | CMD [ "./docker-entrypoint.sh" ]
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dexpy
2 | ## Why
3 | Many dexcom users get the official receiver as part of their standard kit. It is a neat little device that connects to the transmitter via bluetooth low energy and displays readings as they arrive. Unfortunately it lacks the connectivity options of the 21st century; there is no wireless, no bluetooth, no 2G/3G/4G, no nothing. On top of this, it is very sluggish to operate, has a very low quality display & touch screen and is extremely frustrating (to me personally) to unlock.
4 |
5 | Dexcom provides alternatively smartphone software, for only a limited number of "compatible" brands, which can do the same things that the receiver can and in addition use the smartphone environment to connect to online services for sharing data, analytics etc. There is however a big limitation of the dexcom provided smartphone software: You can only connect one smartphone to read the data from the CGM transmitter. That means increasing coverage for your CGM is not really an option by using multiple phones and the dexcom software.
6 |
7 | This limitation however does not apply when using the receiver, the smartphone and the receiver can both read data at the same time.
8 |
9 | ## What
10 | The purpose of this software is to make the receiver more useful by giving the users the option to connect and integrate it into other systems. It also provides an option to read data additionally from dexcom's own servers, if you happen to use the smartphone option, so you can consolidate data coming from two receivers in a single place.
11 |
12 | ## How
13 | In order to transfer the readings in real-time, you need to connect the receiver via usb to a computer where you run this software. It could be for example a raspberry pi (small and mobile) or just any computer in a fixed spot (say, livingroom?).
14 |
15 | # Features
16 | ## It can:
17 | - .. read sensor data directly from the dexcom receiver
18 | - .. read sensor data online from the dexcom share server (sharing must be enabled with the official dexcom app)
19 | - .. publish sensor data to a Nightscout instance
20 | - .. publish sensor data to an MQTT server
21 | - .. publish sensor data to an InfluxDB server
22 | - .. automatically transmit back-fill data as it becomes available
23 | ## It cannot:
24 | - Update data on the dexcom share server with receiver readings
25 | ## It will not:
26 | - Update data on the dexcom share server, because the hours long outage on new years eve of 2019 was simply unacceptable. (and it happened again in 2020 -so unexpectedly)
27 |
28 | # Setup
29 | * Download dexpy to your (preferably) debian based installation
30 |
31 | ```
32 | sudo apt install -y git
33 | git clone https://github.com/winemug/dexpy.git
34 | cd dexpy
35 | ```
36 |
37 | * Edit the configuration:
38 | ```
39 | nano dexpy.json
40 | ```
41 |
42 | Configuration example:
43 | ```
44 | {
45 | "USB_RECEIVER": true,
46 | "DEXCOM_SHARE_SERVER": "eu",
47 | "DEXCOM_SHARE_USERNAME": "username",
48 | "DEXCOM_SHARE_PASSWORD": "password",
49 | "MQTT_SERVER": "mqtt.myserver.example",
50 | "MQTT_PORT": 1883,
51 | "MQTT_SSL": false,
52 | "MQTT_CLIENTID": "dexpy-mqtt-client",
53 | "MQTT_TOPIC": "cgm",
54 | "INFLUXDB_SERVER": "influxdb.myserver.example",
55 | "INFLUXDB_PORT": 8086,
56 | "INFLUXDB_SSL": false,
57 | "INFLUXDB_SSL_VERIFY": false,
58 | "INFLUXDB_USERNAME": "username",
59 | "INFLUXDB_PASSWORD": "password",
60 | "INFLUXDB_DATABASE": "dexpy",
61 | "INFLUXDB_MEASUREMENT": "bg",
62 | "NIGHTSCOUT_URL": "https://nightscout.myserver.example",
63 | "NIGHTSCOUT_SECRET": null,
64 | "NIGHTSCOUT_TOKEN": "ns-yadayadayada"
65 | }
66 | ```
67 |
68 | * Run the install script that registers the usb device driver, downloads dependencies and starts dexpy as a systemd service
69 | ```
70 | sudo ./install.sh
71 | ```
72 |
73 | ### Reading from Dexcom Receiver via USB
74 | **USB_RECEIVER**: _true_ to enable reading from the receiver, otherwise _false_
75 |
76 | ### Reading from Dexcom Share online
77 | **DEXCOM_SHARE_SERVER**: "us" or "eu" based on your location, set to _null_ if you don't store your data in dexcom's cloud.
78 | **DEXCOM_SHARE_USERNAME**: Username for your dexcom share account.
79 | **DEXCOM_SHARE_PASSWORD**: Password for your dexcom share account.
80 |
81 | ### Sending data to an MQTT server
82 | **MQTT_SERVER**: Hostname for an MQTT server to post received glucose values or set to _null_ if not using mqtt
83 | **MQTT_PORT**: Port number for the mqtt server
84 | **MQTT_SSL**: _true_ if you're using ssl, otherwise _false_
85 | **MQTT_TOPIC**: Full name of the topic to post messages to
86 |
87 | ### Writing data to an Influx database
88 | **INFLUXDB_SERVER**: Hostname for your influxdb server or _null_ if not using influxdb
89 | **INFLUXDB_PORT**: Port for the http interface to your influxdb server
90 | **INFLUXDB_SSL**: _true_ if you're using ssl, otherwise _false_
91 | **INFLUXDB_SSL_VERIFY**: _true_ to enable certificate verification, otherwise _false_ (e.g. self-signed certificates)
92 |
93 | ### Sending data to a Nightscout instance
94 | **NIGHTSCOUT_URL**: Full url of your nightscout website (only root, no api links etc, i.e. https://mynightscout.azureblabla.local/) _null_ if not using nightscout.
95 | **NIGHTSCOUT_SECRET**: Password (the 12 character passphrase) used to access nightscout or if you're using a token, set to _null_
96 | **NIGHTSCOUT_TOKEN**: Enter the token you've generated using nightscout or if you're using the nightscout-secret option, set to _null_.
97 |
98 | Note: If you enable the "Dexcom Share Server" option, dexpy will read cgm data from dexcom's servers (whether it's available on the receiver or not) and publish it to other services you have configured. This is useful if you're using the Dexcom app on a phone to connect to the transmitter but want your data consolidated elsewhere.
99 |
100 | ## Run with docker (experimental)
101 | * Command line (to be described)
102 | ```
103 | docker pull wynmug/dexpy
104 | ```
105 | * Using docker-compose (to be described)
106 | ```
107 | dexpy:
108 | image: wynmug/dexpy
109 | restart: always
110 | container_name: dexpy
111 | environment:
112 | - INFLUXDB_SERVER=influxdb
113 | - INFLUXDB_PORT=8086
114 | - INFLUXDB_USERNAME=username
115 | - INFLUXDB_PASSWORD=pwd
116 | - INFLUXDB_DATABASE=db
117 | - MQTT_SERVER=
118 | - MQTT_PORT=
119 | - MQTT_SSL=
120 | - MQTT_CLIENTID=
121 | - MQTT_TOPIC=
122 | ```
123 |
124 | # Acknowledgements
125 |
126 | Dexcom Share protocol is implemented according to the [reverse engineering](https://gist.github.com/StephenBlackWasAlreadyTaken/adb0525344bedade1e25) performed by github user [StephenBlackWasAlreadyTaken](https://gist.github.com/StephenBlackWasAlreadyTaken)
127 |
128 | Dexcom Receiver code for communicating with the receiver via USB is borrowed from the [dexctrack](https://github.com/DexcTrack/dexctrack) project, which in turn is based on the [dexcom_reader](https://github.com/openaps/dexcom_reader) project. Further enhanced to support Dexcom G6 receiver backfill.
129 |
--------------------------------------------------------------------------------
/dexcom_receiver.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | from glucose import GlucoseValue
4 | import threading
5 | import logging
6 |
7 | from usbreceiver import constants
8 | from usbreceiver.readdata import Dexcom
9 |
10 |
11 | class DexcomReceiverSession():
12 | def __init__(self, callback, usb_reset_cmd = None):
13 | self.logger = logging.getLogger('DEXPY')
14 | self.callback = callback
15 | self.device = None
16 | self.timer = None
17 | self.lock = threading.RLock()
18 | self.initial_backfill_executed = False
19 | self.last_gv = None
20 | self.system_time_offset = None
21 | self.usb_reset_cmd = usb_reset_cmd
22 | self.ts_usb_reset = time.time() + 360
23 |
24 | def start_monitoring(self):
25 | self.on_timer()
26 |
27 | def on_timer(self):
28 | with self.lock:
29 | if not self.ensure_connected():
30 | self.set_timer(15)
31 | elif self.read_glucose_values():
32 | self.ts_usb_reset = time.time() + 360
33 | self.set_timer(30)
34 | else:
35 | if self.usb_reset_cmd is not None:
36 | ts_now = time.time()
37 | if ts_now > self.ts_usb_reset:
38 | self.logger.debug('performing usb reset')
39 | os.system(self.usb_reset_cmd)
40 | ts_now = time.time()
41 | self.ts_usb_reset = ts_now + 360
42 | self.set_timer(10)
43 |
44 | def ensure_connected(self):
45 | try:
46 | if self.device is None:
47 | port = Dexcom.FindDevice()
48 | if port is None:
49 | self.logger.warning("Dexcom receiver not found")
50 | return False
51 | else:
52 | self.device = Dexcom(port)
53 | self.system_time_offset = self.get_device_time_offset()
54 | return True
55 | except Exception as e:
56 | self.logger.warning("Error reading from usb device\n" + str(e))
57 | self.device = None
58 | self.system_time_offset = None
59 | return False
60 |
61 | def set_timer(self, seconds):
62 | self.timer = threading.Timer(seconds, self.on_timer)
63 | self.timer.setDaemon(True)
64 | self.logger.debug("timer set to %d seconds" % seconds)
65 | self.timer.start()
66 |
67 | def stop_monitoring(self):
68 | with self.lock:
69 | self.timer.cancel()
70 |
71 | def read_glucose_values(self, ts_cut_off: float = None):
72 | try:
73 | if ts_cut_off is None:
74 | if self.initial_backfill_executed:
75 | ts_cut_off = time.time() - 3 * 60 * 60
76 | else:
77 | ts_cut_off = time.time() - 24 * 60 * 60
78 |
79 | records = self.device.iter_records('EGV_DATA')
80 | new_value_received = False
81 |
82 | for rec in records:
83 | if not rec.display_only:
84 | gv = self._as_gv(rec)
85 | if self.last_gv is None or self.last_gv.st != gv.st:
86 | self.last_gv = gv
87 | new_value_received = True
88 | self.callback([gv])
89 | break
90 |
91 | if new_value_received:
92 | for rec in records:
93 | if not rec.display_only:
94 | gv = self._as_gv(rec)
95 | if gv.st >= ts_cut_off:
96 | self.callback([gv])
97 | else:
98 | break
99 |
100 | for rec in self.device.iter_records('BACKFILLED_EGV'):
101 | if not rec.display_only:
102 | gv = self._as_gv(rec)
103 | if gv.st >= ts_cut_off:
104 | self.callback([gv])
105 | else:
106 | break
107 |
108 | self.initial_backfill_executed = True
109 | return new_value_received
110 | except Exception as e:
111 | self.logger.warning("Error reading from usb device\n" + str(e))
112 | return False
113 |
114 | def get_device_time_offset(self):
115 | now_time = time.time()
116 | device_time = self.device.ReadSystemTime()
117 | return now_time - device_time
118 |
119 | def _as_gv(self, record):
120 | st = record.meter_time + self.system_time_offset
121 | direction = record.full_trend & constants.EGV_TREND_ARROW_MASK
122 | return GlucoseValue(None, None, st, record.glucose, direction)
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/dexcom_share.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import threading
3 | import requests
4 | import json
5 | from glucose import GlucoseValue
6 | import time
7 |
8 |
9 | # Dexcom Share API credits:
10 | # https://gist.github.com/StephenBlackWasAlreadyTaken/adb0525344bedade1e25
11 |
12 | class DexcomShareSession():
13 | def __init__(self, location, username, password, callback):
14 | self.logger = logging.getLogger('DEXPY')
15 | self.callback = callback
16 |
17 | if location == "us":
18 | self.address = "share2.dexcom.com"
19 | elif location == "eu":
20 | self.address = "shareous1.dexcom.com"
21 | else:
22 | raise ValueError("Unknown location type")
23 |
24 | self.username = username
25 | self.password = password
26 | self.session = None
27 | self.dexcom_session_id = None
28 |
29 | self.lock = threading.RLock()
30 | self.timer = None
31 | self.initial_backfill_executed = False
32 | self.gvs = []
33 |
34 | def start_monitoring(self):
35 | self.session = requests.Session()
36 | self.logger.info("started dexcom share client")
37 | self.on_timer()
38 |
39 | def stop_monitoring(self):
40 | with self.lock:
41 | if self.timer is not None:
42 | self.timer.cancel()
43 | self.timer = None
44 |
45 | self.session.close()
46 |
47 | def on_timer(self):
48 | with self.lock:
49 | request_wait = self.perform_request()
50 | self.logger.debug("next request in %d seconds" % request_wait)
51 | self.timer = threading.Timer(request_wait, self.on_timer)
52 | self.timer.setDaemon(True)
53 | self.timer.start()
54 |
55 | def perform_request(self) -> float:
56 | if self.dexcom_session_id is None:
57 | self.login()
58 |
59 | if self.dexcom_session_id is None:
60 | return 30
61 |
62 | self.logger.debug("Requesting glucose value")
63 | gv = self.get_last_gv()
64 | if gv is None:
65 | self.logger.warning("Received no glucose value")
66 | else:
67 | if len(self.gvs) == 0 or self.gvs[-1].__ne__(gv):
68 | self.gvs.append(gv)
69 | self.callback([gv])
70 | self.backfill()
71 | if gv is None:
72 | return 60
73 |
74 | time_since = time.time() - gv.st
75 | g6_phase = time_since % 300
76 | if time_since < 330:
77 | return 330 - time_since
78 | elif g6_phase < 90:
79 | return 15
80 | else:
81 | return 330 - g6_phase
82 |
83 | def backfill(self):
84 | cut_off = time.time() - 3 * 60 * 60 - 5 * 60
85 | self.gvs = [gv for gv in self.gvs if gv.st > cut_off]
86 |
87 | if self.initial_backfill_executed and len(self.gvs) >= 36:
88 | return
89 |
90 | if self.initial_backfill_executed:
91 | self.logger.info("Missing measurements within the last 3 hours, attempting to backfill..")
92 | gvs = self.get_gvs(180, 40)
93 | else:
94 | self.logger.info("Executing initial backfill with the last 24 hours of data..")
95 | gvs = self.get_gvs(1440, 300)
96 |
97 | if gvs is None:
98 | self.logger.warning("No data received")
99 | return
100 |
101 | self.initial_backfill_executed = True
102 | self.logger.debug("Received %d glucose values from history" % len(gvs))
103 | self.callback(gvs)
104 | self.gvs = gvs
105 |
106 | def login(self):
107 | url = "https://%s/ShareWebServices/Services/General/LoginPublisherAccountByName" % self.address
108 | headers = {"Accept": "application/json",
109 | "Content-Type": "application/json",
110 | "User-Agent": "Dexcom Share/3.0.2.11 CFNetwork/711.2.23 Darwin/14.0.0"}
111 | payload = {"accountName": self.username,
112 | "password": self.password,
113 | "applicationId": "d8665ade-9673-4e27-9ff6-92db4ce13d13"}
114 |
115 | self.logger.debug("Attempting to login")
116 | result = None
117 | try:
118 | result = self.session.post(url, data=json.dumps(payload), headers=headers)
119 | except Exception as e:
120 | self.logger.error(e)
121 |
122 | if result is None or result.status_code != 200:
123 | self.recreate_session()
124 | self.logger.error("Login failed")
125 | else:
126 | self.dexcom_session_id = result.text[1:-1]
127 | self.logger.info("Login successful, session id: %s" % self.dexcom_session_id)
128 |
129 | def recreate_session(self):
130 | try:
131 | self.session.close()
132 | except Exception as ex:
133 | self.logger.warning("Error while closing session", exc_info=ex)
134 | self.session = requests.Session()
135 |
136 | def get_gvs(self, minutes, maxCount):
137 | url = "https://%s/ShareWebServices/Services/Publisher/ReadPublisherLatestGlucoseValues" % self.address
138 | url += "?sessionId=%s&minutes=%d&maxCount=%d" % (self.dexcom_session_id, minutes, maxCount)
139 | headers = {"Accept": "application/json",
140 | "User-Agent": "Dexcom Share/3.0.2.11 CFNetwork/711.2.23 Darwin/14.0.0"}
141 | result = None
142 | try:
143 | result = self.session.post(url, headers=headers)
144 | except Exception as ex:
145 | self.logger.error(exc_info=ex)
146 |
147 | gvs = []
148 | if result is not None and result.status_code == 200:
149 | for jsonResult in result.json():
150 | gvs.append(GlucoseValue.from_json(jsonResult))
151 | return gvs
152 | else:
153 | self.recreate_session()
154 | return None
155 |
156 | def get_last_gv(self):
157 | r = self.get_gvs(1440, 1)
158 | if r is not None and len(r) > 0:
159 | return r[0]
160 | return None
161 |
--------------------------------------------------------------------------------
/dexpy.json:
--------------------------------------------------------------------------------
1 | {
2 | "USB_RECEIVER": true,
3 | "DEXCOM_SHARE_SERVER": "eu",
4 | "DEXCOM_SHARE_USERNAME": "username",
5 | "DEXCOM_SHARE_PASSWORD": "password",
6 | "MQTT_SERVER": null,
7 | "MQTT_PORT": null,
8 | "MQTT_SSL": false,
9 | "MQTT_CLIENTID": "",
10 | "MQTT_TOPIC": "",
11 | "INFLUXDB_SERVER": null,
12 | "INFLUXDB_PORT": 8086,
13 | "INFLUXDB_SSL": true,
14 | "INFLUXDB_SSL_VERIFY": true,
15 | "INFLUXDB_USERNAME": "",
16 | "INFLUXDB_PASSWORD": "",
17 | "INFLUXDB_DATABASE": "dexpy",
18 | "INFLUXDB_MEASUREMENT": "bg",
19 | "NIGHTSCOUT_URL": null,
20 | "NIGHTSCOUT_SECRET": null,
21 | "NIGHTSCOUT_TOKEN": null
22 | }
--------------------------------------------------------------------------------
/dexpy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | import argparse
3 | import bisect
4 | import datetime as dt
5 | import logging
6 | import signal
7 | import sqlite3
8 | import ssl
9 | import threading
10 | from queue import Queue, Empty
11 |
12 | import paho.mqtt.client as mqttc
13 | import requests
14 | import simplejson as json
15 | from influxdb import InfluxDBClient
16 | from paho.mqtt.client import MQTTv311
17 |
18 | from dexcom_receiver import DexcomReceiverSession
19 | from dexcom_share import DexcomShareSession
20 | import os
21 | import distro
22 |
23 | class DexPy:
24 | def __init__(self, args):
25 | self.logger = logging.getLogger('DEXPY')
26 |
27 | self.args = args
28 | self.exit_event = threading.Event()
29 | self.message_published_event = threading.Event()
30 |
31 | self.initialize_db()
32 | self.mqtt_client = None
33 | if args.MQTT_SERVER is not None:
34 | self.mqtt_client = mqttc.Client(client_id=args.MQTT_CLIENTID, clean_session=True, protocol=MQTTv311,
35 | transport="tcp")
36 |
37 | if args.MQTT_SSL != "":
38 | self.mqtt_client.tls_set(certfile=None,
39 | keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
40 | tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
41 | self.mqtt_client.tls_insecure_set(True)
42 |
43 | self.mqtt_client.on_connect = self.on_mqtt_connect
44 | self.mqtt_client.on_disconnect = self.on_mqtt_disconnect
45 | self.mqtt_client.on_message = self.on_mqtt_message_receive
46 | self.mqtt_client.on_publish = self.on_mqtt_message_publish
47 |
48 | self.influx_client = None
49 | if self.args.INFLUXDB_SERVER is not None:
50 | self.influx_client = InfluxDBClient(self.args.INFLUXDB_SERVER, self.args.INFLUXDB_PORT, self.args.INFLUXDB_USERNAME,
51 | self.args.INFLUXDB_PASSWORD, self.args.INFLUXDB_DATABASE,
52 | ssl=self.args.INFLUXDB_SSL, verify_ssl=self.args.INFLUXDB_SSL_VERIFY)
53 |
54 | self.callback_queue = Queue()
55 | self.glucose_values = []
56 | self.mqtt_pending = {}
57 | self.influx_pending = []
58 | self.ns_pending = []
59 |
60 | self.ns_session = None
61 | if self.args.NIGHTSCOUT_URL is not None:
62 | self.ns_session = requests.Session()
63 |
64 | self.dexcom_share_session = None
65 | if self.args.DEXCOM_SHARE_SERVER is not None:
66 | self.logger.info("starting dexcom share session")
67 | self.dexcom_share_session = DexcomShareSession(self.args.DEXCOM_SHARE_SERVER,
68 | self.args.DEXCOM_SHARE_USERNAME,
69 | self.args.DEXCOM_SHARE_PASSWORD,
70 | self.glucose_values_received)
71 |
72 | self.dexcom_receiver_session = None
73 | if self.args.USB_RECEIVER is not None and self.args.USB_RECEIVER:
74 | self.dexcom_receiver_session = DexcomReceiverSession(self.glucose_values_received, self.args.USB_RESET_COMMAND)
75 |
76 | for sig in ('HUP', 'INT'):
77 | signal.signal(getattr(signal, 'SIG' + sig), lambda _0, _1: self.exit_event.set())
78 |
79 | def run(self):
80 | if self.mqtt_client is not None:
81 | self.logger.info("starting mqtt service connection")
82 | self.mqtt_client.reconnect_delay_set(min_delay=15, max_delay=120)
83 | self.mqtt_client.connect_async(self.args.MQTT_SERVER, port=self.args.MQTT_PORT, keepalive=60)
84 | self.mqtt_client.retry_first_connection = True
85 | self.mqtt_client.loop_start()
86 |
87 | if self.dexcom_share_session is not None:
88 | self.logger.info("starting monitoring dexcom share server")
89 | self.dexcom_share_session.start_monitoring()
90 |
91 | if self.dexcom_receiver_session is not None:
92 | self.logger.info("starting usb receiver service")
93 | self.dexcom_receiver_session.start_monitoring()
94 |
95 | queue_thread = threading.Thread(target=self.queue_handler)
96 | queue_thread.start()
97 |
98 | try:
99 | while not self.exit_event.wait(timeout=1000):
100 | pass
101 | except KeyboardInterrupt:
102 | pass
103 |
104 | self.exit_event.clear()
105 | if self.dexcom_receiver_session is not None:
106 | self.logger.info("stopping dexcom receiver service")
107 | self.dexcom_receiver_session.stop_monitoring()
108 |
109 | if self.dexcom_share_session is not None:
110 | self.logger.info("stopping listening on dexcom share server")
111 | self.dexcom_share_session.stop_monitoring()
112 |
113 | if self.mqtt_client is not None:
114 | self.logger.info("stopping mqtt client")
115 | self.mqtt_client.loop_stop()
116 | self.mqtt_client.disconnect()
117 |
118 | if self.influx_client is not None:
119 | self.logger.info("closing influxdb client")
120 | self.influx_client.close()
121 |
122 | if self.ns_session is not None:
123 | self.logger.info("closing nightscout session")
124 | self.ns_session.close()
125 |
126 | def on_mqtt_connect(self, client, userdata, flags, rc):
127 | self.logger.info("Connected to mqtt server with result code " + str(rc))
128 | self.logger.debug("Pending %d messages in local queue" % len(self.mqtt_pending))
129 |
130 | def on_mqtt_disconnect(self, client, userdata, rc):
131 | self.logger.info("Disconnected from mqtt with result code " + str(rc))
132 | self.logger.debug("Pending %d messages in local queue" % len(self.mqtt_pending))
133 |
134 | def on_mqtt_message_receive(self, client, userdata, msg):
135 | self.logger.info("mqtt message received: " + msg)
136 |
137 | def on_mqtt_message_publish(self, client, userdata, msg_id):
138 | self.logger.info("mqtt message published: " + str(msg_id))
139 | if msg_id in self.mqtt_pending:
140 | self.mqtt_pending.pop(msg_id)
141 | else:
142 | self.logger.debug("unknown message id: " + str(msg_id))
143 | self.logger.debug("Pending %d messages in local queue" % len(self.mqtt_pending))
144 |
145 | def glucose_values_received(self, gvs):
146 | for gv in gvs:
147 | self.callback_queue.put(gv)
148 |
149 | def queue_handler(self):
150 | while not self.exit_event.wait(timeout=0.200):
151 | gvs = []
152 | while True:
153 | try:
154 | gv = self.callback_queue.get(block=True, timeout=5)
155 | gvs.append(gv)
156 | except Empty:
157 | if len(gvs) > 0:
158 | self.process_glucose_values(gvs)
159 | gvs = []
160 |
161 | def process_glucose_values(self, gvs):
162 | new_values = []
163 | for gv in gvs:
164 | new_val = True
165 | for gv_check in self.glucose_values:
166 | if gv_check == gv:
167 | new_val = False
168 | break
169 | if new_val:
170 | new_values.append(gv)
171 | self.logger.info(f"New gv: {gv}")
172 |
173 | if self.mqtt_client is not None:
174 | for gv in new_values:
175 | msg = "%d|%s|%s" % (gv.st, gv.trend, gv.value)
176 | x, mid = self.mqtt_client.publish(self.args.MQTT_TOPIC, payload=msg, qos=1)
177 | self.mqtt_pending[mid] = gv
178 | self.logger.debug("publish to mqtt requested with message id: " + str(mid))
179 |
180 | if self.influx_client is not None:
181 | for gv in new_values:
182 | point = {
183 | "measurement": self.args.INFLUXDB_MEASUREMENT,
184 | "tags": {"device": "dexcomg6", "source": "dexpy"},
185 | "time": dt.datetime.utcfromtimestamp(gv.st).strftime("%Y-%m-%dT%H:%M:%SZ"),
186 | "fields": {"cbg": float(gv.value), "direction": int(gv.trend)}
187 | }
188 | self.influx_pending.append(point)
189 | try:
190 | if self.influx_client.write_points(self.influx_pending):
191 | self.influx_pending = []
192 | except Exception as ex:
193 | self.logger.error("Error writing to influxdb", exc_info=ex)
194 |
195 | if self.ns_session is not None:
196 | apiUrl = self.args.NIGHTSCOUT_URL
197 | if apiUrl[-1] != "/":
198 | apiUrl += "/"
199 | apiUrl += "api/v1/entries/"
200 | headers = {"Content-Type": "application/json"}
201 | if self.args.NIGHTSCOUT_SECRET:
202 | headers["api-secret"] = self.args.NIGHTSCOUT_SECRET
203 | if self.args.NIGHTSCOUT_TOKEN:
204 | apiUrl += "?token=" + self.args.NIGHTSCOUT_TOKEN
205 |
206 | for gv in new_values:
207 | payload = {"sgv": gv.value, "type": "sgv", "direction": gv.trend_string(), "date": gv.st * 1000}
208 | self.ns_pending.append(json.dumps(payload))
209 | posted_entries = []
210 | for pendingEntry in self.ns_pending:
211 | try:
212 | response = self.ns_session.post(apiUrl, headers=headers, data=pendingEntry)
213 | if response is not None and response.status_code == 200:
214 | posted_entries.append(pendingEntry)
215 | else:
216 | self.logger.error(f"NS server returned invalid response {response}")
217 | except Exception as ex:
218 | self.logger.error("Error posting value to nightscout", ex)
219 | for posted_entry in posted_entries:
220 | self.ns_pending.remove(posted_entry)
221 |
222 | for gv in new_values:
223 | i = bisect.bisect_right(self.glucose_values, gv)
224 | self.glucose_values.insert(i + 1, gv)
225 |
226 | if len(self.glucose_values) > 4096:
227 | self.glucose_values = self.glucose_values[4096 - len(self.glucose_values):]
228 |
229 | def initialize_db(self):
230 | try:
231 | with sqlite3.connect(self.args.DB_PATH) as conn:
232 | sql = """ CREATE TABLE IF NOT EXISTS gv (
233 | ts REAL,
234 | gv REAL,
235 | trend TEXT
236 | ) """
237 | conn.execute(sql)
238 |
239 | sql = """ CREATE INDEX "idx_ts" ON "gv" ("ts");"""
240 | try:
241 | conn.execute(sql)
242 | except:
243 | self.logger.debug("Index creation skipped")
244 |
245 | except Exception as ex:
246 | self.logger.warning("Error initializing local db", exc_info=ex)
247 |
248 |
249 | if __name__ == '__main__':
250 | logger = logging.getLogger('DEXPY')
251 | logger.setLevel(logging.DEBUG)
252 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
253 |
254 | ch = logging.StreamHandler()
255 | ch.setLevel(logging.DEBUG)
256 | ch.setFormatter(formatter)
257 | logger.addHandler(ch)
258 |
259 | parser = argparse.ArgumentParser()
260 | parser.add_argument("--CONFIGURATION", required=False, default=None, nargs="?")
261 | parser.add_argument("--DEXCOM-SHARE-SERVER", required=False, default=None, nargs="?")
262 | parser.add_argument("--DEXCOM-SHARE-USERNAME", required=False, default="", nargs="?")
263 | parser.add_argument("--DEXCOM-SHARE-PASSWORD", required=False, default="", nargs="?")
264 | parser.add_argument("--MQTT-SERVER", required=False, default=None, nargs="?")
265 | parser.add_argument("--MQTT-PORT", required=False, default="1881", nargs="?")
266 | parser.add_argument("--MQTT-SSL", required=False, default="", nargs="?")
267 | parser.add_argument("--MQTT-CLIENTID", required=False, default="dexpy", nargs="?")
268 | parser.add_argument("--MQTT-TOPIC", required=False, default="cgm", nargs="?")
269 | parser.add_argument("--INFLUXDB-SERVER", required=False, default=None, nargs="?")
270 | parser.add_argument("--INFLUXDB-PORT", required=False, default="8086", nargs="?")
271 | parser.add_argument("--INFLUXDB-SSL", required=False, default=False, nargs="?")
272 | parser.add_argument("--INFLUXDB-SSL-VERIFY", required=False, default=False, nargs="?")
273 | parser.add_argument("--INFLUXDB-USERNAME", required=False, default="", nargs="?")
274 | parser.add_argument("--INFLUXDB-PASSWORD", required=False, default="", nargs="?")
275 | parser.add_argument("--INFLUXDB-DATABASE", required=False, default="", nargs="?")
276 | parser.add_argument("--INFLUXDB-MEASUREMENT", required=False, default="", nargs="?")
277 | parser.add_argument("--NIGHTSCOUT-URL", required=False, default=None, nargs="?")
278 | parser.add_argument("--NIGHTSCOUT-SECRET", required=False, default=None, nargs="?")
279 | parser.add_argument("--NIGHTSCOUT-TOKEN", required=False, default=None, nargs="?")
280 | parser.add_argument("--DB-PATH", required=False, default="dexpy.db", nargs="?")
281 | parser.add_argument("--USB-RECEIVER", required=False, default=True, nargs="?")
282 | parser.add_argument("--USB-RESET-COMMAND", required=False, default=None, nargs="?")
283 |
284 | args = parser.parse_args()
285 |
286 | if args.CONFIGURATION is not None:
287 | with open(args.CONFIGURATION, 'r') as stream:
288 | js = json.load(stream)
289 |
290 | for js_arg in js:
291 | args.__dict__[js_arg] = js[js_arg]
292 |
293 | dexpy = DexPy(args)
294 | dexpy.run()
295 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.5"
2 | services:
3 | dexpy:
4 | build: ./
5 | image: dexpy:latest
6 | privileged: True
7 | volumes:
8 | - /dev/bus/usb:/dev/bus/usb
9 | environment:
10 | - DEXCOM_SHARE_SERVER=
11 | - DEXCOM_SHARE_USERNAME=
12 | - DEXCOM_SHARE_PASSWORD=
13 | - MQTT_SERVER=
14 | - MQTT_PORT=
15 | - MQTT_SSL=
16 | - MQTT_CLIENTID=
17 | - MQTT_TOPIC=
18 | - INFLUXDB_SERVER=
19 | - INFLUXDB_PORT=
20 | - INFLUXDB_SSL=
21 | - INFLUXDB_USERNAME=
22 | - INFLUXDB_PASSWORD=
23 | - INFLUXDB_DATABASE=
24 | - NIGHTSCOUT_URL=
25 | - NIGHTSCOUT_SECRET=
26 | - NIGHTSCOUT_TOKEN=
27 | - LOG_LEVEL=DEBUG
28 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | python3 dexpy.py \
2 | --DEXCOM-SHARE-SERVER $DEXCOM_SHARE_SERVER \
3 | --DEXCOM-SHARE-USERNAME $DEXCOM_SHARE_USERNAME \
4 | --DEXCOM-SHARE-PASSWORD $DEXCOM_SHARE_PASSWORD \
5 | --MQTT-SERVER $MQTT_SERVER \
6 | --MQTT-PORT $MQTT_PORT \
7 | --MQTT-SSL $MQTT_SSL \
8 | --MQTT-CLIENTID $MQTT_CLIENTID \
9 | --MQTT-TOPIC $MQTT_TOPIC \
10 | --INFLUXDB-SERVER $INFLUXDB_SERVER \
11 | --INFLUXDB-PORT $INFLUXDB_PORT \
12 | --INFLUXDB-SSL $INFLUXDB_SSL \
13 | --INFLUXDB-USERNAME $INFLUXDB_USERNAME \
14 | --INFLUXDB-PASSWORD $INFLUXDB_PASSWORD \
15 | --INFLUXDB-DATABASE $INFLUXDB_DATABASE \
16 | --NIGHTSCOUT-URL $NIGHTSCOUT_URL \
17 | --NIGHTSCOUT-SECRET $NIGHTSCOUT_SECRET \
18 | --NIGHTSCOUT-TOKEN $NIGHTSCOUT_TOKEN \
19 | --LOG-LEVEL $LOG_LEVEL
20 |
--------------------------------------------------------------------------------
/glucose.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import re
3 |
4 | NightscoutTrendStrings = ['None', 'DoubleUp', 'SingleUp', 'FortyFiveUp', 'Flat', 'FortyFiveDown', 'SingleDown', 'DoubleDown', 'NotComputable', 'OutOfRange']
5 |
6 |
7 | def _as_ts(val):
8 | res = re.search("Date\\((\\d*)", val)
9 | return float(res.group(1)) / 1000
10 |
11 |
12 | class GlucoseValue():
13 | def __init__(self, dt, wt, st, value, trend):
14 | self.logger = logging.getLogger('DEXPY')
15 | self.dt = dt
16 | self.wt = wt
17 | self.st = st
18 | self.value = value
19 | self.trend = trend
20 |
21 | def trend_string(self):
22 | return NightscoutTrendStrings[self.trend]
23 |
24 | @staticmethod
25 | def from_json(jsonResponse, timeoffset=0):
26 | dt = _as_ts(jsonResponse["DT"]) + timeoffset
27 | wt = _as_ts(jsonResponse["WT"]) + timeoffset
28 | st = _as_ts(jsonResponse["ST"]) + timeoffset
29 | value = float(jsonResponse["Value"])
30 | trend = int(jsonResponse["Trend"])
31 | return GlucoseValue(dt, wt, st, value, trend)
32 |
33 | def __eq__(self, other):
34 | return self.same_ts(other) and self.same_val(other)
35 |
36 | def __ne__(self, other):
37 | return not self.__eq__(other)
38 |
39 | def __gt__(self, other):
40 | return self.__ne__(other) and self.st > other.st
41 |
42 | def __lt__(self, other):
43 | return self.__ne__(other) and self.st < other.st
44 |
45 | def __ge__(self, other):
46 | return self.__eq__(other) or self.st > other.st
47 |
48 | def __le__(self, other):
49 | return self.__eq__(other) or self.st < other.st
50 |
51 | def same_ts(self, other):
52 | seconds_diff = self.st - other.st
53 | return abs(seconds_diff) < 240
54 |
55 | def same_val(self, other):
56 | return int(round(self.value)) == int(round(other.value))
57 |
58 | def equals(self, other):
59 | seconds_difference = abs((self.st - other.st))
60 | if seconds_difference >= 240:
61 | return False
62 | if self.trend != other.trend:
63 | return False
64 | if int(round(self.value)) != int(round(other.value)):
65 | return False
66 |
67 | return True
68 |
69 | def __str__(self):
70 | return "DT: %s WT: %s ST: %s Trend: %s Value: %f" % (self.dt, self.wt, self.st, self.trend_string(), self.value)
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | apt update
3 | apt install -y udev python3 python3-pip python3-venv
4 | python3 -m venv venv
5 | ./venv/bin/python3 -m pip install pip setuptools --upgrade
6 | ./venv/bin/python3 -m pip install -r requirements.txt --upgrade
7 | mkdir -p /etc/udev/rules.d
8 | cp 80-dexcom.rules /etc/udev/rules.d/
9 | echo "
10 | [Unit]
11 | Description=Dexpy
12 | After=network.target
13 |
14 | [Service]
15 | ExecStart=$(pwd)/venv/bin/python3 dexpy.py --CONFIGURATION dexpy.json
16 | WorkingDirectory=$(pwd)
17 | StandardOutput=inherit
18 | StandardError=inherit
19 | TimeoutStopSec=30
20 | Restart=on-abort
21 | User=$(logname)
22 |
23 | [Install]
24 | WantedBy=multi-user.target" > dexpy.service
25 | cp dexpy.service /etc/systemd/system/
26 | systemctl enable dexpy.service
27 | systemctl start dexpy.service
28 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | paho_mqtt==1.5.1
2 | pyserial==3.4
3 | requests==2.24.0
4 | influxdb==5.3.0
5 | simplejson
6 | distro
7 |
--------------------------------------------------------------------------------
/usbreceiver/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/winemug/dexpy/72f3f3ca4adf6cb773484d05c0042f01f5b8c857/usbreceiver/__init__.py
--------------------------------------------------------------------------------
/usbreceiver/constants.py:
--------------------------------------------------------------------------------
1 | #########################################################################
2 | # This source file is from the openaps/dexcom_reader project.
3 | #
4 | # https://github.com/openaps/dexcom_reader
5 | #
6 | # It is under an MIT licence described in the 3 paragraphs below:
7 | #
8 | #########################################################################
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a
11 | # copy of this software and associated documentation files (the "Software"),
12 | # to deal in the Software without restriction, including without limitation
13 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | # and/or sell copies of the Software, and to permit persons to whom the
15 | # Software is furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included
18 | # in all copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | # OTHER DEALINGS IN THE SOFTWARE.
27 | #
28 | #########################################################################
29 |
30 | import datetime
31 |
32 |
33 | class Error(Exception):
34 | """Base error for dexcom reader."""
35 |
36 |
37 | class CrcError(Error):
38 | """Failed to CRC properly."""
39 |
40 |
41 | DEXCOM_G4_USB_VENDOR = 0x22a3
42 | DEXCOM_G4_USB_PRODUCT = 0x0047
43 |
44 | BASE_TIME = datetime.datetime(2009, 1, 1)
45 | DEXCOM_EPOCH = 1230768000
46 |
47 | NULL = 0
48 | ACK = 1
49 | NAK = 2
50 | INVALID_COMMAND = 3
51 | INVALID_PARAM = 4
52 | INCOMPLETE_PACKET_RECEIVED = 5
53 | RECEIVER_ERROR = 6
54 | INVALID_MODE = 7
55 | PING = 10
56 | READ_FIRMWARE_HEADER = 11
57 | READ_DATABASE_PARTITION_INFO = 15
58 | READ_DATABASE_PAGE_RANGE = 16
59 | READ_DATABASE_PAGES = 17
60 | READ_DATABASE_PAGE_HEADER = 18
61 | READ_TRANSMITTER_ID = 25
62 | WRITE_TRANSMITTER_ID = 26
63 | READ_LANGUAGE = 27
64 | WRITE_LANGUAGE = 28
65 | READ_DISPLAY_TIME_OFFSET = 29
66 | WRITE_DISPLAY_TIME_OFFSET = 30
67 | READ_RTC = 31
68 | RESET_RECEIVER = 32
69 | READ_BATTERY_LEVEL = 33
70 | READ_SYSTEM_TIME = 34
71 | READ_SYSTEM_TIME_OFFSET = 35
72 | WRITE_SYSTEM_TIME = 36
73 | READ_GLUCOSE_UNIT = 37
74 | WRITE_GLUCOSE_UNIT = 38
75 | READ_BLINDED_MODE = 39
76 | WRITE_BLINDED_MODE = 40
77 | READ_CLOCK_MODE = 41
78 | WRITE_CLOCK_MODE = 42
79 | READ_DEVICE_MODE = 43
80 | ERASE_DATABASE = 45
81 | SHUTDOWN_RECEIVER = 46
82 | WRITE_PC_PARAMETERS = 47
83 | READ_BATTERY_STATE = 48
84 | READ_HARDWARE_BOARD_ID = 49
85 | READ_FIRMWARE_SETTINGS = 54
86 | READ_ENABLE_SETUP_WIZARD_FLAG = 55
87 | READ_SETUP_WIZARD_STATE = 57
88 | READ_CHARGER_CURRENT_SETTING = 59
89 | WRITE_CHARGER_CURRENT_SETTING = 60
90 | MAX_COMMAND = 61
91 | MAX_POSSIBLE_COMMAND = 255
92 |
93 | EGV_VALUE_MASK = 1023
94 | EGV_DISPLAY_ONLY_MASK = 32768
95 | EGV_TREND_ARROW_MASK = 15
96 |
97 | BATTERY_STATES = [None, 'CHARGING', 'NOT_CHARGING', 'NTC_FAULT', 'BAD_BATTERY']
98 |
99 |
100 | RECORD_TYPES = [
101 | 'MANUFACTURING_DATA', 'FIRMWARE_PARAMETER_DATA', 'PC_SOFTWARE_PARAMETER',
102 | 'SENSOR_DATA', 'EGV_DATA', 'CAL_SET', 'ABERRATION', 'INSERTION_TIME',
103 | 'RECEIVER_LOG_DATA', 'RECEIVER_ERROR_DATA', 'METER_DATA', 'USER_EVENT_DATA',
104 | 'USER_SETTING_DATA', 'SESSION_COMMAND_DATA', 'TRANSMITTER_INFO_DATA', 'UNKNOWN', 'TRANSMITTER_PRIVATE_DATA',
105 | 'TRANSMITTER_MANIFEST', 'BACKFILLED_EGV'
106 | ]
107 |
108 | TREND_ARROW_VALUES = [None, 'DOUBLE_UP', 'SINGLE_UP', '45_UP', 'FLAT',
109 | '45_DOWN', 'SINGLE_DOWN', 'DOUBLE_DOWN', 'NOT_COMPUTABLE',
110 | 'OUT_OF_RANGE']
111 |
112 | SPECIAL_GLUCOSE_VALUES = {0: None,
113 | 1: 'SENSOR_NOT_ACTIVE',
114 | 2: 'MINIMAL_DEVIATION',
115 | 3: 'NO_ANTENNA',
116 | 5: 'SENSOR_NOT_CALIBRATED',
117 | 6: 'COUNTS_DEVIATION',
118 | 9: 'ABSOLUTE_DEVIATION',
119 | 10: 'POWER_DEVIATION',
120 | 12: 'BAD_RF'}
121 |
122 |
123 | LANGUAGES = {
124 | 0: None,
125 | 1033: 'ENGLISH',
126 | }
127 |
128 |
129 |
--------------------------------------------------------------------------------
/usbreceiver/crc16.py:
--------------------------------------------------------------------------------
1 | #########################################################################
2 | # This source file is from the openaps/dexcom_reader project.
3 | #
4 | # https://github.com/openaps/dexcom_reader
5 | #
6 | # It is under an MIT licence described in the 3 paragraphs below:
7 | #
8 | #########################################################################
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a
11 | # copy of this software and associated documentation files (the "Software"),
12 | # to deal in the Software without restriction, including without limitation
13 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | # and/or sell copies of the Software, and to permit persons to whom the
15 | # Software is furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included
18 | # in all copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | # OTHER DEALINGS IN THE SOFTWARE.
27 | #
28 | #########################################################################
29 |
30 | TABLE = [
31 | 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290,
32 | 45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044,
33 | 29431, 25302, 37689, 33560, 45947, 41818, 54205, 50076, 62463, 58334, 9314,
34 | 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088, 38153,
35 | 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165,
36 | 18100, 46939, 42874, 38681, 34616, 63455, 59390, 55197, 51132, 18628, 22757,
37 | 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789, 59790, 63919, 35144,
38 | 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770,
39 | 56317, 52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684,
40 | 23749, 11298, 15363, 3168, 7233, 60846, 64911, 52716, 56781, 44330, 48395,
41 | 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761, 3696, 65439,
42 | 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451,
43 | 53516, 49453, 61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742,
44 | 24679, 33721, 37784, 41979, 46042, 49981, 54044, 58239, 62302, 689, 4752,
45 | 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830,
46 | 58703, 54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413,
47 | 42971, 47098, 34713, 38840, 59231, 63358, 50973, 55100, 9939, 14066, 1681,
48 | 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305,
49 | 47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093,
50 | 56156, 60223, 64286, 35833, 39896, 43963, 48026, 19061, 23124, 27191, 31254,
51 | 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557, 48554, 44427, 40424,
52 | 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342,
53 | 53085, 57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923,
54 | 16050, 3793, 7920
55 | ]
56 |
57 |
58 | def crc16(buf, start=None, end=None):
59 | if start is None:
60 | start = 0
61 | if end is None:
62 | end = len(buf)
63 | num = 0
64 | for i in range(start, end):
65 | num = ((num<<8)&0xff00) ^ TABLE[((num>>8)&0xff)^buf[i]]
66 | return num & 0xffff
67 |
--------------------------------------------------------------------------------
/usbreceiver/database_records.py:
--------------------------------------------------------------------------------
1 | #########################################################################
2 | # This source file is from the openaps/dexcom_reader project.
3 | #
4 | # https://github.com/openaps/dexcom_reader
5 | #
6 | # It is under an MIT licence described in the 3 paragraphs below:
7 | #
8 | #########################################################################
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a
11 | # copy of this software and associated documentation files (the "Software"),
12 | # to deal in the Software without restriction, including without limitation
13 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | # and/or sell copies of the Software, and to permit persons to whom the
15 | # Software is furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included
18 | # in all copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | # OTHER DEALINGS IN THE SOFTWARE.
27 | #
28 | #########################################################################
29 | #
30 | # The original file has been modified to dump data in a less annoying
31 | # format in BaseDatabaseRecord. Methods system_secs() and display_secs()
32 | # have been added to GenericTimestampedRecord. This is because I needed
33 | # to store the extracted data in numeric format in a database file.
34 | # The existing methods system_time() and display_time() return
35 | # datetime structures which would have consumed more space in the
36 | # database. In several methods, the FORMAT strings have been modified
37 | # to replace 'c' (char) with 'B' (byte). This makes it easier to
38 | # work with the extracted data. There's no longer any need to
39 | # call ord() every time the value is referenced.
40 | # New class, G5UserSettings & G6UserSettings have been added. These
41 | # allows one to read the user configuration settings.
42 | #
43 | #########################################################################
44 | from msgpack.fallback import xrange
45 |
46 | import usbreceiver.crc16
47 | import usbreceiver.constants
48 | import struct
49 | import usbreceiver.util
50 | import binascii
51 |
52 | from usbreceiver import constants, util
53 |
54 | EGV_TESTNUM_MASK = 0x00ffffff
55 |
56 | class BaseDatabaseRecord(object):
57 | FORMAT = None
58 |
59 | @classmethod
60 | def _CheckFormat(cls):
61 | if cls.FORMAT is None or not cls.FORMAT:
62 | raise NotImplementedError("Subclasses of %s need to define FORMAT"
63 | % cls.__name__)
64 |
65 | @classmethod
66 | def _ClassFormat(cls):
67 | cls._CheckFormat()
68 | return struct.Struct(cls.FORMAT)
69 |
70 | @classmethod
71 | def _ClassSize(cls):
72 | return cls._ClassFormat().size
73 |
74 | @property
75 | def FMT(self):
76 | self._CheckFormat()
77 | return self._ClassFormat()
78 |
79 | @property
80 | def SIZE(self):
81 | return self._ClassSize()
82 |
83 | @property
84 | def crc(self):
85 | return self.data[-1]
86 |
87 | def __init__(self, data, raw_data):
88 | self.raw_data = raw_data
89 | self.data = data
90 | self.check_crc()
91 |
92 | def check_crc(self):
93 | local_crc = self.calculate_crc()
94 | if local_crc != self.crc:
95 | raise constants.CrcError('Could not parse %s' % self.__class__.__name__)
96 |
97 | def dump(self):
98 | return ''.join(' %02x' % ord(c) for c in self.raw_data)
99 |
100 | def calculate_crc(self):
101 | return usbreceiver.crc16.crc16(self.raw_data[:-2])
102 |
103 | @classmethod
104 | def Create(cls, data, record_counter):
105 | offset = record_counter * cls._ClassSize()
106 | raw_data = data[offset:offset + cls._ClassSize()]
107 | unpacked_data = cls._ClassFormat().unpack(raw_data)
108 | return cls(unpacked_data, raw_data)
109 |
110 |
111 | class GenericTimestampedRecord(BaseDatabaseRecord):
112 | FIELDS = [ ]
113 | BASE_FIELDS = [ 'system_time', 'display_time' ]
114 |
115 | @property
116 | def system_time(self):
117 | return util.ReceiverTimeToTime(self.data[0])
118 |
119 | @property
120 | def display_time(self):
121 | return util.ReceiverTimeToTime(self.data[1])
122 |
123 | @property
124 | def system_secs(self):
125 | return self.data[0]
126 |
127 | @property
128 | def display_secs(self):
129 | return self.data[1]
130 |
131 | def to_dict (self):
132 | d = dict( )
133 | for k in self.BASE_FIELDS + self.FIELDS:
134 | d[k] = getattr(self, k)
135 | if callable(getattr(d[k], 'isoformat', None)):
136 | d[k] = d[k].isoformat( )
137 | return d
138 |
139 | class GenericXMLRecord(GenericTimestampedRecord):
140 | FORMAT = '' helps make
240 | # a subsequent serial port access work.
241 | stat_info = os.stat(self._port_name)
242 | time.sleep(15)
243 | self._port = serial.Serial(port=self._port_name, baudrate=115200)
244 |
245 | except serial.SerialException:
246 | print('Read/Write permissions missing for', self._port_name)
247 | if sys.platform == "linux" or sys.platform == "linux2" or sys.platform == "darwin":
248 | stat_info = os.stat(self._port_name)
249 | port_gid = stat_info.st_gid
250 | port_group = grp.getgrgid(port_gid)[0]
251 | username = pwd.getpwuid(os.getuid())[0]
252 | # print '\nFor a persistent solution (recommended), run ...'
253 | # if sys.platform == "darwin":
254 | # print '\n sudo dseditgroup -o edit -a', username, '-t user', port_group
255 | # else:
256 | # # On Mint, Ubuntu, etc.
257 | # print '\n sudo addgroup', username, port_group
258 | # print '\n sudo -', username
259 | # print '\n OR'
260 | # # On Fedora, Red Hat, etc.
261 | # print '\n sudo usermod -a -G', port_group, username
262 | # print '\n su -', username
263 | # print '\nFor a short term solution, run ...'
264 | # print '\n sudo chmod 666', self._port_name,'\n'
265 | if self._port is not None:
266 | try:
267 | self.clear()
268 | except Exception as e:
269 | pass
270 |
271 | try:
272 | self.flush()
273 | except Exception as e:
274 | pass
275 |
276 | def Disconnect(self):
277 | if self._port is not None:
278 | # If the user disconnects the USB cable while in the middle
279 | # of a Write/Read operation, we can end up with junk in the
280 | # serial port buffers. After reconnecting the cable, this
281 | # junk can cause a lock-up on that port. So, clear and
282 | # flush the port during this Disconnect operation to prevent
283 | # a possible future lock-up. Note: the clear() and flush()
284 | # operations can throw exceptions when there is nothing to
285 | # be cleaned up, so we use try ... except to ignore those.
286 | try:
287 | self.clear()
288 | except Exception as e:
289 | #print 'Disconnect() : Exception =', e
290 | pass
291 |
292 | try:
293 | self.flush()
294 | except Exception as e:
295 | #print 'Disconnect() : Exception =', e
296 | pass
297 | self._port.close()
298 | self._port = None
299 |
300 | @property
301 | def port(self):
302 | if self._port is None:
303 | self.Connect()
304 | return self._port
305 |
306 | def write(self, *args, **kwargs):
307 | return self.port.write(*args, **kwargs)
308 |
309 | def read(self, *args, **kwargs):
310 | return self.port.read(*args, **kwargs)
311 |
312 | def readpacket(self, timeout=None):
313 | total_read = 4
314 | initial_read = self.read(total_read)
315 | all_data = initial_read
316 | if initial_read[0] == 1:
317 | command = initial_read[3]
318 | data_number = struct.unpack(' 6:
320 | toread = abs(data_number-6)
321 | second_read = self.read(toread)
322 | all_data += second_read
323 | total_read += toread
324 | out = second_read
325 | else:
326 | out = ''
327 | suffix = self.read(2)
328 | sent_crc = struct.unpack(' 1590:
347 | raise constants.Error('Invalid packet length')
348 | self.flush()
349 | self.write(packet)
350 |
351 | def WriteCommand(self, command_id, *args, **kwargs):
352 | p = packetwriter.PacketWriter()
353 | p.ComposePacket(command_id, *args, **kwargs)
354 | self.WritePacket(p.get_packet_bytes())
355 |
356 | def GenericReadCommand(self, command_id):
357 | self.WriteCommand(command_id)
358 | return self.readpacket()
359 |
360 | def ReadTransmitterId(self):
361 | return self.GenericReadCommand(constants.READ_TRANSMITTER_ID).data
362 |
363 | def ReadLanguage(self):
364 | lang = self.GenericReadCommand(constants.READ_LANGUAGE).data
365 | return constants.LANGUAGES[struct.unpack('H', lang)[0]]
366 |
367 | def ReadBatteryLevel(self):
368 | level = self.GenericReadCommand(constants.READ_BATTERY_LEVEL).data
369 | return struct.unpack('I', level)[0]
370 |
371 | def ReadBatteryState(self):
372 | state = self.GenericReadCommand(constants.READ_BATTERY_STATE).data
373 | return constants.BATTERY_STATES[ord(state)]
374 |
375 | def ReadRTC(self):
376 | rtc = self.GenericReadCommand(constants.READ_RTC).data
377 | return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0])
378 |
379 | def ReadSystemTime(self):
380 | rtc = self.GenericReadCommand(constants.READ_SYSTEM_TIME).data
381 | return util.ReceiverTimeToTime(struct.unpack('I', rtc)[0])
382 |
383 | def ReadSystemTimeOffset(self):
384 | raw = self.GenericReadCommand(constants.READ_SYSTEM_TIME_OFFSET).data
385 | return datetime.timedelta(seconds=struct.unpack('i', raw)[0])
386 |
387 | def ReadDisplayTimeOffset(self):
388 | raw = self.GenericReadCommand(constants.READ_DISPLAY_TIME_OFFSET).data
389 | return datetime.timedelta(seconds=struct.unpack('i', raw)[0])
390 |
391 | def WriteDisplayTimeOffset(self, offset=None):
392 | payload = struct.pack('i', offset)
393 | self.WriteCommand(constants.WRITE_DISPLAY_TIME_OFFSET, payload)
394 | packet = self.readpacket()
395 | return dict(ACK=ord(packet.command) == constants.ACK)
396 |
397 |
398 | def ReadDisplayTime(self):
399 | return self.ReadSystemTime() + self.ReadDisplayTimeOffset()
400 |
401 | def ReadGlucoseUnit(self):
402 | UNIT_TYPE = (None, 'mg/dL', 'mmol/L')
403 | gu = self.GenericReadCommand(constants.READ_GLUCOSE_UNIT).data
404 | return UNIT_TYPE[ord(gu[0])]
405 |
406 | def ReadClockMode(self):
407 | CLOCK_MODE = (24, 12)
408 | cm = self.GenericReadCommand(constants.READ_CLOCK_MODE).data
409 | return CLOCK_MODE[ord(cm[0])]
410 |
411 | def ReadDeviceMode(self):
412 | # ???
413 | return self.GenericReadCommand(constants.READ_DEVICE_MODE).data
414 |
415 | def ReadBlindedMode(self):
416 | MODES = { 0: False }
417 | raw = self.GenericReadCommand(constants.READ_BLINDED_MODE).data
418 | mode = MODES.get(bytearray(raw)[0], True)
419 | return mode
420 |
421 | def ReadHardwareBoardId(self):
422 | return self.GenericReadCommand(constants.READ_HARDWARE_BOARD_ID).data
423 |
424 | def ReadEnableSetupWizardFlag (self):
425 | # ???
426 | return self.GenericReadCommand(constants.READ_ENABLE_SETUP_WIZARD_FLAG).data
427 |
428 | def ReadSetupWizardState (self):
429 | # ???
430 | return self.GenericReadCommand(constants.READ_SETUP_WIZARD_STATE).data
431 |
432 | def WriteChargerCurrentSetting (self, status):
433 | MAP = ( 'Off', 'Power100mA', 'Power500mA', 'PowerMax', 'PowerSuspended' )
434 | payload = str(bytearray([MAP.index(status)]))
435 | self.WriteCommand(constants.WRITE_CHARGER_CURRENT_SETTING, payload)
436 | packet = self.readpacket()
437 | raw = bytearray(packet.data)
438 | return dict(ACK=ord(packet.command) == constants.ACK, raw=list(raw))
439 |
440 | def ReadChargerCurrentSetting (self):
441 | MAP = ( 'Off', 'Power100mA', 'Power500mA', 'PowerMax', 'PowerSuspended' )
442 | raw = bytearray(self.GenericReadCommand(constants.READ_CHARGER_CURRENT_SETTING).data)
443 | return MAP[raw[0]]
444 |
445 |
446 | # ManufacturingParameters: SerialNumber, HardwarePartNumber, HardwareRevision, DateTimeCreated, HardwareId
447 | def ReadManufacturingData(self):
448 | data = self.ReadRecords('MANUFACTURING_DATA')[0].xmldata
449 | return ET.fromstring(data)
450 |
451 | def ReadAllManufacturingData(self):
452 | data = self.ReadRecords('MANUFACTURING_DATA')[0].xmldata
453 | return data
454 |
455 | def flush(self):
456 | self.port.flush()
457 |
458 | def clear(self):
459 | self.port.flushInput()
460 | self.port.flushOutput()
461 |
462 | def GetFirmwareHeader(self):
463 | i = self.GenericReadCommand(constants.READ_FIRMWARE_HEADER)
464 | return ET.fromstring(i.data)
465 |
466 | # FirmwareSettingsParameters: FirmwareImageId
467 | def GetFirmwareSettings(self):
468 | i = self.GenericReadCommand(constants.READ_FIRMWARE_SETTINGS)
469 | return ET.fromstring(i.data)
470 |
471 | def DataPartitions(self):
472 | i = self.GenericReadCommand(constants.READ_DATABASE_PARTITION_INFO)
473 | return ET.fromstring(i.data)
474 |
475 | def ReadDatabasePageRange(self, record_type):
476 | record_type_index = constants.RECORD_TYPES.index(record_type)
477 | self.WriteCommand(constants.READ_DATABASE_PAGE_RANGE,
478 | chr(record_type_index))
479 | packet = self.readpacket()
480 | return struct.unpack('II', packet.data)
481 |
482 | def ReadDatabasePage(self, record_type, page):
483 | record_type_index = constants.RECORD_TYPES.index(record_type)
484 | self.WriteCommand(constants.READ_DATABASE_PAGES,
485 | (chr(record_type_index), struct.pack('I', page), chr(1)))
486 | packet = self.readpacket()
487 | assert packet.command == 1
488 | # first index (uint), numrec (uint), record_type (byte), revision (byte),
489 | # page# (uint), r1 (uint), r2 (uint), r3 (uint), ushort (Crc)
490 | header_format = '<2IcB4IH'
491 | header_data_len = struct.calcsize(header_format)
492 | header = struct.unpack_from(header_format, packet.data)
493 | header_crc = usbreceiver.crc16.crc16(packet.data[:header_data_len - 2])
494 | assert header_crc == header[-1]
495 | assert ord(header[2]) == record_type_index
496 | assert header[4] == page
497 | packet_data = packet.data[header_data_len:]
498 |
499 | return self.ParsePage(header, packet_data)
500 |
501 | def GenericRecordYielder(self, header, data, record_type):
502 | for x in xrange(header[1]):
503 | yield record_type.Create(data, x)
504 |
505 | def ParsePage(self, header, data):
506 | record_type = constants.RECORD_TYPES[ord(header[2])]
507 | revision = int(header[3])
508 | generic_parser_map = self.PARSER_MAP
509 | if revision < 2 and record_type == 'CAL_SET':
510 | generic_parser_map.update(CAL_SET=database_records.LegacyCalibration)
511 | xml_parsed = ['PC_SOFTWARE_PARAMETER', 'MANUFACTURING_DATA']
512 | if record_type in generic_parser_map:
513 | return self.GenericRecordYielder(header, data,
514 | generic_parser_map[record_type])
515 | elif record_type in xml_parsed:
516 | return [database_records.GenericXMLRecord.Create(data, 0)]
517 | else:
518 | raise NotImplementedError('Parsing of %s has not yet been implemented'
519 | % record_type)
520 |
521 | def GetLastRecords(self, record_type):
522 | assert record_type in constants.RECORD_TYPES
523 | page_range = self.ReadDatabasePageRange(record_type)
524 | start, end = page_range
525 | records = list(self.ReadDatabasePage(record_type, end))
526 | records.reverse( )
527 | return records
528 |
529 | def iter_records (self, record_type):
530 | assert record_type in constants.RECORD_TYPES
531 | page_range = self.ReadDatabasePageRange(record_type)
532 | start, end = page_range
533 | if start != end or not end:
534 | end += 1
535 | for x in reversed(xrange(start, end)):
536 | records = list(self.ReadDatabasePage(record_type, x))
537 | records.reverse( )
538 | for record in records:
539 | yield record
540 |
541 | def ReadRecords(self, record_type):
542 | records = []
543 | assert record_type in constants.RECORD_TYPES
544 | page_range = self.ReadDatabasePageRange(record_type)
545 | start, end = page_range
546 | if start != end or not end:
547 | end += 1
548 | for x in range(start, end):
549 | records.extend(self.ReadDatabasePage(record_type, x))
550 | return records
--------------------------------------------------------------------------------
/usbreceiver/util.py:
--------------------------------------------------------------------------------
1 | #########################################################################
2 | # This source file is from the openaps/dexcom_reader project.
3 | #
4 | # https://github.com/openaps/dexcom_reader
5 | #
6 | # It is under an MIT licence described in the 3 paragraphs below:
7 | #
8 | #########################################################################
9 | #
10 | # Permission is hereby granted, free of charge, to any person obtaining a
11 | # copy of this software and associated documentation files (the "Software"),
12 | # to deal in the Software without restriction, including without limitation
13 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
14 | # and/or sell copies of the Software, and to permit persons to whom the
15 | # Software is furnished to do so, subject to the following conditions:
16 | #
17 | # The above copyright notice and this permission notice shall be included
18 | # in all copies or substantial portions of the Software.
19 | #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
24 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
25 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 | # OTHER DEALINGS IN THE SOFTWARE.
27 | #
28 | #########################################################################
29 |
30 | import usbreceiver.constants
31 | import datetime
32 | import os
33 | import platform
34 | import plistlib
35 | import re
36 | import subprocess
37 | import sys
38 |
39 | from usbreceiver import constants
40 |
41 | if sys.platform == 'win32':
42 | import serial.tools.list_ports
43 | #from _winreg import *
44 |
45 |
46 | def ReceiverTimeToTime(rtime):
47 | return constants.DEXCOM_EPOCH + rtime
48 |
49 |
50 | def linux_find_usbserial(vendor, product):
51 | DEV_REGEX = re.compile('^tty(USB|ACM)[0-9]+$')
52 | for usb_dev_root in os.listdir('/sys/bus/usb/devices'):
53 | device_name = os.path.join('/sys/bus/usb/devices', usb_dev_root)
54 | if not os.path.exists(os.path.join(device_name, 'idVendor')):
55 | continue
56 | idv = open(os.path.join(device_name, 'idVendor')).read().strip()
57 | if idv != vendor:
58 | continue
59 | idp = open(os.path.join(device_name, 'idProduct')).read().strip()
60 | if idp != product:
61 | continue
62 | for root, dirs, files in os.walk(device_name):
63 | for option in dirs + files:
64 | if DEV_REGEX.match(option):
65 | return os.path.join('/dev', option)
66 |
67 |
68 | def osx_find_usbserial(vendor, product):
69 | def recur(v):
70 | if hasattr(v, '__iter__') and 'idVendor' in v and 'idProduct' in v:
71 | if v['idVendor'] == vendor and v['idProduct'] == product:
72 | tmp = v
73 | while True:
74 | if 'IODialinDevice' not in tmp and 'IORegistryEntryChildren' in tmp:
75 | tmp = tmp['IORegistryEntryChildren']
76 | elif 'IODialinDevice' in tmp:
77 | return tmp['IODialinDevice']
78 | else:
79 | break
80 |
81 | if type(v) == list:
82 | for x in v:
83 | out = recur(x)
84 | if out is not None:
85 | return out
86 | elif type(v) == dict or issubclass(type(v), dict):
87 | for x in v.values():
88 | out = recur(x)
89 | if out is not None:
90 | return out
91 |
92 | sp = subprocess.Popen(['/usr/sbin/ioreg', '-k', 'IODialinDevice',
93 | '-r', '-t', '-l', '-a', '-x'],
94 | stdout=subprocess.PIPE,
95 | stdin=subprocess.PIPE, stderr=subprocess.PIPE)
96 | stdout, _ = sp.communicate()
97 | plist = plistlib.readPlistFromString(stdout.decode())
98 | return recur(plist)
99 |
100 |
101 | def thisIsWine():
102 | # if sys.platform == 'win32':
103 | # try:
104 | # registry = ConnectRegistry(None, HKEY_LOCAL_MACHINE)
105 | # if registry is not None:
106 | # try:
107 | # winekey = OpenKey(registry, 'Software\Wine')
108 | # if winekey is not None:
109 | # return True
110 | # else:
111 | # return False
112 | # except Exception as e:
113 | # #print 'OpenKey failed. Exception =', e
114 | # return False
115 | # else:
116 | # return False
117 | #
118 | # except Exception as f:
119 | # #print 'ConnectRegistry failed. Exception =', f
120 | # return False
121 | # else:
122 | return False
123 |
124 |
125 | def win_find_usbserial(vendor, product):
126 | if thisIsWine():
127 | # When running under WINE, we have no access to real USB information, such
128 | # as the Vendor & Product ID values. Also, serial.tools.list_ports.comports()
129 | # returns nothing. The real port under Linux (or OSX?) is mapped to a windows
130 | # serial port at \dosdevices\COMxx, but we don't know which one. Normally,
131 | # COM1 - COM32 are automatically mapped to /dev/ttyS0 - /dev/ttyS31.
132 | # If the Dexcom device is plugged in, it will be mapped to COM33 or greater.
133 | # We have no way of identifying which port >= COM33 is the right one, so
134 | # we'll just guess the first available one.
135 | return "\\\\.\\com33"
136 | else:
137 | for cport in serial.tools.list_ports.comports():
138 | if (cport.vid == vendor) and (cport.pid == product):
139 | # found a port which matches vendor and product IDs
140 | if cport.device is not None:
141 | return cport.device
142 | return None
143 |
144 |
145 |
146 | def find_usbserial(vendor, product):
147 | """Find the tty device for a given usbserial devices identifiers.
148 |
149 | Args:
150 | vendor: (int) something like 0x0000
151 | product: (int) something like 0x0000
152 |
153 | Returns:
154 | String, like /dev/ttyACM0 or /dev/tty.usb...
155 | """
156 | if platform.system() == 'Linux':
157 | vendor, product = [('%04x' % (x)).strip() for x in (vendor, product)]
158 | return linux_find_usbserial(vendor, product)
159 | elif platform.system() == 'Darwin':
160 | return osx_find_usbserial(vendor, product)
161 | elif platform.system() == 'Windows':
162 | return win_find_usbserial(vendor, product)
163 | else:
164 | raise NotImplementedError('Cannot find serial ports on %s'
165 | % platform.system())
166 |
--------------------------------------------------------------------------------