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 ( ec6a22...c11c64 )
by Virantha
01:33
created

bricknil.sensor.ExternalMotor.set_pos()   A

Complexity

Conditions 1

Size

Total Lines 17
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 3
dl 0
loc 17
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
from .const import Color
22
23
from .peripheral import Peripheral, Motor
24
25
26
class InternalMotor(Peripheral):
27
    """ Access the internal motor(s) in the Boost Move Hub.
28
29
        Unlike the train motors, these motors have a built-in sensor for sending back
30
        the motor's current speed and position.  You don't need to use the sensors, and
31
        can treat this as strictly an output.
32
33
        Examples::
34
35
            # Basic connection to the motor on Port A
36
            @attach(InternalMotor, name='left_motor', port=InternalMotor.Port.A)
37
38
            # Basic connection to both motors at the same time (virtual I/O port).
39
            # Any speed command will cause both motors to rotate at the same speed
40
            @attach(InternalMotor, name='motors', port=InternalMotor.Port.AB)
41
42
            # Report back when motor speed changes. You must have a motors_change method defined 
43
            @attach(InternalMotor, name='motors', port=InternalMotor.Port.A, capabilities=['sense_speed'])
44
            # Only report back when speed change exceeds 5 units
45
            @attach(InternalMotor, name='motors', port=InternalMotor.Port.A, capabilities=[('sense_speed', 5)])
46
47
        See Also:
48
            :class:`TrainMotor` for connecting to a train motor
49
50
    """
51
    _sensor_id = 0x0027
52
    _DEFAULT_THRESHOLD=2
53
    """Set to 2 to avoid a lot of updates since the speed seems to oscillate a lot"""
54
55
    capability = Enum("capability", {"sense_speed":1, "sense_pos":2})
56
57
    Port = Enum('Port', 'A B AB', start=0)
58
    """Address either motor A or Motor B, or both AB at the same time"""
59
60
    # Dict of cap: (num_datasets, bytes_per_dataset)
61
    datasets = { capability.sense_speed: (1, 1),
62
                 capability.sense_pos: (1, 4),
63
                }
64
    """ Dict of (num_datasets, bytes_per_dataset).
65
       `sense_speed` (1-byte), and `sense_pos` (uint32)"""
66
67
    allowed_combo = [ capability.sense_speed,
68
                      capability.sense_pos,
69
                    ]
70
71
    def __init__(self, name, port=None, capabilities=[]):
72
        """Maps the port names `A`, `B`, `AB` to hard-coded port numbers"""
73
        if port:
74
            port_map = [55, 56, 57]
75
            port = port_map[port.value]
76
        self.speed = 0
77
        super().__init__(name, port, capabilities)
78
    
79
    async def set_speed(self, speed):
80
        """Sets the speed of the motor, and calls the :func:`bricknil.peripheral.Peripheral._convert_speed_to_val` method
81
           to do some sanity checking and bounding.
82
83
           Args:
84
               speed (int) : -100 to 100 (I believe this is a percentage)
85
        """
86
        self.speed = speed
87
        speed = self._convert_speed_to_val(speed)
88
        mode = 0
89
        await self.set_output(mode, speed)
90
91
    async def ramp_speed2(self, target_speed, ramp_time_ms):
92
        
93
        # Set acceleration profile
94
        delta_speed = target_speed - self.speed
95
        zero_100_ramp_time_ms = int(ramp_time_ms/delta_speed * 100.0) 
96
        zero_100_ramp_time_ms = zero_100_ramp_time_ms % 10000 # limit time to 10s
97
98
        hi = (zero_100_ramp_time_ms >> 8) & 255
99
        lo = zero_100_ramp_time_ms & 255
100
101
        profile = 1
102
        b = [0x00, 0x81, self.port, 0x01, 0x05, 10, 10, profile]
103
        await self.send_message(f'set accel profile {zero_100_ramp_time_ms} {hi} {lo} ', b)
104
        b = [0x00, 0x81, self.port, 0x01, 0x07, self._convert_speed_to_val(target_speed), 80, 1]
105
        await self.send_message('set speed', b)
106
        
107
class ExternalMotor(Motor):
108
109
    _sensor_id = 0x26
110
111
    capability = Enum("capability", {"sense_power": 0, "sense_speed":1, "sense_pos":2})
