Browse Source

stuff from the other night

i am a professional and my usage of version control reflects years of
honed experience or something.
Brennen Bearnes 5 months ago
parent
commit
27c25c6356
5 changed files with 574 additions and 28 deletions
  1. 0
    1
      compass.py
  2. 49
    0
      limor_calibrated_recent.py
  3. 74
    27
      src/glitterpos.py
  4. 20
    0
      src/glitterpos_util.py
  5. 431
    0
      src/lib/adafruit_lsm9ds1.py

+ 0
- 1
compass.py View File

@@ -24,7 +24,6 @@ def bearing_to_pixel(bearing, count=16):
24 24
 def map_range(x, in_min, in_max, out_min, out_max):
25 25
     """
26 26
     Maps a number from one range to another.
27
-    Note: This implementation handles values < in_min differently than arduino's map function does.
28 27
     :return: Returns value mapped to new range
29 28
     :rtype: float
30 29
     """

+ 49
- 0
limor_calibrated_recent.py View File

@@ -0,0 +1,49 @@
1
+import time
2
+import math
3
+import board
4
+import busio
5
+import adafruit_lsm9ds1
6
+
7
+i2c = busio.I2C(board.SCL, board.SDA)
8
+compass = adafruit_lsm9ds1.LSM9DS1_I2C(i2c)
9
+
10
+mag_min = [-0.10206, 0.00238, -0.10458]
11
+mag_max = [0.44926, 0.56938, 0.4501]
12
+
13
+def calibrate_compass(compass):
14
+    mag_min = [1000, 1000, 1000]
15
+    mag_max = [-1000, -1000, -1000]
16
+    print("Magnetometer Calibration")
17
+    lastDisplayTime = time.monotonic()
18
+    while True:
19
+        x, y, z = compass.magnetometer
20
+        mag_vals = [x, y, z]
21
+        for i in range(3):
22
+            mag_min[i] = min(mag_min[i], mag_vals[i])
23
+            mag_max[i] = max(mag_max[i], mag_vals[i])
24
+        # display once every two seconds
25
+        if (time.monotonic() - lastDisplayTime >= 1):
26
+            print("Mag Min: ", mag_min, "& Max:", mag_max)
27
+            lastDisplayTime = time.monotonic();
28
+
29
+#calibrate_compass(compass)
30
+
31
+def map_range(x, in_min, in_max, out_min, out_max):
32
+    mapped = (x-in_min) * (out_max - out_min) / (in_max-in_min) + out_min
33
+    if out_min <= out_max:
34
+        return max(min(mapped, out_max), out_min)
35
+    return min(max(mapped, out_max), out_min)
36
+
37
+while True:
38
+    x, y, z = compass.magnetometer
39
+    print("Uncalibrated: ", x, y, z, end="")
40
+    x = map_range(x, mag_min[0], mag_max[0], -1, 1)
41
+    y = map_range(y, mag_min[1], mag_max[1], -1, 1)
42
+    z = map_range(z, mag_min[2], mag_max[2], -1, 1)
43
+    print(" \tCalibrated: ", x, y, z)
44
+    
45
+    compass_heading = (math.atan2(y, x) * 180) / math.pi
46
+    if compass_heading < 0:
47
+        compass_heading += 360
48
+    print("Heading: ", compass_heading)
49
+    time.sleep(0.5)

+ 74
- 27
src/glitterpos.py View File

@@ -2,16 +2,23 @@
2 2
 
3 3
 import time
4 4
 import gc
5
+import math
6
+import adafruit_lsm9ds1
5 7
 import adafruit_gps
6 8
 import adafruit_rfm9x
7 9
 import board
8 10
 import busio
9
-import crc16pure
10 11
 import digitalio
11 12
 import neopixel
12 13
 import rtc
13
-from glitterpos_util import timestamp, compass_bearing
14
-from glitterpos_id import MY_ID
14
+from glitterpos_util import timestamp, compass_bearing, bearing_to_pixel, map_range
15
+
16
+# glitterpos_id.py should be formatted as follows:
17
+#
18
+# MY_ID = 2 # must be a unique integer
19
+# MAG_MIN = (-0.25046, -0.23506, -0.322)
20
+# MAG_MAX = (0.68278, 0.70882, 0.59654)
21
+from glitterpos_id import MY_ID, MAG_MIN, MAG_MAX
15 22
 
16 23
 # Colors for status lights, etc.
17 24
 RED = (255, 0, 0)
@@ -24,6 +31,10 @@ PURPLE = (180, 0, 255)
24 31
 MAN_ID = 23
25 32
 ELECTRICITY_ID = 42
26 33
 
