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.
Passed
Push — master ( a3394c...b99246 )
by Virantha
01:31
created

bricknil.sensor.sensor.RemoteButtons.__init__()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 4
dl 0
loc 5
rs 10
c 0
b 0
f 0
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
    async def update_value(self, msg_bytes):
258
        """If angle, convert the bytes being returned to twos complement ints
259
           If orientation, then convert to the `orientation` enumeration.
260
261
        """
262
        await super().update_value(msg_bytes)
263
        # No combinations possible, so only one capability with len(self.capabilities[])==1
264
        if self.capabilities[0] == self.capability.sense_angle:
265
            sa = self.capability.sense_angle
266
            sx, sy = self.value[sa]
267
            if sx & 128:  # negative sign bit
268
                sx = -(256-sx)
269
            if sy & 128:
270
                sy = -(256-sy)
271
            self.value[sa] = [sx, sy]
272
        elif self.capabilities[0] == self.capability.sense_orientation:
273
            so = self.capability.sense_orientation
274
            self.value[so] = self.orientation(self.value[so])
275
276
277
278
279
class RemoteButtons(Peripheral):
280
    """Represents one set of '+', '-', 'red' buttons on the PoweredHub Remote
281
282
       Each remote has two sets of buttons, on the left and right side.  Pick the one
283
       your want to attach to by using the port argument with either Port.L or Port.R.
284
285
       There are actually a few different modes that the hardware supports, but we are
286
       only going to use one of them called 'KEYSD' (see the notes in the documentation on the
287
       raw values reported by the hub).  This mode makes the remote send three values back
288
       in a list.  To access each button state, there are three helper methods provided 
289
       (see below)
290
291
       Examples::
292
293
            # Basic connection to the left buttons
294
            @attach(RemoteButtons, name='left_buttons', port=RemoteButtons.Port.L)
295
296
            # Getting values back in the handler
297
            async def left_buttons_change(self):
298
299
                is_plus_pressed = self.left_buttons.plus_pressed()
300
                is_minus_pressed = self.left_buttons.minus_pressed()
301
                is_red_pressed = self.left_buttons.red_pressed()
302
303
    """
304
305
    _sensor_id = 0x0037
306
    Port = Enum('Port', 'L R', start=0)
307
    Button = IntEnum('Button', 'PLUS RED MINUS', start=0)
308
    """The button index in the value list returned by the sensor"""
309
310
    capability = Enum('capability', {'sense_press':4},)
311
312
    datasets = { capability.sense_press: (3,1) }
313
    allowed_combo = []
314
315
    def __init__(self, name, port=None, capabilities=[]):
316
        """Maps the port names `L`, `R`"""
317
        if port:
318
            port = port.value
319
        super().__init__(name, port, capabilities)
320
321
    def plus_pressed(self):
322
        """Return whether `value` reflects that the PLUS button is pressed"""
323
        button_list = self.value[self.capability.sense_press]
324
        return button_list[self.Button.PLUS] == 1
325
    def minus_pressed(self):
326
        """Return whether `value` reflects that the MINUS button is pressed"""
327
        button_list = self.value[self.capability.sense_press]
328
        return button_list[self.Button.MINUS] == 1
329
    def red_pressed(self):
330
        """Return whether `value` reflects that the RED button is pressed"""
331
        button_list = self.value[self.capability.sense_press]
332
        return button_list[self.Button.RED] == 1
333
334
class Button(Peripheral):
335
    """ Register to be notified of button presses on the Hub (Boost or PoweredUp)
336
337
        This is actually a slight hack, since the Hub button is not a peripheral that is 
338
        attached like other sensors in the Lego protocol.  Instead, the buttons are accessed
339
        through Hub property messages.  We abstract away these special messages to make the
340
        button appear to be like any other peripheral sensor.
341
342
        Examples::
343
344
            @attach(Button, name='hub_btn')
345
346
        Notes:
347
            Since there is no attach I/O message from the hub to trigger the
348
            :func:`activate_updates` method, we instead insert a fake
349
            "attaach" message from this fake sensor on port 255 in the
350
            `BLEventQ.get_messages` method that is used to register for updates
351
            from a given sensor.
352
353
    """
354
    _sensor_id = 0x0005
355
    """Piggy back the hub button off the normal peripheral button id 0x0005.
356
       Might need to change this in the future"""
357
358
    capability = Enum('capability', {'sense_press':0})
359
360
    datasets = { capability.sense_press: (1,1)
361
               }
362
    allowed_combo = [capability.sense_press]
363
364
    def __init__(self, name, port=None, capabilities=[]):
365
        """Call super-class with port set to 255 """
366
        super().__init__(name, 255, capabilities)
367
368
    async def activate_updates(self):
369
        """Use a special Hub Properties button message updates activation message"""
370
        self.value = {}
371
        for cap in self.capabilities:
372
            self.value[cap] = [None]*self.datasets[cap][0]
373
374
        b = [0x00, 0x01, 0x02, 0x02]  # Button reports from "Hub Properties Message Type"
