GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

bricknil.sensor.sensor   A
last analyzed

Complexity

Total Complexity 20

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 172
dl 0
loc 539
rs 10
c 0
b 0
f 0
wmc 20

9 Methods

Rating   Name   Duplication   Size   Complexity  
A InternalTiltSensor.update_value() 0 9 2
A RemoteButtons.minus_pressed() 0 4 1
A DuploSpeedSensor.update_value() 0 12 5
A Button.__init__() 0 3 1
A RemoteButtons.plus_pressed() 0 4 1
A RemoteButtons.red_pressed() 0 4 1
A RemoteButtons.__init__() 0 5 2
A Button.activate_updates() 0 8 2
A ExternalTiltSensor.update_value() 0 18 5
1
# Copyright 2019 Virantha N. Ekanayake 
2
# 
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
# 
7
# http://www.apache.org/licenses/LICENSE-2.0
8
# 
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
15
"""Actual sensor and motor peripheral definitions from Boost and PoweredUp
16
"""
17
from curio import sleep, current_task, spawn  # Needed for motor speed ramp
18
19
from enum import Enum, IntEnum
20
from struct import pack
21
22
from ..const import Color
23
from .peripheral import Peripheral
24
25
class VisionSensor(Peripheral):
26
    """ Access the Boost Vision/Distance Sensor
27
28
        Only the sensing capabilities of this sensor is supported right now.
29
30
        - *sense_color*: Returns one of the 10 predefined colors
31
        - *sense_distance*: Distance from 0-7 in roughly inches
32
        - *sense_count*: Running count of waving your hand/item over the sensor (32-bit)
33
        - *sense_reflectivity*: Under distances of one inch, the inverse of the distance
34
        - *sense_ambient*: Distance under one inch (so inverse of the preceeding)
35
        - *sense_rgb*: R, G, B values (3 sets of uint16)
36
37
        Any combination of sense_color, sense_distance, sense_count, sense_reflectivity, 
38
        and sense_rgb is supported.
39
40
        Examples::
41
42
            # Basic distance sensor
43
            @attach(VisionSensor, name='vision', capabilities=['sense_color'])
44
            # Or use the capability Enum
45
            @attach(VisionSensor, name='vision', capabilities=[VisionSensor.capability.sense_color])
46
47
            # Distance and color sensor
48
            @attach(VisionSensor, name='vision', capabilities=['sense_color', 'sense_distance'])
49
50
            # Distance and rgb sensor with different thresholds to trigger updates
51
            @attach(VisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)])
52
53
        The values returned by the sensor will always be available in the instance variable
54
        `self.value`.  For example, when the `sense_color` and `sense_rgb` capabilities are 
55
        enabled, the following values will be stored and updated::
56
57
            self.value = { VisionSensor.capability.sense_color:  uint8,
58
                           VisionSensor.capability.sense_rgb: 
59
                                            [ uint16, uint16, uint16 ]
60
                         }
61
62
        Notes:
63
            The actual modes supported by the sensor are as follows:
64
65
            -  0 = color (0-10)
66
            -  1 = IR proximity (0-7)
67
            -  2 = count (32-bit int)
68
            -  3 = Reflt   (inverse of distance when closer than 1")
69
            -  4 = Amb  (distance when closer than 1")
70
            -  5 = COL (output) ?
71
            -  6 = RGB I
72
            -  7 = IR tx (output) ?
73
            -  8 = combined:  Color byte, Distance byte, 0xFF, Reflected light
74
75
    """
76
77
    _sensor_id = 0x0025
78
    capability = Enum("capability", 
79
                      [('sense_color', 0),
80
                       ('sense_distance', 1),
81
                       ('sense_count', 2),
82
                       ('sense_reflectivity', 3),
83
                       ('sense_ambient', 4),
84
                       ('sense_rgb', 6),
85
                       ])
86
87
    datasets = { capability.sense_color: (1, 1),
88
                 capability.sense_distance: (1, 1),
89
                 capability.sense_count: (1, 4),  # 4-bytes (32-bit)
90
                 capability.sense_reflectivity: (1, 1),
91
                 capability.sense_ambient: (1, 1),
92
                 capability.sense_rgb: (3, 2)   # 3 16-bit values
93
                }
