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/gps-module)
|
||||||
add_subdirectory(Modules/graphics-module)
|
add_subdirectory(Modules/graphics-module)
|
||||||
add_subdirectory(Modules/http-module)
|
add_subdirectory(Modules/http-module)
|
||||||
|
add_subdirectory(Modules/location-module)
|
||||||
add_subdirectory(Modules/mbedtls-module)
|
add_subdirectory(Modules/mbedtls-module)
|
||||||
add_subdirectory(Modules/mesh-module)
|
add_subdirectory(Modules/mesh-module)
|
||||||
add_subdirectory(Modules/mesh-meshcore-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
|
gps-module
|
||||||
graphics-module
|
graphics-module
|
||||||
http-module
|
http-module
|
||||||
|
location-module
|
||||||
mbedtls-module
|
mbedtls-module
|
||||||
mesh-module
|
mesh-module
|
||||||
mesh-meshcore-module
|
mesh-meshcore-module
|
||||||
|
|||||||
@ -58,6 +58,7 @@ namespace app {
|
|||||||
namespace launcher { extern const ::AppManifest manifest; }
|
namespace launcher { extern const ::AppManifest manifest; }
|
||||||
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
||||||
namespace localesettings { extern const ::AppManifest manifest; }
|
namespace localesettings { extern const ::AppManifest manifest; }
|
||||||
|
namespace locationsettings { extern const ::AppManifest manifest; }
|
||||||
namespace notes { extern const ::AppManifest manifest; }
|
namespace notes { extern const ::AppManifest manifest; }
|
||||||
namespace power { extern const ::AppManifest manifest; }
|
namespace power { extern const ::AppManifest manifest; }
|
||||||
namespace poweroff { 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::inputdialog::manifest);
|
||||||
app_manager_add(&app::launcher::manifest);
|
app_manager_add(&app::launcher::manifest);
|
||||||
app_manager_add(&app::localesettings::manifest);
|
app_manager_add(&app::localesettings::manifest);
|
||||||
|
app_manager_add(&app::locationsettings::manifest);
|
||||||
app_manager_add(&app::notes::manifest);
|
app_manager_add(&app::notes::manifest);
|
||||||
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
||||||
app_manager_add(&app::poweroff::manifest);
|
app_manager_add(&app::poweroff::manifest);
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <Tactility/service/ServiceRegistration.h>
|
#include <Tactility/service/ServiceRegistration.h>
|
||||||
#include <Tactility/settings/RadioSettings.h>
|
#include <Tactility/settings/RadioSettings.h>
|
||||||
|
|
||||||
|
#include <location/module.h>
|
||||||
#include <wifi/module.h>
|
#include <wifi/module.h>
|
||||||
#if TT_FEATURE_MESH
|
#if TT_FEATURE_MESH
|
||||||
#include <mesh/module.h>
|
#include <mesh/module.h>
|
||||||
@ -49,6 +50,7 @@ void registerAndStartServices() {
|
|||||||
addService(service::audio::manifest);
|
addService(service::audio::manifest);
|
||||||
}
|
}
|
||||||
check(module_ensure_started(&wifi_module) == ERROR_NONE);
|
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
|
// Before the mesh service starts, as it needs the region to turn on the radio
|
||||||
settings::radio::applyRegion();
|
settings::radio::applyRegion();
|
||||||
#if TT_FEATURE_MESH
|
#if TT_FEATURE_MESH
|
||||||
|
|||||||
@ -29,6 +29,7 @@ constexpr IconEntry ICONS[] = {
|
|||||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||||
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||||
|
{"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON},
|
||||||
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
|
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
|
||||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
{"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/scheduler.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
#include <lvgl_window_manager/window_manager.h>
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
#include <tactility/check.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 {};
|
TaskEventGroup event_group {};
|
||||||
task_event_group_construct(&event_group);
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,7 @@ target_link_libraries(TactilityTests PRIVATE
|
|||||||
gps-module
|
gps-module
|
||||||
graphics-module
|
graphics-module
|
||||||
http-module
|
http-module
|
||||||
|
location-module
|
||||||
mbedtls-module
|
mbedtls-module
|
||||||
posix-symbols-module
|
posix-symbols-module
|
||||||
pthread-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);
|
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.
|
* @brief Sets the CPU core affinity for the thread.
|
||||||
* @param[in] thread The thread instance.
|
* @param[in] thread The thread instance.
|
||||||
|
|||||||
@ -28,6 +28,9 @@ struct Thread {
|
|||||||
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
||||||
Mutex mutex = { 0 };
|
Mutex mutex = { 0 };
|
||||||
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
||||||
|
bool stackExternal = false;
|
||||||
|
// Whether the task was created with xTaskCreatePinnedToCoreWithCaps()
|
||||||
|
bool createdWithCaps = false;
|
||||||
portBASE_TYPE affinity = -1;
|
portBASE_TYPE affinity = -1;
|
||||||
|
|
||||||
Thread() {
|
Thread() {
|
||||||
@ -77,10 +80,17 @@ static void thread_main_body(void* context) {
|
|||||||
|
|
||||||
vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr);
|
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->lock();
|
||||||
thread->taskHandle = nullptr;
|
thread->taskHandle = nullptr;
|
||||||
thread->unlock();
|
thread->unlock();
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
if (createdWithCaps) {
|
||||||
|
vTaskDeleteWithCaps(nullptr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
vTaskDelete(nullptr);
|
vTaskDelete(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +145,13 @@ void thread_set_stack_size(Thread* thread, size_t stack_size) {
|
|||||||
thread->unlock();
|
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) {
|
void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) {
|
||||||
thread->lock();
|
thread->lock();
|
||||||
check(thread->state == THREAD_STATE_STOPPED);
|
check(thread->state == THREAD_STATE_STOPPED);
|
||||||
@ -187,10 +204,31 @@ error_t thread_start(Thread* thread) {
|
|||||||
thread->lock();
|
thread->lock();
|
||||||
auto priority = static_cast<UBaseType_t>(thread->priority);
|
auto priority = static_cast<UBaseType_t>(thread->priority);
|
||||||
portBASE_TYPE affinity = thread->affinity;
|
portBASE_TYPE affinity = thread->affinity;
|
||||||
|
bool stackExternal = thread->stackExternal;
|
||||||
|
thread->createdWithCaps = false;
|
||||||
thread->unlock();
|
thread->unlock();
|
||||||
|
|
||||||
BaseType_t result;
|
BaseType_t result = pdFAIL;
|
||||||
if (affinity != -1) {
|
#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
|
#ifdef ESP_PLATFORM
|
||||||
result = xTaskCreatePinnedToCore(
|
result = xTaskCreatePinnedToCore(
|
||||||
thread_main_body,
|
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/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/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/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-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-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)
|
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 BinFontModuleTests)
|
||||||
add_dependencies(build-tests LodePngModuleTests)
|
add_dependencies(build-tests LodePngModuleTests)
|
||||||
add_dependencies(build-tests LvglModuleTests)
|
add_dependencies(build-tests LvglModuleTests)
|
||||||
|
add_dependencies(build-tests LocationModuleTests)
|
||||||
add_dependencies(build-tests MeshModuleTests)
|
add_dependencies(build-tests MeshModuleTests)
|
||||||
add_dependencies(build-tests MeshMeshCoreModuleTests)
|
add_dependencies(build-tests MeshMeshCoreModuleTests)
|
||||||
add_dependencies(build-tests MeshMeshtasticModuleTests)
|
add_dependencies(build-tests MeshMeshtasticModuleTests)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user