112
    # Dict of cap: (num_datasets, bytes_per_dataset)
113
    datasets = { capability.sense_power: (1, 1), 
114
                 capability.sense_speed: (1, 1),
115
                 capability.sense_pos: (1, 4),
116
                }
117
118
    allowed_combo = [ capability.sense_speed,
119
                      capability.sense_pos,
120
                    ]
121
122
    async def set_pos(self, pos, speed=50):
123
        """Set the position of the motor
124
125
           Use command GotoAbsolutePosition
126
            * 0x00 = hub id
127
            * 0x81 = Port Output command
128
            * port
129
            * 0x11 = Upper nibble (0=buffer, 1=immediate execution), Lower nibble (0=No ack, 1=command feedback)
130
            * 0x0d = Subcommand
131
            * abs_pos (int32)
132
            * speed -100 - 100
133
            * max_power abs(0-100%)
134
            * endstate = 0 (float), 126 (hold), 127 (brake)
135
            * Use Accel profile = (bit 0 = acc profile, bit 1 = decc profile)
136
            *
137
        """
138
        b = [0x00, 0x81, self.port, 0x11, 0x0d,]
139
140
class VisionSensor(Peripheral):
141
    """ Access the Boost Vision/Distance Sensor
142
143
        Only the sensing capabilities of this sensor is supported right now.
144
145
        - *sense_color*: Returns one of the 10 predefined colors
146
        - *sense_distance*: Distance from 0-7 in roughly inches
147
        - *sense_count*: Running count of waving your hand/item over the sensor (32-bit)
148
        - *sense_reflectivity*: Under distances of one inch, the inverse of the distance
149
        - *sense_ambient*: Distance under one inch (so inverse of the preceeding)
150
        - *sense_rgb*: R, G, B values (3 sets of uint16)
151
152
        Any combination of sense_color, sense_distance, sense_count, sense_reflectivity, 
153
        and sense_rgb is supported.
154
155
        Examples::
156
157
            # Basic distance sensor
158
            @attach(VisionSensor, name='vision', capabilities=['sense_color'])
159
            # Or use the capability Enum
160
            @attach(VisionSensor, name='vision', capabilities=[VisionSensor.capability.sense_color])
161
162
            # Distance and color sensor
163
            @attach(VisionSensor, name='vision', capabilities=['sense_color', 'sense_distance'])
164
165
            # Distance and rgb sensor with different thresholds to trigger updates
166
            @attach(VisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)])
167
168
        The values returned by the sensor will always be available in the instance variable
169
        `self.value`.  For example, when the `sense_color` and `sense_rgb` capabilities are 
170
        enabled, the following values will be stored and updated::
171
172
            self.value = { VisionSensor.capability.sense_color:  uint8,
173
                           VisionSensor.capability.sense_rgb: 
174
                                            [ uint16, uint16, uint16 ]
175
                         }
176
177
        Notes:
178
            The actual modes supported by the sensor are as follows:
179
180
            -  0 = color (0-10)
181
            -  1 = IR proximity (0-7)
182
            -  2 = count (32-bit int)
183
            -  3 = Reflt   (inverse of distance when closer than 1")
184
            -  4 = Amb  (distance when closer than 1")
185
            -  5 = COL (output) ?
186
            -  6 = RGB I
187
            -  7 = IR tx (output) ?
188
            -  8 = combined:  Color byte, Distance byte, 0xFF, Reflected light
189
190
    """
191
192
    _sensor_id = 0x0025
193
    capability = Enum("capability", 
194
                      [('sense_color', 0),
195
                       ('sense_distance', 1),
196
                       ('sense_count', 2),
197
                       ('sense_reflectivity', 3),
198
                       ('sense_ambient', 4),
199
                       ('sense_rgb', 6),
200
                       ])
201
202
    datasets = { capability.sense_color: (1, 1),
203
                 capability.sense_distance: (1, 1),
204
                 capability.sense_count: (1, 4),  # 4-bytes (32-bit)
205
                 capability.sense_reflectivity: (1, 1),
206
                 capability.sense_ambient: (1, 1),
207
                 capability.sense_rgb: (3, 2)   # 3 16-bit values
208
                }
209
210
    allowed_combo = [ capability.sense_color,
211
                      capability.sense_distance,
212
                      capability.sense_count,
213
                      capability.sense_reflectivity,
214
                      capability.sense_rgb,
215
                    ]