94
95
    allowed_combo = [ capability.sense_color,
96
                      capability.sense_distance,
97
                      capability.sense_count,
98
                      capability.sense_reflectivity,
99
                      capability.sense_rgb,
100
                    ]
101
102
class InternalTiltSensor(Peripheral):
103
    """
104
        Access the internal tilt sensor in the Boost Move Hub.
105
        
106
        The various modes are:
107
108
        - **sense_angle** - X, Y angles.  Both are 0 if hub is lying flat with button up
109
        - **sense_tilt** - value from 0-9 if hub is tilted around any of its axis. Seems to be
110
          a rough mesaure of how much the hub is tilted away from lying flat.
111
          There is no update for just a translation along an axis
112
        - **sense_orientation** - returns one of the nine orientations below (0-9)
113
            - `InternalTiltSensor.orientation`.up = flat with button on top
114
            - `InternalTiltSensor.orientation`.right - standing up on side closest to button
115
            - `InternalTiltSensor.orientation`.left - standing up on side furthest from button
116
            - `InternalTiltSensor.orientation`.far_side - on long face facing away
117
            - `InternalTiltSensor.orientation`.near_side -  on long face facing you
118
            - `InternalTiltSensor.orientation`.down - upside down
119
        - **sense_impact** - 32-bit count of impacts to sensor
120
        - **sense_acceleration_3_axis** - 3 bytes of raw accelerometer data.
121
122
        Any combination of the above modes are allowed.
123
124
        Examples::
125
126
            # Basic tilt sensor
127
            @attach(InternalTiltSensor, name='tilt', capabilities=['sense_tilt'])
128
            # Or use the capability Enum
129
            @attach(InternalTiltSensor, name='tilt', capabilities=[InternalTiltSensor.sense_tilt])
130
131
            # Tilt and orientation sensor
132
            @attach(InternalTiltSensor, name='tilt', capabilities=['sense_tilt, sense_orientation'])
133
134
        The values returned by the sensor will always be available in the
135
        instance variable `self.value`.  For example, when the `sense_angle`
136
        and `sense_orientation` capabilities are enabled, the following values
137
        will be stored and updated::
138
139
            self.value = { InternalTiltSensor.capability.sense_angle:  [uint8, uint8],
140
                           InternalTiltSensor.capability.sense_orientation: 
141
                                            Enum(InternalTiltSensor.orientation)
142
                         }
143
    """
144
    _sensor_id = 0x0028
145
    capability = Enum("capability", 
146
                      [('sense_angle', 0),
147
                       ('sense_tilt', 1),
148
                       ('sense_orientation', 2),
149
                       ('sense_impact', 3),
150
                       ('sense_acceleration_3_axis', 4),
151
                       ])
152
153
    datasets = { capability.sense_angle: (2, 1),
154
                 capability.sense_tilt: (1, 1),
155
                 capability.sense_orientation: (1, 1),  
156
                 capability.sense_impact: (1, 4),
157
                 capability.sense_acceleration_3_axis: (3, 1),
158
                }
159
160
    allowed_combo = [ capability.sense_angle,
161
                      capability.sense_tilt,
162
                      capability.sense_orientation,
163
                      capability.sense_impact,
164
                      capability.sense_acceleration_3_axis,
165
                    ]
166
167
    orientation = Enum('orientation', 
168
                        {   'up': 0,
169
                            'right': 1, 
170
                            'left': 2, 
171
                            'far_side':3,
172
                            'near_side':4,
173
                            'down':5,
174
                        })
175
176
177
    async def update_value(self, msg_bytes):
178
        """If sense_orientation, then substitute the `IntenalTiltSensor.orientation`
179
           enumeration value into the self.value dict.  Otherwise, don't do anything
180
           special to the self.value dict.
181
        """
182
        await super().update_value(msg_bytes)
183
        so = self.capability.sense_orientation
184
        if so in self.value:
185
            self.value[so] = self.orientation(self.value[so])
186
187
188
189
class ExternalMotionSensor(Peripheral):
190
    """Access the external motion sensor (IR) provided in the Wedo sets
191
192
       Measures distance to object, or if an object is moving (distance varying). 
193
194
       - **sense_distance** - distance in inches from 0-10
195
       - **sense_count**  - Increments every time it detects motion (32-bit value)
196
197
       These are mutually exclusive (non-combinable)
198
199
       Examples::
200
201
            # Distance measurement
202
            @attach(ExternalMotionSensor, name='motion_sensor', capabilities=['sense_distance'])
203
204
205
            # Motion detection
206
            @attach(ExternalMotionSensor, name='motion_sensor', capabilities=['sense_count'])
207
    """
