Skip to content

Commit 38497fe

Browse files
authored
fix(joystick): Update joystick to fix edge case (#411)
* fix(joystick): Update joystick to fix edge case * Update to ensure we handle the edge case that `center_deadzone_radius == range_deadzone == 0.5f`. In this case, both of the first conditionals would return false, then magnitude range would be 0.0, leading to div/0 error. This PR fixes it and ensures that there is not a case for which the magnitude_range will be calculated to be 0. Build and run `joystick/example` on QtPy ESP32-S3 and ensure new test passes * update example to explicitly test large / overlapping deadzones
1 parent a1566cb commit 38497fe

File tree

2 files changed

+159
-104
lines changed

2 files changed

+159
-104
lines changed

components/joystick/example/main/joystick_example.cpp

Lines changed: 158 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -8,116 +8,171 @@
88
using namespace std::chrono_literals;
99

1010
extern "C" void app_main(void) {
11+
espp::Logger logger({.tag = "Joystick", .level = espp::Logger::Verbosity::DEBUG});
12+
logger.info("Running joystick example");
1113
{
12-
fmt::print("Running joystick example\n");
13-
{
14-
//! [circular joystick example]
15-
float min = 0;
16-
float max = 255;
17-
float center = 127;
18-
float deadband_percent = 0.1;
19-
float deadband = deadband_percent * (max - min);
14+
logger.info("Starting circular / square joystick example");
15+
//! [circular joystick example]
16+
float min = 0;
17+
float max = 255;
18+
float center = 127;
19+
float deadband_percent = 0.1;
20+
float deadband = deadband_percent * (max - min);
2021

21-
// circular joystick
22-
espp::Joystick js1({
23-
.x_calibration = {.center = center, .minimum = min, .maximum = max},
24-
.y_calibration = {.center = center, .minimum = min, .maximum = max},
25-
.type = espp::Joystick::Type::CIRCULAR,
26-
.center_deadzone_radius = deadband_percent,
27-
.range_deadzone = deadband_percent,
28-
});
29-
// square joystick (for comparison)
30-
espp::Joystick js2({
31-
.x_calibration = {.center = center,
32-
.center_deadband = deadband,
33-
.minimum = min,
34-
.maximum = max,
35-
.range_deadband = deadband},
36-
.y_calibration = {.center = center,
37-
.center_deadband = deadband,
38-
.minimum = min,
39-
.maximum = max,
40-
.range_deadband = deadband},
41-
});
42-
// now make a loop where we update the raw valuse and print out the joystick values
43-
fmt::print("raw x, raw y, js1 x, js1 y, js2 x, js2 y\n");
44-
for (float x = min - 10.0f; x <= max + 10.0f; x += 10.0f) {
45-
for (float y = min - 10.0f; y <= max + 10.0f; y += 10.0f) {
46-
js1.update(x, y);
47-
js2.update(x, y);
48-
fmt::print("{}, {}, {}, {}, {}, {}\n", x, y, js1.x(), js1.y(), js2.x(), js2.y());
49-
}
22+
// circular joystick
23+
espp::Joystick js1({
24+
.x_calibration = {.center = center, .minimum = min, .maximum = max},
25+
.y_calibration = {.center = center, .minimum = min, .maximum = max},
26+
.type = espp::Joystick::Type::CIRCULAR,
27+
.center_deadzone_radius = deadband_percent,
28+
.range_deadzone = deadband_percent,
29+
});
30+
// square joystick (for comparison)
31+
espp::Joystick js2({
32+
.x_calibration = {.center = center,
33+
.center_deadband = deadband,
34+
.minimum = min,
35+
.maximum = max,
36+
.range_deadband = deadband},
37+
.y_calibration = {.center = center,
38+
.center_deadband = deadband,
39+
.minimum = min,
40+
.maximum = max,
41+
.range_deadband = deadband},
42+
});
43+
// now make a loop where we update the raw valuse and print out the joystick values
44+
fmt::print("raw x, raw y, js1 x, js1 y, js2 x, js2 y\n");
45+
for (float x = min - 10.0f; x <= max + 10.0f; x += 10.0f) {
46+
for (float y = min - 10.0f; y <= max + 10.0f; y += 10.0f) {
47+
js1.update(x, y);
48+
js2.update(x, y);
49+
fmt::print("{}, {}, {}, {}, {}, {}\n", x, y, js1.x(), js1.y(), js2.x(), js2.y());
5050
}
51-
//! [circular joystick example]
5251
}
53-
{
54-
//! [adc joystick example]
55-
static constexpr adc_unit_t ADC_UNIT = CONFIG_EXAMPLE_ADC_UNIT == 1 ? ADC_UNIT_1 : ADC_UNIT_2;
56-
static constexpr adc_channel_t ADC_CHANNEL_X = (adc_channel_t)CONFIG_EXAMPLE_ADC_CHANNEL_X;
57-
static constexpr adc_channel_t ADC_CHANNEL_Y = (adc_channel_t)CONFIG_EXAMPLE_ADC_CHANNEL_Y;
52+
//! [circular joystick example]
53+
}
54+
55+
{
56+
logger.info("Starting joystick deadzone (.5, .5) example");
57+
//! [joystick deadzone example]
58+
float min = 0;
59+
float max = 255;
60+
float center = 127;
61+
float center_deadzone_radius = 0.5;
62+
float range_deadzone_radius = 0.5;
63+
64+
// circular joystick
65+
espp::Joystick js1({
66+
.x_calibration = {.center = center, .minimum = min, .maximum = max},
67+
.y_calibration = {.center = center, .minimum = min, .maximum = max},
68+
.type = espp::Joystick::Type::CIRCULAR,
69+
.center_deadzone_radius = center_deadzone_radius,
70+
.range_deadzone = range_deadzone_radius,
71+
});
72+
// now make a loop where we update the raw valuse and print out the joystick values
73+
fmt::print("raw x, raw y, js x, js y\n");
74+
for (float x = min - 10.0f; x <= max + 10.0f; x += 10.0f) {
75+
for (float y = min - 10.0f; y <= max + 10.0f; y += 10.0f) {
76+
js1.update(x, y);
77+
fmt::print("{}, {}, {}, {}\n", x, y, js1.x(), js1.y());
78+
}
79+
}
80+
// update the deadzones to be very large (overlapping)
81+
logger.info("Setting deadzones to overlap");
82+
js1.set_center_deadzone_radius(0.75);
83+
js1.set_range_deadzone(0.75);
84+
// and run the loop again
85+
fmt::print("raw x, raw y, js x, js y\n");
86+
for (float x = min - 10.0f; x <= max + 10.0f; x += 10.0f) {
87+
for (float y = min - 10.0f; y <= max + 10.0f; y += 10.0f) {
88+
js1.update(x, y);
89+
fmt::print("{}, {}, {}, {}\n", x, y, js1.x(), js1.y());
90+
}
91+
}
92+
// extreme case: set both deadzones to be greater than 1, which should
93+
// mean they are both clamped to 1 by joystick.
94+
logger.info("Setting deadzones to be greater than 1");
95+
js1.set_center_deadzone_radius(2);
96+
js1.set_range_deadzone(2);
97+
// and run the loop again
98+
fmt::print("raw x, raw y, js x, js y\n");
99+
for (float x = min - 10.0f; x <= max + 10.0f; x += 10.0f) {
100+
for (float y = min - 10.0f; y <= max + 10.0f; y += 10.0f) {
101+
js1.update(x, y);
102+
fmt::print("{}, {}, {}, {}\n", x, y, js1.x(), js1.y());
103+
}
104+
}
105+
//! [joystick deadzone example]
106+
}
58107

59-
std::vector<espp::AdcConfig> channels{
60-
{.unit = ADC_UNIT, .channel = ADC_CHANNEL_X, .attenuation = ADC_ATTEN_DB_12},
61-
{.unit = ADC_UNIT, .channel = ADC_CHANNEL_Y, .attenuation = ADC_ATTEN_DB_12}};
62-
espp::OneshotAdc adc({
63-
.unit = ADC_UNIT_2,
64-
.channels = channels,
65-
});
66-
auto read_joystick = [&adc, &channels](float *x, float *y) -> bool {
67-
// this will be in mv
68-
auto maybe_x_mv = adc.read_mv(channels[0]);
69-
auto maybe_y_mv = adc.read_mv(channels[1]);
70-
if (maybe_x_mv.has_value() && maybe_y_mv.has_value()) {
71-
auto x_mv = maybe_x_mv.value();
72-
auto y_mv = maybe_y_mv.value();
73-
*x = (float)(x_mv);
74-
*y = (float)(y_mv);
75-
return true;
76-
}
77-
return false;
78-
};
79-
espp::Joystick js1({
80-
// convert [0, 3300]mV to approximately [-1.0f, 1.0f]
81-
.x_calibration =
82-
{.center = 1700.0f, .center_deadband = 100.0f, .minimum = 0.0f, .maximum = 3300.0f},
83-
.y_calibration =
84-
{.center = 1700.0f, .center_deadband = 100.0f, .minimum = 0.0f, .maximum = 3300.0f},
85-
.get_values = read_joystick,
86-
});
87-
espp::Joystick js2({
88-
// convert [0, 3300]mV to approximately [-1.0f, 1.0f]
89-
.x_calibration =
90-
{.center = 1700.0f, .center_deadband = 0.0f, .minimum = 0.0f, .maximum = 3300.0f},
91-
.y_calibration =
92-
{.center = 1700.0f, .center_deadband = 0.0f, .minimum = 0.0f, .maximum = 3300.0f},
93-
.type = espp::Joystick::Type::CIRCULAR,
94-
.center_deadzone_radius = 0.1f,
95-
.get_values = read_joystick,
96-
});
97-
auto task_fn = [&js1, &js2](std::mutex &m, std::condition_variable &cv) {
98-
js1.update();
99-
js2.update();
100-
fmt::print("{}, {}\n", js1, js2);
101-
// NOTE: sleeping in this way allows the sleep to exit early when the
102-
// task is being stopped / destroyed
103-
{
104-
std::unique_lock<std::mutex> lk(m);
105-
cv.wait_for(lk, 500ms);
106-
}
107-
// don't want to stop the task
108-
return false;
109-
};
110-
auto task = espp::Task({.callback = task_fn,
111-
.task_config = {.name = "Joystick"},
112-
.log_level = espp::Logger::Verbosity::INFO});
113-
fmt::print("js1 x, js1 y, js2 x, js2 y\n");
114-
task.start();
115-
//! [adc joystick example]
108+
{
109+
logger.info("Starting ADC joystick example");
110+
//! [adc joystick example]
111+
static constexpr adc_unit_t ADC_UNIT = CONFIG_EXAMPLE_ADC_UNIT == 1 ? ADC_UNIT_1 : ADC_UNIT_2;
112+
static constexpr adc_channel_t ADC_CHANNEL_X = (adc_channel_t)CONFIG_EXAMPLE_ADC_CHANNEL_X;
113+
static constexpr adc_channel_t ADC_CHANNEL_Y = (adc_channel_t)CONFIG_EXAMPLE_ADC_CHANNEL_Y;
116114

117-
// loop forever to let the task run and user to play with the joystick
118-
while (true) {
119-
std::this_thread::sleep_for(1s);
115+
std::vector<espp::AdcConfig> channels{
116+
{.unit = ADC_UNIT, .channel = ADC_CHANNEL_X, .attenuation = ADC_ATTEN_DB_12},
117+
{.unit = ADC_UNIT, .channel = ADC_CHANNEL_Y, .attenuation = ADC_ATTEN_DB_12}};
118+
espp::OneshotAdc adc({
119+
.unit = ADC_UNIT_2,
120+
.channels = channels,
121+
});
122+
auto read_joystick = [&adc, &channels](float *x, float *y) -> bool {
123+
// this will be in mv
124+
auto maybe_x_mv = adc.read_mv(channels[0]);
125+
auto maybe_y_mv = adc.read_mv(channels[1]);
126+
if (maybe_x_mv.has_value() && maybe_y_mv.has_value()) {
127+
auto x_mv = maybe_x_mv.value();
128+
auto y_mv = maybe_y_mv.value();
129+
*x = (float)(x_mv);
130+
*y = (float)(y_mv);
131+
return true;
132+
}
133+
return false;
134+
};
135+
espp::Joystick js1({
136+
// convert [0, 3300]mV to approximately [-1.0f, 1.0f]
137+
.x_calibration =
138+
{.center = 1700.0f, .center_deadband = 100.0f, .minimum = 0.0f, .maximum = 3300.0f},
139+
.y_calibration =
140+
{.center = 1700.0f, .center_deadband = 100.0f, .minimum = 0.0f, .maximum = 3300.0f},
141+
.get_values = read_joystick,
142+
});
143+
espp::Joystick js2({
144+
// convert [0, 3300]mV to approximately [-1.0f, 1.0f]
145+
.x_calibration =
146+
{.center = 1700.0f, .center_deadband = 0.0f, .minimum = 0.0f, .maximum = 3300.0f},
147+
.y_calibration =
148+
{.center = 1700.0f, .center_deadband = 0.0f, .minimum = 0.0f, .maximum = 3300.0f},
149+
.type = espp::Joystick::Type::CIRCULAR,
150+
.center_deadzone_radius = 0.1f,
151+
.get_values = read_joystick,
152+
});
153+
auto task_fn = [&js1, &js2](std::mutex &m, std::condition_variable &cv) {
154+
js1.update();
155+
js2.update();
156+
fmt::print("{}, {}\n", js1, js2);
157+
// NOTE: sleeping in this way allows the sleep to exit early when the
158+
// task is being stopped / destroyed
159+
{
160+
std::unique_lock<std::mutex> lk(m);
161+
cv.wait_for(lk, 500ms);
120162
}
163+
// don't want to stop the task
164+
return false;
165+
};
166+
auto task = espp::Task({.callback = task_fn,
167+
.task_config = {.name = "Joystick"},
168+
.log_level = espp::Logger::Verbosity::INFO});
169+
fmt::print("js1 x, js1 y, js2 x, js2 y\n");
170+
task.start();
171+
//! [adc joystick example]
172+
173+
// loop forever to let the task run and user to play with the joystick
174+
while (true) {
175+
std::this_thread::sleep_for(1s);
121176
}
122177
}
123178

components/joystick/src/joystick.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void espp::Joystick::recalculate(float raw_x, float raw_y) {
8888
if (magnitude < center_deadzone_radius_) {
8989
position_.x(0);
9090
position_.y(0);
91-
} else if (magnitude > 1.0f - range_deadzone_) {
91+
} else if (magnitude >= 1.0f - range_deadzone_) {
9292
position_ = position_.normalized();
9393
} else {
9494
const float magnitude_range = 1.0f - center_deadzone_radius_ - range_deadzone_;

0 commit comments

Comments
 (0)