216
217
class InternalTiltSensor(Peripheral):
218
    """
219
        Access the internal tilt sensor in the Boost Move Hub.
220
        
221
        The various modes are:
222
223
        - **sense_angle** - X, Y angles.  Both are 0 if hub is lying flat with button up
224
        - **sense_tilt** - value from 0-9 if hub is tilted around any of its axis. Seems to be
225
          a rough mesaure of how much the hub is tilted away from lying flat.
226
          There is no update for just a translation along an axis
227
        - **sense_orientation** - returns one of the nine orientations below (0-9)
228
            - `InternalTiltSensor.orientation`.up = flat with button on top
229
            - `InternalTiltSensor.orientation`.right - standing up on side closest to button
230
            - `InternalTiltSensor.orientation`.left - standing up on side furthest from button
231
            - `InternalTiltSensor.orientation`.far_side - on long face facing away
232
            - `InternalTiltSensor.orientation`.near_side -  on long face facing you
233
            - `InternalTiltSensor.orientation`.down - upside down
234
        - **sense_impact** - 32-bit count of impacts to sensor
235
        - **sense_acceleration_3_axis** - 3 bytes of raw accelerometer data.
236
237
        Any combination of the above modes are allowed.
238
239
        Examples::
240
241
            # Basic tilt sensor
242
            @attach(InternalTiltSensor, name='tilt', capabilities=['sense_tilt'])
243
            # Or use the capability Enum
244
            @attach(InternalTiltSensor, name='tilt', capabilities=[InternalTiltSensor.sense_tilt])
245
246
            # Tilt and orientation sensor
247
            @attach(InternalTiltSensor, name='tilt', capabilities=['sense_tilt, sense_orientation'])
248
249
        The values returned by the sensor will always be available in the
250
        instance variable `self.value`.  For example, when the `sense_angle`
251
        and `sense_orientation` capabilities are enabled, the following values
252
        will be stored and updated::
253
254
            self.value = { InternalTiltSensor.capability.sense_angle:  [uint8, uint8],
255
                           InternalTiltSensor.capability.sense_orientation: 
256
                                            Enum(InternalTiltSensor.orientation)
257
                         }
258
    """
259
    _sensor_id = 0x0028
260
    capability = Enum("capability", 
261
                      [('sense_angle', 0),
262
                       ('sense_tilt', 1),
263
                       ('sense_orientation', 2),
264
                       ('sense_impact', 3),
265
                       ('sense_acceleration_3_axis', 4),
266
                       ])
267
268
    datasets = { capability.sense_angle: (2, 1),
269
                 capability.sense_tilt: (1, 1),
270
                 capability.sense_orientation: (1, 1),  
271
                 capability.sense_impact: (1, 4),
272
                 capability.sense_acceleration_3_axis: (3, 1),
273
                }
274
275
    allowed_combo = [ capability.sense_angle,
276
                      capability.sense_tilt,
277
                      capability.sense_orientation,
278
                      capability.sense_impact,
279
                      capability.sense_acceleration_3_axis,
280
                    ]
281
282
    orientation = Enum('orientation', 
283
                        {   'up': 0,
284
                            'right': 1, 
285
                            'left': 2, 
286
                            'far_side':3,
287
                            'near_side':4,
288
                            'down':5,
289
                        })
290
291
292
    async def update_value(self, msg_bytes):
293
        """If sense_orientation, then substitute the `IntenalTiltSensor.orientation`
294
           enumeration value into the self.value dict.  Otherwise, don't do anything
295
           special to the self.value dict.
296
        """
297
        await super().update_value(msg_bytes)
298
        so = self.capability.sense_orientation
299
        if so in self.value:
300
            self.value[so] = self.orientation(self.value[so])
301
302
303
class LED(Peripheral):
304
    """ Changes the LED color on the Hubs::
305
306
            @attach(LED, name='hub_led')
307
308
            self.hub_led.set_output(Color.red)
309
    """
310
    _sensor_id = 0x0017
311
312
    async def set_color(self, color: Color):
313
        """ Converts a Color enumeration to a color value"""
314
315
        # For now, only support preset colors
316
        assert isinstance(color, Color)
317
        col = color.value
318
        assert col < 11
319
        mode = 0
320
        await self.set_output(mode, col)