208
    _sensor_id = 0x0023
209
    capability = Enum("capability", 
210
                      [('sense_distance', 0),
211
                       ('sense_count', 1),
212
                       ])
213
214
    datasets = { capability.sense_distance: (1, 1),  
215
                 capability.sense_count: (1, 4),
216
                }
217
    allowed_combo = [ ]
218
219
class ExternalTiltSensor(Peripheral):
220
    """Access the External tilt sensor provided in the Wedo sets
221
222
       Three modes are supported (non-combinable):
223
224
       - **sense_angle** - X (around long axis), Y (around short axis) angles.  -45 to 45 degrees
225
       - **sense_orientation** - returns one of the orientations below (wrt looking at the sensor from the side opposite the wiring harness)
226
            - `ExternalTiltSensor.orientation`.up = flat with studs on top
227
            - `ExternalTiltSensor.orientation`.right = studs facing rigth
228
            - `ExternalTiltSensor.orientation`.left = studs facing left
229
            - `ExternalTiltSensor.orientation`.far_side = studs facing away from you
230
            - `ExternalTiltSensor.orientation`.near_side = studs facing towards you
231
       - **sense_impact** - Keeps a count of impacts, but sends three bytes (direction of hit?)
232
233
       These are mutually exclusive (non-combinable).
234
235
    """
236
    _sensor_id = 0x0022
237
    capability = Enum("capability", 
238
                      [('sense_angle', 0),
239
                       ('sense_orientation', 1),
240
                       ('sense_impact', 2),
241
                       ])
242
243
    datasets = { capability.sense_angle: (2, 1),  
244
                 capability.sense_orientation: (1, 1),
245
                 capability.sense_impact: (3, 1),
246
                }
247
    allowed_combo = [ ]
248
249
    orientation = Enum('orientation', 
250
                        {   'up': 0,
251
                            'right': 7, 
252
                            'left': 5, 
253
                            'far_side':3,
254
                            'near_side':9,
255
                        })
256
257
258
    async def update_value(self, msg_bytes):
259
        """If angle, convert the bytes being returned to twos complement ints
260
           If orientation, then convert to the `orientation` enumeration.
261
262
        """
263
        await super().update_value(msg_bytes)
264
        # No combinations possible, so only one capability with len(self.capabilities[])==1
265
        if self.capabilities[0] == self.capability.sense_angle:
266
            sa = self.capability.sense_angle
267
            sx, sy = self.value[sa]
268
            if sx & 128:  # negative sign bit
269
                sx = -(256-sx)
270
            if sy & 128:
271
                sy = -(256-sy)
272
            self.value[sa] = [sx, sy]
273
        elif self.capabilities[0] == self.capability.sense_orientation:
274
            so = self.capability.sense_orientation
275
            self.value[so] = self.orientation(self.value[so])
276
277
278
279
280
class RemoteButtons(Peripheral):
281
    """Represents one set of '+', '-', 'red' buttons on the PoweredHub Remote
282
283
       Each remote has two sets of buttons, on the left and right side.  Pick the one
284
       your want to attach to by using the port argument with either Port.L or Port.R.
285
286
       There are actually a few different modes that the hardware supports, but we are
287
       only going to use one of them called 'KEYSD' (see the notes in the documentation on the
288
       raw values reported by the hub).  This mode makes the remote send three values back
289
       in a list.  To access each button state, there are three helper methods provided 
290
       (see below)
291
292
       Examples::
293
294
            # Basic connection to the left buttons
295
            @attach(RemoteButtons, name='left_buttons', port=RemoteButtons.Port.L)
296
297
            # Getting values back in the handler
298
            async def left_buttons_change(self):
299
300
                is_plus_pressed = self.left_buttons.plus_pressed()
301
                is_minus_pressed = self.left_buttons.minus_pressed()
302
                is_red_pressed = self.left_buttons.red_pressed()
303
304
    """
305
306
    _sensor_id = 0x0037
307
    Port = Enum('Port', 'L R', start=0)
