mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Location services
This commit is contained in:
parent
c6e3366164
commit
c45a3a5769
@ -173,6 +173,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
||||
add_subdirectory(Modules/gps-module)
|
||||
add_subdirectory(Modules/graphics-module)
|
||||
add_subdirectory(Modules/http-module)
|
||||
add_subdirectory(Modules/location-module)
|
||||
add_subdirectory(Modules/mbedtls-module)
|
||||
add_subdirectory(Modules/mesh-module)
|
||||
add_subdirectory(Modules/mesh-meshcore-module)
|
||||
|
||||
24
Modules/location-module/CMakeLists.txt
Normal file
24
Modules/location-module/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
TactilityKernel
|
||||
TactilityKernelCpp
|
||||
service-module
|
||||
gps-module
|
||||
minmea
|
||||
)
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
list(APPEND REQUIRES_LIST lwip)
|
||||
endif ()
|
||||
|
||||
tactility_add_module(location-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES ${REQUIRES_LIST}
|
||||
)
|
||||
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
116
Modules/location-module/include/location/location.h
Normal file
116
Modules/location-module/include/location/location.h
Normal file
@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Location service. Location providers (e.g. GPS) report fixes to the service.
|
||||
* The most recent fix of a regular provider is used while it's less than 2 minutes old.
|
||||
* Otherwise, the most recent fix of a fallback provider (e.g. IP geolocation) is used, if there is one.
|
||||
* Providers only run while there is at least one subscription.
|
||||
*/
|
||||
|
||||
#define LOCATION_PROVIDER_ID_MAX_LENGTH 15
|
||||
|
||||
/** Horizontal accuracy in meters of a fix with LOCATION_PRECISION_APPROXIMATE */
|
||||
#define LOCATION_APPROXIMATE_ACCURACY_METERS 3000.0f
|
||||
|
||||
enum LocationPrecision {
|
||||
/** Providers only report fixes with a horizontal accuracy of LOCATION_APPROXIMATE_ACCURACY_METERS or worse */
|
||||
LOCATION_PRECISION_APPROXIMATE,
|
||||
/** Providers report fixes as accurate as they can */
|
||||
LOCATION_PRECISION_PRECISE,
|
||||
};
|
||||
|
||||
/**
|
||||
* A location fix. Values the provider doesn't know are NAN for floating point fields and 0 for the others.
|
||||
* Accuracies are the radius of 68% confidence, in the unit of the value they belong to.
|
||||
*/
|
||||
struct Location {
|
||||
/** Degrees in WGS84, positive is north */
|
||||
double latitude;
|
||||
/** Degrees in WGS84, positive is east */
|
||||
double longitude;
|
||||
/** Meters */
|
||||
float horizontal_accuracy;
|
||||
/** Meters above mean sea level */
|
||||
float altitude;
|
||||
/** Meters above the WGS84 ellipsoid */
|
||||
float ellipsoidal_altitude;
|
||||
/** Meters */
|
||||
float vertical_accuracy;
|
||||
/** Speed over ground in meters per second */
|
||||
float speed;
|
||||
/** Meters per second */
|
||||
float speed_accuracy;
|
||||
/** Direction of travel in degrees clockwise from true north, in range [0, 360) */
|
||||
float course;
|
||||
/** Degrees */
|
||||
float course_accuracy;
|
||||
/** Number of satellites used for the fix */
|
||||
uint8_t satellites;
|
||||
/** Time of the fix according to the provider, in milliseconds since the Unix epoch */
|
||||
int64_t time_ms;
|
||||
/** Milliseconds since boot when the service received the fix */
|
||||
uint64_t uptime_ms;
|
||||
/** Id of the provider that reported the fix */
|
||||
char provider[LOCATION_PROVIDER_ID_MAX_LENGTH + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow or disallow the use of location services. The service only runs while allowed. The choice is persisted.
|
||||
* Location services are disallowed by default.
|
||||
*/
|
||||
error_t location_set_enabled(bool enabled);
|
||||
|
||||
bool location_is_enabled(void);
|
||||
|
||||
/** Set the precision that providers should report. The choice is persisted. */
|
||||
error_t location_set_precision(enum LocationPrecision precision);
|
||||
|
||||
enum LocationPrecision location_get_precision(void);
|
||||
|
||||
/**
|
||||
* Get the current fix. Without subscriptions, this is the last known fix.
|
||||
* @param[out] location the fix
|
||||
* @retval ERROR_NOT_FOUND when no fix was reported since the service started, or location services are disabled
|
||||
*/
|
||||
error_t location_get(struct Location* location);
|
||||
|
||||
/**
|
||||
* Caller-owned subscription: the service signals @a bit on the event group whenever a new current fix is reported.
|
||||
* Read the fix with location_get().
|
||||
* @warning Fields other than `bit` are for internal use only.
|
||||
*/
|
||||
struct LocationSubscription {
|
||||
uint32_t bit;
|
||||
|
||||
struct {
|
||||
struct TaskEventGroup* event_group;
|
||||
struct LocationSubscription* next;
|
||||
} internal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribing starts the providers when location services are enabled. They stop when the last subscription is removed.
|
||||
* @param[in,out] sub subscription to register, must stay alive until unsubscribed
|
||||
* @param[in] event_group caller-owned group to wait on, must outlive @a sub
|
||||
* @retval ERROR_RESOURCE @a event_group has no free bits left
|
||||
* @retval ERROR_INVALID_STATE @a sub is already registered
|
||||
*/
|
||||
error_t location_subscribe(struct LocationSubscription* sub, struct TaskEventGroup* event_group);
|
||||
|
||||
/** @retval ERROR_NOT_FOUND when @a sub is not registered */
|
||||
error_t location_unsubscribe(struct LocationSubscription* sub);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
14
Modules/location-module/include/location/module.h
Normal file
14
Modules/location-module/include/location/module.h
Normal file
@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module location_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
69
Modules/location-module/include/location/provider.h
Normal file
69
Modules/location-module/include/location/provider.h
Normal file
@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Interface between the location service and a location provider. Not part of the app SDK.
|
||||
*/
|
||||
|
||||
/** The service, as seen by a provider */
|
||||
struct LocationHost;
|
||||
|
||||
struct LocationProviderApi {
|
||||
/** Unique id, at most LOCATION_PROVIDER_ID_MAX_LENGTH characters */
|
||||
const char* id;
|
||||
|
||||
/**
|
||||
* A fallback provider (e.g. IP geolocation) is only used while no other provider reported a fix in the last
|
||||
* 2 minutes. It should only work when location_host_needs_fallback() returns true.
|
||||
*/
|
||||
bool fallback;
|
||||
|
||||
/**
|
||||
* Start the provider. Called when the service starts, or when the provider registers while the service runs.
|
||||
* @return the provider state, NULL on failure
|
||||
*/
|
||||
void* (*create)(struct LocationHost* host);
|
||||
|
||||
/** Stop the provider. It must not report anymore once this returns. */
|
||||
void (*destroy)(void* provider);
|
||||
};
|
||||
|
||||
/** Maximum number of registered providers */
|
||||
#define LOCATION_PROVIDER_MAX_COUNT 4
|
||||
|
||||
/**
|
||||
* Register a provider. Usually called from the provider module's start().
|
||||
* @retval ERROR_ALREADY_EXISTS when a provider with the same id is registered
|
||||
* @retval ERROR_RESOURCE when LOCATION_PROVIDER_MAX_COUNT providers are registered
|
||||
*/
|
||||
error_t location_provider_register(const struct LocationProviderApi* api);
|
||||
|
||||
/** @retval ERROR_NOT_FOUND when @a api is not registered */
|
||||
error_t location_provider_unregister(const struct LocationProviderApi* api);
|
||||
|
||||
/**
|
||||
* Report a new fix. Can be called from any task.
|
||||
* The service sets the `provider` and `uptime_ms` fields.
|
||||
*/
|
||||
void location_host_report(struct LocationHost* host, const struct Location* location);
|
||||
|
||||
/**
|
||||
* Reduce a fix to LOCATION_PRECISION_APPROXIMATE: the position snaps to a grid and only position and time remain.
|
||||
* Providers that can report precise fixes apply this when location_get_precision() is LOCATION_PRECISION_APPROXIMATE.
|
||||
*/
|
||||
void location_make_approximate(struct Location* location);
|
||||
|
||||
/** @return true when no provider that isn't a fallback reported a fix in the last 2 minutes */
|
||||
bool location_host_needs_fallback(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
17
Modules/location-module/private/location/gps_provider.h
Normal file
17
Modules/location-module/private/location/gps_provider.h
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <minmea.h>
|
||||
|
||||
/** Uses the first started GPS device */
|
||||
extern const LocationProviderApi gps_location_provider;
|
||||
|
||||
/**
|
||||
* Convert the sentences of one GPS epoch into a fix.
|
||||
* @param[in] gga the GGA sentence of the same epoch, NULL when there is none
|
||||
* @return false when @a rmc has no valid fix
|
||||
*/
|
||||
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location);
|
||||
32
Modules/location-module/private/location/ip_provider.h
Normal file
32
Modules/location-module/private/location/ip_provider.h
Normal file
@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/** Fallback provider that looks up the location of the public IP address with ip-api.com */
|
||||
extern const LocationProviderApi ip_location_provider;
|
||||
|
||||
#define IP_API_MESSAGE_MAX_LENGTH 63
|
||||
|
||||
struct IpApiResponse {
|
||||
/** HTTP status code */
|
||||
int status_code;
|
||||
/** Requests left in the rate limit window, -1 when unknown */
|
||||
int rate_remaining;
|
||||
/** Seconds until the rate limit window resets, -1 when unknown */
|
||||
int rate_reset_seconds;
|
||||
bool success;
|
||||
/** Reason of failure, empty on success */
|
||||
char message[IP_API_MESSAGE_MAX_LENGTH + 1];
|
||||
double latitude;
|
||||
double longitude;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an HTTP response of the newline separated ip-api endpoint, requested with fields "status,message,lat,lon"
|
||||
* @param[in] response the NUL-terminated response, including the status line and headers
|
||||
* @return false when the response is malformed
|
||||
*/
|
||||
bool ip_api_parse_response(const char* response, IpApiResponse* out);
|
||||
@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <service/manifest.h>
|
||||
|
||||
extern const ServiceManifest location_service_manifest;
|
||||
16
Modules/location-module/private/location/location_settings.h
Normal file
16
Modules/location-module/private/location/location_settings.h
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#define LOCATION_SERVICE_ID "tactility.location"
|
||||
|
||||
struct LocationSettings {
|
||||
bool enabled;
|
||||
enum LocationPrecision precision;
|
||||
};
|
||||
|
||||
/** Loads the settings, or defaults when there are none */
|
||||
void location_settings_load(struct LocationSettings* settings);
|
||||
|
||||
error_t location_settings_save(const struct LocationSettings* settings);
|
||||
261
Modules/location-module/source/gps_provider.cpp
Normal file
261
Modules/location-module/source/gps_provider.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/gps_provider.h>
|
||||
|
||||
#include <gps/gps.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/device_listener.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
|
||||
#define TAG "location_gps"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t AWAIT_TIMEOUT_MS = 250;
|
||||
constexpr uint32_t DEVICE_POLL_INTERVAL_MS = 1000;
|
||||
constexpr uint32_t THREAD_STOP_TIMEOUT_MS = 3000;
|
||||
constexpr float METERS_PER_SECOND_PER_KNOT = 0.514444f;
|
||||
// Typical user equivalent range error of consumer receivers, which turns dilution of precision into meters
|
||||
constexpr float RANGE_ERROR_METERS = 5.0f;
|
||||
|
||||
/** The sentences received for one GPS epoch */
|
||||
struct Epoch {
|
||||
minmea_time time;
|
||||
bool hasRmc;
|
||||
bool hasGga;
|
||||
bool reported;
|
||||
minmea_sentence_rmc rmc;
|
||||
minmea_sentence_gga gga;
|
||||
};
|
||||
|
||||
struct GpsProvider {
|
||||
LocationHost* host = nullptr;
|
||||
Thread* thread = nullptr;
|
||||
std::atomic<bool> running = false;
|
||||
// Guards device and subscription. The device listener can outlive a provider run, so this state is static.
|
||||
Mutex mutex {};
|
||||
Device* device = nullptr;
|
||||
GpsSubscription subscription {};
|
||||
Epoch epoch {};
|
||||
|
||||
GpsProvider() { mutex_construct(&mutex); }
|
||||
~GpsProvider() { mutex_destruct(&mutex); }
|
||||
};
|
||||
|
||||
GpsProvider provider;
|
||||
|
||||
double toDegrees(const minmea_float& value) {
|
||||
const double raw = static_cast<double>(value.value) / value.scale;
|
||||
const int degrees = static_cast<int>(raw / 100);
|
||||
return degrees + (raw - degrees * 100) / 60.0;
|
||||
}
|
||||
|
||||
// Days from 1970-01-01 to a date in the proleptic Gregorian calendar
|
||||
int64_t daysSinceEpoch(int year, int month, int day) {
|
||||
year -= month <= 2;
|
||||
const int64_t era = (year >= 0 ? year : year - 399) / 400;
|
||||
const int yearOfEra = static_cast<int>(year - era * 400);
|
||||
const int dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
|
||||
const int dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
|
||||
return era * 146097 + dayOfEra - 719468;
|
||||
}
|
||||
|
||||
bool isSameTime(const minmea_time& a, const minmea_time& b) {
|
||||
return a.hours == b.hours && a.minutes == b.minutes && a.seconds == b.seconds && a.microseconds == b.microseconds;
|
||||
}
|
||||
|
||||
void report(Epoch& epoch) {
|
||||
Location location;
|
||||
if (gps_location_from_nmea(&epoch.rmc, epoch.hasGga ? &epoch.gga : nullptr, &location)) {
|
||||
if (location_get_precision() == LOCATION_PRECISION_APPROXIMATE) {
|
||||
location_make_approximate(&location);
|
||||
}
|
||||
location_host_report(provider.host, &location);
|
||||
}
|
||||
epoch.reported = true;
|
||||
}
|
||||
|
||||
void onSentence(const GpsEvent& event) {
|
||||
auto& epoch = provider.epoch;
|
||||
const minmea_time& time = event.type == GPS_EVENT_MESSAGE_RMC ? event.data.rmc.time : event.data.gga.time;
|
||||
if (!isSameTime(epoch.time, time)) {
|
||||
// Receivers that don't send GGA only get reported once the next epoch starts
|
||||
if (epoch.hasRmc && !epoch.reported) {
|
||||
report(epoch);
|
||||
}
|
||||
epoch = {};
|
||||
epoch.time = time;
|
||||
}
|
||||
|
||||
if (event.type == GPS_EVENT_MESSAGE_RMC) {
|
||||
epoch.rmc = event.data.rmc;
|
||||
epoch.hasRmc = true;
|
||||
} else {
|
||||
epoch.gga = event.data.gga;
|
||||
epoch.hasGga = true;
|
||||
}
|
||||
|
||||
if (epoch.hasRmc && epoch.hasGga && !epoch.reported) {
|
||||
report(epoch);
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribes before the driver stops, as the subscription can't be removed afterwards
|
||||
void onDeviceEvent(Device* device, DeviceEvent event, void*) {
|
||||
if (event != DEVICE_EVENT_STOPPING) {
|
||||
return;
|
||||
}
|
||||
mutex_lock(&provider.mutex);
|
||||
if (device == provider.device) {
|
||||
gps_event_unsubscribe(device, &provider.subscription);
|
||||
provider.device = nullptr;
|
||||
}
|
||||
mutex_unlock(&provider.mutex);
|
||||
}
|
||||
|
||||
/** Must be called with the mutex held */
|
||||
bool subscribe() {
|
||||
Device* device;
|
||||
if (device_get_first_active_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
const error_t error = gps_event_subscribe(device, &provider.subscription);
|
||||
device_put(device);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to subscribe to %s", device->name);
|
||||
return false;
|
||||
}
|
||||
LOG_I(TAG, "Using %s", device->name);
|
||||
provider.device = device;
|
||||
provider.epoch = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Must be called with the mutex held and a device subscribed */
|
||||
void receive() {
|
||||
auto& sub = provider.subscription;
|
||||
if (sub.sequence != sub.consumed_sequence) {
|
||||
sub.consumed_sequence = sub.sequence;
|
||||
} else if (gps_event_await(provider.device, &sub, millis_to_ticks(AWAIT_TIMEOUT_MS)) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (sub.event.type) {
|
||||
case GPS_EVENT_MESSAGE_RMC:
|
||||
case GPS_EVENT_MESSAGE_GGA:
|
||||
onSentence(sub.event);
|
||||
break;
|
||||
case GPS_EVENT_UNSUBSCRIBED:
|
||||
provider.device = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t threadMain(void*) {
|
||||
while (provider.running) {
|
||||
mutex_lock(&provider.mutex);
|
||||
const bool subscribed = provider.device != nullptr || subscribe();
|
||||
if (subscribed) {
|
||||
receive();
|
||||
}
|
||||
mutex_unlock(&provider.mutex);
|
||||
|
||||
if (!subscribed) {
|
||||
delay_millis(DEVICE_POLL_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
mutex_lock(&provider.mutex);
|
||||
if (provider.device != nullptr) {
|
||||
gps_event_unsubscribe(provider.device, &provider.subscription);
|
||||
provider.device = nullptr;
|
||||
}
|
||||
mutex_unlock(&provider.mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* create(LocationHost* host) {
|
||||
provider.host = host;
|
||||
provider.running = true;
|
||||
provider.thread = thread_alloc_full("location_gps", 4096, threadMain, nullptr, -1);
|
||||
if (provider.thread == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||
thread_set_stack_external(provider.thread, true);
|
||||
device_listener_add(onDeviceEvent, nullptr);
|
||||
if (thread_start(provider.thread) != ERROR_NONE) {
|
||||
device_listener_remove(onDeviceEvent, nullptr);
|
||||
thread_free(provider.thread);
|
||||
provider.thread = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
return &provider;
|
||||
}
|
||||
|
||||
void destroy(void*) {
|
||||
provider.running = false;
|
||||
if (thread_join(provider.thread, millis_to_ticks(THREAD_STOP_TIMEOUT_MS), millis_to_ticks(10)) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Thread did not stop in time");
|
||||
return;
|
||||
}
|
||||
device_listener_remove(onDeviceEvent, nullptr);
|
||||
thread_free(provider.thread);
|
||||
provider.thread = nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location) {
|
||||
if (!rmc->valid || rmc->latitude.scale == 0 || rmc->longitude.scale == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*location = Location {
|
||||
.latitude = toDegrees(rmc->latitude),
|
||||
.longitude = toDegrees(rmc->longitude),
|
||||
.horizontal_accuracy = NAN,
|
||||
.altitude = NAN,
|
||||
.ellipsoidal_altitude = NAN,
|
||||
.vertical_accuracy = NAN,
|
||||
.speed = minmea_tofloat(&rmc->speed) * METERS_PER_SECOND_PER_KNOT,
|
||||
.speed_accuracy = NAN,
|
||||
.course = minmea_tofloat(&rmc->course),
|
||||
.course_accuracy = NAN,
|
||||
.satellites = 0,
|
||||
.time_ms = 0,
|
||||
.uptime_ms = 0,
|
||||
.provider = {},
|
||||
};
|
||||
|
||||
tm dateTime;
|
||||
if (minmea_getdatetime(&dateTime, &rmc->date, &rmc->time) == 0) {
|
||||
const int64_t seconds = daysSinceEpoch(dateTime.tm_year + 1900, dateTime.tm_mon + 1, dateTime.tm_mday) * 86400 +
|
||||
dateTime.tm_hour * 3600 + dateTime.tm_min * 60 + dateTime.tm_sec;
|
||||
location->time_ms = seconds * 1000 + rmc->time.microseconds / 1000;
|
||||
}
|
||||
|
||||
if (gga != nullptr && gga->fix_quality > 0) {
|
||||
location->horizontal_accuracy = minmea_tofloat(&gga->hdop) * RANGE_ERROR_METERS;
|
||||
location->altitude = minmea_tofloat(&gga->altitude);
|
||||
location->ellipsoidal_altitude = location->altitude + minmea_tofloat(&gga->height);
|
||||
location->satellites = static_cast<uint8_t>(gga->satellites_tracked);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi gps_location_provider = {
|
||||
.id = "gps",
|
||||
.fallback = false,
|
||||
.create = create,
|
||||
.destroy = destroy,
|
||||
};
|
||||
369
Modules/location-module/source/ip_provider.cpp
Normal file
369
Modules/location-module/source/ip_provider.cpp
Normal file
@ -0,0 +1,369 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/ip_provider.h>
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/system_event.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <TactilityCpp/std/string.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
#include <strings.h>
|
||||
|
||||
#define TAG "location_ip"
|
||||
|
||||
namespace {
|
||||
|
||||
// The free ip-api endpoint only supports plain HTTP
|
||||
constexpr auto* API_HOST = "ip-api.com";
|
||||
constexpr auto* API_PATH = "/line/?fields=status,message,lat,lon";
|
||||
constexpr uint64_t REFRESH_INTERVAL_MS = 30 * 60 * 1000;
|
||||
constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000;
|
||||
// Bounds the request rate when the network connects repeatedly
|
||||
constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000;
|
||||
// Wake up interval to check whether the fallback became needed
|
||||
constexpr uint32_t CHECK_INTERVAL_MS = 10 * 1000;
|
||||
constexpr int SOCKET_TIMEOUT_SECONDS = 5;
|
||||
constexpr size_t RESPONSE_MAX_LENGTH = 1024;
|
||||
// DNS resolution has no timeout of its own
|
||||
constexpr uint32_t THREAD_STOP_TIMEOUT_MS = 30 * 1000;
|
||||
// IP geolocation is accurate to roughly city level
|
||||
constexpr float ACCURACY_METERS = 25000.0f;
|
||||
|
||||
struct IpProvider {
|
||||
LocationHost* host = nullptr;
|
||||
Thread* thread = nullptr;
|
||||
std::atomic<bool> running = false;
|
||||
TaskEventGroup group {};
|
||||
uint32_t stopBit = 0;
|
||||
SystemEventSubscription networkSubscription {};
|
||||
};
|
||||
|
||||
IpProvider provider;
|
||||
|
||||
uint64_t getUptimeMs() {
|
||||
return get_micros_since_boot() / 1000;
|
||||
}
|
||||
|
||||
// The FreeRTOS POSIX port's tick signal can interrupt blocking socket calls, so they retry on EINTR
|
||||
|
||||
bool connectWithTimeout(int fd, const sockaddr* address, socklen_t addressLength) {
|
||||
const int flags = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||
int result = connect(fd, address, addressLength);
|
||||
if (result < 0 && errno == EINPROGRESS) {
|
||||
fd_set writeSet;
|
||||
do {
|
||||
FD_ZERO(&writeSet);
|
||||
FD_SET(fd, &writeSet);
|
||||
timeval timeout { .tv_sec = SOCKET_TIMEOUT_SECONDS, .tv_usec = 0 };
|
||||
result = select(fd + 1, nullptr, &writeSet, nullptr, &timeout);
|
||||
} while (result < 0 && errno == EINTR);
|
||||
int error = 0;
|
||||
socklen_t errorLength = sizeof(error);
|
||||
if (result == 1 && getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &errorLength) == 0 && error == 0) {
|
||||
result = 0;
|
||||
} else {
|
||||
result = -1;
|
||||
}
|
||||
}
|
||||
fcntl(fd, F_SETFL, flags);
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
bool sendAll(int fd, const char* data, size_t length) {
|
||||
size_t sent = 0;
|
||||
while (sent < length) {
|
||||
const ssize_t result = send(fd, data + sent, length - sent, 0);
|
||||
if (result < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
sent += static_cast<size_t>(result);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Receive until the server closes the connection */
|
||||
bool receiveAll(int fd, char* buffer, size_t bufferSize) {
|
||||
size_t received = 0;
|
||||
while (received < bufferSize - 1) {
|
||||
const ssize_t result = recv(fd, buffer + received, bufferSize - 1 - received, 0);
|
||||
if (result < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (result == 0) {
|
||||
break;
|
||||
}
|
||||
received += static_cast<size_t>(result);
|
||||
}
|
||||
buffer[received] = '\0';
|
||||
return received > 0;
|
||||
}
|
||||
|
||||
bool httpGet(const char* host, const char* path, char* response, size_t responseSize) {
|
||||
addrinfo hints {};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
addrinfo* addresses = nullptr;
|
||||
if (getaddrinfo(host, "80", &hints, &addresses) != 0 || addresses == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int fd = socket(addresses->ai_family, addresses->ai_socktype, addresses->ai_protocol);
|
||||
if (fd < 0) {
|
||||
freeaddrinfo(addresses);
|
||||
return false;
|
||||
}
|
||||
|
||||
timeval timeout { .tv_sec = SOCKET_TIMEOUT_SECONDS, .tv_usec = 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||
|
||||
char request[160];
|
||||
const int requestLength = snprintf(request, sizeof(request), "GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n", path, host);
|
||||
|
||||
const bool success = connectWithTimeout(fd, addresses->ai_addr, addresses->ai_addrlen) &&
|
||||
sendAll(fd, request, static_cast<size_t>(requestLength)) &&
|
||||
receiveAll(fd, response, responseSize);
|
||||
|
||||
close(fd);
|
||||
freeaddrinfo(addresses);
|
||||
return success;
|
||||
}
|
||||
|
||||
/** @return the delay until the next lookup */
|
||||
uint64_t lookup() {
|
||||
tt::std::string response(RESPONSE_MAX_LENGTH, '\0');
|
||||
if (!httpGet(API_HOST, API_PATH, response.data(), response.size())) {
|
||||
LOG_W(TAG, "Request failed");
|
||||
return RETRY_INTERVAL_MS;
|
||||
}
|
||||
|
||||
IpApiResponse parsed;
|
||||
if (!ip_api_parse_response(response.c_str(), &parsed)) {
|
||||
LOG_W(TAG, "Malformed response");
|
||||
return RETRY_INTERVAL_MS;
|
||||
}
|
||||
|
||||
uint64_t delay = RETRY_INTERVAL_MS;
|
||||
if (parsed.status_code != 200) {
|
||||
LOG_W(TAG, "HTTP status %d", parsed.status_code);
|
||||
} else if (!parsed.success) {
|
||||
LOG_W(TAG, "Lookup failed: %s", parsed.message);
|
||||
} else {
|
||||
const Location location {
|
||||
.latitude = parsed.latitude,
|
||||
.longitude = parsed.longitude,
|
||||
.horizontal_accuracy = ACCURACY_METERS,
|
||||
.altitude = NAN,
|
||||
.ellipsoidal_altitude = NAN,
|
||||
.vertical_accuracy = NAN,
|
||||
.speed = NAN,
|
||||
.speed_accuracy = NAN,
|
||||
.course = NAN,
|
||||
.course_accuracy = NAN,
|
||||
.satellites = 0,
|
||||
.time_ms = 0,
|
||||
.uptime_ms = 0,
|
||||
.provider = {},
|
||||
};
|
||||
location_host_report(provider.host, &location);
|
||||
LOG_D(TAG, "Lookup succeeded");
|
||||
delay = REFRESH_INTERVAL_MS;
|
||||
}
|
||||
|
||||
if (parsed.rate_remaining == 0 && parsed.rate_reset_seconds > 0) {
|
||||
delay = std::max(delay, static_cast<uint64_t>(parsed.rate_reset_seconds) * 1000);
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
|
||||
int32_t threadMain(void*) {
|
||||
uint64_t lastAttemptMs = 0;
|
||||
uint64_t nextAttemptMs = 0;
|
||||
bool attempted = false;
|
||||
|
||||
while (provider.running) {
|
||||
const uint64_t now = getUptimeMs();
|
||||
const uint64_t waitMs = std::min<uint64_t>(CHECK_INTERVAL_MS, nextAttemptMs > now ? nextAttemptMs - now : 0);
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&provider.group, &flags, millis_to_ticks(static_cast<uint32_t>(waitMs)));
|
||||
if (!provider.running) {
|
||||
break;
|
||||
}
|
||||
|
||||
// The public IP address can change with the network
|
||||
if (system_event_poll(&provider.networkSubscription) == ERROR_NONE && attempted) {
|
||||
nextAttemptMs = lastAttemptMs + MIN_INTERVAL_MS;
|
||||
}
|
||||
|
||||
const uint64_t attemptMs = getUptimeMs();
|
||||
if (attemptMs < nextAttemptMs || !location_host_needs_fallback()) {
|
||||
continue;
|
||||
}
|
||||
attempted = true;
|
||||
lastAttemptMs = attemptMs;
|
||||
nextAttemptMs = attemptMs + lookup();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* create(LocationHost* host) {
|
||||
provider.host = host;
|
||||
provider.running = true;
|
||||
task_event_group_construct(&provider.group);
|
||||
if (task_event_group_claim_bit(&provider.group, &provider.stopBit) != ERROR_NONE) {
|
||||
task_event_group_destruct(&provider.group);
|
||||
return nullptr;
|
||||
}
|
||||
provider.networkSubscription = {};
|
||||
provider.networkSubscription.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
if (system_event_subscribe(&provider.networkSubscription, &provider.group) != ERROR_NONE) {
|
||||
task_event_group_destruct(&provider.group);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
provider.thread = thread_alloc_full("location_ip", 4096, threadMain, nullptr, -1);
|
||||
if (provider.thread != nullptr) {
|
||||
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||
thread_set_stack_external(provider.thread, true);
|
||||
}
|
||||
if (provider.thread == nullptr || thread_start(provider.thread) != ERROR_NONE) {
|
||||
if (provider.thread != nullptr) {
|
||||
thread_free(provider.thread);
|
||||
provider.thread = nullptr;
|
||||
}
|
||||
system_event_unsubscribe(&provider.networkSubscription);
|
||||
task_event_group_destruct(&provider.group);
|
||||
return nullptr;
|
||||
}
|
||||
return &provider;
|
||||
}
|
||||
|
||||
void destroy(void*) {
|
||||
provider.running = false;
|
||||
task_event_group_signal(&provider.group, provider.stopBit);
|
||||
if (thread_join(provider.thread, millis_to_ticks(THREAD_STOP_TIMEOUT_MS), millis_to_ticks(10)) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Thread did not stop in time");
|
||||
return;
|
||||
}
|
||||
thread_free(provider.thread);
|
||||
provider.thread = nullptr;
|
||||
system_event_unsubscribe(&provider.networkSubscription);
|
||||
task_event_group_destruct(&provider.group);
|
||||
}
|
||||
|
||||
std::string_view nextLine(std::string_view& text) {
|
||||
const size_t end = text.find('\n');
|
||||
std::string_view line = text.substr(0, end);
|
||||
text = end == std::string_view::npos ? std::string_view {} : text.substr(end + 1);
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.remove_suffix(1);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
int parseHeaderNumber(std::string_view line, const char* name) {
|
||||
const size_t nameLength = strlen(name);
|
||||
if (line.size() <= nameLength || line[nameLength] != ':' || strncasecmp(line.data(), name, nameLength) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return atoi(tt::std::string(line.substr(nameLength + 1)).c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
||||
*out = IpApiResponse {
|
||||
.status_code = 0,
|
||||
.rate_remaining = -1,
|
||||
.rate_reset_seconds = -1,
|
||||
.success = false,
|
||||
.message = {},
|
||||
.latitude = NAN,
|
||||
.longitude = NAN,
|
||||
};
|
||||
|
||||
std::string_view text = response;
|
||||
const std::string_view statusLine = nextLine(text);
|
||||
if (!statusLine.starts_with("HTTP/1.")) {
|
||||
return false;
|
||||
}
|
||||
const size_t space = statusLine.find(' ');
|
||||
if (space == std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
out->status_code = atoi(tt::std::string(statusLine.substr(space + 1)).c_str());
|
||||
|
||||
while (true) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view header = nextLine(text);
|
||||
if (header.empty()) {
|
||||
break;
|
||||
}
|
||||
const int remaining = parseHeaderNumber(header, "X-Rl");
|
||||
if (remaining >= 0) {
|
||||
out->rate_remaining = remaining;
|
||||
}
|
||||
const int resetSeconds = parseHeaderNumber(header, "X-Ttl");
|
||||
if (resetSeconds >= 0) {
|
||||
out->rate_reset_seconds = resetSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
if (out->status_code != 200) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fields are returned one per line, in the order of the API documentation
|
||||
const std::string_view status = nextLine(text);
|
||||
if (status == "success") {
|
||||
const tt::std::string latitude(nextLine(text));
|
||||
const tt::std::string longitude(nextLine(text));
|
||||
char* latitudeEnd;
|
||||
char* longitudeEnd;
|
||||
out->latitude = strtod(latitude.c_str(), &latitudeEnd);
|
||||
out->longitude = strtod(longitude.c_str(), &longitudeEnd);
|
||||
if (latitude.empty() || *latitudeEnd != '\0' || longitude.empty() || *longitudeEnd != '\0') {
|
||||
return false;
|
||||
}
|
||||
out->success = true;
|
||||
} else if (status == "fail") {
|
||||
const std::string_view message = nextLine(text).substr(0, IP_API_MESSAGE_MAX_LENGTH);
|
||||
memcpy(out->message, message.data(), message.size());
|
||||
out->message[message.size()] = '\0';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi ip_location_provider = {
|
||||
.id = "ip",
|
||||
.fallback = true,
|
||||
.create = create,
|
||||
.destroy = destroy,
|
||||
};
|
||||
385
Modules/location-module/source/location_service.cpp
Normal file
385
Modules/location-module/source/location_service.cpp
Normal file
@ -0,0 +1,385 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/location.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/location_settings.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
#include <service/manifest.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "location"
|
||||
|
||||
struct LocationHost {
|
||||
// Guarded by lifecycleMutex
|
||||
const LocationProviderApi* api;
|
||||
void* provider;
|
||||
|
||||
// Guarded by dataMutex
|
||||
bool fallback;
|
||||
bool hasLocation;
|
||||
Location location;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
/** A fix of a provider that isn't a fallback is preferred over fallback fixes for this long */
|
||||
constexpr uint64_t FRESH_MS = 120000;
|
||||
// About 2 km north to south
|
||||
constexpr double APPROXIMATE_GRID_DEGREES = 0.02;
|
||||
|
||||
struct GlobalMutex {
|
||||
Mutex handle {};
|
||||
GlobalMutex() { mutex_construct(&handle); }
|
||||
~GlobalMutex() { mutex_destruct(&handle); }
|
||||
};
|
||||
|
||||
// Serializes provider registration, service start/stop and (un)subscribing, which create and destroy providers
|
||||
GlobalMutex lifecycleMutex;
|
||||
LocationHost hosts[LOCATION_PROVIDER_MAX_COUNT] {};
|
||||
bool running = false;
|
||||
// Providers only exist while the service runs and has subscribers
|
||||
bool providersActive = false;
|
||||
|
||||
// Not held while calling provider functions, so providers can report while being destroyed
|
||||
GlobalMutex dataMutex;
|
||||
LocationSubscription* subscriptions = nullptr;
|
||||
|
||||
GlobalMutex settingsMutex;
|
||||
LocationSettings settings {};
|
||||
bool settingsLoaded = false;
|
||||
|
||||
/** Must be called with settingsMutex held */
|
||||
void ensureSettingsLoaded() {
|
||||
if (!settingsLoaded) {
|
||||
location_settings_load(&settings);
|
||||
settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t getUptimeMs() {
|
||||
return get_micros_since_boot() / 1000;
|
||||
}
|
||||
|
||||
/** Must be called with dataMutex held */
|
||||
int getRank(const LocationHost& host, uint64_t now) {
|
||||
if (host.fallback) {
|
||||
return 1;
|
||||
}
|
||||
return now - host.location.uptime_ms < FRESH_MS ? 2 : 0;
|
||||
}
|
||||
|
||||
/** Must be called with dataMutex held */
|
||||
const LocationHost* findBest() {
|
||||
const uint64_t now = getUptimeMs();
|
||||
const LocationHost* best = nullptr;
|
||||
int bestRank = -1;
|
||||
for (const auto& host : hosts) {
|
||||
if (!host.hasLocation) {
|
||||
continue;
|
||||
}
|
||||
const int rank = getRank(host, now);
|
||||
if (rank > bestRank || (rank == bestRank && host.location.uptime_ms > best->location.uptime_ms)) {
|
||||
best = &host;
|
||||
bestRank = rank;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void clearLocation(LocationHost* host) {
|
||||
mutex_lock(&dataMutex.handle);
|
||||
host->hasLocation = false;
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
}
|
||||
|
||||
void startProvider(LocationHost* host) {
|
||||
host->provider = host->api->create(host);
|
||||
if (host->provider == nullptr) {
|
||||
LOG_E(TAG, "Failed to start provider %s", host->api->id);
|
||||
}
|
||||
}
|
||||
|
||||
void stopProvider(LocationHost* host) {
|
||||
if (host->provider != nullptr) {
|
||||
host->api->destroy(host->provider);
|
||||
host->provider = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/** Must be called with lifecycleMutex held */
|
||||
void updateProviders() {
|
||||
mutex_lock(&dataMutex.handle);
|
||||
const bool hasSubscribers = subscriptions != nullptr;
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
|
||||
const bool shouldBeActive = running && hasSubscribers;
|
||||
if (shouldBeActive == providersActive) {
|
||||
return;
|
||||
}
|
||||
providersActive = shouldBeActive;
|
||||
for (auto& host : hosts) {
|
||||
if (host.api == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (shouldBeActive) {
|
||||
startProvider(&host);
|
||||
} else {
|
||||
// Fixes are kept as the last known location
|
||||
stopProvider(&host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void* createService(const ServiceManifest*) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void destroyService(const ServiceManifest*, void*) {}
|
||||
|
||||
error_t onStart(ServiceInstance*, void*) {
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
running = true;
|
||||
updateProviders();
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void onStop(ServiceInstance*, void*) {
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
running = false;
|
||||
updateProviders();
|
||||
for (auto& host : hosts) {
|
||||
clearLocation(&host);
|
||||
}
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t location_set_enabled(bool enabled) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
settings.enabled = enabled;
|
||||
const LocationSettings copy = settings;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
|
||||
const error_t saved = location_settings_save(©);
|
||||
error_t result;
|
||||
if (enabled) {
|
||||
result = service_manager_start(LOCATION_SERVICE_ID);
|
||||
if (result == ERROR_INVALID_STATE) {
|
||||
result = ERROR_NONE;
|
||||
}
|
||||
} else {
|
||||
result = service_manager_stop(LOCATION_SERVICE_ID);
|
||||
if (result == ERROR_NOT_FOUND) {
|
||||
result = ERROR_NONE;
|
||||
}
|
||||
}
|
||||
return result != ERROR_NONE ? result : saved;
|
||||
}
|
||||
|
||||
bool location_is_enabled(void) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
const bool enabled = settings.enabled;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
error_t location_set_precision(LocationPrecision precision) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
settings.precision = precision;
|
||||
const LocationSettings copy = settings;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
|
||||
if (precision == LOCATION_PRECISION_APPROXIMATE) {
|
||||
// Fixes reported with precise precision are dropped until their providers report again
|
||||
mutex_lock(&dataMutex.handle);
|
||||
for (auto& host : hosts) {
|
||||
if (host.hasLocation && !(host.location.horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS)) {
|
||||
host.hasLocation = false;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
}
|
||||
return location_settings_save(©);
|
||||
}
|
||||
|
||||
LocationPrecision location_get_precision(void) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
const LocationPrecision precision = settings.precision;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
return precision;
|
||||
}
|
||||
|
||||
void location_make_approximate(Location* location) {
|
||||
location->latitude = std::round(location->latitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||
location->longitude = std::round(location->longitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||
if (!(location->horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS)) {
|
||||
location->horizontal_accuracy = LOCATION_APPROXIMATE_ACCURACY_METERS;
|
||||
}
|
||||
location->altitude = NAN;
|
||||
location->ellipsoidal_altitude = NAN;
|
||||
location->vertical_accuracy = NAN;
|
||||
location->speed = NAN;
|
||||
location->speed_accuracy = NAN;
|
||||
location->course = NAN;
|
||||
location->course_accuracy = NAN;
|
||||
location->satellites = 0;
|
||||
}
|
||||
|
||||
error_t location_provider_register(const LocationProviderApi* api) {
|
||||
if (strlen(api->id) > LOCATION_PROVIDER_ID_MAX_LENGTH) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
LocationHost* freeHost = nullptr;
|
||||
for (auto& host : hosts) {
|
||||
if (host.api == nullptr) {
|
||||
if (freeHost == nullptr) {
|
||||
freeHost = &host;
|
||||
}
|
||||
} else if (strcmp(host.api->id, api->id) == 0) {
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
}
|
||||
if (freeHost == nullptr) {
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
freeHost->api = api;
|
||||
mutex_lock(&dataMutex.handle);
|
||||
freeHost->fallback = api->fallback;
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
if (providersActive) {
|
||||
startProvider(freeHost);
|
||||
}
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t location_provider_unregister(const LocationProviderApi* api) {
|
||||
error_t result = ERROR_NOT_FOUND;
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
for (auto& host : hosts) {
|
||||
if (host.api == api) {
|
||||
stopProvider(&host);
|
||||
clearLocation(&host);
|
||||
host.api = nullptr;
|
||||
result = ERROR_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
void location_host_report(LocationHost* host, const Location* location) {
|
||||
mutex_lock(&dataMutex.handle);
|
||||
host->location = *location;
|
||||
strcpy(host->location.provider, host->api->id);
|
||||
host->location.uptime_ms = getUptimeMs();
|
||||
host->hasLocation = true;
|
||||
if (findBest() == host) {
|
||||
for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
}
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
}
|
||||
|
||||
bool location_host_needs_fallback(void) {
|
||||
mutex_lock(&dataMutex.handle);
|
||||
const uint64_t now = getUptimeMs();
|
||||
bool needed = true;
|
||||
for (const auto& host : hosts) {
|
||||
if (host.hasLocation && getRank(host, now) == 2) {
|
||||
needed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
return needed;
|
||||
}
|
||||
|
||||
error_t location_get(Location* location) {
|
||||
mutex_lock(&dataMutex.handle);
|
||||
const auto* best = findBest();
|
||||
if (best != nullptr) {
|
||||
*location = best->location;
|
||||
}
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
return best != nullptr ? ERROR_NONE : ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
error_t location_subscribe(LocationSubscription* sub, TaskEventGroup* event_group) {
|
||||
uint32_t bit;
|
||||
const error_t claimed = task_event_group_claim_bit(event_group, &bit);
|
||||
if (claimed != ERROR_NONE) {
|
||||
return claimed;
|
||||
}
|
||||
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
mutex_lock(&dataMutex.handle);
|
||||
for (auto* existing = subscriptions; existing != nullptr; existing = existing->internal.next) {
|
||||
if (existing == sub) {
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
task_event_group_release_bit(event_group, bit);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
sub->bit = bit;
|
||||
sub->internal.event_group = event_group;
|
||||
sub->internal.next = subscriptions;
|
||||
subscriptions = sub;
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
updateProviders();
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t location_unsubscribe(LocationSubscription* sub) {
|
||||
error_t result = ERROR_NOT_FOUND;
|
||||
mutex_lock(&lifecycleMutex.handle);
|
||||
mutex_lock(&dataMutex.handle);
|
||||
for (auto** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||
if (*link == sub) {
|
||||
*link = sub->internal.next;
|
||||
result = ERROR_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&dataMutex.handle);
|
||||
updateProviders();
|
||||
mutex_unlock(&lifecycleMutex.handle);
|
||||
if (result == ERROR_NONE) {
|
||||
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern const ServiceManifest location_service_manifest = {
|
||||
.id = LOCATION_SERVICE_ID,
|
||||
.create_service = createService,
|
||||
.destroy_service = destroyService,
|
||||
.on_start = onStart,
|
||||
.on_stop = onStop,
|
||||
};
|
||||
66
Modules/location-module/source/location_settings.cpp
Normal file
66
Modules/location-module/source/location_settings.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/location_settings.h>
|
||||
|
||||
#include <service/paths.h>
|
||||
|
||||
#include <tactility/filesystem/fs.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
#include <tactility/properties_file.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "location_settings";
|
||||
constexpr auto* SETTINGS_FILE_NAME = "settings.properties";
|
||||
constexpr auto* KEY_ENABLED = "enabled";
|
||||
constexpr auto* KEY_PRECISION = "precision";
|
||||
constexpr auto* PRECISION_APPROXIMATE = "approximate";
|
||||
constexpr auto* PRECISION_PRECISE = "precise";
|
||||
|
||||
void location_settings_load(LocationSettings* settings) {
|
||||
*settings = LocationSettings {
|
||||
.enabled = false,
|
||||
.precision = LOCATION_PRECISION_PRECISE,
|
||||
};
|
||||
|
||||
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
if (service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
char value[32];
|
||||
if (properties_file_get(file, KEY_ENABLED, value, sizeof(value)) == ERROR_NONE) {
|
||||
settings->enabled = strcmp(value, "true") == 0;
|
||||
}
|
||||
if (properties_file_get(file, KEY_PRECISION, value, sizeof(value)) == ERROR_NONE) {
|
||||
settings->precision = strcmp(value, PRECISION_APPROXIMATE) == 0 ? LOCATION_PRECISION_APPROXIMATE : LOCATION_PRECISION_PRECISE;
|
||||
}
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
error_t location_settings_save(const LocationSettings* settings) {
|
||||
char directory[FILE_MAX_PATH_STRING_LENGTH];
|
||||
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
if (service_paths_get_user_data_directory(LOCATION_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE ||
|
||||
service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
const error_t made = directory_make(directory, true);
|
||||
if (made != ERROR_NONE && made != ERROR_ALREADY_EXISTS) {
|
||||
LOG_E(TAG, "Failed to create %s", directory);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
if (file == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
properties_file_set(file, KEY_ENABLED, settings->enabled ? "true" : "false");
|
||||
properties_file_set(file, KEY_PRECISION, settings->precision == LOCATION_PRECISION_APPROXIMATE ? PRECISION_APPROXIMATE : PRECISION_PRECISE);
|
||||
return properties_file_close(file);
|
||||
}
|
||||
58
Modules/location-module/source/module.cpp
Normal file
58
Modules/location-module/source/module.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/gps_provider.h>
|
||||
#include <location/ip_provider.h>
|
||||
#include <location/location.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
static error_t start() {
|
||||
error_t error = location_provider_register(&gps_location_provider);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
error = location_provider_register(&ip_location_provider);
|
||||
if (error != ERROR_NONE) {
|
||||
location_provider_unregister(&gps_location_provider);
|
||||
return error;
|
||||
}
|
||||
return service_manager_add(&location_service_manifest, location_is_enabled());
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
const error_t error = service_manager_stop(location_service_manifest.id);
|
||||
if (error != ERROR_NONE && error != ERROR_NOT_FOUND) {
|
||||
return error;
|
||||
}
|
||||
location_provider_unregister(&ip_location_provider);
|
||||
location_provider_unregister(&gps_location_provider);
|
||||
return service_manager_remove(location_service_manifest.id);
|
||||
}
|
||||
|
||||
static const ModuleSymbol location_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(location_set_enabled),
|
||||
DEFINE_MODULE_SYMBOL(location_is_enabled),
|
||||
DEFINE_MODULE_SYMBOL(location_set_precision),
|
||||
DEFINE_MODULE_SYMBOL(location_get_precision),
|
||||
DEFINE_MODULE_SYMBOL(location_get),
|
||||
DEFINE_MODULE_SYMBOL(location_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(location_unsubscribe),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module location_module = {
|
||||
.name = "location",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = location_module_symbols,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
20
Modules/location-module/tests/CMakeLists.txt
Normal file
20
Modules/location-module/tests/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
||||
project(LocationModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(LocationModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
target_include_directories(LocationModuleTests PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../private
|
||||
)
|
||||
|
||||
add_test(NAME LocationModuleTests COMMAND LocationModuleTests)
|
||||
|
||||
target_link_libraries(LocationModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
platform-posix
|
||||
service-module
|
||||
location-module
|
||||
)
|
||||
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/gps_provider.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* RMC = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||
constexpr auto* GGA = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea converts RMC and GGA") {
|
||||
minmea_sentence_rmc rmc;
|
||||
minmea_sentence_gga gga;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||
REQUIRE(minmea_parse_gga(&gga, GGA));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, &gga, &location));
|
||||
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||
CHECK(location.longitude == doctest::Approx(11.516667));
|
||||
CHECK(location.speed == doctest::Approx(11.5235).epsilon(0.001));
|
||||
CHECK(location.course == doctest::Approx(84.4));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(4.5));
|
||||
CHECK(location.altitude == doctest::Approx(545.4));
|
||||
CHECK(location.ellipsoidal_altitude == doctest::Approx(592.3));
|
||||
CHECK(std::isnan(location.vertical_accuracy));
|
||||
CHECK(location.satellites == 8);
|
||||
CHECK(location.time_ms == 764426119000LL);
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea without GGA has no altitude or accuracy") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||
CHECK(std::isnan(location.altitude));
|
||||
CHECK(std::isnan(location.horizontal_accuracy));
|
||||
CHECK(location.satellites == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea handles the southern and western hemispheres") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,A,3352.128,S,15112.558,W,000.0,,230394,,*00"));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
CHECK(location.latitude == doctest::Approx(-33.8688));
|
||||
CHECK(location.longitude == doctest::Approx(-151.209300));
|
||||
CHECK(location.speed == doctest::Approx(0.0));
|
||||
CHECK(std::isnan(location.course));
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea rejects a fix that is not valid") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,V,,,,,,,230394,,*00"));
|
||||
|
||||
Location location;
|
||||
CHECK_FALSE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
}
|
||||
53
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
53
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/ip_provider.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
||||
const char* response =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
"X-Ttl: 60\r\n"
|
||||
"X-Rl: 44\r\n"
|
||||
"\r\n"
|
||||
"success\n"
|
||||
"39.03\n"
|
||||
"-77.5\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK(parsed.status_code == 200);
|
||||
CHECK(parsed.success);
|
||||
CHECK(parsed.latitude == doctest::Approx(39.03));
|
||||
CHECK(parsed.longitude == doctest::Approx(-77.5));
|
||||
CHECK(parsed.rate_remaining == 44);
|
||||
CHECK(parsed.rate_reset_seconds == 60);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response parses a failed lookup") {
|
||||
const char* response = "HTTP/1.1 200 OK\r\nx-rl: 0\r\nx-ttl: 12\r\n\r\nfail\nprivate range\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK_FALSE(parsed.success);
|
||||
CHECK(strcmp(parsed.message, "private range") == 0);
|
||||
CHECK(parsed.rate_remaining == 0);
|
||||
CHECK(parsed.rate_reset_seconds == 12);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response handles a throttled request") {
|
||||
const char* response = "HTTP/1.1 429 Too Many Requests\r\nX-Rl: 0\r\nX-Ttl: 30\r\n\r\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK(parsed.status_code == 429);
|
||||
CHECK_FALSE(parsed.success);
|
||||
CHECK(parsed.rate_reset_seconds == 30);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response rejects malformed responses") {
|
||||
IpApiResponse parsed;
|
||||
CHECK_FALSE(ip_api_parse_response("", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("garbage", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\nX-Rl: 1\r\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\nabc\n1.0\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nunknown\n", &parsed));
|
||||
}
|
||||
48
Modules/location-module/tests/source/main.cpp
Normal file
48
Modules/location-module/tests/source/main.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <tactility/dts.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/kernel_init.h>
|
||||
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
extern "C" struct Module platform_posix_module;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
context.setOption("no-breaks", true);
|
||||
|
||||
Module* dts_modules[] = { &platform_posix_module, nullptr };
|
||||
DtsDevice dts_devices[] = { DTS_DEVICE_TERMINATOR };
|
||||
check(kernel_init(dts_modules, dts_devices) == ERROR_NONE);
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
if (xTaskCreate(test_task, "test_task", 16384, &data, 1, nullptr) != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
return data.result;
|
||||
}
|
||||
221
Modules/location-module/tests/source/service_test.cpp
Normal file
221
Modules/location-module/tests/source/service_test.cpp
Normal file
@ -0,0 +1,221 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/location.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
LocationHost* fakeHost = nullptr;
|
||||
int createCount = 0;
|
||||
int destroyCount = 0;
|
||||
|
||||
void* fakeCreate(LocationHost* host) {
|
||||
fakeHost = host;
|
||||
createCount++;
|
||||
return host;
|
||||
}
|
||||
|
||||
void fakeDestroy(void*) {
|
||||
fakeHost = nullptr;
|
||||
destroyCount++;
|
||||
}
|
||||
|
||||
const LocationProviderApi fakeProvider = {
|
||||
.id = "fake",
|
||||
.fallback = false,
|
||||
.create = fakeCreate,
|
||||
.destroy = fakeDestroy,
|
||||
};
|
||||
|
||||
Location makeLocation(double latitude, double longitude) {
|
||||
return Location {
|
||||
.latitude = latitude,
|
||||
.longitude = longitude,
|
||||
.horizontal_accuracy = 3.0f,
|
||||
.altitude = NAN,
|
||||
.ellipsoidal_altitude = NAN,
|
||||
.vertical_accuracy = NAN,
|
||||
.speed = NAN,
|
||||
.speed_accuracy = NAN,
|
||||
.course = NAN,
|
||||
.course_accuracy = NAN,
|
||||
.satellites = 0,
|
||||
.time_ms = 1000,
|
||||
.uptime_ms = 0,
|
||||
.provider = {},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service reports fixes of its providers") {
|
||||
createCount = 0;
|
||||
destroyCount = 0;
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_provider_register(&fakeProvider) == ERROR_ALREADY_EXISTS);
|
||||
|
||||
// Providers don't run without subscriptions
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
CHECK(createCount == 0);
|
||||
|
||||
Location location;
|
||||
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(location_subscribe(&sub, &group) == ERROR_INVALID_STATE);
|
||||
CHECK(createCount == 1);
|
||||
REQUIRE(fakeHost != nullptr);
|
||||
|
||||
const Location reported = makeLocation(51.05, 3.72);
|
||||
location_host_report(fakeHost, &reported);
|
||||
|
||||
uint32_t flags = 0;
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, &flags, 0) == ERROR_NONE);
|
||||
CHECK(flags == sub.bit);
|
||||
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(location.latitude == doctest::Approx(51.05));
|
||||
CHECK(location.longitude == doctest::Approx(3.72));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(3.0));
|
||||
CHECK(location.time_ms == 1000);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// The last known fix remains available after the last subscription is removed
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NOT_FOUND);
|
||||
CHECK(destroyCount == 1);
|
||||
CHECK(location_get(&location) == ERROR_NONE);
|
||||
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(createCount == 2);
|
||||
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(destroyCount == 2);
|
||||
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||
|
||||
REQUIRE(service_manager_start(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(createCount == 3);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(destroyCount == 3);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NOT_FOUND);
|
||||
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(destroyCount == 3);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("location provider registration is limited") {
|
||||
LocationProviderApi providers[LOCATION_PROVIDER_MAX_COUNT + 1];
|
||||
char ids[LOCATION_PROVIDER_MAX_COUNT + 1][8];
|
||||
for (size_t i = 0; i <= LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
snprintf(ids[i], sizeof(ids[i]), "p%zu", i);
|
||||
providers[i] = { .id = ids[i], .fallback = false, .create = fakeCreate, .destroy = fakeDestroy };
|
||||
}
|
||||
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
REQUIRE(location_provider_register(&providers[i]) == ERROR_NONE);
|
||||
}
|
||||
CHECK(location_provider_register(&providers[LOCATION_PROVIDER_MAX_COUNT]) == ERROR_RESOURCE);
|
||||
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
CHECK(location_provider_unregister(&providers[i]) == ERROR_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
LocationHost* fallbackHost = nullptr;
|
||||
|
||||
void* fallbackCreate(LocationHost* host) {
|
||||
fallbackHost = host;
|
||||
return host;
|
||||
}
|
||||
|
||||
void fallbackDestroy(void*) {
|
||||
fallbackHost = nullptr;
|
||||
}
|
||||
|
||||
const LocationProviderApi fakeFallbackProvider = {
|
||||
.id = "fakefallback",
|
||||
.fallback = true,
|
||||
.create = fallbackCreate,
|
||||
.destroy = fallbackDestroy,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service prefers fixes of providers that aren't a fallback") {
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(location_provider_register(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
REQUIRE(fakeHost != nullptr);
|
||||
REQUIRE(fallbackHost != nullptr);
|
||||
|
||||
CHECK(location_host_needs_fallback());
|
||||
const Location fallback = makeLocation(10.0, 20.0);
|
||||
location_host_report(fallbackHost, &fallback);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||
|
||||
Location location;
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||
|
||||
const Location primary = makeLocation(51.05, 3.72);
|
||||
location_host_report(fakeHost, &primary);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||
CHECK_FALSE(location_host_needs_fallback());
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// A fallback fix doesn't replace a fresh fix, so subscribers aren't signalled
|
||||
location_host_report(fallbackHost, &fallback);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_TIMEOUT);
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// Without the primary provider, the fallback fix is used again
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_host_needs_fallback());
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("location_make_approximate keeps only a coarse position") {
|
||||
Location location = makeLocation(51.054321, 3.721234);
|
||||
location.altitude = 12.0f;
|
||||
location.speed = 1.5f;
|
||||
location.satellites = 7;
|
||||
location_make_approximate(&location);
|
||||
CHECK(location.latitude == doctest::Approx(51.06));
|
||||
CHECK(location.longitude == doctest::Approx(3.72));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(LOCATION_APPROXIMATE_ACCURACY_METERS));
|
||||
CHECK(std::isnan(location.altitude));
|
||||
CHECK(std::isnan(location.speed));
|
||||
CHECK(location.satellites == 0);
|
||||
CHECK(location.time_ms == 1000);
|
||||
|
||||
location.horizontal_accuracy = 25000.0f;
|
||||
location_make_approximate(&location);
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(25000.0));
|
||||
}
|
||||
@ -41,6 +41,7 @@ list(APPEND REQUIRES_LIST
|
||||
gps-module
|
||||
graphics-module
|
||||
http-module
|
||||
location-module
|
||||
mbedtls-module
|
||||
mesh-module
|
||||
mesh-meshcore-module
|
||||
|
||||
@ -58,6 +58,7 @@ namespace app {
|
||||
namespace launcher { extern const ::AppManifest manifest; }
|
||||
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
||||
namespace localesettings { extern const ::AppManifest manifest; }
|
||||
namespace locationsettings { extern const ::AppManifest manifest; }
|
||||
namespace notes { extern const ::AppManifest manifest; }
|
||||
namespace power { extern const ::AppManifest manifest; }
|
||||
namespace poweroff { extern const ::AppManifest manifest; }
|
||||
@ -130,6 +131,7 @@ static void registerInternalApps() {
|
||||
app_manager_add(&app::inputdialog::manifest);
|
||||
app_manager_add(&app::launcher::manifest);
|
||||
app_manager_add(&app::localesettings::manifest);
|
||||
app_manager_add(&app::locationsettings::manifest);
|
||||
app_manager_add(&app::notes::manifest);
|
||||
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
||||
app_manager_add(&app::poweroff::manifest);
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/settings/RadioSettings.h>
|
||||
|
||||
#include <location/module.h>
|
||||
#include <wifi/module.h>
|
||||
#if TT_FEATURE_MESH
|
||||
#include <mesh/module.h>
|
||||
@ -49,6 +50,7 @@ void registerAndStartServices() {
|
||||
addService(service::audio::manifest);
|
||||
}
|
||||
check(module_ensure_started(&wifi_module) == ERROR_NONE);
|
||||
check(module_ensure_started(&location_module) == ERROR_NONE);
|
||||
// Before the mesh service starts, as it needs the region to turn on the radio
|
||||
settings::radio::applyRegion();
|
||||
#if TT_FEATURE_MESH
|
||||
|
||||
@ -29,6 +29,7 @@ constexpr IconEntry ICONS[] = {
|
||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||
{"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON},
|
||||
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
|
||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
||||
|
||||
244
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
244
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
@ -0,0 +1,244 @@
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <TactilityCpp/std/string.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
|
||||
namespace tt::app::locationsettings {
|
||||
|
||||
extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
struct PrecisionOption {
|
||||
LocationPrecision precision;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr PrecisionOption PRECISION_OPTIONS[] = {
|
||||
{ LOCATION_PRECISION_APPROXIMATE, "Approximate" },
|
||||
{ LOCATION_PRECISION_PRECISE, "Precise" },
|
||||
};
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t enableBit = 0;
|
||||
uint32_t precisionBit = 0;
|
||||
::std::atomic<bool> requestedEnabled = false;
|
||||
::std::atomic<LocationPrecision> requestedPrecision = LOCATION_PRECISION_PRECISE;
|
||||
// Valid while the window is shown
|
||||
lv_obj_t* enableSwitch = nullptr;
|
||||
lv_obj_t* precisionChips = nullptr;
|
||||
lv_obj_t* statusLabel = nullptr;
|
||||
};
|
||||
|
||||
LocationPrecision getChipPrecision(lv_obj_t* chip) {
|
||||
return static_cast<LocationPrecision>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip)));
|
||||
}
|
||||
|
||||
std::string getStatusText() {
|
||||
if (!location_is_enabled()) {
|
||||
return "Location services are off";
|
||||
}
|
||||
Location location;
|
||||
if (location_get(&location) != ERROR_NONE) {
|
||||
return "Waiting for location...";
|
||||
}
|
||||
std::string text;
|
||||
auto output = ::std::back_inserter(text);
|
||||
::std::format_to(output, "{:.5f}, {:.5f}", location.latitude, location.longitude);
|
||||
if (!::std::isnan(location.horizontal_accuracy)) {
|
||||
::std::format_to(output, " (±{:.0f} m)", location.horizontal_accuracy);
|
||||
}
|
||||
::std::format_to(output, "\nSource: {}", location.provider);
|
||||
return text;
|
||||
}
|
||||
|
||||
void updateWidgets(Context* ctx) {
|
||||
if (ctx->statusLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_set_state(ctx->enableSwitch, LV_STATE_CHECKED, location_is_enabled());
|
||||
|
||||
const LocationPrecision precision = location_get_precision();
|
||||
const uint32_t count = lv_obj_get_child_count(ctx->precisionChips);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto* chip = lv_obj_get_child(ctx->precisionChips, static_cast<int32_t>(i));
|
||||
lv_obj_set_state(chip, LV_STATE_CHECKED, getChipPrecision(chip) == precision);
|
||||
}
|
||||
|
||||
lv_label_set_text(ctx->statusLabel, getStatusText().c_str());
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
// Starting and stopping the service blocks, so it happens on the app's own task
|
||||
void onEnableSwitchChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->requestedEnabled = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED);
|
||||
task_event_group_signal(ctx->eventGroup, ctx->enableBit);
|
||||
}
|
||||
|
||||
void onPrecisionChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->requestedPrecision = getChipPrecision(lv_event_get_target_obj(event));
|
||||
task_event_group_signal(ctx->eventGroup, ctx->precisionBit);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Location");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
ctx->enableSwitch = lvgl_toolbar_add_switch_action(toolbar);
|
||||
lv_obj_add_event_cb(ctx->enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
auto* precisionLabel = lv_label_create(wrapper);
|
||||
lv_label_set_text(precisionLabel, "Precision");
|
||||
|
||||
// The chips are centered in the card while they fit, and scroll when they don't
|
||||
auto* precisionCard = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(precisionCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(precisionCard, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(precisionCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_left(precisionLabel, lv_obj_get_style_pad_left(precisionCard, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
|
||||
ctx->precisionChips = lv_obj_create(precisionCard);
|
||||
lv_obj_set_size(ctx->precisionChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(ctx->precisionChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->precisionChips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(ctx->precisionChips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(ctx->precisionChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
|
||||
for (const auto& option : PRECISION_OPTIONS) {
|
||||
auto* chip = lvgl_chip_create(ctx->precisionChips);
|
||||
lv_label_set_text(lv_label_create(chip), option.name);
|
||||
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<intptr_t>(option.precision)));
|
||||
lv_obj_add_event_cb(chip, onPrecisionChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(ctx->precisionChips);
|
||||
|
||||
auto* precisionHint = lv_label_create(precisionCard);
|
||||
lv_obj_set_width(precisionHint, LV_PCT(100));
|
||||
lv_label_set_long_mode(precisionHint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(precisionHint, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(precisionHint, "Approximate location is accurate to a few kilometers. Location based on the IP address is never precise.");
|
||||
|
||||
auto* statusCard = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(statusCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
ctx->statusLabel = lv_label_create(statusCard);
|
||||
lv_obj_set_width(ctx->statusLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(ctx->statusLabel, LV_LABEL_LONG_WRAP);
|
||||
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->enableSwitch = nullptr;
|
||||
ctx->precisionChips = nullptr;
|
||||
ctx->statusLabel = nullptr;
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = app_scheduler_current_app_id();
|
||||
|
||||
TaskEventGroup eventGroup {};
|
||||
task_event_group_construct(&eventGroup);
|
||||
ctx.eventGroup = &eventGroup;
|
||||
check(task_event_group_claim_bit(&eventGroup, &ctx.enableBit) == ERROR_NONE);
|
||||
check(task_event_group_claim_bit(&eventGroup, &ctx.precisionBit) == ERROR_NONE);
|
||||
|
||||
AppEventSubscription appEvents {};
|
||||
check(app_event_subscribe(&appEvents, &eventGroup) == ERROR_NONE);
|
||||
LocationSubscription locationEvents {};
|
||||
check(location_subscribe(&locationEvents, &eventGroup) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&eventGroup, &flags, portMAX_DELAY);
|
||||
|
||||
if (flags & ctx.enableBit) {
|
||||
location_set_enabled(ctx.requestedEnabled);
|
||||
}
|
||||
if (flags & ctx.precisionBit) {
|
||||
location_set_precision(ctx.requestedPrecision);
|
||||
}
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&appEvents, &event) == ERROR_NONE) {
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
updateWidgets(&ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
check(location_unsubscribe(&locationEvents) == ERROR_NONE);
|
||||
check(app_event_unsubscribe(&appEvents) == ERROR_NONE);
|
||||
task_event_group_destruct(&eventGroup);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.locationsettings",
|
||||
.name = "Location",
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
.stack = {}
|
||||
};
|
||||
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
@ -259,6 +261,23 @@ int32_t appMain(int argc, char* argv[]) {
|
||||
});
|
||||
}
|
||||
|
||||
ctx.steps.push_back({
|
||||
.title = "Location Services",
|
||||
.description = "Location services let apps use your location. It comes from GPS, or from your IP address via ip-api.com when there is no GPS fix.",
|
||||
.run = [&ctx] {
|
||||
const std::vector<std::string> items = { "Allow precise location", "Allow approximate location", "Don't allow" };
|
||||
ctx.pendingStepDialogId = selectiondialog::start(ctx.appInstanceId, "Location", items);
|
||||
},
|
||||
.onResult = [](int32_t selected) {
|
||||
if (selected == 0 || selected == 1) {
|
||||
location_set_precision(selected == 0 ? LOCATION_PRECISION_PRECISE : LOCATION_PRECISION_APPROXIMATE);
|
||||
location_set_enabled(true);
|
||||
} else if (selected == 2) {
|
||||
location_set_enabled(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
|
||||
@ -51,6 +51,7 @@ target_link_libraries(TactilityTests PRIVATE
|
||||
gps-module
|
||||
graphics-module
|
||||
http-module
|
||||
location-module
|
||||
mbedtls-module
|
||||
posix-symbols-module
|
||||
pthread-module
|
||||
|
||||
@ -87,6 +87,16 @@ void thread_set_name(Thread* thread, const char* name);
|
||||
*/
|
||||
void thread_set_stack_size(Thread* thread, size_t stack_size);
|
||||
|
||||
/**
|
||||
* @brief Sets whether the stack should be allocated in external memory (PSRAM) when available.
|
||||
* The stack is allocated in internal memory when there is no external memory.
|
||||
* @warning A thread with its stack in external memory must not access flash (e.g. write to the internal filesystem or NVS).
|
||||
* @param[in] thread The thread instance.
|
||||
* @param[in] external true to prefer external memory
|
||||
* @note Can only be called when the thread is in the STOPPED state.
|
||||
*/
|
||||
void thread_set_stack_external(Thread* thread, bool external);
|
||||
|
||||
/**
|
||||
* @brief Sets the CPU core affinity for the thread.
|
||||
* @param[in] thread The thread instance.
|
||||
|
||||
@ -28,6 +28,9 @@ struct Thread {
|
||||
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
||||
Mutex mutex = { 0 };
|
||||
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
||||
bool stackExternal = false;
|
||||
// Whether the task was created with xTaskCreatePinnedToCoreWithCaps()
|
||||
bool createdWithCaps = false;
|
||||
portBASE_TYPE affinity = -1;
|
||||
|
||||
Thread() {
|
||||
@ -77,10 +80,17 @@ static void thread_main_body(void* context) {
|
||||
|
||||
vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr);
|
||||
|
||||
// The thread can be freed once taskHandle is cleared
|
||||
const bool createdWithCaps = thread->createdWithCaps;
|
||||
thread->lock();
|
||||
thread->taskHandle = nullptr;
|
||||
thread->unlock();
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
if (createdWithCaps) {
|
||||
vTaskDeleteWithCaps(nullptr);
|
||||
}
|
||||
#endif
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
@ -135,6 +145,13 @@ void thread_set_stack_size(Thread* thread, size_t stack_size) {
|
||||
thread->unlock();
|
||||
}
|
||||
|
||||
void thread_set_stack_external(Thread* thread, bool external) {
|
||||
thread->lock();
|
||||
check(thread->state == THREAD_STATE_STOPPED);
|
||||
thread->stackExternal = external;
|
||||
thread->unlock();
|
||||
}
|
||||
|
||||
void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) {
|
||||
thread->lock();
|
||||
check(thread->state == THREAD_STATE_STOPPED);
|
||||
@ -187,10 +204,31 @@ error_t thread_start(Thread* thread) {
|
||||
thread->lock();
|
||||
auto priority = static_cast<UBaseType_t>(thread->priority);
|
||||
portBASE_TYPE affinity = thread->affinity;
|
||||
bool stackExternal = thread->stackExternal;
|
||||
thread->createdWithCaps = false;
|
||||
thread->unlock();
|
||||
|
||||
BaseType_t result;
|
||||
if (affinity != -1) {
|
||||
BaseType_t result = pdFAIL;
|
||||
#ifdef ESP_PLATFORM
|
||||
if (stackExternal) {
|
||||
// Set before the task can read it, as the task can run before the creation call returns
|
||||
thread->createdWithCaps = true;
|
||||
result = xTaskCreatePinnedToCoreWithCaps(
|
||||
thread_main_body,
|
||||
thread->name.c_str(),
|
||||
thread->stackDepth,
|
||||
thread,
|
||||
priority,
|
||||
&thread->taskHandle,
|
||||
affinity != -1 ? affinity : tskNO_AFFINITY,
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
|
||||
);
|
||||
thread->createdWithCaps = (result == pdPASS);
|
||||
}
|
||||
#endif
|
||||
if (result == pdPASS) {
|
||||
// Stack is in external memory
|
||||
} else if (affinity != -1) {
|
||||
#ifdef ESP_PLATFORM
|
||||
result = xTaskCreatePinnedToCore(
|
||||
thread_main_body,
|
||||
|
||||
@ -15,6 +15,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_B
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/binfont-module/tests ${CMAKE_CURRENT_BINARY_DIR}/binfont-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lodepng-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lodepng-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lvgl-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lvgl-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/location-module/tests ${CMAKE_CURRENT_BINARY_DIR}/location-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshcore-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshcore-module)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshtastic-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshtastic-module)
|
||||
@ -32,6 +33,7 @@ add_dependencies(build-tests HttpModuleTests)
|
||||
add_dependencies(build-tests BinFontModuleTests)
|
||||
add_dependencies(build-tests LodePngModuleTests)
|
||||
add_dependencies(build-tests LvglModuleTests)
|
||||
add_dependencies(build-tests LocationModuleTests)
|
||||
add_dependencies(build-tests MeshModuleTests)
|
||||
add_dependencies(build-tests MeshMeshCoreModuleTests)
|
||||
add_dependencies(build-tests MeshMeshtasticModuleTests)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user