321
        #b = [0x00, 0x81, self.port, 0x11, 0x51, mode, col ]
322
        #await self.message_info(f'set color to {color}')
323
324
325
class Light(Peripheral):
326
    """
327
        Connects to the external light.
328
329
        Example::
330
331
             @attach(Light, name='light')
332
333
        And then within the run body, use::
334
335
            await self.light.set_brightness(brightness)
336
    """
337
    _sensor_id = 0x0008
338
339
    async def set_brightness(self, brightness: int):
340
        """Sets the brightness of the light.
341
342
        Args:
343
            brightness (int) : A value between -100 and 100 where 0 is off and
344
                -100 or 100 are both maximum brightness.
345
        """
346
        mode = 0
347
        brightness, = pack('b', int(brightness))
348
        await self.set_output(mode, brightness)
349
350
351
class TrainMotor(Motor):
352
    """
353
        Connects to the train motors.
354
355
        TrainMotor has no sensing capabilities and only supports a single output mode that
356
        sets the speed.
357
358
        Examples::
359
360
             @attach(TrainMotor, name='train')
361
362
        And then within the run body, use::
363
364
            await self.train.set_speed(speed)
365
366
        Attributes:
367
            speed (int) : Keep track of the current speed in order to ramp it
368
369
        See Also:
370
            :class:`InternalMotor`
371
    """
372
    _sensor_id = 0x0002
373
374
375
376
class RemoteButtons(Peripheral):
377
    """Represents one set of '+', '-', 'red' buttons on the PoweredHub Remote
378
379
       Each remote has two sets of buttons, on the left and right side.  Pick the one
380
       your want to attach to by using the port argument with either Port.L or Port.R.
381
382
       There are actually a few different modes that the hardware supports, but we are
383
       only going to use one of them called 'KEYSD' (see the notes in the documentation on the
384
       raw values reported by the hub).  This mode makes the remote send three values back
385
       in a list.  To access each button state, there are three helper methods provided 
386
       (see below)
387
388
       Examples::
389
390
            # Basic connection to the left buttons
391
            @attach(RemoteButtons, name='left_buttons', port=RemoteButtons.Port.L)
392
393
            # Getting values back in the handler
394
            async def left_buttons_change(self):
395
396
                is_plus_pressed = self.left_buttons.plus_pressed()
397
                is_minus_pressed = self.left_buttons.minus_pressed()
398
                is_red_pressed = self.left_buttons.red_pressed()
399
400
    """
401
402
    _sensor_id = 0x0037
403
    Port = Enum('Port', 'L R', start=0)
404
    Button = IntEnum('Button', 'PLUS RED MINUS', start=0)
405
    """The button index in the value list returned by the sensor"""
406
407
    capability = Enum('capability', {'sense_press':4},)
408
409
    datasets = { capability.sense_press: (3,1) }
410
    allowed_combo = []
411
412
    def __init__(self, name, port=None, capabilities=[]):
413
        """Maps the port names `L`, `R`"""
414
        if port:
415
            port = port.value
416
        super().__init__(name, port, capabilities)
417
418
    def plus_pressed(self):
419
        """Return whether `value` reflects that the PLUS button is pressed"""
420
        button_list = self.value[self.capability.sense_press]
421
        return button_list[self.Button.PLUS] == 1
422
    def minus_pressed(self):
423
        """Return whether `value` reflects that the MINUS button is pressed"""
424
        button_list = self.value[self.capability.sense_press]
425
        return button_list[self.Button.MINUS] == 1
426
    def red_pressed(self):
427
        """Return whether `value` reflects that the RED button is pressed"""
428
        button_list = self.value[self.capability.sense_press]
429
        return button_list[self.Button.RED] == 1
430
431
class Button(Peripheral):
432
    """ Register to be notified of button presses on the Hub (Boost or PoweredUp)
433
434
        This is actually a slight hack, since the Hub button is not a peripheral that is 
435
        attached like other sensors in the Lego protocol.  Instead, the buttons are accessed
436
        through Hub property messages.  We abstract away these special messages to make the
437
        button appear to be like any other peripheral sensor.
438
439
        Examples::
440
441
            @attach(Button, name='hub_btn')
442
443
        Notes:
444
            Since there is no attach I/O message from the hub to trigger the
445
            :func:`activate_updates` method, we instead insert a fake
446
            "attaach" message from this fake sensor on port 255 in the
447
            `BLEventQ.get_messages` method that is used to register for updates
448
            from a given sensor.
449
450
    """