308
    Button = IntEnum('Button', 'PLUS RED MINUS', start=0)
309
    """The button index in the value list returned by the sensor"""
310
311
    capability = Enum('capability', {'sense_press':4},)
312
313
    datasets = { capability.sense_press: (3,1) }
314
    allowed_combo = []
315
316
    def __init__(self, name, port=None, capabilities=[]):
317
        """Maps the port names `L`, `R`"""
318
        if port:
319
            port = port.value
320
        super().__init__(name, port, capabilities)
321
322
    def plus_pressed(self):
323
        """Return whether `value` reflects that the PLUS button is pressed"""
324
        button_list = self.value[self.capability.sense_press]
325
        return button_list[self.Button.PLUS] == 1
326
    def minus_pressed(self):
327
        """Return whether `value` reflects that the MINUS button is pressed"""
328
        button_list = self.value[self.capability.sense_press]
329
        return button_list[self.Button.MINUS] == 1
330
    def red_pressed(self):
331
        """Return whether `value` reflects that the RED button is pressed"""
332
        button_list = self.value[self.capability.sense_press]
333
        return button_list[self.Button.RED] == 1
334
335
class Button(Peripheral):
336
    """ Register to be notified of button presses on the Hub (Boost or PoweredUp)
337
338
        This is actually a slight hack, since the Hub button is not a peripheral that is 
339
        attached like other sensors in the Lego protocol.  Instead, the buttons are accessed
340
        through Hub property messages.  We abstract away these special messages to make the
341
        button appear to be like any other peripheral sensor.
342
343
        Examples::
344
345
            @attach(Button, name='hub_btn')
346
347
        Notes:
348
            Since there is no attach I/O message from the hub to trigger the
349
            :func:`activate_updates` method, we instead insert a fake
350
            "attaach" message from this fake sensor on port 255 in the
351
            `BLEventQ.get_messages` method that is used to register for updates
352
            from a given sensor.
353
354
    """
355
    _sensor_id = 0x0005
356
    """Piggy back the hub button off the normal peripheral button id 0x0005.
357
       Might need to change this in the future"""
358
359
    capability = Enum('capability', {'sense_press':0})
360
361
    datasets = { capability.sense_press: (1,1)
362
               }
363
    allowed_combo = [capability.sense_press]
364
365
    def __init__(self, name, port=None, capabilities=[]):
366
        """Call super-class with port set to 255 """
367
        super().__init__(name, 255, capabilities)
368
369
    async def activate_updates(self):
370
        """Use a special Hub Properties button message updates activation message"""
371
        self.value = {}
372
        for cap in self.capabilities:
373
            self.value[cap] = [None]*self.datasets[cap][0]
374
375
        b = [0x00, 0x01, 0x02, 0x02]  # Button reports from "Hub Properties Message Type"
376
        await self.send_message(f'Activate button reports: port {self.port}', b) 
377
378
379
class DuploVisionSensor(Peripheral):
380
    """ Access the Duplo Vision/Distance Sensor
381
382
        - *sense_color*: Returns one of the 10 predefined colors
383
        - *sense_ctag*: Returns one of the 10 predefined tags
384
        - *sense_reflectivity*: Under distances of one inch, the inverse of the distance
385
        - *sense_rgb*: R, G, B values (3 sets of uint16)
386
387
        Any combination of sense_color, sense_ctag, sense_reflectivity, 
388
        and sense_rgb is supported.
389
390
        Examples::
391
392
            # Basic color sensor
393
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_color'])
394
            # Or use the capability Enum
395
            @attach(DuploVisionSensor, name='vision', capabilities=[DuploVisionSensor.capability.sense_color])
396
397
            # Ctag and reflectivity sensor
398
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_ctag', 'sense_reflectivity'])
399
400
            # Distance and rgb sensor with different thresholds to trigger updates
401
            @attach(DuploVisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)])
402
403
        The values returned by the sensor will always be available in the instance variable
404
        `self.value`.  For example, when the `sense_color` and `sense_rgb` capabilities are 
405
        enabled, the following values will be stored and updated::
406
407
            self.value = { DuploVisionSensor.capability.sense_color:  uint8,
408
                           DuploVisionSensor.capability.sense_rgb: 
409
                                            [ uint16, uint16, uint16 ]
410
                         }
411
412
        Notes:
413
            The actual modes supported by the sensor are as follows:
414
415
            -  0 = color (0-10)
416
            -  1 = ctag (32-bit int)
417
            -  2 = Reflt   (inverse of distance when closer than 1")
418
            -  3 = RGB I
419
    """