34
+# Magnetic North:
35
+DECLINATION_RAD = 145.4 / 1000.0 # Lyons in radians
36
+# DECLINATION = 235.27 / 1000.0 # Black Rock City in mrad
37
+
27 38
 COLOR_LOOKUP = {
28 39
     0: GREEN,
29 40
     1: BLUE,
@@ -64,6 +75,7 @@ class GlitterPOS:
64 75
 
65 76
         # A tuple for our lat/long:
66 77
         self.coords = (0, 0)
78
+        self.heading = 0.0
67 79
 
68 80
         # Status light on the board, we'll use to indicate GPS fix, etc.:
69 81
         self.statuslight = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.005, auto_write=True)
@@ -71,35 +83,41 @@ class GlitterPOS:
71 83
 
72 84
         # Neopixel ring:
73 85
         self.pixels = neopixel.NeoPixel(board.A1, 16, brightness=0.01, auto_write=False)
74
-
75
-        # Startup animation:
76
-        for i in range(len(self.pixels)):
77
-            self.pixels[i] = GREEN
78
-            self.pixels.show()
79
-            time.sleep(0.2)
80
-        self.pixels.fill((0,0,0))
81
-        self.pixels[0] = PURPLE
82
-        self.pixels.show()
86
+        self.startup_animation()
87
+        time.sleep(2)
83 88
 
84 89
         self.init_radio()
85
-        time.sleep(1)
86
-
87 90
         self.init_gps()
88
-        time.sleep(1)
91
+        self.init_compass()
89 92
 
90 93
         self.statuslight.fill(YELLOW)
91 94
 
95
+    def startup_animation(self):
96
+        self.pixels[bearing_to_pixel(0)] = PURPLE
97
+        self.pixels.show()
98
+        time.sleep(.5)
99
+        self.pixels[bearing_to_pixel(90)] = GREEN
100
+        self.pixels.show()
101
+        time.sleep(.5)
102
+        self.pixels[bearing_to_pixel(180)] = YELLOW
103
+        self.pixels.show()
104
+        time.sleep(.5)
105
+        self.pixels[bearing_to_pixel(270)] = BLUE
106
+        self.pixels.show()
107
+
92 108
     def init_radio(self):
93 109
         """Set up RFM95."""
94 110
         spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
95 111
         self.rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
96 112
         self.rfm9x.tx_power = 18 # Default is 13 dB, but the RFM95 can go up to 23 dB
97 113
         self.radio_tx('d', 'hello world')
114
+        time.sleep(1)
98 115
 
99 116
     def init_gps(self):
100 117
         """Some GPS module setup."""
101 118
         uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=3000)
102 119
         gps = adafruit_gps.GPS(uart)
120
+        time.sleep(1)
103 121
 
104 122
         # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf
105 123
         # Turn on the basic GGA and RMC info (what you typically want), then
@@ -109,22 +127,25 @@ class GlitterPOS:
109 127
 
110 128
         self.gps = gps
111 129
 
130
+    def init_compass(self):
131
+        i2c = busio.I2C(board.SCL, board.SDA)
132
+        self.compass = adafruit_lsm9ds1.LSM9DS1_I2C(i2c)
133
+        time.sleep(1)
134
+
112 135
     def advance_frame(self):
113 136
         """Essentially our main program loop."""
114 137
 
115 138
         current = time.monotonic()
116
-
117 139
         self.radio_rx(timeout=0.5)
118
-
119 140
         new_gps_data = self.gps.update()
141
+        self.update_heading()
142
+        self.display_pixels()
120 143
 
121 144
         if not self.gps.has_fix:
122 145
             # Try again if we don't have a fix yet.
123 146
             self.statuslight.fill(RED)
124 147
             return
125 148
 
126
-        self.display_pixels()
127
-
128 149
         # We want to send coordinates out either on new GPS data or roughly every 15 seconds.
129 150
         if (not new_gps_data) and (current - self.last_send < 15):
130 151
             return
@@ -148,7 +169,7 @@ class GlitterPOS:
148 169
             self.gps.latitude,
149 170
             self.gps.longitude,
150 171
             self.gps.speed_knots,
151
-            self.gps.track_angle_deg
172
+            self.heading
152 173
         )
153 174
 
154 175
         print('   quality: {}'.format(self.gps.fix_quality))
@@ -157,15 +178,33 @@ class GlitterPOS:
157 178
         # Send a location packet:
158 179
         self.radio_tx('l', send_packet)
159 180
 