451
    _sensor_id = 0x0005
452
    """Piggy back the hub button off the normal peripheral button id 0x0005.
453
       Might need to change this in the future"""
454
455
    capability = Enum('capability', {'sense_press':0})
456
457
    datasets = { capability.sense_press: (1,1)
458
               }
459
    allowed_combo = [capability.sense_press]
460
461
    def __init__(self, name, port=None, capabilities=[]):
462
        """Call super-class with port set to 255 """
463
        super().__init__(name, 255, capabilities)
464
465
    async def activate_updates(self):
466
        """Use a special Hub Properties button message updates activation message"""
467
        self.value = {}
468
        for cap in self.capabilities:
469
            self.value[cap] = [None]*self.datasets[cap][0]
470
471
        b = [0x00, 0x01, 0x02, 0x02]  # Button reports from "Hub Properties Message Type"
472
        await self.send_message(f'Activate button reports: port {self.port}', b) 
473
474
class DuploTrainMotor(Motor):
475
    """Train Motor on Duplo Trains
476
477
       Make sure that the train is sitting on the ground (the front wheels need to keep rotating) in 
478
       order to keep the train motor powered.  If you pick up the train, the motor will stop operating
479
       withina few seconds.
480
481
       Examples::
482
483
            @attach(DuploTrainMotor, name='motor')
484
485
       And then within the run body, use::
486
487
            await self.train.set_speed(speed)
488
489
       Attributes:
490
            speed (int): Keep track of the current speed in order to ramp it
491
492
       See Also:
493
            :class:`TrainMotor` for connecting to a PoweredUp train motor
494
    """
495
    _sensor_id = 0x0029
496
497
class DuploSpeedSensor(Peripheral):
498
    """Speedometer on Duplo train base that measures front wheel speed.
499
500
       This can measure the following values:
501
502
       - *sense_speed*: Returns the speed of the front wheels
503
       - *sense_count*: Keeps count of the number of revolutions the front wheels have spun
504
505
       Either or both can be enabled for measurement. 
506
507
       Examples::
508
509
            # Report speed changes
510
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed'])
511
512
            # Report all
513
            @attach(DuploSpeedSensor, name='speed_sensor', capabilities=['sense_speed', 'sense_count'])
514
515
       The values returned by the sensor will be in `self.value`.  For the first example, get the
516
       current speed by::
517
518
            speed = self.speed_sensor.value
519
        
520
       For the second example, the two values will be in a dict::
521
522
            speed = self.speed_sensor.value[DuploSpeedSensor.sense_speed]
523
            revs  = self.speed_sensor.value[DuploSpeedSensor.sense_count]
524
525
    """
526
    _sensor_id = 0x002C
527
    capability = Enum("capability", 
528
                      [('sense_speed', 0),
529
                       ('sense_count', 1),
530
                       ])
531
532
    datasets = { capability.sense_speed: (1, 2),
533
                 capability.sense_count: (1, 4),
534
                }
535
536
    allowed_combo = [ capability.sense_speed,
537
                      capability.sense_count,
538
                    ]
539
540
class DuploVisionSensor(Peripheral):
541
    """ Access the Duplo Vision/Distance Sensor
542
543
        - *sense_color*: Returns one of the 10 predefined colors
544
        - *sense_ctag*: Returns one of the 10 predefined tags
545
        - *sense_reflectivity*: Under distances of one inch, the inverse of the distance
546
        - *sense_rgb*: R, G, B values (3 sets of uint16)
547
548
        Any combination of sense_color, sense_ctag, sense_reflectivity, 
549
        and sense_rgb is supported.
550
551
        Examples::
552
553
            # Basic color sensor
554
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_color'])
555
            # Or use the capability Enum
556
            @attach(DuploVisionSensor, name='vision', capabilities=[DuploVisionSensor.capability.sense_color])
557
558
            # Ctag and reflectivity sensor
559
            @attach(DuploVisionSensor, name='vision', capabilities=['sense_ctag', 'sense_reflectivity'])
560
561
            # Distance and rgb sensor with different thresholds to trigger updates
562
            @attach(DuploVisionSensor, name='vision', capabilities=[('sense_color', 1), ('sense_rgb', 5)])
563
564
        The values returned by the sensor will always be available in the instance variable
565
        `self.value`.  For example, when the `sense_color` and `sense_rgb` capabilities are 
566
        enabled, the following values will be stored and updated::
567
568
            self.value = { DuploVisionSensor.capability.sense_color:  uint8,
569
                           DuploVisionSensor.capability.sense_rgb: 
570
                                            [ uint16, uint16, uint16 ]
571
                         }
572
573
        Notes:
574
            The actual modes supported by the sensor are as follows:
575
576
            -  0 = color (0-10)
577
            -  1 = ctag (32-bit int)
578
            -  2 = Reflt   (inverse of distance when closer than 1")
579
            -  3 = RGB I
580
    """
