Skip to content

Commit 995349e

Browse files
committed
When using an analog sensor, wait for valid value
Problem reported by Max on nxt-python mailing list: when the color sensor is reset, because of a program start or stop for example, it takes the NXT firmware some time to return a valid value. Add a new get_valid_input_values to retry reading when value is not valid. Raise an nxt.sensor.analog.InvalidSensorReading on timeout. Use this new method for all "analog" sensors. Unrelated change: use dataclass for nxt.sensor.analog.RawReading class and rename mode to sensor_mode for coherency. This should not have any impact on user code as this is used mostly internally, and standard usage stay unchanged.
1 parent d46b6b4 commit 995349e

6 files changed

Lines changed: 110 additions & 42 deletions

File tree

nxt/sensor/analog.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,40 @@
1313
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1414
# GNU General Public License for more details.
1515

16+
import time
17+
from dataclasses import dataclass
18+
1619
import nxt.sensor
1720

1821

22+
class InvalidReading(Exception):
23+
"""Exception raised on timeout trying to get valid readings."""
24+
25+
pass
26+
27+
28+
@dataclass
1929
class RawReading:
2030
"""A object holding the raw sensor values for a sensor."""
2131

22-
def __init__(self, values):
23-
(
24-
self.port,
25-
self.valid,
26-
self.calibrated,
27-
self.sensor_type,
28-
self.mode,
29-
self.raw_value,
30-
self.normalized_value,
31-
self.scaled_value,
32-
self.calibrated_value,
33-
) = values
34-
35-
def __repr__(self):
36-
return str(
37-
(
38-
self.port,
39-
self.valid,
40-
self.calibrated,
41-
self.sensor_type,
42-
self.mode,
43-
self.raw_value,
44-
self.normalized_value,
45-
self.scaled_value,
46-
self.calibrated_value,
47-
)
48-
)
32+
#: Input port identifier.
33+
port: nxt.sensor.Port
34+
#: ``True`` if the value is valid, else ``False``.
35+
valid: bool
36+
#: Always ``False``, there is no calibration in NXT firmware.
37+
calibrated: bool
38+
#: Sensor type.
39+
sensor_type: nxt.sensor.Type
40+
#: Sensor mode.
41+
sensor_mode: nxt.sensor.Mode
42+
#: Raw analog to digital converter value.
43+
raw_value: int
44+
#: Normalized value.
45+
normalized_value: int
46+
#: Scaled value.
47+
scaled_value: int
48+
#: Always normalized value, there is no calibration in NXT firmware.
49+
calibrated_value: int
4950

5051

5152
class BaseAnalogSensor(nxt.sensor.Sensor):
@@ -57,7 +58,25 @@ def get_input_values(self):
5758
:return: An object with the read values.
5859
:rtype: RawReading
5960
"""
60-
return RawReading(self._brick.get_input_values(self._port))
61+
return RawReading(*self._brick.get_input_values(self._port))
62+
63+
def get_valid_input_values(self):
64+
"""Wait until input is valid, then get raw sensor readings.
65+
66+
:return: An object with the read values.
67+
:rtype: RawReading
68+
:raises InvalidReading: On timeout trying to get valid readings.
69+
"""
70+
tries = 10
71+
tries_delay_s = 0.1
72+
readings = self.get_input_values()
73+
while not readings.valid:
74+
tries -= 1
75+
if tries == 0:
76+
raise InvalidReading()
77+
time.sleep(tries_delay_s)
78+
readings = self.get_input_values()
79+
return readings
6180

6281
def reset_input_scaled_value(self):
6382
"""Reset sensor scaled value.

nxt/sensor/generic.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def is_pressed(self):
3434
:return: ``True`` if the sensor is pressed, else ``False``.
3535
:rtype: bool
3636
"""
37-
return bool(self.get_input_values().scaled_value)
37+
return bool(self.get_valid_input_values().scaled_value)
3838

3939
get_sample = is_pressed
4040

@@ -70,7 +70,7 @@ def get_lightness(self):
7070
:return: Detected light level between 0 and 1023.
7171
:rtype: int
7272
"""
73-
return self.get_input_values().scaled_value
73+
return self.get_valid_input_values().scaled_value
7474

7575
get_sample = get_lightness
7676

@@ -103,7 +103,7 @@ def get_loudness(self):
103103
:return: Detected sound level between 0 and 1023.
104104
:rtype: int
105105
"""
106-
return self.get_input_values().scaled_value
106+
return self.get_valid_input_values().scaled_value
107107

108108
get_sample = get_loudness
109109

@@ -266,7 +266,7 @@ def get_light_color(self):
266266
:return: Light color, one of the Type.COLOR_* values.
267267
:rtype: Type
268268
"""
269-
return self.get_input_values().sensor_type
269+
return self.get_valid_input_values().sensor_type
270270

271271
def get_reflected_light(self, color):
272272
"""Get detected light level.
@@ -276,7 +276,7 @@ def get_reflected_light(self, color):
276276
:rtype: int
277277
"""
278278
self.set_light_color(color)
279-
return self.get_input_values().scaled_value
279+
return self.get_valid_input_values().scaled_value
280280