181
+    def update_heading(self):
182
+        mag_x, mag_y, mag_z = self.compass.magnetometer
183
+        # print('Magnetometer: ({0:10.3f}, {1:10.3f}, {2:10.3f})'.format(mag_x, mag_y, mag_z))
184
+        mag_x = map_range(mag_x, MAG_MIN[0], MAG_MAX[0], -1, 1)
185
+        mag_y = map_range(mag_y, MAG_MIN[1], MAG_MAX[1], -1, 1)
186
+        mag_z = map_range(mag_z, MAG_MIN[2], MAG_MAX[2], -1, 1)
187
+
188
+        heading_mag = (math.atan2(mag_y, mag_x) * 180) / math.pi
189
+        if heading_mag < 0:
190
+            heading_mag = 360 + heading_mag
191
+
192
+        # Account for declination (given in radians above):
193
+        heading = heading_mag + (DECLINATION_RAD * 180 / math.pi)
194
+        if heading > 360:
195
+            heading = heading - 360
196
+
197
+        print('heading: {}'.format(heading))
198
+        self.heading = heading
199
+
160 200
     def radio_tx(self, msg_type, msg):
161 201
         """send a packet over radio with id prefix and checksum"""
162 202
         # XXX: implement checksum?
163 203
         packet = 'e:' + msg_type + ':' + str(self.electron_id) + ':' + msg
164
-        packet_with_crc = packet + ':' + str(crc16pure.crc16xmodem(bytes(packet, 'ascii')))
165
-        print('   sending: ' + packet_with_crc)
204
+        print('   sending: ' + packet)
166 205
 
167 206
         # Blocking, max of 252 bytes:
168
-        self.rfm9x.send(packet_with_crc)
207
+        self.rfm9x.send(packet)
169 208
         self.last_send = time.monotonic()
170 209
 
171 210
     def radio_rx(self, timeout=0.5):
@@ -202,12 +241,20 @@ class GlitterPOS:
202 241
     def display_pixels(self):
203 242
         self.pixels.fill((0, 0, 0))
204 243
 
244
+        if not self.gps.has_fix:
245
+            return
246
+
205 247
         for electron in self.electrons:
206
-            angle_to_electron = compass_bearing(self.coords, self.electrons[electron])
207
-            # print('angle to ' + str(electron) + ': ' + str(angle_to_electron))
248
+            bearing_to_electron = compass_bearing(self.coords, self.electrons[electron])
249
+
250
+            # Treat current compass heading as our origin point for display purposes:
251
+            display_bearing = bearing_to_electron - self.heading
252
+            if display_bearing < 0:
253
+                display_bearing = display_bearing + 360
254
+
255
+            pixel = bearing_to_pixel(display_bearing)
256
+            # print('display pixel: {}'.format(pixel))
208 257
 
209
-            # Subtract from 16 since the neopixel ring runs counterclockwise:
210
-            pixel = 16 - int(round((angle_to_electron / 360) * 16))
211 258
             color = (15, 15, 15)
212 259
             if electron in COLOR_LOOKUP:
213 260
                 color = COLOR_LOOKUP[electron]

+ 20
- 0
src/glitterpos_util.py View File

@@ -1,3 +1,4 @@
1
+"""a handful of utility functions used by GlitterPOS."""
1 2
 import math
2 3
 import time
3 4
 
@@ -44,6 +45,25 @@ def compass_bearing(pointA, pointB):
44 45
 
45 46
     return compass_bearing
46 47
 
48
+def bearing_to_pixel(bearing, count=16):
49
+    # Subtract from count since the neopixel ring runs counterclockwise:
50
+    pixel = count - int(round((bearing / 360) * count))
51
+    if pixel == 16:
52
+      return 0
53
+    return pixel
54
+
55
+def map_range(x, in_min, in_max, out_min, out_max):
56
+    """
57
+    Maps a number from one range to another.
58
+    :return: Returns value mapped to new range
59
+    :rtype: float
60
+    """
61
+    mapped = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
62
+    if out_min <= out_max:
63
+        return max(min(mapped, out_max), out_min)
64
+
65
+    return min(max(mapped, out_max), out_min)
66
+
47 67
 def timestamp():
48 68
     """print a human-readable timestamp"""
49 69
     timestamp = time.localtime()

+ 431
- 0
src/lib/adafruit_lsm9ds1.py View File