420
    _sensor_id = 0x002B
421
    capability = Enum("capability", 
422
                      [('sense_color', 0),
423
                       ('sense_ctag', 1),
424
                       ('sense_reflectivity', 2),
425
                       ('sense_rgb', 3),
426
                       ])
427
428
    datasets = { capability.sense_color: (1, 1),
429
                 capability.sense_ctag: (1, 1),  # 4-bytes (32-bit)
430
                 capability.sense_reflectivity: (1, 1),
431
                 capability.sense_rgb: (3, 2)   # 3 16-bit values
432
                }
433
434
    allowed_combo = [ capability.sense_color,
435
                      capability.sense_ctag,
436
                      capability.sense_reflectivity,
437
                      capability.sense_rgb,
438
                    ]
439
440
class VoltageSensor(Peripheral):
441
    """Voltage sensor
442
443
       Returns the raw mV value (0-3893) which probably needs to be scaled to 0-9600.
444
445
       It contains two capabilities, although they both appear to do the same thing:
446
       * sense_l
447
       * sense_s
448
449
       Examples::
450
451
            @attach(VoltageSensor, name='volts', capabilities=['sense_l'])
452
453
    """
454
    _sensor_id = 0x14
455
456
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
457
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
458
                capability.sense_l: (1, 2), 
459
               }
460
    allowed_combo = [ ]
461
462
class CurrentSensor(Peripheral):
463
    """Voltage sensor
464
465
       Returns the raw mA value (0-4095) which probably needs to be scaled to 0-2444.
466
467
       It contains two capabilities, although they both appear to do the same thing:
468
       * sense_l
469
       * sense_s
470
471
       Examples::
472
473
            @attach(CurrentSensor, name='cur', capabilities=['sense_l'])
474
475
    """
476
    _sensor_id = 0x15
477
478
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
479
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
480
                capability.sense_l: (1, 2), 
481
               }
482
    allowed_combo = [ ]
483
484
class DuploSpeedSensor(Peripheral):
485
    """Speedometer on Duplo train base that measures front wheel speed.
486
487
       This can measure the following values:
488
489
       - *sense_speed*: Returns the speed of the front wheels
490
       - *sense_count*: Keeps count of the number of revolutions the front wheels have spun
491
492
       Either or both can be enabled for measurement. 
493
494
       Examples::
495
496
            # Report speed changes
497
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed'])
498
499
            # Report all
500
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed', 'sense_count'])
501
502
       The values returned by the sensor will be in `self.value`.  For the first example, get the
503
       current speed by::
504
505
            speed = self.speed_sensor.value
506
        
507
       For the second example, the two values will be in a dict::
508
509
            speed = self.speed_sensor.value[DuploSpeedSensor.sense_speed]
510
            revs  = self.speed_sensor.value[DuploSpeedSensor.sense_count]
511
512
    """
513
    _sensor_id = 0x002C
514
    capability = Enum("capability", 
515
                      [('sense_speed', 0),
516
                       ('sense_count', 1),
517
                       ])
518
519
    datasets = { capability.sense_speed: Peripheral.Dataset(n=1, w=2, min=-300, max=300),
520
                 capability.sense_count: Peripheral.Dataset(n=1, w=4, min=-(1<<31), max=(1<<31-1)),
521
                }
522
523
    allowed_combo = [ capability.sense_speed,
524
                      capability.sense_count,
525
                    ]
526
527
    async def update_value(self, msg_bytes):
528
        """Hack to negate reverse speeds.  This should really be specified elsewehre
529
        """
530
        await super().update_value(msg_bytes)
531
        ss = self.capability.sense_speed
532
        sc = self.capability.sense_count
533
        if ss in self.value:
534
            if self.value[ss] & (1<<15):  # negative sign bit
535
                self.value[ss] = -((1<<16) - self.value[ss])
536
        if sc in self.value:
537
            if self.value[sc] & (1<<31):  # negative sign bit
538
                self.value[sc] = -((1<<32) - self.value[sc])
539
540