281281
def get_color(self):
282282
"""Get detected color.
@@ -288,7 +288,7 @@ def get_color(self):
288288
determine color).
289289
"""
290290
self.get_reflected_light(Type.COLOR_FULL)
291-
return self.DetectedColor(self.get_input_values().scaled_value)
291+
return self.DetectedColor(self.get_valid_input_values().scaled_value)
292292

293293
get_sample = get_color
294294

nxt/sensor/hitechnic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def set_range_short(self):
352352
def get_raw_value(self):
353353
"""Unscaled value read from sensor."""
354354

355-
return self._MAX_DISTANCE - self.get_input_values().raw_value
355+
return self._MAX_DISTANCE - self.get_valid_input_values().raw_value
356356

357357
def get_processed_value(self):
358358
"""Derived from the square root of the raw value."""
@@ -496,7 +496,7 @@ def __init__(self, brick, port):
496496
self.offset = 0
497497

498498
def get_rotation_speed(self):
499-
return self.get_input_values().scaled_value - self.offset
499+
return self.get_valid_input_values().scaled_value - self.offset
500500

501501
def set_zero(self, value):
502502
self.offset = value

nxt/sensor/mindsensors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def set_long_range(self, val):
6262

6363
def get_sample(self):
6464
"""Returns the processed meaningful values of the sensor"""
65-
return self.Reading(self.get_input_values())
65+
return self.Reading(self.get_valid_input_values())
6666

6767

6868
class Compassv2(BaseDigitalSensor):

tests/conftest.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ def sleepf(delay):
6060
mtime.time.side_effect = timef
6161
mtime.sleep.side_effect = sleepf
6262

63-
with patch("nxt.brick.time", new=mtime), patch(
64-
"nxt.motcont.time", new=mtime
65-
), patch("nxt.motor.time", new=mtime), patch("nxt.sensor.digital.time", new=mtime):
63+
with (
64+
patch("nxt.brick.time", new=mtime),
65+
patch("nxt.motcont.time", new=mtime),
66+
patch("nxt.motor.time", new=mtime),
67+
patch("nxt.sensor.analog.time", new=mtime),
68+
patch("nxt.sensor.digital.time", new=mtime),
69+
):
6670
yield mtime
6771

6872

tests/test_sensors.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,64 @@ def test_analog(self, mbrick):
5050
assert v.valid is True
5151
assert v.calibrated is False
5252
assert v.sensor_type == Type.SWITCH
53-
assert v.mode == Mode.BOOL
53+
assert v.sensor_mode == Mode.BOOL
5454
assert v.raw_value == 1
5555
assert v.normalized_value == 2
5656
assert v.scaled_value == 3
5757
assert v.calibrated_value == 4
58-
assert str(v).startswith("(")
58+
assert str(v).startswith("RawReading(")
5959
s.reset_input_scaled_value()
6060
assert mbrick.mock_calls == [
6161
call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL),
6262
call.get_input_values(Port.S1),
6363
call.reset_input_scaled_value(Port.S1),
6464
]
6565

66+
def test_analog_retry(self, mbrick, mtime):
67+
s = mbrick.get_sensor(Port.S1, nxt.sensor.analog.BaseAnalogSensor)
68+
mbrick.get_input_values.side_effect = [
69+
(Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4),
70+
(Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4),
71+
(Port.S1, True, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4),
72+
]
73+
s.set_input_mode(Type.SWITCH, Mode.BOOL)
74+
v = s.get_valid_input_values()
75+
assert v.port == Port.S1
76+
assert v.valid is True
77+
assert v.calibrated is False
78+
assert v.sensor_type == Type.SWITCH
79+
assert v.sensor_mode == Mode.BOOL
80+
assert v.raw_value == 1
81+
assert v.normalized_value == 2
82+
assert v.scaled_value == 3
83+
assert v.calibrated_value == 4
84+
assert mbrick.mock_calls == [
85+
call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL),
86+
call.get_input_values(Port.S1),
87+
call.get_input_values(Port.S1),
88+
call.get_input_values(Port.S1),
89+
]
90+
assert mtime.sleep.mock_calls == [
91+
call(0.1),
92+
call(0.1),
93+
]
94+
95+
def test_analog_retry_fail(self, mbrick, mtime):
96+
retries = 10
97+
s = mbrick.get_sensor(Port.S1, nxt.sensor.analog.BaseAnalogSensor)
98+
mbrick.get_input_values.side_effect = retries * [
99+
(Port.S1, False, False, Type.SWITCH, Mode.BOOL, 1, 2, 3, 4),
100+
]
101+
s.set_input_mode(Type.SWITCH, Mode.BOOL)
102+
with pytest.raises(nxt.sensor.analog.InvalidReading):
103+
s.get_valid_input_values()
104+
assert mbrick.mock_calls == [
105+
call.set_input_mode(Port.S1, Type.SWITCH, Mode.BOOL),
106+
] + retries * [
107+
call.get_input_values(Port.S1),
108+
]
109+
assert mtime.sleep.mock_calls == (retries - 1) * [call(0.1)]
110+
66111
def test_touch(self, mbrick):
67112
assert (
68113
nxt.sensor.generic.Touch.get_sample is nxt.sensor.generic.Touch.is_pressed

0 commit comments

Comments
 (0)