375
        await self.send_message(f'Activate button reports: port {self.port}', b) 
376
377
378
class DuploVisionSensor(Peripheral):
379
    """ Access the Duplo Vision/Distance Sensor
380
381
        - *sense_color*: Returns one of the 10 predefined colors
382
        - *sense_ctag*: Returns one of the 10 predefined tags
383
        - *sense_reflectivity*: Under distances of one inch, the inverse of the distance
384
        - *sense_rgb*: R, G, B values (3 sets of uint16)
385
386
        Any combination of sense_color, sense_ctag, sense_reflectivity, 
387
        and sense_rgb is supported.
388
389
        Examples::
390
391
            # Basic color sensor
392
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_color'])
393
            # Or use the capability Enum
394
            @attach(DuploVisionSensor, name='vision', capabilities=[DuploVisionSensor.capability.sense_color])
395
396
            # Ctag and reflectivity sensor
397
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_ctag', 'sense_reflectivity'])
398
399
            # Distance and rgb sensor with different thresholds to trigger updates
400
            @attach(DuploVisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)])
401
402
        The values returned by the sensor will always be available in the instance variable
403
        `self.value`.  For example, when the `sense_color` and `sense_rgb` capabilities are 
404
        enabled, the following values will be stored and updated::
405
406
            self.value = { DuploVisionSensor.capability.sense_color:  uint8,
407
                           DuploVisionSensor.capability.sense_rgb: 
408
                                            [ uint16, uint16, uint16 ]
409
                         }
410
411
        Notes:
412
            The actual modes supported by the sensor are as follows:
413
414
            -  0 = color (0-10)
415
            -  1 = ctag (32-bit int)
416
            -  2 = Reflt   (inverse of distance when closer than 1")
417
            -  3 = RGB I
418
    """
419
    _sensor_id = 0x002B
420
    capability = Enum("capability", 
421
                      [('sense_color', 0),
422
                       ('sense_ctag', 1),
423
                       ('sense_reflectivity', 2),
424
                       ('sense_rgb', 3),
425
                       ])
426
427
    datasets = { capability.sense_color: (1, 1),
428
                 capability.sense_ctag: (1, 1),  # 4-bytes (32-bit)
429
                 capability.sense_reflectivity: (1, 1),
430
                 capability.sense_rgb: (3, 2)   # 3 16-bit values
431
                }
432
433
    allowed_combo = [ capability.sense_color,
434
                      capability.sense_ctag,
435
                      capability.sense_reflectivity,
436
                      capability.sense_rgb,
437
                    ]
438
439
class VoltageSensor(Peripheral):
440
    """Voltage sensor
441
442
       Returns the raw mV value (0-3893) which probably needs to be scaled to 0-9600.
443
444
       It contains two capabilities, although they both appear to do the same thing:
445
       * sense_l
446
       * sense_s
447
448
       Examples::
449
450
            @attach(VoltageSensor, name='volts', capabilities=['sense_l'])
451
452
    """
453
    _sensor_id = 0x14
454
455
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
456
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
457
                capability.sense_l: (1, 2), 
458
               }
459
    allowed_combo = [ ]
460
461
class CurrentSensor(Peripheral):
462
    """Voltage sensor
463
464
       Returns the raw mA value (0-4095) which probably needs to be scaled to 0-2444.
465
466
       It contains two capabilities, although they both appear to do the same thing:
467
       * sense_l
468
       * sense_s
469
470
       Examples::
471
472
            @attach(CurrentSensor, name='cur', capabilities=['sense_l'])
473
474
    """
475
    _sensor_id = 0x15
476
477
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
478
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
479
                capability.sense_l: (1, 2), 
480
               }
481
    allowed_combo = [ ]
482
483
class DuploSpeedSensor(Peripheral):
484
    """Speedometer on Duplo train base that measures front wheel speed.
485
486
       This can measure the following values:
487
488
       - *sense_speed*: Returns the speed of the front wheels
489
       - *sense_count*: Keeps count of the number of revolutions the front wheels have spun
490
491
       Either or both can be enabled for measurement. 
492
493
       Examples::
494
495
            # Report speed changes
496
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed'])
497
498
            # Report all
499
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed', 'sense_count'])
500
501
       The values returned by the sensor will be in `self.value`.  For the first example, get the
502
       current speed by::
503
504
            speed = self.speed_sensor.value
505
        
506
       For the second example, the two values will be in a dict::
507
508
            speed = self.speed_sensor.value[DuploSpeedSensor.sense_speed]
509
            revs  = self.speed_sensor.value[DuploSpeedSensor.sense_count]
510
511
    """
512
    _sensor_id = 0x002C
513
    capability = Enum("capability", 
514
                      [('sense_speed', 0),
515
                       ('sense_count', 1),
516
                       ])
517
518
    datasets = { capability.sense_speed: (1, 2),
519
                 capability.sense_count: (1, 4),
520
                }
521
522
    allowed_combo = [ capability.sense_speed,
523
                      capability.sense_count,
524
                    ]
525