1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051 | # -----------------------------------------------------------------------------
# Copyright (c) 2015 Ralph Hempel <rhempel@hempeldesigngroup.com>
# Copyright (c) 2015 Anton Vanhoucke <antonvh@gmail.com>
# Copyright (c) 2015 Denis Demidov <dennis.demidov@gmail.com>
# Copyright (c) 2015 Eric Pascual <eric@pobot.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# -----------------------------------------------------------------------------
import sys
import logging
import time
from ev3dev2.button import ButtonBase
from ev3dev2.sensor import Sensor
if sys.version_info < (3, 4):
raise SystemError('Must be using Python 3.4 or higher')
log = logging.getLogger(__name__)
class TouchSensor(Sensor):
"""
Touch Sensor
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Button state
MODE_TOUCH = 'TOUCH'
MODES = (MODE_TOUCH, )
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(TouchSensor, self).__init__(address,
name_pattern,
name_exact,
driver_name=['lego-ev3-touch', 'lego-nxt-touch'],
**kwargs)
@property
def is_pressed(self):
"""
A boolean indicating whether the current touch sensor is being
pressed.
"""
self._ensure_mode(self.MODE_TOUCH)
return self.value(0)
@property
def is_released(self):
return not self.is_pressed
def _wait(self, wait_for_press, timeout_ms, sleep_ms):
tic = time.time()
if sleep_ms:
sleep_ms = float(sleep_ms / 1000)
# The kernel does not supoort POLLPRI or POLLIN for sensors so we have
# to drop into a loop and check often
while True:
if self.is_pressed == wait_for_press:
return True
if timeout_ms is not None and time.time() >= tic + timeout_ms / 1000:
return False
if sleep_ms:
time.sleep(sleep_ms)
def wait_for_pressed(self, timeout_ms=None, sleep_ms=10):
"""
Wait for the touch sensor to be pressed down.
"""
return self._wait(True, timeout_ms, sleep_ms)
def wait_for_released(self, timeout_ms=None, sleep_ms=10):
"""
Wait for the touch sensor to be released.
"""
return self._wait(False, timeout_ms, sleep_ms)
def wait_for_bump(self, timeout_ms=None, sleep_ms=10):
"""
Wait for the touch sensor to be pressed down and then released.
Both actions must happen within timeout_ms.
"""
start_time = time.time()
if self.wait_for_pressed(timeout_ms, sleep_ms):
if timeout_ms is not None:
timeout_ms -= int((time.time() - start_time) * 1000)
return self.wait_for_released(timeout_ms, sleep_ms)
return False
class ColorSensor(Sensor):
"""
LEGO EV3 color sensor.
"""
__slots__ = ['red_max', 'green_max', 'blue_max']
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Reflected light. Red LED on.
MODE_COL_REFLECT = 'COL-REFLECT'
#: Ambient light. Blue LEDs on.
MODE_COL_AMBIENT = 'COL-AMBIENT'
#: Color. All LEDs rapidly cycling, appears white.
MODE_COL_COLOR = 'COL-COLOR'
#: Raw reflected. Red LED on
MODE_REF_RAW = 'REF-RAW'
#: Raw Color Components. All LEDs rapidly cycling, appears white.
MODE_RGB_RAW = 'RGB-RAW'
#: No color.
COLOR_NOCOLOR = 0
#: Black color.
COLOR_BLACK = 1
#: Blue color.
COLOR_BLUE = 2
#: Green color.
COLOR_GREEN = 3
#: Yellow color.
COLOR_YELLOW = 4
#: Red color.
COLOR_RED = 5
#: White color.
COLOR_WHITE = 6
#: Brown color.
COLOR_BROWN = 7
MODES = (MODE_COL_REFLECT, MODE_COL_AMBIENT, MODE_COL_COLOR, MODE_REF_RAW, MODE_RGB_RAW)
COLORS = (
'NoColor',
'Black',
'Blue',
'Green',
'Yellow',
'Red',
'White',
'Brown',
)
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(ColorSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-color', **kwargs)
# See calibrate_white() for more details
self.red_max = 300
self.green_max = 300
self.blue_max = 300
@property
def reflected_light_intensity(self):
"""
Reflected light intensity as a percentage (0 to 100). Light on sensor is red.
"""
self._ensure_mode(self.MODE_COL_REFLECT)
return self.value(0)
@property
def ambient_light_intensity(self):
"""
Ambient light intensity, as a percentage (0 to 100). Light on sensor is dimly lit blue.
"""
self._ensure_mode(self.MODE_COL_AMBIENT)
return self.value(0)
@property
def color(self):
"""
Color detected by the sensor, categorized by overall value.
- 0: No color
- 1: Black
- 2: Blue
- 3: Green
- 4: Yellow
- 5: Red
- 6: White
- 7: Brown
"""
self._ensure_mode(self.MODE_COL_COLOR)
return self.value(0)
@property
def color_name(self):
"""
Returns NoColor, Black, Blue, etc
"""
return self.COLORS[self.color]
@property
def raw(self):
"""
Red, green, and blue components of the detected color, as a tuple.
Officially in the range 0-1020 but the values returned will never be
that high. We do not yet know why the values returned are low, but
pointing the color sensor at a well lit sheet of white paper will return
values in the 250-400 range.
If this is an issue, check out the rgb() and calibrate_white() methods.
"""
self._ensure_mode(self.MODE_RGB_RAW)
return self.value(0), self.value(1), self.value(2)
def calibrate_white(self):
"""
The RGB raw values are on a scale of 0-1020 but you never see a value
anywhere close to 1020. This function is designed to be called when
the sensor is placed over a white object in order to figure out what
are the maximum RGB values the robot can expect to see. We will use
these maximum values to scale future raw values to a 0-255 range in
rgb().
If you never call this function red_max, green_max, and blue_max will
use a default value of 300. This default was selected by measuring
the RGB values of a white sheet of paper in a well lit room.
Note that there are several variables that influence the maximum RGB
values detected by the color sensor
- the distance of the color sensor to the white object
- the amount of light in the room
- shadows that the robot casts on the sensor
"""
(self.red_max, self.green_max, self.blue_max) = self.raw
@property
def rgb(self):
"""
Same as raw() but RGB values are scaled to 0-255
"""
(red, green, blue) = self.raw
return (min(int((red * 255) / self.red_max), 255), min(int((green * 255) / self.green_max),
255), min(int((blue * 255) / self.blue_max), 255))
@property
def lab(self):
"""
Return colors in Lab color space
"""
RGB = [0, 0, 0]
XYZ = [0, 0, 0]
for (num, value) in enumerate(self.rgb):
if value > 0.04045:
value = pow(((value + 0.055) / 1.055), 2.4)
else:
value = value / 12.92
RGB[num] = value * 100.0
# http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
# sRGB
# 0.4124564 0.3575761 0.1804375
# 0.2126729 0.7151522 0.0721750
# 0.0193339 0.1191920 0.9503041
X = (RGB[0] * 0.4124564) + (RGB[1] * 0.3575761) + (RGB[2] * 0.1804375)
Y = (RGB[0] * 0.2126729) + (RGB[1] * 0.7151522) + (RGB[2] * 0.0721750)
Z = (RGB[0] * 0.0193339) + (RGB[1] * 0.1191920) + (RGB[2] * 0.9503041)
XYZ[0] = X / 95.047 # ref_X = 95.047
XYZ[1] = Y / 100.0 # ref_Y = 100.000
XYZ[2] = Z / 108.883 # ref_Z = 108.883
for (num, value) in enumerate(XYZ):
if value > 0.008856:
value = pow(value, (1.0 / 3.0))
else:
value = (7.787 * value) + (16 / 116.0)
XYZ[num] = value
L = (116.0 * XYZ[1]) - 16
a = 500.0 * (XYZ[0] - XYZ[1])
b = 200.0 * (XYZ[1] - XYZ[2])
L = round(L, 4)
a = round(a, 4)
b = round(b, 4)
return (L, a, b)
@property
def hsv(self):
"""
HSV: Hue, Saturation, Value
H: position in the spectrum
S: color saturation ("purity")
V: color brightness
"""
(r, g, b) = self.rgb
maxc = max(r, g, b)
minc = min(r, g, b)
v = maxc
if minc == maxc:
return 0.0, 0.0, v
s = (maxc - minc) / maxc
rc = (maxc - r) / (maxc - minc)
gc = (maxc - g) / (maxc - minc)
bc = (maxc - b) / (maxc - minc)
if r == maxc:
h = bc - gc
elif g == maxc:
h = 2.0 + rc - bc
else:
h = 4.0 + gc - rc
h = (h / 6.0) % 1.0
return (h, s, v)
@property
def hls(self):
"""
HLS: Hue, Luminance, Saturation
H: position in the spectrum
L: color lightness
S: color saturation
"""
(red, green, blue) = self.rgb
maxc = max(red, green, blue)
minc = min(red, green, blue)
luminance = (minc + maxc) / 2.0
if minc == maxc:
return 0.0, luminance, 0.0
if luminance <= 0.5:
saturation = (maxc - minc) / (maxc + minc)
else:
if 2.0 - maxc - minc == 0:
saturation = 0
else:
saturation = (maxc - minc) / (2.0 - maxc - minc)
rc = (maxc - red) / (maxc - minc)
gc = (maxc - green) / (maxc - minc)
bc = (maxc - blue) / (maxc - minc)
if red == maxc:
hue = bc - gc
elif green == maxc:
hue = 2.0 + rc - bc
else:
hue = 4.0 + gc - rc
hue = (hue / 6.0) % 1.0
return (hue, luminance, saturation)
@property
def red(self):
"""
Red component of the detected color, in the range 0-1020.
"""
self._ensure_mode(self.MODE_RGB_RAW)
return self.value(0)
@property
def green(self):
"""
Green component of the detected color, in the range 0-1020.
"""
self._ensure_mode(self.MODE_RGB_RAW)
return self.value(1)
@property
def blue(self):
"""
Blue component of the detected color, in the range 0-1020.
"""
self._ensure_mode(self.MODE_RGB_RAW)
return self.value(2)
class UltrasonicSensor(Sensor):
"""
LEGO EV3 ultrasonic sensor.
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Continuous measurement in centimeters.
MODE_US_DIST_CM = 'US-DIST-CM'
#: Continuous measurement in inches.
MODE_US_DIST_IN = 'US-DIST-IN'
#: Listen.
MODE_US_LISTEN = 'US-LISTEN'
#: Single measurement in centimeters.
MODE_US_SI_CM = 'US-SI-CM'
#: Single measurement in inches.
MODE_US_SI_IN = 'US-SI-IN'
MODES = (
MODE_US_DIST_CM,
MODE_US_DIST_IN,
MODE_US_LISTEN,
MODE_US_SI_CM,
MODE_US_SI_IN,
)
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(UltrasonicSensor, self).__init__(address,
name_pattern,
name_exact,
driver_name=['lego-ev3-us', 'lego-nxt-us'],
**kwargs)
@property
def distance_centimeters_continuous(self):
"""
Measurement of the distance detected by the sensor,
in centimeters.
The sensor will continue to take measurements so
they are available for future reads.
Prefer using the equivalent :meth:`UltrasonicSensor.distance_centimeters` property.
"""
self._ensure_mode(self.MODE_US_DIST_CM)
return self.value(0) * self._scale('US_DIST_CM')
@property
def distance_centimeters_ping(self):
"""
Measurement of the distance detected by the sensor,
in centimeters.
The sensor will take a single measurement then stop
broadcasting.
If you use this property too frequently (e.g. every
100msec), the sensor will sometimes lock up and writing
to the mode attribute will return an error. A delay of
250msec between each usage seems sufficient to keep the
sensor from locking up.
"""
# This mode is special; setting the mode causes the sensor to send out
# a "ping", but the mode isn't actually changed.
self.mode = self.MODE_US_SI_CM
return self.value(0) * self._scale('US_DIST_CM')
@property
def distance_centimeters(self):
"""
Measurement of the distance detected by the sensor,
in centimeters.
Equivalent to :meth:`UltrasonicSensor.distance_centimeters_continuous`.
"""
return self.distance_centimeters_continuous
@property
def distance_inches_continuous(self):
"""
Measurement of the distance detected by the sensor,
in inches.
The sensor will continue to take measurements so
they are available for future reads.
Prefer using the equivalent :meth:`UltrasonicSensor.distance_inches` property.
"""
self._ensure_mode(self.MODE_US_DIST_IN)
return self.value(0) * self._scale('US_DIST_IN')
@property
def distance_inches_ping(self):
"""
Measurement of the distance detected by the sensor,
in inches.
The sensor will take a single measurement then stop
broadcasting.
If you use this property too frequently (e.g. every
100msec), the sensor will sometimes lock up and writing
to the mode attribute will return an error. A delay of
250msec between each usage seems sufficient to keep the
sensor from locking up.
"""
# This mode is special; setting the mode causes the sensor to send out
# a "ping", but the mode isn't actually changed.
self.mode = self.MODE_US_SI_IN
return self.value(0) * self._scale('US_DIST_IN')
@property
def distance_inches(self):
"""
Measurement of the distance detected by the sensor,
in inches.
Equivalent to :meth:`UltrasonicSensor.distance_inches_continuous`.
"""
return self.distance_inches_continuous
@property
def other_sensor_present(self):
"""
Boolean indicating whether another ultrasonic sensor could
be heard nearby.
"""
self._ensure_mode(self.MODE_US_LISTEN)
return bool(self.value(0))
class GyroSensor(Sensor):
"""
LEGO EV3 gyro sensor.
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Angle
MODE_GYRO_ANG = 'GYRO-ANG'
#: Rotational speed
MODE_GYRO_RATE = 'GYRO-RATE'
#: Raw sensor value
MODE_GYRO_FAS = 'GYRO-FAS'
#: Angle and rotational speed
MODE_GYRO_G_A = 'GYRO-G&A'
#: Calibration ???
MODE_GYRO_CAL = 'GYRO-CAL'
# Newer versions of the Gyro sensor also have an additional second axis
# accessible via the TILT-ANG and TILT-RATE modes that is not usable
# using the official EV3-G blocks
MODE_TILT_ANG = 'TILT-ANG'
MODE_TILT_RATE = 'TILT-RATE'
MODES = (
MODE_GYRO_ANG,
MODE_GYRO_RATE,
MODE_GYRO_FAS,
MODE_GYRO_G_A,
MODE_GYRO_CAL,
MODE_TILT_ANG,
MODE_TILT_RATE,
)
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(GyroSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-gyro', **kwargs)
self._direct = None
self._init_angle = self.angle
@property
def angle(self):
"""
The number of degrees that the sensor has been rotated
since it was put into this mode.
"""
self._ensure_mode(self.MODE_GYRO_ANG)
return self.value(0)
@property
def rate(self):
"""
The rate at which the sensor is rotating, in degrees/second.
"""
self._ensure_mode(self.MODE_GYRO_RATE)
return self.value(0)
@property
def angle_and_rate(self):
"""
Angle (degrees) and Rotational Speed (degrees/second).
"""
self._ensure_mode(self.MODE_GYRO_G_A)
return self.value(0), self.value(1)
@property
def tilt_angle(self):
self._ensure_mode(self.MODE_TILT_ANG)
return self.value(0)
@property
def tilt_rate(self):
self._ensure_mode(self.MODE_TILT_RATE)
return self.value(0)
def calibrate(self):
"""
The robot should be perfectly still when you call this
"""
current_mode = self.mode
self._ensure_mode(self.MODE_GYRO_CAL)
time.sleep(2)
self._ensure_mode(current_mode)
def reset(self):
"""Resets the angle to 0.
Caveats:
- This function only resets the angle to 0, it does not fix drift.
- This function only works on EV3, it does not work on BrickPi,
PiStorms, or with any sensor multiplexors.
"""
# 17 comes from inspecting the .vix file of the Gyro sensor block in EV3-G
self._direct = self.set_attr_raw(self._direct, 'direct', b'\x11')
self._init_angle = self.angle
def wait_until_angle_changed_by(self, delta, direction_sensitive=False):
"""
Wait until angle has changed by specified amount.
If ``direction_sensitive`` is True we will wait until angle has changed
by ``delta`` and with the correct sign.
If ``direction_sensitive`` is False (default) we will wait until angle has changed
by ``delta`` in either direction.
"""
assert self.mode in (self.MODE_GYRO_G_A, self.MODE_GYRO_ANG,
self.MODE_TILT_ANG),\
'Gyro mode should be MODE_GYRO_ANG, MODE_GYRO_G_A or MODE_TILT_ANG'
start_angle = self.value(0)
if direction_sensitive:
if delta > 0:
while (self.value(0) - start_angle) < delta:
time.sleep(0.01)
else:
delta *= -1
while (start_angle - self.value(0)) < delta:
time.sleep(0.01)
else:
while abs(start_angle - self.value(0)) < delta:
time.sleep(0.01)
def circle_angle(self):
"""
As the gryo rotates clockwise the angle increases, it will increase
by 360 for each full rotation. As the gyro rotates counter-clockwise
the gyro angle will decrease.
The angles on a circle have the opposite behavior though, they start
at 0 and increase as you move counter-clockwise around the circle.
Convert the gyro angle to the angle on a circle. We consider the initial
position of the gyro to be at 90 degrees on the cirlce.
"""
current_angle = self.angle
delta = abs(current_angle - self._init_angle) % 360
if delta == 0:
result = 90
# the gyro has turned clockwise relative to where we started
elif current_angle > self._init_angle:
if delta <= 90:
result = 90 - delta
elif delta <= 180:
result = 360 - (delta - 90)
elif delta <= 270:
result = 270 - (delta - 180)
else:
result = 180 - (delta - 270)
# This can be chatty (but helpful) so save it for a rainy day
# log.info("%s moved clockwise %s degrees to %s" % (self, delta, result))
# the gyro has turned counter-clockwise relative to where we started
else:
if delta <= 90:
result = 90 + delta
elif delta <= 180:
result = 180 + (delta - 90)
elif delta <= 270:
result = 270 + (delta - 180)
else:
result = delta - 270
# This can be chatty (but helpful) so save it for a rainy day
# log.info("%s moved counter-clockwise %s degrees to %s" % (self, delta, result))
return result
class InfraredSensor(Sensor, ButtonBase):
"""
LEGO EV3 infrared sensor.
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Proximity
MODE_IR_PROX = 'IR-PROX'
#: IR Seeker
MODE_IR_SEEK = 'IR-SEEK'
#: IR Remote Control
MODE_IR_REMOTE = 'IR-REMOTE'
#: IR Remote Control. State of the buttons is coded in binary
MODE_IR_REM_A = 'IR-REM-A'
#: Calibration ???
MODE_IR_CAL = 'IR-CAL'
MODES = (MODE_IR_PROX, MODE_IR_SEEK, MODE_IR_REMOTE, MODE_IR_REM_A, MODE_IR_CAL)
# The following are all of the various combinations of button presses for
# the remote control. The key/index is the number that will be written in
# the attribute file to indicate what combination of buttons are currently
# pressed.
_BUTTON_VALUES = {
0: [],
1: ['top_left'],
2: ['bottom_left'],
3: ['top_right'],
4: ['bottom_right'],
5: ['top_left', 'top_right'],
6: ['top_left', 'bottom_right'],
7: ['bottom_left', 'top_right'],
8: ['bottom_left', 'bottom_right'],
9: ['beacon'],
10: ['top_left', 'bottom_left'],
11: ['top_right', 'bottom_right']
}
_BUTTONS = ('top_left', 'bottom_left', 'top_right', 'bottom_right', 'beacon')
# Button codes for doing rapid check of remote status
NO_BUTTON = 0
TOP_LEFT = 1
BOTTOM_LEFT = 2
TOP_RIGHT = 3
BOTTOM_RIGHT = 4
TOP_LEFT_TOP_RIGHT = 5
TOP_LEFT_BOTTOM_RIGHT = 6
BOTTOM_LEFT_TOP_RIGHT = 7
BOTTOM_LEFT_BOTTOM_RIGHT = 8
BEACON = 9
TOP_LEFT_BOTTOM_LEFT = 10
TOP_RIGHT_BOTTOM_RIGHT = 11
#: Handler for top-left button events on channel 1. See :meth:`InfraredSensor.process`.
on_channel1_top_left = None
#: Handler for bottom-left button events on channel 1. See :meth:`InfraredSensor.process`.
on_channel1_bottom_left = None
#: Handler for top-right button events on channel 1. See :meth:`InfraredSensor.process`.
on_channel1_top_right = None
#: Handler for bottom-right button events on channel 1. See :meth:`InfraredSensor.process`.
on_channel1_bottom_right = None
#: Handler for beacon button events on channel 1. See :meth:`InfraredSensor.process`.
on_channel1_beacon = None
#: Handler for top-left button events on channel 2. See :meth:`InfraredSensor.process`.
on_channel2_top_left = None
#: Handler for bottom-left button events on channel 2. See :meth:`InfraredSensor.process`.
on_channel2_bottom_left = None
#: Handler for top-right button events on channel 2. See :meth:`InfraredSensor.process`.
on_channel2_top_right = None
#: Handler for bottom-right button events on channel 2. See :meth:`InfraredSensor.process`.
on_channel2_bottom_right = None
#: Handler for beacon button events on channel 2. See :meth:`InfraredSensor.process`.
on_channel2_beacon = None
#: Handler for top-left button events on channel 3. See :meth:`InfraredSensor.process`.
on_channel3_top_left = None
#: Handler for bottom-left button events on channel 3. See :meth:`InfraredSensor.process`.
on_channel3_bottom_left = None
#: Handler for top-right button events on channel 3. See :meth:`InfraredSensor.process`.
on_channel3_top_right = None
#: Handler for bottom-right button events on channel 3. See :meth:`InfraredSensor.process`.
on_channel3_bottom_right = None
#: Handler for beacon button events on channel 3. See :meth:`InfraredSensor.process`.
on_channel3_beacon = None
#: Handler for top-left button events on channel 4. See :meth:`InfraredSensor.process`.
on_channel4_top_left = None
#: Handler for bottom-left button events on channel 4. See :meth:`InfraredSensor.process`.
on_channel4_bottom_left = None
#: Handler for top-right button events on channel 4. See :meth:`InfraredSensor.process`.
on_channel4_top_right = None
#: Handler for bottom-right button events on channel 4. See :meth:`InfraredSensor.process`.
on_channel4_bottom_right = None
#: Handler for beacon button events on channel 4. See :meth:`InfraredSensor.process`.
on_channel4_beacon = None
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(InfraredSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-ev3-ir', **kwargs)
def _normalize_channel(self, channel):
assert channel >= 1 and channel <= 4, "channel is %s, it must be 1, 2, 3, or 4" % channel
channel = max(1, min(4, channel)) - 1
return channel
@property
def proximity(self):
"""
An estimate of the distance between the sensor and objects in front of
it, as a percentage. 100% is approximately 70cm/27in.
"""
self._ensure_mode(self.MODE_IR_PROX)
return self.value(0)
def heading(self, channel=1):
"""
Returns heading (-25, 25) to the beacon on the given channel.
"""
self._ensure_mode(self.MODE_IR_SEEK)
channel = self._normalize_channel(channel)
return self.value(channel * 2)
def distance(self, channel=1):
"""
Returns distance (0, 100) to the beacon on the given channel.
Returns None when beacon is not found.
"""
self._ensure_mode(self.MODE_IR_SEEK)
channel = self._normalize_channel(channel)
ret_value = self.value((channel * 2) + 1)
# The value will be -128 if no beacon is found, return None instead
return None if ret_value == -128 else ret_value
def heading_and_distance(self, channel=1):
"""
Returns heading and distance to the beacon on the given channel as a
tuple.
"""
return (self.heading(channel), self.distance(channel))
def top_left(self, channel=1):
"""
Checks if ``top_left`` button is pressed.
"""
return 'top_left' in self.buttons_pressed(channel)
def bottom_left(self, channel=1):
"""
Checks if ``bottom_left`` button is pressed.
"""
return 'bottom_left' in self.buttons_pressed(channel)
def top_right(self, channel=1):
"""
Checks if ``top_right`` button is pressed.
"""
return 'top_right' in self.buttons_pressed(channel)
def bottom_right(self, channel=1):
"""
Checks if ``bottom_right`` button is pressed.
"""
return 'bottom_right' in self.buttons_pressed(channel)
def beacon(self, channel=1):
"""
Checks if ``beacon`` button is pressed.
"""
return 'beacon' in self.buttons_pressed(channel)
def buttons_pressed(self, channel=1):
"""
Returns list of currently pressed buttons.
Note that the sensor can only identify up to two buttons pressed at once.
"""
self._ensure_mode(self.MODE_IR_REMOTE)
channel = self._normalize_channel(channel)
return self._BUTTON_VALUES.get(self.value(channel), [])
def process(self):
"""
Check for currenly pressed buttons. If the new state differs from the
old state, call the appropriate button event handlers.
To use the on_channel1_top_left, etc handlers your program would do something like:
.. code:: python
def top_left_channel_1_action(state):
print("top left on channel 1: %s" % state)
def bottom_right_channel_4_action(state):
print("bottom right on channel 4: %s" % state)
ir = InfraredSensor()
ir.on_channel1_top_left = top_left_channel_1_action
ir.on_channel4_bottom_right = bottom_right_channel_4_action
while True:
ir.process()
time.sleep(0.01)
"""
new_state = []
state_diff = []
for channel in range(1, 5):
for button in self.buttons_pressed(channel):
new_state.append((button, channel))
# Key was not pressed before but now is pressed
if (button, channel) not in self._state:
state_diff.append((button, channel))
# Key was pressed but is no longer pressed
for button in self._BUTTONS:
if (button, channel) not in new_state and (button, channel) in self._state:
state_diff.append((button, channel))
self._state = new_state
for (button, channel) in state_diff:
handler = getattr(self, 'on_channel' + str(channel) + '_' + button)
if handler is not None:
handler((button, channel) in new_state)
if self.on_change is not None and state_diff:
self.on_change([(button, channel, button in new_state) for (button, channel) in state_diff])
class SoundSensor(Sensor):
"""
LEGO NXT Sound Sensor
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Sound pressure level. Flat weighting
MODE_DB = 'DB'
#: Sound pressure level. A weighting
MODE_DBA = 'DBA'
MODES = (
MODE_DB,
MODE_DBA,
)
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(SoundSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-nxt-sound', **kwargs)
@property
def sound_pressure(self):
"""
A measurement of the measured sound pressure level, as a
percent. Uses a flat weighting.
"""
self._ensure_mode(self.MODE_DB)
return self.value(0) * self._scale('DB')
@property
def sound_pressure_low(self):
"""
A measurement of the measured sound pressure level, as a
percent. Uses A-weighting, which focuses on levels up to 55 dB.
"""
self._ensure_mode(self.MODE_DBA)
return self.value(0) * self._scale('DBA')
class LightSensor(Sensor):
"""
LEGO NXT Light Sensor
"""
SYSTEM_CLASS_NAME = Sensor.SYSTEM_CLASS_NAME
SYSTEM_DEVICE_NAME_CONVENTION = Sensor.SYSTEM_DEVICE_NAME_CONVENTION
#: Reflected light. LED on
MODE_REFLECT = 'REFLECT'
#: Ambient light. LED off
MODE_AMBIENT = 'AMBIENT'
MODES = (
MODE_REFLECT,
MODE_AMBIENT,
)
def __init__(self, address=None, name_pattern=SYSTEM_DEVICE_NAME_CONVENTION, name_exact=False, **kwargs):
super(LightSensor, self).__init__(address, name_pattern, name_exact, driver_name='lego-nxt-light', **kwargs)
@property
def reflected_light_intensity(self):
"""
A measurement of the reflected light intensity, as a percentage.
"""
self._ensure_mode(self.MODE_REFLECT)
return self.value(0) * self._scale('REFLECT')
@property
def ambient_light_intensity(self):
"""
A measurement of the ambient light intensity, as a percentage.
"""
self._ensure_mode(self.MODE_AMBIENT)
return self.value(0) * self._scale('AMBIENT')
|