581
    _sensor_id = 0x002B
582
    capability = Enum("capability", 
583
                      [('sense_color', 0),
584
                       ('sense_ctag', 1),
585
                       ('sense_reflectivity', 2),
586
                       ('sense_rgb', 3),
587
                       ])
588
589
    datasets = { capability.sense_color: (1, 1),
590
                 capability.sense_ctag: (1, 1),  # 4-bytes (32-bit)
591
                 capability.sense_reflectivity: (1, 1),
592
                 capability.sense_rgb: (3, 2)   # 3 16-bit values
593
                }
594
595
    allowed_combo = [ capability.sense_color,
596
                      capability.sense_ctag,
597
                      capability.sense_reflectivity,
598
                      capability.sense_rgb,
599
                    ]
600
601
class DuploSpeaker(Peripheral):
602
    """Plays one of five preset sounds through the Duplo built-in speaker
603
604
       See :class:`sounds` for the list.
605
606
       Examples::
607
608
            @attach(DuploSpeaker, name='speaker')
609
            ...
610
            await self.speaker.play_sound(DuploSpeaker.sounds.brake)
611
           
612
       Notes:
613
            Uses Mode 1 to play the presets
614
615
    """
616
    _sensor_id = 0x002A
617
    sounds = Enum('sounds', { 'brake': 3,
618
                              'station': 5,
619
                              'water': 7,
620
                              'horn': 9,
621
                              'steam': 10,
622
                              })
623
624
    async def activate_updates(self):
625
        """For some reason, even though the speaker is an output device
626
           we need to send a Port Input Format Setup command (0x41) to enable
627
           notifications.  Otherwise, none of the sound output commands will play.  This function
628
           is called automatically after this sensor is attached.
629
        """
630
        mode = 1
631
        b = [0x00, 0x41, self.port, mode, 0x01, 0x00, 0x00, 0x00, 0x01]
632
        await self.send_message('Activate DUPLO Speaker: port {self.port}', b)
633
634
    async def play_sound(self, sound):
635
        assert isinstance(sound, self.sounds), 'Can only play sounds that are enums (DuploSpeaker.sounds.brake, etc)'
636
        mode = 1
637
        self.message_info(f'Playing sound {sound.name}:{sound.value}')
638
        await self.set_output(mode, sound.value)
639
640
641
class VoltageSensor(Peripheral):
642
    """Voltage sensor
643
644
       Returns the raw mV value (0-3893) which probably needs to be scaled to 0-9600.
645
646
       It contains two capabilities, although they both appear to do the same thing:
647
       * sense_l
648
       * sense_s
649
650
       Examples::
651
652
            @attach(VoltageSensor, name='volts', capabilities=['sense_l'])
653
654
    """
655
    _sensor_id = 0x14
656
657
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
658
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
659
                capability.sense_l: (1, 2), 
660
               }
661
    allowed_combo = [ ]
662
663
class CurrentSensor(Peripheral):
664
    """Voltage sensor
665
666
       Returns the raw mA value (0-4095) which probably needs to be scaled to 0-2444.
667
668
       It contains two capabilities, although they both appear to do the same thing:
669
       * sense_l
670
       * sense_s
671
672
       Examples::
673
674
            @attach(CurrentSensor, name='cur', capabilities=['sense_l'])
675
676
    """
677
    _sensor_id = 0x15
678
679
    capability = Enum("capability", {'sense_s': 0, 'sense_l': 1})
680
    datasets = {capability.sense_s: (1, 2),   # 2-bytes (16-bit)
681
                capability.sense_l: (1, 2), 
682
               }
683
    allowed_combo = [ ]
684
685
686