Repository navigation
Export location module and implement compass API #725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| #pragma once | ||
|
|
||
| #include <tactility/device.h> | ||
| #include <tactility/error.h> | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" { | ||
| #endif | ||
|
|
||
| /** | ||
| * Magnetic field strength in µT, in the device frame (right-handed): | ||
| * +x points to the front of the device, +y to its left and +z up. | ||
| * Drivers transform the sensor axes into this frame. | ||
| * A level device facing magnetic north reads +x, and facing east reads +y. | ||
| */ | ||
| struct CompassData { | ||
| float x, y, z; | ||
| }; | ||
|
|
||
| /** | ||
| * @brief API for compass (magnetometer) drivers. | ||
| * | ||
| * @note read_field() is required. read_heading() is optional: a driver that can't service it | ||
| * leaves it NULL, and compass_read_heading() derives the heading from the x and y axes. | ||
| */ | ||
| struct CompassApi { | ||
| /** | ||
| * @brief Reads the current magnetic field. | ||
| * @param[in] device the compass device | ||
| * @param[out] data pointer to store the current reading | ||
| * @retval ERROR_NONE on success | ||
| */ | ||
| error_t (*read_field)(struct Device* device, struct CompassData* data); | ||
|
|
||
| /** | ||
| * @brief Reads the current heading. | ||
| * @param[in] device the compass device | ||
| * @param[out] heading_deg pointer to store the heading, in degrees clockwise from magnetic north [0, 360) | ||
| * @retval ERROR_NONE on success | ||
| */ | ||
| error_t (*read_heading)(struct Device* device, float* heading_deg); | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Reads the current magnetic field using the specified compass device, in µT. | ||
| */ | ||
| error_t compass_read_field(struct Device* device, struct CompassData* data); | ||
|
|
||
| /** | ||
| * @brief Reads the current heading using the specified compass device. | ||
| * Assumes the device is held level when the driver has no heading of its own. | ||
| * @retval ERROR_INVALID_STATE when the driver has no heading of its own and the horizontal field is zero | ||
| * @param[out] heading_deg the heading in degrees clockwise from magnetic north [0, 360) | ||
| */ | ||
| error_t compass_read_heading(struct Device* device, float* heading_deg); | ||
|
|
||
| extern const struct DeviceType COMPASS_TYPE; | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| #include <tactility/drivers/compass.h> | ||
| #include <tactility/device.h> | ||
|
|
||
| #include <cmath> | ||
|
|
||
| #define COMPASS_DRIVER_API(driver) ((struct CompassApi*)driver->api) | ||
|
|
||
| extern "C" { | ||
|
|
||
| error_t compass_read_field(Device* device, CompassData* data) { | ||
| const auto* driver = device_get_driver(device); | ||
| return COMPASS_DRIVER_API(driver)->read_field(device, data); | ||
| } | ||
|
|
||
| error_t compass_read_heading(Device* device, float* heading_deg) { | ||
| const auto* driver = device_get_driver(device); | ||
| auto* api = COMPASS_DRIVER_API(driver); | ||
| if (api->read_heading) return api->read_heading(device, heading_deg); | ||
|
|
||
| CompassData data; | ||
| error_t error = api->read_field(device, &data); | ||
| if (error != ERROR_NONE) return error; | ||
| if (data.x == 0.0f && data.y == 0.0f) return ERROR_INVALID_STATE; | ||
|
|
||
| float heading = std::atan2(data.y, data.x) * (180.0f / static_cast<float>(M_PI)); | ||
| if (heading < 0.0f) { | ||
| heading += 360.0f; | ||
| if (heading >= 360.0f) heading = 0.0f; | ||
| } | ||
| *heading_deg = heading; | ||
| return ERROR_NONE; | ||
| } | ||
|
|
||
| const DeviceType COMPASS_TYPE { | ||
| .name = "compass" | ||
| }; | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| #include "doctest.h" | ||
|
|
||
| #include <cmath> | ||
|
|
||
| #include <tactility/device.h> | ||
| #include <tactility/driver.h> | ||
| #include <tactility/drivers/compass.h> | ||
|
|
||
| namespace { | ||
|
|
||
| CompassData mock_field = {}; | ||
| error_t mock_field_error = ERROR_NONE; | ||
| float mock_heading = 0.0f; | ||
|
|
||
| int start(Device*) { return ERROR_NONE; } | ||
| int stop(Device*) { return ERROR_NONE; } | ||
|
|
||
| error_t read_field(Device*, CompassData* data) { | ||
| if (mock_field_error != ERROR_NONE) return mock_field_error; | ||
| *data = mock_field; | ||
| return ERROR_NONE; | ||
| } | ||
|
|
||
| error_t read_heading(Device*, float* heading_deg) { | ||
| *heading_deg = mock_heading; | ||
| return ERROR_NONE; | ||
| } | ||
|
|
||
| const CompassApi derived_api = { .read_field = read_field, .read_heading = nullptr }; | ||
| const CompassApi native_api = { .read_field = read_field, .read_heading = read_heading }; | ||
|
|
||
| Driver derived_driver = { | ||
| .name = "compass_test_derived", | ||
| .compatible = (const char*[]) { "compass_test_derived", nullptr }, | ||
| .start_device = start, | ||
| .stop_device = stop, | ||
| .api = &derived_api, | ||
| .device_type = &COMPASS_TYPE, | ||
| .internal = nullptr, | ||
| }; | ||
|
|
||
| Driver native_driver = { | ||
| .name = "compass_test_native", | ||
| .compatible = (const char*[]) { "compass_test_native", nullptr }, | ||
| .start_device = start, | ||
| .stop_device = stop, | ||
| .api = &native_api, | ||
| .device_type = &COMPASS_TYPE, | ||
| .internal = nullptr, | ||
| }; | ||
|
|
||
| /** Runs a body with a device bound to the driver */ | ||
| template <typename Body> | ||
| void with_device(Driver* driver, Body body) { | ||
| Device device = { .name = "compass_test_device", .config = nullptr, .parent = nullptr }; | ||
| REQUIRE_EQ(driver_construct_add(driver), ERROR_NONE); | ||
| REQUIRE_EQ(device_construct(&device), ERROR_NONE); | ||
| device_set_driver(&device, driver); | ||
| REQUIRE_EQ(device_add(&device), ERROR_NONE); | ||
|
|
||
| body(&device); | ||
|
|
||
| CHECK_EQ(device_remove(&device), ERROR_NONE); | ||
| CHECK_EQ(device_destruct(&device), ERROR_NONE); | ||
| CHECK_EQ(driver_remove_destruct(driver), ERROR_NONE); | ||
| } | ||
|
|
||
| float derived_heading(float x, float y) { | ||
| mock_field = { x, y, 0.0f }; | ||
| float heading = -1.0f; | ||
| with_device(&derived_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_heading(device, &heading), ERROR_NONE); | ||
| }); | ||
| return heading; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| TEST_CASE("compass_read_heading derives the cardinal directions from the field") { | ||
| mock_field_error = ERROR_NONE; | ||
| CHECK(derived_heading(1.0f, 0.0f) == doctest::Approx(0.0f)); | ||
| CHECK(derived_heading(0.0f, 1.0f) == doctest::Approx(90.0f)); | ||
| CHECK(derived_heading(-1.0f, 0.0f) == doctest::Approx(180.0f)); | ||
| CHECK(derived_heading(0.0f, -1.0f) == doctest::Approx(270.0f)); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading derives headings in the range [0, 360)") { | ||
| mock_field_error = ERROR_NONE; | ||
| CHECK(derived_heading(1.0f, 1.0f) == doctest::Approx(45.0f)); | ||
| CHECK(derived_heading(1.0f, -1.0f) == doctest::Approx(315.0f)); | ||
| CHECK(derived_heading(-1.0f, -1.0f) == doctest::Approx(225.0f)); | ||
|
|
||
| // Slightly below the x axis must wrap to just under 360, not go negative | ||
| const float heading = derived_heading(1.0f, -0.001f); | ||
| CHECK(heading >= 0.0f); | ||
| CHECK(heading < 360.0f); | ||
| CHECK(heading == doctest::Approx(359.94f).epsilon(0.001)); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading keeps a tiny negative angle below 360") { | ||
| mock_field_error = ERROR_NONE; | ||
| // Adding 360 to such a small negative angle rounds to exactly 360.0f in float | ||
| const float heading = derived_heading(1.0f, -1e-9f); | ||
| CHECK(heading >= 0.0f); | ||
| CHECK(heading < 360.0f); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading ignores field strength and the z axis") { | ||
| mock_field_error = ERROR_NONE; | ||
| CHECK(derived_heading(40.0f, 40.0f) == doctest::Approx(45.0f)); | ||
|
|
||
| mock_field = { 1.0f, 0.0f, 500.0f }; | ||
| float heading = -1.0f; | ||
| with_device(&derived_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_heading(device, &heading), ERROR_NONE); | ||
| }); | ||
| CHECK(heading == doctest::Approx(0.0f)); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading rejects a zero horizontal field and leaves the output untouched") { | ||
| mock_field_error = ERROR_NONE; | ||
| mock_field = { 0.0f, 0.0f, 50.0f }; | ||
| float heading = 123.0f; | ||
| with_device(&derived_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_heading(device, &heading), ERROR_INVALID_STATE); | ||
| }); | ||
| CHECK_EQ(heading, 123.0f); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading propagates the field read error and leaves the output untouched") { | ||
| mock_field_error = ERROR_TIMEOUT; | ||
| float heading = 123.0f; | ||
| with_device(&derived_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_heading(device, &heading), ERROR_TIMEOUT); | ||
| }); | ||
| CHECK_EQ(heading, 123.0f); | ||
| mock_field_error = ERROR_NONE; | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_heading prefers the driver's own heading") { | ||
| mock_field_error = ERROR_NONE; | ||
| mock_field = { 1.0f, 0.0f, 0.0f }; | ||
| mock_heading = 200.0f; | ||
| float heading = -1.0f; | ||
| with_device(&native_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_heading(device, &heading), ERROR_NONE); | ||
| }); | ||
| CHECK(heading == doctest::Approx(200.0f)); | ||
| } | ||
|
|
||
| TEST_CASE("compass_read_field returns the driver's reading") { | ||
| mock_field_error = ERROR_NONE; | ||
| mock_field = { 1.5f, -2.5f, 3.5f }; | ||
| CompassData data = {}; | ||
| with_device(&derived_driver, [&](Device* device) { | ||
| CHECK_EQ(compass_read_field(device, &data), ERROR_NONE); | ||
| }); | ||
| CHECK(data.x == doctest::Approx(1.5f)); | ||
| CHECK(data.y == doctest::Approx(-2.5f)); | ||
| CHECK(data.z == doctest::Approx(3.5f)); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.