@@ -0,0 +1,431 @@
1
+# The MIT License (MIT)
2
+#
3
+# Copyright (c) 2017 Tony DiCola for Adafruit Industries
4
+#
5
+# Permission is hereby granted, free of charge, to any person obtaining a copy
6
+# of this software and associated documentation files (the "Software"), to deal
7
+# in the Software without restriction, including without limitation the rights
8
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+# copies of the Software, and to permit persons to whom the Software is
10
+# furnished to do so, subject to the following conditions:
11
+#
12
+# The above copyright notice and this permission notice shall be included in
13
+# all copies or substantial portions of the Software.
14
+#
15
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+# THE SOFTWARE.
22
+"""
23
+`adafruit_lsm9ds1`
24
+====================================================
25
+
26
+CircuitPython module for the LSM9DS1 accelerometer, magnetometer, gyroscope.
27
+Based on the driver from:
28
+  https://github.com/adafruit/Adafruit_LSM9DS1
29
+
30
+See examples/simpletest.py for a demo of the usage.
31
+
32
+* Author(s): Tony DiCola
33
+"""
34
+
35
+__version__ = "1.0.1"
36
+__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LSM9DS1.git"
37
+
38
+import time
39
+try:
40
+    import struct
41
+except ImportError:
42
+    import ustruct as struct
43
+
44
+import adafruit_bus_device.i2c_device as i2c_device
45
+import adafruit_bus_device.spi_device as spi_device
46
+from micropython import const
47
+
48
+# Internal constants and register values:
49
+# pylint: disable=bad-whitespace
50
+_LSM9DS1_ADDRESS_ACCELGYRO       = const(0x6B)
51
+_LSM9DS1_ADDRESS_MAG             = const(0x1E)
52
+_LSM9DS1_XG_ID                   = const(0b01101000)
53
+_LSM9DS1_MAG_ID                  = const(0b00111101)
54
+_LSM9DS1_ACCEL_MG_LSB_2G         = 0.061
55
+_LSM9DS1_ACCEL_MG_LSB_4G         = 0.122
56
+_LSM9DS1_ACCEL_MG_LSB_8G         = 0.244
57
+_LSM9DS1_ACCEL_MG_LSB_16G        = 0.732
58
+_LSM9DS1_MAG_MGAUSS_4GAUSS       = 0.14
59
+_LSM9DS1_MAG_MGAUSS_8GAUSS       = 0.29
60
+_LSM9DS1_MAG_MGAUSS_12GAUSS      = 0.43
61
+_LSM9DS1_MAG_MGAUSS_16GAUSS      = 0.58
62
+_LSM9DS1_GYRO_DPS_DIGIT_245DPS   = 0.00875
63
+_LSM9DS1_GYRO_DPS_DIGIT_500DPS   = 0.01750
64
+_LSM9DS1_GYRO_DPS_DIGIT_2000DPS  = 0.07000
65
+_LSM9DS1_TEMP_LSB_DEGREE_CELSIUS = 8 # 1°C = 8, 25° = 200, etc.
66
+_LSM9DS1_REGISTER_WHO_AM_I_XG    = const(0x0F)
67
+_LSM9DS1_REGISTER_CTRL_REG1_G    = const(0x10)
68
+_LSM9DS1_REGISTER_CTRL_REG2_G    = const(0x11)
69
+_LSM9DS1_REGISTER_CTRL_REG3_G    = const(0x12)
70
+_LSM9DS1_REGISTER_TEMP_OUT_L     = const(0x15)
71
+_LSM9DS1_REGISTER_TEMP_OUT_H     = const(0x16)
72
+_LSM9DS1_REGISTER_STATUS_REG     = const(0x17)
73
+_LSM9DS1_REGISTER_OUT_X_L_G      = const(0x18)
74
+_LSM9DS1_REGISTER_OUT_X_H_G      = const(0x19)
75
+_LSM9DS1_REGISTER_OUT_Y_L_G      = const(0x1A)
76
+_LSM9DS1_REGISTER_OUT_Y_H_G      = const(0x1B)
77
+_LSM9DS1_REGISTER_OUT_Z_L_G      = const(0x1C)
78
+_LSM9DS1_REGISTER_OUT_Z_H_G      = const(0x1D)
79
+_LSM9DS1_REGISTER_CTRL_REG4      = const(0x1E)
80
+_LSM9DS1_REGISTER_CTRL_REG5_XL   = const(0x1F)
81
+_LSM9DS1_REGISTER_CTRL_REG6_XL   = const(0x20)
82
+_LSM9DS1_REGISTER_CTRL_REG7_XL   = const(0x21)
83
+_LSM9DS1_REGISTER_CTRL_REG8      = const(0x22)
84
+_LSM9DS1_REGISTER_CTRL_REG9      = const(0x23)
85
+_LSM9DS1_REGISTER_CTRL_REG10     = const(0x24)
86
+_LSM9DS1_REGISTER_OUT_X_L_XL     = const(0x28)
87
+_LSM9DS1_REGISTER_OUT_X_H_XL     = const(0x29)
88
+_LSM9DS1_REGISTER_OUT_Y_L_XL     = const(0x2A)
89
+_LSM9DS1_REGISTER_OUT_Y_H_XL     = const(0x2B)
90
+_LSM9DS1_REGISTER_OUT_Z_L_XL     = const(0x2C)
91
+_LSM9DS1_REGISTER_OUT_Z_H_XL     = const(0x2D)
92
+_LSM9DS1_REGISTER_WHO_AM_I_M     = const(0x0F)
93
+_LSM9DS1_REGISTER_CTRL_REG1_M    = const(0x20)
94
+_LSM9DS1_REGISTER_CTRL_REG2_M    = const(0x21)
95
+_LSM9DS1_REGISTER_CTRL_REG3_M    = const(0x22)
96
+_LSM9DS1_REGISTER_CTRL_REG4_M    = const(0x23)
97
+_LSM9DS1_REGISTER_CTRL_REG5_M    = const(0x24)
98
+_LSM9DS1_REGISTER_STATUS_REG_M   = const(0x27)
99
+_LSM9DS1_REGISTER_OUT_X_L_M      = const(0x28)
100
+_LSM9DS1_REGISTER_OUT_X_H_M      = const(0x29)
101
+_LSM9DS1_REGISTER_OUT_Y_L_M      = const(0x2A)
102
+_LSM9DS1_REGISTER_OUT_Y_H_M      = const(0x2B)
103
+_LSM9DS1_REGISTER_OUT_Z_L_M      = const(0x2C)
104
+_LSM9DS1_REGISTER_OUT_Z_H_M      = const(0x2D)
105
+_LSM9DS1_REGISTER_CFG_M          = const(0x30)
106
+_LSM9DS1_REGISTER_INT_SRC_M      = const(0x31)
107
+_MAGTYPE                         = True
108
+_XGTYPE                          = False
109
+_SENSORS_GRAVITY_STANDARD        = 9.80665
110
+
111
+# User facing constants/module globals.
112
+ACCELRANGE_2G                = (0b00 << 3)
113
+ACCELRANGE_16G               = (0b01 << 3)
114
+ACCELRANGE_4G                = (0b10 << 3)
115
+ACCELRANGE_8G                = (0b11 << 3)
116
+MAGGAIN_4GAUSS               = (0b00 << 5)  # +/- 4 gauss
117
+MAGGAIN_8GAUSS               = (0b01 << 5)  # +/- 8 gauss
118
+MAGGAIN_12GAUSS              = (0b10 << 5)  # +/- 12 gauss
119
+MAGGAIN_16GAUSS              = (0b11 << 5)  # +/- 16 gauss
120
+GYROSCALE_245DPS             = (0b00 << 4)  # +/- 245 degrees/s rotation
121
+GYROSCALE_500DPS             = (0b01 << 4)  # +/- 500 degrees/s rotation
122
+GYROSCALE_2000DPS            = (0b11 << 4)  # +/- 2000 degrees/s rotation
123
+# pylint: enable=bad-whitespace
124
+
125
+
126
+def _twos_comp(val, bits):
127
+    # Convert an unsigned integer in 2's compliment form of the specified bit
128
+    # length to its signed integer value and return it.
129
+    if val & (1 << (bits - 1)) != 0:
130
+        return val - (1 << bits)
131
+    return val
132
+
133
+
134
+class LSM9DS1:
135
+    """Driver for the LSM9DS1 accelerometer, magnetometer, gyroscope."""
136
+
137
+    # Class-level buffer for reading and writing data with the sensor.
138
+    # This reduces memory allocations but means the code is not re-entrant or
139
+    # thread safe!
140
+    _BUFFER = bytearray(6)
141
+
142
+    def __init__(self):
143
+        # soft reset & reboot accel/gyro
144
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG8, 0x05)
145
+        # soft reset & reboot magnetometer
146
+        self._write_u8(_MAGTYPE, _LSM9DS1_REGISTER_CTRL_REG2_M, 0x0C)
147
+        time.sleep(0.01)
148
+        # Check ID registers.
149
+        if self._read_u8(_XGTYPE, _LSM9DS1_REGISTER_WHO_AM_I_XG) != _LSM9DS1_XG_ID or \
150
+           self._read_u8(_MAGTYPE, _LSM9DS1_REGISTER_WHO_AM_I_M) != _LSM9DS1_MAG_ID:
151
+            raise RuntimeError('Could not find LSM9DS1, check wiring!')
152
+        # enable gyro continuous
153
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG1_G, 0xC0) # on XYZ
154
+        # Enable the accelerometer continous
155
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG5_XL, 0x38)
156
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG6_XL, 0xC0)
157
+        # enable mag continuous
158
+        self._write_u8(_MAGTYPE, _LSM9DS1_REGISTER_CTRL_REG3_M, 0x00)
159
+        # Set default ranges for the various sensors
160
+        self._accel_mg_lsb = None
161
+        self._mag_mgauss_lsb = None
162
+        self._gyro_dps_digit = None
163
+        self.accel_range = ACCELRANGE_2G
164
+        self.mag_gain = MAGGAIN_4GAUSS
165
+        self.gyro_scale = GYROSCALE_245DPS
166
+
167
+    @property
168
+    def accel_range(self):
169
+        """Get and set the accelerometer range.  Must be a value of:
170
+          - ACCELRANGE_2G
171
+          - ACCELRANGE_4G
172
+          - ACCELRANGE_8G
173
+          - ACCELRANGE_16G
174
+        """
175
+        reg = self._read_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG6_XL)
176
+        return (reg & 0b00011000) & 0xFF
177
+
178
+    @accel_range.setter
179
+    def accel_range(self, val):
180
+        assert val in (ACCELRANGE_2G, ACCELRANGE_4G, ACCELRANGE_8G,
181
+                       ACCELRANGE_16G)
182
+        reg = self._read_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG6_XL)
183
+        reg = (reg & ~(0b00011000)) & 0xFF
184
+        reg |= val
185
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG6_XL, reg)
186
+        if val == ACCELRANGE_2G:
187
+            self._accel_mg_lsb = _LSM9DS1_ACCEL_MG_LSB_2G
188
+        elif val == ACCELRANGE_4G:
189
+            self._accel_mg_lsb = _LSM9DS1_ACCEL_MG_LSB_4G
190
+        elif val == ACCELRANGE_8G:
191
+            self._accel_mg_lsb = _LSM9DS1_ACCEL_MG_LSB_8G
192
+        elif val == ACCELRANGE_16G:
193
+            self._accel_mg_lsb = _LSM9DS1_ACCEL_MG_LSB_16G
194
+
195
+    @property
196
+    def mag_gain(self):
197
+        """Get and set the magnetometer gain.  Must be a value of:
198
+          - MAGGAIN_4GAUSS
199
+          - MAGGAIN_8GAUSS
200
+          - MAGGAIN_12GAUSS
201
+          - MAGGAIN_16GAUSS
202
+        """
203
+        reg = self._read_u8(_MAGTYPE, _LSM9DS1_REGISTER_CTRL_REG2_M)
204
+        return (reg & 0b01100000) & 0xFF
205
+
206
+    @mag_gain.setter
207
+    def mag_gain(self, val):
208
+        assert val in (MAGGAIN_4GAUSS, MAGGAIN_8GAUSS, MAGGAIN_12GAUSS,
209
+                       MAGGAIN_16GAUSS)
210
+        reg = self._read_u8(_MAGTYPE, _LSM9DS1_REGISTER_CTRL_REG2_M)
211
+        reg = (reg & ~(0b01100000)) & 0xFF
212
+        reg |= val
213
+        self._write_u8(_MAGTYPE, _LSM9DS1_REGISTER_CTRL_REG2_M, reg)
214
+        if val == MAGGAIN_4GAUSS:
215
+            self._mag_mgauss_lsb = _LSM9DS1_MAG_MGAUSS_4GAUSS
216
+        elif val == MAGGAIN_8GAUSS:
217
+            self._mag_mgauss_lsb = _LSM9DS1_MAG_MGAUSS_8GAUSS
218
+        elif val == MAGGAIN_12GAUSS:
219
+            self._mag_mgauss_lsb = _LSM9DS1_MAG_MGAUSS_12GAUSS
220
+        elif val == MAGGAIN_16GAUSS:
221
+            self._mag_mgauss_lsb = _LSM9DS1_MAG_MGAUSS_16GAUSS
222
+
223
+    @property
224
+    def gyro_scale(self):
225
+        """Get and set the gyroscope scale.  Must be a value of:
226
+          - GYROSCALE_245DPS
227
+          - GYROSCALE_500DPS
228
+          - GYROSCALE_2000DPS
229
+        """
230
+        reg = self._read_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG1_G)
231
+        return (reg & 0b00110000) & 0xFF
232
+
233
+    @gyro_scale.setter
234
+    def gyro_scale(self, val):
235
+        assert val in (GYROSCALE_245DPS, GYROSCALE_500DPS, GYROSCALE_2000DPS)
236
+        reg = self._read_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG1_G)
237
+        reg = (reg & ~(0b00110000)) & 0xFF
238
+        reg |= val
239
+        self._write_u8(_XGTYPE, _LSM9DS1_REGISTER_CTRL_REG1_G, reg)
240
+        if val == GYROSCALE_245DPS:
241
+            self._gyro_dps_digit = _LSM9DS1_GYRO_DPS_DIGIT_245DPS
242
+        elif val == GYROSCALE_500DPS:
243
+            self._gyro_dps_digit = _LSM9DS1_GYRO_DPS_DIGIT_500DPS
244
+        elif val == GYROSCALE_2000DPS:
245
+            self._gyro_dps_digit = _LSM9DS1_GYRO_DPS_DIGIT_2000DPS
246
+
247
+    def read_accel_raw(self):
248
+        """Read the raw accelerometer sensor values and return it as a
249
+        3-tuple of X, Y, Z axis values that are 16-bit unsigned values.  If you
250
+        want the acceleration in nice units you probably want to use the
251
+        accelerometer property!
252
+        """
253
+        # Read the accelerometer
254
+        self._read_bytes(_XGTYPE, 0x80 | _LSM9DS1_REGISTER_OUT_X_L_XL, 6,
255
+                         self._BUFFER)
256
+        raw_x, raw_y, raw_z = struct.unpack_from('<hhh', self._BUFFER[0:6])
257
+        return (raw_x, raw_y, raw_z)
258
+
259
+    @property
260
+    def accelerometer(self):
261
+        """Get the accelerometer X, Y, Z axis values as a 3-tuple of
262
+        m/s^2 values.
263
+        """
264
+        raw = self.read_accel_raw()
265
+        return map(lambda x: x * self._accel_mg_lsb / 1000.0 * _SENSORS_GRAVITY_STANDARD,
266
+                   raw)
267
+
268
+    def read_mag_raw(self):
269
+        """Read the raw magnetometer sensor values and return it as a
270
+        3-tuple of X, Y, Z axis values that are 16-bit unsigned values.  If you
271
+        want the magnetometer in nice units you probably want to use the
272
+        magnetometer property!
273
+        """
274
+        # Read the magnetometer
275
+        self._read_bytes(_MAGTYPE, 0x80 | _LSM9DS1_REGISTER_OUT_X_L_M, 6,
276
+                         self._BUFFER)
277
+        raw_x, raw_y, raw_z = struct.unpack_from('<hhh', self._BUFFER[0:6])
278
+        return (raw_x, raw_y, raw_z)
279
+
280
+    @property
281
+    def magnetometer(self):
282
+        """Get the magnetometer X, Y, Z axis values as a 3-tuple of
283
+        gauss values.
284
+        """
285
+        raw = self.read_mag_raw()
286
+        return map(lambda x: x * self._mag_mgauss_lsb / 1000.0, raw)
287
+
288
+    def read_gyro_raw(self):
289
+        """Read the raw gyroscope sensor values and return it as a
290
+        3-tuple of X, Y, Z axis values that are 16-bit unsigned values.  If you
291
+        want the gyroscope in nice units you probably want to use the
292
+        gyroscope property!
293
+        """
294
+        # Read the gyroscope
295
+        self._read_bytes(_XGTYPE, 0x80 | _LSM9DS1_REGISTER_OUT_X_L_G, 6,
296
+                         self._BUFFER)
297
+        raw_x, raw_y, raw_z = struct.unpack_from('<hhh', self._BUFFER[0:6])
298
+        return (raw_x, raw_y, raw_z)
299
+
300
+    @property
301
+    def gyroscope(self):
302
+        """Get the gyroscope X, Y, Z axis values as a 3-tuple of
303
+        degrees/second values.
304
+        """
305
+        raw = self.read_mag_raw()
306
+        return map(lambda x: x * self._gyro_dps_digit, raw)
307
+
308
+    def read_temp_raw(self):
309
+        """Read the raw temperature sensor value and return it as a 12-bit
310
+        signed value.  If you want the temperature in nice units you probably
311
+        want to use the temperature property!
312
+        """
313
+        # Read temp sensor
314
+        self._read_bytes(_XGTYPE, 0x80 | _LSM9DS1_REGISTER_TEMP_OUT_L, 2,
315
+                         self._BUFFER)
316
+        temp = ((self._BUFFER[1] << 8) | self._BUFFER[0]) >> 4
317
+        return _twos_comp(temp, 12)
318
+
319
+    @property
320
+    def temperature(self):
321
+        """Get the temperature of the sensor in degrees Celsius."""
322
+        # This is just a guess since the starting point (21C here) isn't documented :(
323
+        # See discussion from:
324
+        #  https://github.com/kriswiner/LSM9DS1/issues/3
325
+        temp = self.read_temp_raw()
326
+        temp = 27.5 + temp/16
327
+        return temp
328
+
329
+    def _read_u8(self, sensor_type, address):
330
+        # Read an 8-bit unsigned value from the specified 8-bit address.
331
+        # The sensor_type boolean should be _MAGTYPE when talking to the
332
+        # magnetometer, or _XGTYPE when talking to the accel or gyro.
333
+        # MUST be implemented by subclasses!
334
+        raise NotImplementedError()
335
+
336
+    def _read_bytes(self, sensor_type, address, count, buf):
337
+        # Read a count number of bytes into buffer from the provided 8-bit
338
+        # register address.  The sensor_type boolean should be _MAGTYPE when
339
+        # talking to the magnetometer, or _XGTYPE when talking to the accel or
340
+        # gyro.  MUST be implemented by subclasses!
341
+        raise NotImplementedError()
342
+
343
+    def _write_u8(self, sensor_type, address, val):
344
+        # Write an 8-bit unsigned value to the specified 8-bit address.
345
+        # The sensor_type boolean should be _MAGTYPE when talking to the
346
+        # magnetometer, or _XGTYPE when talking to the accel or gyro.
347
+        # MUST be implemented by subclasses!
348
+        raise NotImplementedError()
349
+
350
+
351
+class LSM9DS1_I2C(LSM9DS1):
352
+    """Driver for the LSM9DS1 connect over I2C."""
353
+
354
+    def __init__(self, i2c):
355
+        self._mag_device = i2c_device.I2CDevice(i2c, _LSM9DS1_ADDRESS_MAG)
356
+        self._xg_device = i2c_device.I2CDevice(i2c, _LSM9DS1_ADDRESS_ACCELGYRO)
357
+        super().__init__()
358
+
359
+    def _read_u8(self, sensor_type, address):
360
+        if sensor_type == _MAGTYPE:
361
+            device = self._mag_device
362
+        else:
363
+            device = self._xg_device
364
+        with device as i2c:
365
+            self._BUFFER[0] = address & 0xFF
366
+            i2c.write(self._BUFFER, end=1, stop=False)
367
+            i2c.readinto(self._BUFFER, end=1)
368
+        return self._BUFFER[0]
369
+
370
+    def _read_bytes(self, sensor_type, address, count, buf):
371
+        if sensor_type == _MAGTYPE:
372
+            device = self._mag_device
373
+        else:
374
+            device = self._xg_device
375
+        with device as i2c:
376
+            buf[0] = address & 0xFF
377
+            i2c.write(buf, end=1, stop=False)
378
+            i2c.readinto(buf, end=count)
379
+
380
+    def _write_u8(self, sensor_type, address, val):
381
+        if sensor_type == _MAGTYPE:
382
+            device = self._mag_device
383
+        else:
384
+            device = self._xg_device
385
+        with device as i2c:
386
+            self._BUFFER[0] = address & 0xFF
387
+            self._BUFFER[1] = val & 0xFF
388
+            i2c.write(self._BUFFER, end=2)
389
+
390
+
391
+class LSM9DS1_SPI(LSM9DS1):
392
+    """Driver for the LSM9DS1 connect over SPI."""
393
+
394
+    def __init__(self, spi, xgcs, mcs):
395
+        self._mag_device = spi_device.I2CDevice(spi, mcs)
396
+        self._xg_device = spi_device.I2CDevice(spi, xgcs)
397
+        super().__init__()
398
+
399
+    def _read_u8(self, sensor_type, address):
400
+        if sensor_type == _MAGTYPE:
401
+            device = self._mag_device
402
+        else:
403
+            device = self._xg_device
404
+        with device as spi:
405
+            spi.configure(baudrate=200000, phase=0, polarity=0)
406
+            self._BUFFER[0] = (address | 0x80) & 0xFF
407
+            spi.write(self._BUFFER, end=1)
408
+            spi.readinto(self._BUFFER, end=1)
409
+        return self._BUFFER[0]
410
+
411
+    def _read_bytes(self, sensor_type, address, count, buf):
412
+        if sensor_type == _MAGTYPE:
413
+            device = self._mag_device
414
+        else:
415
+            device = self._xg_device
416
+        with device as spi:
417
+            spi.configure(baudrate=200000, phase=0, polarity=0)
418
+            buf[0] = (address | 0x80) & 0xFF
419
+            spi.write(buf, end=1)
420
+            spi.readinto(buf, end=count)
421
+
422
+    def _write_u8(self, sensor_type, address, val):
423
+        if sensor_type == _MAGTYPE:
424
+            device = self._mag_device
425
+        else:
426
+            device = self._xg_device
427
+        with device as spi:
428
+            spi.configure(baudrate=200000, phase=0, polarity=0)
429
+            self._BUFFER[0] = (address & 0x7F) & 0xFF
430
+            self._BUFFER[1] = val & 0xFF
431
+            spi.write(self._BUFFER, end=2)