Added xMasterX API v11.x source

This commit is contained in:
UberGuidoZ 2023-01-25 23:52:38 -08:00
parent 33322c0d73
commit e292fbfcd5
1094 changed files with 102160 additions and 0 deletions

View File

@ -0,0 +1,201 @@
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.

View File

@ -0,0 +1,60 @@
# Flipper Air Mouse
## Brief
> "You can turn anything into an air mouse if you're brave enough"
— Piper, a.k.a. Pez
Naturally, the quote above applies to [Flipper](https://flipperzero.one/) as well.
## What?
The app allows you to turn your Flipper into a USB or Bluetooth air mouse (you do need an extra module, see the Hardware section below)...
Using it is really simple:
* Connect the Flipper via a USB cable and pick `USB`, or pick `Bluetooth` and pair it with your PC;
* Hold the Flipper in your hand with the buttons pointing towards the screen;
* Wave your Flipper like you don't care to move the cursor;
* Up button for Left mouse click;
* Down button for Right mouse click;
* Center button for Middle mouse click;
* Left and Right buttons for scrolling;
* Use calibration menu option if you notice significant drift (place your Flipper onto a level surface, make sure it doesn't move, run this option, wait 2 seconds, done).
See early prototype [in action](https://www.youtube.com/watch?v=DdxAmmsYfMA).
## Hardware
The custom module is using Bosch BMI160 accelerometer/gyroscope chip connected via I2C.
Take a look into the [schematic](https://github.com/ginkage/FlippAirMouse/tree/main/schematic) folder for Gerber, BOM and CPL files, so you can order directly from JLCPCB.
Original idea:
![What I thought it would look like](https://github.com/ginkage/FlippAirMouse/blob/main/schematic/schematic.png)
Expectation:
![What EDA though it would look like](https://github.com/ginkage/FlippAirMouse/blob/main/schematic/render.png)
Reality:
![What it looks like](https://github.com/ginkage/FlippAirMouse/blob/main/schematic/flipper.jpg)
## Software
The code is based on the original Bosch [driver](https://github.com/BoschSensortec/BMI160_driver/) and an orientation tracking implementation from the Google [Cardboard](https://github.com/googlevr/cardboard/tree/master/sdk/sensors) project
If you're familiar with Flipper applications, start in the [firmware](https://github.com/flipperdevices/flipperzero-firmware) checkout folder and do the following:
```
cd applications/plugins
git clone https://github.com/ginkage/FlippAirMouse
cd ../..
./fbt fap_air_mouse
```
If you're not familiar with those, just grab a `fap` file from Releases.
## License
TL;DR: Use the code however you want, give credit where it's due, no warranty of any kind is provided.

View File

@ -0,0 +1,156 @@
#include "air_mouse.h"
#include <furi.h>
#include <dolphin/dolphin.h>
#include "tracking/imu/imu.h"
#define TAG "AirMouseApp"
enum AirMouseSubmenuIndex {
AirMouseSubmenuIndexBtMouse,
AirMouseSubmenuIndexUsbMouse,
AirMouseSubmenuIndexCalibration,
};
void air_mouse_submenu_callback(void* context, uint32_t index) {
furi_assert(context);
AirMouse* app = context;
if(index == AirMouseSubmenuIndexBtMouse) {
app->view_id = AirMouseViewBtMouse;
view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewBtMouse);
} else if(index == AirMouseSubmenuIndexUsbMouse) {
app->view_id = AirMouseViewUsbMouse;
view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewUsbMouse);
} else if(index == AirMouseSubmenuIndexCalibration) {
app->view_id = AirMouseViewCalibration;
view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewCalibration);
}
}
void air_mouse_dialog_callback(DialogExResult result, void* context) {
furi_assert(context);
AirMouse* app = context;
if(result == DialogExResultLeft) {
view_dispatcher_switch_to_view(app->view_dispatcher, VIEW_NONE); // Exit
} else if(result == DialogExResultRight) {
view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id); // Show last view
} else if(result == DialogExResultCenter) {
view_dispatcher_switch_to_view(app->view_dispatcher, AirMouseViewSubmenu); // Menu
}
}
uint32_t air_mouse_exit_confirm_view(void* context) {
UNUSED(context);
return AirMouseViewExitConfirm;
}
uint32_t air_mouse_exit(void* context) {
UNUSED(context);
return VIEW_NONE;
}
AirMouse* air_mouse_app_alloc() {
AirMouse* app = malloc(sizeof(AirMouse));
// Gui
app->gui = furi_record_open(RECORD_GUI);
// View dispatcher
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_enable_queue(app->view_dispatcher);
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
// Submenu view
app->submenu = submenu_alloc();
submenu_add_item(
app->submenu, "Bluetooth", AirMouseSubmenuIndexBtMouse, air_mouse_submenu_callback, app);
submenu_add_item(
app->submenu, "USB", AirMouseSubmenuIndexUsbMouse, air_mouse_submenu_callback, app);
submenu_add_item(
app->submenu,
"Calibration",
AirMouseSubmenuIndexCalibration,
air_mouse_submenu_callback,
app);
view_set_previous_callback(submenu_get_view(app->submenu), air_mouse_exit);
view_dispatcher_add_view(
app->view_dispatcher, AirMouseViewSubmenu, submenu_get_view(app->submenu));
// Dialog view
app->dialog = dialog_ex_alloc();
dialog_ex_set_result_callback(app->dialog, air_mouse_dialog_callback);
dialog_ex_set_context(app->dialog, app);
dialog_ex_set_left_button_text(app->dialog, "Exit");
dialog_ex_set_right_button_text(app->dialog, "Stay");
dialog_ex_set_center_button_text(app->dialog, "Menu");
dialog_ex_set_header(app->dialog, "Close Current App?", 16, 12, AlignLeft, AlignTop);
view_dispatcher_add_view(
app->view_dispatcher, AirMouseViewExitConfirm, dialog_ex_get_view(app->dialog));
// Bluetooth view
app->bt_mouse = bt_mouse_alloc(app->view_dispatcher);
view_set_previous_callback(bt_mouse_get_view(app->bt_mouse), air_mouse_exit_confirm_view);
view_dispatcher_add_view(
app->view_dispatcher, AirMouseViewBtMouse, bt_mouse_get_view(app->bt_mouse));
// USB view
app->usb_mouse = usb_mouse_alloc(app->view_dispatcher);
view_set_previous_callback(usb_mouse_get_view(app->usb_mouse), air_mouse_exit_confirm_view);
view_dispatcher_add_view(
app->view_dispatcher, AirMouseViewUsbMouse, usb_mouse_get_view(app->usb_mouse));
// Calibration view
app->calibration = calibration_alloc(app->view_dispatcher);
view_set_previous_callback(
calibration_get_view(app->calibration), air_mouse_exit_confirm_view);
view_dispatcher_add_view(
app->view_dispatcher, AirMouseViewCalibration, calibration_get_view(app->calibration));
app->view_id = AirMouseViewSubmenu;
view_dispatcher_switch_to_view(app->view_dispatcher, app->view_id);
return app;
}
void air_mouse_app_free(AirMouse* app) {
furi_assert(app);
// Free views
view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewSubmenu);
submenu_free(app->submenu);
view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewExitConfirm);
dialog_ex_free(app->dialog);
view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewBtMouse);
bt_mouse_free(app->bt_mouse);
view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewUsbMouse);
usb_mouse_free(app->usb_mouse);
view_dispatcher_remove_view(app->view_dispatcher, AirMouseViewCalibration);
calibration_free(app->calibration);
view_dispatcher_free(app->view_dispatcher);
// Close records
furi_record_close(RECORD_GUI);
app->gui = NULL;
// Free rest
free(app);
}
int32_t air_mouse_app(void* p) {
UNUSED(p);
AirMouse* app = air_mouse_app_alloc();
if(!imu_begin()) {
air_mouse_app_free(app);
return -1;
}
DOLPHIN_DEED(DolphinDeedPluginStart);
view_dispatcher_run(app->view_dispatcher);
imu_end();
air_mouse_app_free(app);
return 0;
}

View File

@ -0,0 +1,30 @@
#pragma once
#include <gui/gui.h>
#include <gui/view.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/submenu.h>
#include <gui/modules/dialog_ex.h>
#include "views/bt_mouse.h"
#include "views/usb_mouse.h"
#include "views/calibration.h"
typedef struct {
Gui* gui;
ViewDispatcher* view_dispatcher;
Submenu* submenu;
DialogEx* dialog;
BtMouse* bt_mouse;
UsbMouse* usb_mouse;
Calibration* calibration;
uint32_t view_id;
} AirMouse;
typedef enum {
AirMouseViewSubmenu,
AirMouseViewBtMouse,
AirMouseViewUsbMouse,
AirMouseViewCalibration,
AirMouseViewExitConfirm,
} AirMouseView;

View File

@ -0,0 +1,9 @@
App(
appid="Air_Mouse",
name="[BMI160] Air Mouse",
apptype=FlipperAppType.EXTERNAL,
entry_point="air_mouse_app",
stack_size=10 * 1024,
fap_category="GPIO_Extra",
fap_icon="mouse_10px.png",
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,85 @@
#include <furi.h>
#include <furi_hal.h>
#define TAG "tracker"
#include "calibration_data.h"
#include <cmath>
#include <algorithm>
// Student's distribution T value for 95% (two-sided) confidence interval.
static const double Tn = 1.960;
// Number of samples (degrees of freedom) for the corresponding T values.
static const int Nn = 200;
void CalibrationData::reset()
{
complete = false;
count = 0;
sum = Vector::Zero();
sumSq = Vector::Zero();
mean = Vector::Zero();
median = Vector::Zero();
sigma = Vector::Zero();
delta = Vector::Zero();
xData.clear();
yData.clear();
zData.clear();
}
bool CalibrationData::add(Vector& data)
{
if (complete) {
return true;
}
xData.push_back(data[0]);
yData.push_back(data[1]);
zData.push_back(data[2]);
sum += data;
sumSq += data * data;
count++;
if (count >= Nn) {
calcDelta();
complete = true;
}
return complete;
}
static inline double medianOf(std::vector<double>& list)
{
std::sort(list.begin(), list.end());
int count = list.size();
int middle = count / 2;
return (count % 2 == 1) ? list[middle] : (list[middle - 1] + list[middle]) / 2.0l;
}
void CalibrationData::calcDelta()
{
median.Set(medianOf(xData), medianOf(yData), medianOf(zData));
mean = sum / count;
Vector m2 = mean * mean;
Vector d = sumSq / count - m2;
Vector s2 = (d * count) / (count - 1);
sigma = Vector(std::sqrt(d[0]), std::sqrt(d[1]), std::sqrt(d[2]));
Vector s = Vector(std::sqrt(s2[0]), std::sqrt(s2[1]), std::sqrt(s2[2]));
delta = s * Tn / std::sqrt((double)count);
Vector low = mean - delta;
Vector high = mean + delta;
FURI_LOG_I(TAG,
"M[x] = { %f ... %f } // median = %f // avg = %f // delta = %f // sigma = %f",
low[0], high[0], median[0], mean[0], delta[0], sigma[0]);
FURI_LOG_I(TAG,
"M[y] = { %f ... %f } // median = %f // avg = %f // delta = %f // sigma = %f",
low[1], high[1], median[1], mean[1], delta[1], sigma[1]);
FURI_LOG_I(TAG,
"M[z] = { %f ... %f } // median = %f // avg = %f // delta = %f // sigma = %f",
low[2], high[2], median[2], mean[2], delta[2], sigma[2]);
}

View File

@ -0,0 +1,117 @@
#pragma once
#include <toolbox/saved_struct.h>
#include <storage/storage.h>
#include <vector>
#include "util/vector.h"
#define CALIBRATION_DATA_VER (1)
#define CALIBRATION_DATA_FILE_NAME ".calibration.data"
#define CALIBRATION_DATA_PATH INT_PATH(CALIBRATION_DATA_FILE_NAME)
#define CALIBRATION_DATA_MAGIC (0x23)
#define CALIBRATION_DATA_SAVE(x) \
saved_struct_save( \
CALIBRATION_DATA_PATH, \
(x), \
sizeof(CalibrationMedian), \
CALIBRATION_DATA_MAGIC, \
CALIBRATION_DATA_VER)
#define CALIBRATION_DATA_LOAD(x) \
saved_struct_load( \
CALIBRATION_DATA_PATH, \
(x), \
sizeof(CalibrationMedian), \
CALIBRATION_DATA_MAGIC, \
CALIBRATION_DATA_VER)
typedef struct {
double x;
double y;
double z;
} CalibrationMedian;
typedef cardboard::Vector3 Vector;
/**
* Helper class to gather some stats and store the calibration data. Right now it calculates a lot
* more stats than actually needed. Some of them are used for logging the sensors quality (and
* filing bugs), other may be required in the future, e.g. for bias.
*/
class CalibrationData {
public:
/**
* Check if the sensors were calibrated before.
*
* @return {@code true} if calibration data is available, or {@code false} otherwise.
*/
bool isComplete() {
return complete;
}
/** Prepare to collect new calibration data. */
void reset();
/**
* Retrieve the median gyroscope readings.
*
* @return Three-axis median vector.
*/
Vector getMedian() {
return median;
}
/**
* Retrieve the mean gyroscope readings.
*
* @return Three-axis mean vector.
*/
Vector getMean() {
return mean;
}
/**
* Retrieve the standard deviation of gyroscope readings.
*
* @return Three-axis standard deviation vector.
*/
Vector getSigma() {
return sigma;
}
/**
* Retrieve the confidence interval size of gyroscope readings.
*
* @return Three-axis confidence interval size vector.
*/
Vector getDelta() {
return delta;
}
/**
* Add a new gyroscope reading to the stats.
*
* @param data gyroscope values vector.
* @return {@code true} if we now have enough data for calibration, or {@code false} otherwise.
*/
bool add(Vector& data);
private:
// Calculates the confidence interval (mean +- delta) and some other related values, like
// standard deviation, etc. See https://en.wikipedia.org/wiki/Student%27s_t-distribution
void calcDelta();
int count;
bool complete;
Vector sum;
Vector sumSq;
Vector mean;
Vector median;
Vector sigma;
Vector delta;
std::vector<double> xData;
std::vector<double> yData;
std::vector<double> zData;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,992 @@
/**
* Copyright (c) 2021 Bosch Sensortec GmbH. All rights reserved.
*
* BSD-3-Clause
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @file bmi160.h
* @date 2021-10-05
* @version v3.9.2
*
*/
/*!
* @defgroup bmi160 BMI160
*/
#ifndef BMI160_H_
#define BMI160_H_
/*************************** C++ guard macro *****************************/
#ifdef __cplusplus
extern "C" {
#endif
#include "bmi160_defs.h"
#ifdef __KERNEL__
#include <bmi160_math.h>
#else
#include <math.h>
#include <string.h>
#include <stdlib.h>
#endif
/*********************** User function prototypes ************************/
/**
* \ingroup bmi160
* \defgroup bmi160ApiInit Initialization
* @brief Initialize the sensor and device structure
*/
/*!
* \ingroup bmi160ApiInit
* \page bmi160_api_bmi160_init bmi160_init
* \code
* int8_t bmi160_init(struct bmi160_dev *dev);
* \endcode
* @details This API is the entry point for sensor.It performs
* the selection of I2C/SPI read mechanism according to the
* selected interface and reads the chip-id of bmi160 sensor.
*
* @param[in,out] dev : Structure instance of bmi160_dev
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_init(struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiRegs Registers
* @brief Read data from the given register address of sensor
*/
/*!
* \ingroup bmi160ApiRegs
* \page bmi160_api_bmi160_get_regs bmi160_get_regs
* \code
* int8_t bmi160_get_regs(uint8_t reg_addr, uint8_t *data, uint16_t len, const struct bmi160_dev *dev);
* \endcode
* @details This API reads the data from the given register address of sensor.
*
* @param[in] reg_addr : Register address from where the data to be read
* @param[out] data : Pointer to data buffer to store the read data.
* @param[in] len : No of bytes of data to be read.
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note For most of the registers auto address increment applies, with the
* exception of a few special registers, which trap the address. For e.g.,
* Register address - 0x24(BMI160_FIFO_DATA_ADDR)
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t
bmi160_get_regs(uint8_t reg_addr, uint8_t* data, uint16_t len, const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiRegs
* \page bmi160_api_bmi160_set_regs bmi160_set_regs
* \code
* int8_t bmi160_set_regs(uint8_t reg_addr, uint8_t *data, uint16_t len, const struct bmi160_dev *dev);
* \endcode
* @details This API writes the given data to the register address
* of sensor.
*
* @param[in] reg_addr : Register address from where the data to be written.
* @param[in] data : Pointer to data buffer which is to be written
* in the sensor.
* @param[in] len : No of bytes of data to write..
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t
bmi160_set_regs(uint8_t reg_addr, uint8_t* data, uint16_t len, const struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiSoftreset Soft reset
* @brief Perform soft reset of the sensor
*/
/*!
* \ingroup bmi160ApiSoftreset
* \page bmi160_api_bmi160_soft_reset bmi160_soft_reset
* \code
* int8_t bmi160_soft_reset(struct bmi160_dev *dev);
* \endcode
* @details This API resets and restarts the device.
* All register values are overwritten with default parameters.
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_soft_reset(struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiConfig Configuration
* @brief Configuration of the sensor
*/
/*!
* \ingroup bmi160ApiConfig
* \page bmi160_api_bmi160_set_sens_conf bmi160_set_sens_conf
* \code
* int8_t bmi160_set_sens_conf(struct bmi160_dev *dev);
* \endcode
* @details This API configures the power mode, range and bandwidth
* of sensor.
*
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_set_sens_conf(struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiConfig
* \page bmi160_api_bmi160_get_sens_conf bmi160_get_sens_conf
* \code
* int8_t bmi160_get_sens_conf(struct bmi160_dev *dev);
* \endcode
* @details This API gets accel and gyro configurations.
*
* @param[out] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_get_sens_conf(struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiPowermode Power mode
* @brief Set / Get power mode of the sensor
*/
/*!
* \ingroup bmi160ApiPowermode
* \page bmi160_api_bmi160_set_power_mode bmi160_set_power_mode
* \code
* int8_t bmi160_set_power_mode(struct bmi160_dev *dev);
* \endcode
* @details This API sets the power mode of the sensor.
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_set_power_mode(struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiPowermode
* \page bmi160_api_bmi160_get_power_mode bmi160_get_power_mode
* \code
* int8_t bmi160_get_power_mode(struct bmi160_dev *dev);
* \endcode
* @details This API gets the power mode of the sensor.
*
* @param[in] dev : Structure instance of bmi160_dev
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_get_power_mode(struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiData Sensor Data
* @brief Read sensor data
*/
/*!
* \ingroup bmi160ApiData
* \page bmi160_api_bmi160_get_sensor_data bmi160_get_sensor_data
* \code
* int8_t bmi160_get_sensor_data(uint8_t select_sensor,
* struct bmi160_sensor_data *accel,
* struct bmi160_sensor_data *gyro,
* const struct bmi160_dev *dev);
*
* \endcode
* @details This API reads sensor data, stores it in
* the bmi160_sensor_data structure pointer passed by the user.
* The user can ask for accel data ,gyro data or both sensor
* data using bmi160_select_sensor enum
*
* @param[in] select_sensor : enum to choose accel,gyro or both sensor data
* @param[out] accel : Structure pointer to store accel data
* @param[out] gyro : Structure pointer to store gyro data
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_get_sensor_data(
uint8_t select_sensor,
struct bmi160_sensor_data* accel,
struct bmi160_sensor_data* gyro,
const struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiInt Interrupt configuration
* @brief Set interrupt configuration of the sensor
*/
/*!
* \ingroup bmi160ApiInt
* \page bmi160_api_bmi160_set_int_config bmi160_set_int_config
* \code
* int8_t bmi160_set_int_config(struct bmi160_int_settg *int_config, struct bmi160_dev *dev);
* \endcode
* @details This API configures the necessary interrupt based on
* the user settings in the bmi160_int_settg structure instance.
*
* @param[in] int_config : Structure instance of bmi160_int_settg.
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_set_int_config(struct bmi160_int_settg* int_config, struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiStepC Step counter
* @brief Step counter operations
*/
/*!
* \ingroup bmi160ApiStepC
* \page bmi160_api_bmi160_set_step_counter bmi160_set_step_counter
* \code
* int8_t bmi160_set_step_counter(uint8_t step_cnt_enable, const struct bmi160_dev *dev);
* \endcode
* @details This API enables the step counter feature.
*
* @param[in] step_cnt_enable : value to enable or disable
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_set_step_counter(uint8_t step_cnt_enable, const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiStepC
* \page bmi160_api_bmi160_read_step_counter bmi160_read_step_counter
* \code
* int8_t bmi160_read_step_counter(uint16_t *step_val, const struct bmi160_dev *dev);
* \endcode
* @details This API reads the step counter value.
*
* @param[in] step_val : Pointer to store the step counter value.
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_read_step_counter(uint16_t* step_val, const struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiAux Auxiliary sensor
* @brief Auxiliary sensor operations
*/
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_aux_read bmi160_aux_read
* \code
* int8_t bmi160_aux_read(uint8_t reg_addr, uint8_t *aux_data, uint16_t len, const struct bmi160_dev *dev);
* \endcode
* @details This API reads the mention no of byte of data from the given
* register address of auxiliary sensor.
*
* @param[in] reg_addr : Address of register to read.
* @param[in] aux_data : Pointer to store the read data.
* @param[in] len : No of bytes to read.
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_aux_read(
uint8_t reg_addr,
uint8_t* aux_data,
uint16_t len,
const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_aux_write bmi160_aux_write
* \code
* int8_t bmi160_aux_write(uint8_t reg_addr, uint8_t *aux_data, uint16_t len, const struct bmi160_dev *dev);
* \endcode
* @details This API writes the mention no of byte of data to the given
* register address of auxiliary sensor.
*
* @param[in] reg_addr : Address of register to write.
* @param[in] aux_data : Pointer to write data.
* @param[in] len : No of bytes to write.
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_aux_write(
uint8_t reg_addr,
uint8_t* aux_data,
uint16_t len,
const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_aux_init bmi160_aux_init
* \code
* int8_t bmi160_aux_init(const struct bmi160_dev *dev);
* \endcode
* @details This API initialize the auxiliary sensor
* in order to access it.
*
* @param[in] dev : Structure instance of bmi160_dev.
* @note : Refer user guide for detailed info.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_aux_init(const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_set_aux_auto_mode bmi160_set_aux_auto_mode
* \code
* int8_t bmi160_set_aux_auto_mode(uint8_t *data_addr, struct bmi160_dev *dev);
* \endcode
* @details This API is used to setup the auxiliary sensor of bmi160 in auto mode
* Thus enabling the auto update of 8 bytes of data from auxiliary sensor
* to BMI160 register address 0x04 to 0x0B
*
* @param[in] data_addr : Starting address of aux. sensor's data register
* (BMI160 registers 0x04 to 0x0B will be updated
* with 8 bytes of data from auxiliary sensor
* starting from this register address.)
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note : Set the value of auxiliary polling rate by setting
* dev->aux_cfg.aux_odr to the required value from the table
* before calling this API
*
*@verbatim
* dev->aux_cfg.aux_odr | Auxiliary ODR (Hz)
* -----------------------|-----------------------
* BMI160_AUX_ODR_0_78HZ | 25/32
* BMI160_AUX_ODR_1_56HZ | 25/16
* BMI160_AUX_ODR_3_12HZ | 25/8
* BMI160_AUX_ODR_6_25HZ | 25/4
* BMI160_AUX_ODR_12_5HZ | 25/2
* BMI160_AUX_ODR_25HZ | 25
* BMI160_AUX_ODR_50HZ | 50
* BMI160_AUX_ODR_100HZ | 100
* BMI160_AUX_ODR_200HZ | 200
* BMI160_AUX_ODR_400HZ | 400
* BMI160_AUX_ODR_800HZ | 800
*@endverbatim
*
* @note : Other values of dev->aux_cfg.aux_odr are reserved and not for use
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_set_aux_auto_mode(uint8_t* data_addr, struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_config_aux_mode bmi160_config_aux_mode
* \code
* int8_t bmi160_config_aux_mode(const struct bmi160_dev *dev);
* \endcode
* @details This API configures the 0x4C register and settings like
* Auxiliary sensor manual enable/ disable and aux burst read length.
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_config_aux_mode(const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiAux
* \page bmi160_api_bmi160_read_aux_data_auto_mode bmi160_read_aux_data_auto_mode
* \code
* int8_t bmi160_read_aux_data_auto_mode(uint8_t *aux_data, const struct bmi160_dev *dev);
* \endcode
* @details This API is used to read the raw uncompensated auxiliary sensor
* data of 8 bytes from BMI160 register address 0x04 to 0x0B
*
* @param[in] aux_data : Pointer to user array of length 8 bytes
* Ensure that the aux_data array is of
* length 8 bytes
* @param[in] dev : Structure instance of bmi160_dev
*
* @retval zero -> Success / -ve value -> Error
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_read_aux_data_auto_mode(uint8_t* aux_data, const struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiSelfTest Self test
* @brief Perform self test of the sensor
*/
/*!
* \ingroup bmi160ApiSelfTest
* \page bmi160_api_bmi160_perform_self_test bmi160_perform_self_test
* \code
* int8_t bmi160_perform_self_test(uint8_t select_sensor, struct bmi160_dev *dev);
* \endcode
* @details This is used to perform self test of accel/gyro of the BMI160 sensor
*
* @param[in] select_sensor : enum to choose accel or gyro for self test
* @param[in] dev : Structure instance of bmi160_dev
*
* @note self test can be performed either for accel/gyro at any instant.
*
*@verbatim
* value of select_sensor | Inference
*----------------------------------|--------------------------------
* BMI160_ACCEL_ONLY | Accel self test enabled
* BMI160_GYRO_ONLY | Gyro self test enabled
* BMI160_BOTH_ACCEL_AND_GYRO | NOT TO BE USED
*@endverbatim
*
* @note The return value of this API gives us the result of self test.
*
* @note Performing self test does soft reset of the sensor, User can
* set the desired settings after performing the self test.
*
* @return Result of API execution status
* @retval BMI160_OK Self test success
* @retval BMI160_W_GYRO_SELF_TEST_FAIL Gyro self test fail
* @retval BMI160_W_ACCEl_SELF_TEST_FAIL Accel self test fail
*/
int8_t bmi160_perform_self_test(uint8_t select_sensor, struct bmi160_dev* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiFIFO FIFO
* @brief FIFO operations of the sensor
*/
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_get_fifo_data bmi160_get_fifo_data
* \code
* int8_t bmi160_get_fifo_data(struct bmi160_dev const *dev);
* \endcode
* @details This API reads data from the fifo buffer.
*
* @note User has to allocate the FIFO buffer along with
* corresponding fifo length from his side before calling this API
* as mentioned in the readme.md
*
* @note User must specify the number of bytes to read from the FIFO in
* dev->fifo->length , It will be updated by the number of bytes actually
* read from FIFO after calling this API
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval Zero Success
* @retval Negative Error
*/
int8_t bmi160_get_fifo_data(struct bmi160_dev const* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_set_fifo_flush bmi160_set_fifo_flush
* \code
* int8_t bmi160_set_fifo_flush(const struct bmi160_dev *dev);
* \endcode
* @details This API writes fifo_flush command to command register.This
* action clears all data in the Fifo without changing fifo configuration
* settings.
*
* @param[in] dev : Structure instance of bmi160_dev
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_set_fifo_flush(const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_set_fifo_config bmi160_set_fifo_config
* \code
* int8_t bmi160_set_fifo_config(uint8_t config, uint8_t enable, struct bmi160_dev const *dev);
* \endcode
* @details This API sets the FIFO configuration in the sensor.
*
* @param[in] config : variable used to specify the FIFO
* configurations which are to be enabled or disabled in the sensor.
*
* @note : User can set either set one or more or all FIFO configurations
* by ORing the below mentioned macros.
*
*@verbatim
* config | Value
* ------------------------|---------------------------
* BMI160_FIFO_TIME | 0x02
* BMI160_FIFO_TAG_INT2 | 0x04
* BMI160_FIFO_TAG_INT1 | 0x08
* BMI160_FIFO_HEADER | 0x10
* BMI160_FIFO_AUX | 0x20
* BMI160_FIFO_ACCEL | 0x40
* BMI160_FIFO_GYRO | 0x80
*@endverbatim
*
* @param[in] enable : Parameter used to enable or disable the above
* FIFO configuration
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return status of bus communication result
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_set_fifo_config(uint8_t config, uint8_t enable, struct bmi160_dev const* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_set_fifo_down bmi160_set_fifo_down
* \code
* int8_t bmi160_set_fifo_down(uint8_t fifo_down, const struct bmi160_dev *dev);
* \endcode
* @details This API is used to configure the down sampling ratios of
* the accel and gyro data for FIFO.Also, it configures filtered or
* pre-filtered data for the fifo for accel and gyro.
*
* @param[in] fifo_down : variable used to specify the FIFO down
* configurations which are to be enabled or disabled in the sensor.
*
* @note The user must select one among the following macros to
* select down-sampling ratio for accel
*
*@verbatim
* config | Value
* -------------------------------------|---------------------------
* BMI160_ACCEL_FIFO_DOWN_ZERO | 0x00
* BMI160_ACCEL_FIFO_DOWN_ONE | 0x10
* BMI160_ACCEL_FIFO_DOWN_TWO | 0x20
* BMI160_ACCEL_FIFO_DOWN_THREE | 0x30
* BMI160_ACCEL_FIFO_DOWN_FOUR | 0x40
* BMI160_ACCEL_FIFO_DOWN_FIVE | 0x50
* BMI160_ACCEL_FIFO_DOWN_SIX | 0x60
* BMI160_ACCEL_FIFO_DOWN_SEVEN | 0x70
*@endverbatim
*
* @note The user must select one among the following macros to
* select down-sampling ratio for gyro
*
*@verbatim
* config | Value
* -------------------------------------|---------------------------
* BMI160_GYRO_FIFO_DOWN_ZERO | 0x00
* BMI160_GYRO_FIFO_DOWN_ONE | 0x01
* BMI160_GYRO_FIFO_DOWN_TWO | 0x02
* BMI160_GYRO_FIFO_DOWN_THREE | 0x03
* BMI160_GYRO_FIFO_DOWN_FOUR | 0x04
* BMI160_GYRO_FIFO_DOWN_FIVE | 0x05
* BMI160_GYRO_FIFO_DOWN_SIX | 0x06
* BMI160_GYRO_FIFO_DOWN_SEVEN | 0x07
*@endverbatim
*
* @note The user can enable filtered accel data by the following macro
*
*@verbatim
* config | Value
* -------------------------------------|---------------------------
* BMI160_ACCEL_FIFO_FILT_EN | 0x80
*@endverbatim
*
* @note The user can enable filtered gyro data by the following macro
*
*@verbatim
* config | Value
* -------------------------------------|---------------------------
* BMI160_GYRO_FIFO_FILT_EN | 0x08
*@endverbatim
*
* @note : By ORing the above mentioned macros, the user can select
* the required FIFO down config settings
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return status of bus communication result
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_set_fifo_down(uint8_t fifo_down, const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_set_fifo_wm bmi160_set_fifo_wm
* \code
* int8_t bmi160_set_fifo_wm(uint8_t fifo_wm, const struct bmi160_dev *dev);
* \endcode
* @details This API sets the FIFO watermark level in the sensor.
*
* @note The FIFO watermark is issued when the FIFO fill level is
* equal or above the watermark level and units of watermark is 4 bytes.
*
* @param[in] fifo_wm : Variable used to set the FIFO water mark level
* @param[in] dev : Structure instance of bmi160_dev
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_set_fifo_wm(uint8_t fifo_wm, const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_extract_accel bmi160_extract_accel
* \code
* int8_t bmi160_extract_accel(struct bmi160_sensor_data *accel_data, uint8_t *accel_length, struct bmi160_dev const
**dev);
* \endcode
* @details This API parses and extracts the accelerometer frames from
* FIFO data read by the "bmi160_get_fifo_data" API and stores it in
* the "accel_data" structure instance.
*
* @note The bmi160_extract_accel API should be called only after
* reading the FIFO data by calling the bmi160_get_fifo_data() API.
*
* @param[out] accel_data : Structure instance of bmi160_sensor_data
* where the accelerometer data in FIFO is stored.
* @param[in,out] accel_length : Number of valid accelerometer frames
* (x,y,z axes data) read out from fifo.
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note accel_length is updated with the number of valid accelerometer
* frames extracted from fifo (1 accel frame = 6 bytes) at the end of
* execution of this API.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_extract_accel(
struct bmi160_sensor_data* accel_data,
uint8_t* accel_length,
struct bmi160_dev const* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_extract_gyro bmi160_extract_gyro
* \code
* int8_t bmi160_extract_gyro(struct bmi160_sensor_data *gyro_data, uint8_t *gyro_length, struct bmi160_dev const *dev);
* \endcode
* @details This API parses and extracts the gyro frames from
* FIFO data read by the "bmi160_get_fifo_data" API and stores it in
* the "gyro_data" structure instance.
*
* @note The bmi160_extract_gyro API should be called only after
* reading the FIFO data by calling the bmi160_get_fifo_data() API.
*
* @param[out] gyro_data : Structure instance of bmi160_sensor_data
* where the gyro data in FIFO is stored.
* @param[in,out] gyro_length : Number of valid gyro frames
* (x,y,z axes data) read out from fifo.
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note gyro_length is updated with the number of valid gyro
* frames extracted from fifo (1 gyro frame = 6 bytes) at the end of
* execution of this API.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_extract_gyro(
struct bmi160_sensor_data* gyro_data,
uint8_t* gyro_length,
struct bmi160_dev const* dev);
/*!
* \ingroup bmi160ApiFIFO
* \page bmi160_api_bmi160_extract_aux bmi160_extract_aux
* \code
* int8_t bmi160_extract_aux(struct bmi160_aux_data *aux_data, uint8_t *aux_len, struct bmi160_dev const *dev);
* \endcode
* @details This API parses and extracts the aux frames from
* FIFO data read by the "bmi160_get_fifo_data" API and stores it in
* the bmi160_aux_data structure instance.
*
* @note The bmi160_extract_aux API should be called only after
* reading the FIFO data by calling the bmi160_get_fifo_data() API.
*
* @param[out] aux_data : Structure instance of bmi160_aux_data
* where the aux data in FIFO is stored.
* @param[in,out] aux_len : Number of valid aux frames (8bytes)
* read out from FIFO.
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note aux_len is updated with the number of valid aux
* frames extracted from fifo (1 aux frame = 8 bytes) at the end of
* execution of this API.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*
*/
int8_t bmi160_extract_aux(
struct bmi160_aux_data* aux_data,
uint8_t* aux_len,
struct bmi160_dev const* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiFOC FOC
* @brief Start FOC of accel and gyro sensors
*/
/*!
* \ingroup bmi160ApiFOC
* \page bmi160_api_bmi160_start_foc bmi160_start_foc
* \code
* int8_t bmi160_start_foc(const struct bmi160_foc_conf *foc_conf,
* \endcode
* @details This API starts the FOC of accel and gyro
*
* @note FOC should not be used in low-power mode of sensor
*
* @note Accel FOC targets values of +1g , 0g , -1g
* Gyro FOC always targets value of 0 dps
*
* @param[in] foc_conf : Structure instance of bmi160_foc_conf which
* has the FOC configuration
* @param[in,out] offset : Structure instance to store Offset
* values read from sensor
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note Pre-requisites for triggering FOC in accel , Set the following,
* Enable the acc_off_en
* Ex : foc_conf.acc_off_en = BMI160_ENABLE;
*
* Set the desired target values of FOC to each axes (x,y,z) by using the
* following macros
* - BMI160_FOC_ACCEL_DISABLED
* - BMI160_FOC_ACCEL_POSITIVE_G
* - BMI160_FOC_ACCEL_NEGATIVE_G
* - BMI160_FOC_ACCEL_0G
*
* Ex : foc_conf.foc_acc_x = BMI160_FOC_ACCEL_0G;
* foc_conf.foc_acc_y = BMI160_FOC_ACCEL_0G;
* foc_conf.foc_acc_z = BMI160_FOC_ACCEL_POSITIVE_G;
*
* @note Pre-requisites for triggering FOC in gyro ,
* Set the following parameters,
*
* Ex : foc_conf.foc_gyr_en = BMI160_ENABLE;
* foc_conf.gyro_off_en = BMI160_ENABLE;
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*/
int8_t bmi160_start_foc(
const struct bmi160_foc_conf* foc_conf,
struct bmi160_offsets* offset,
struct bmi160_dev const* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiOffsets Offsets
* @brief Set / Get offset values of accel and gyro sensors
*/
/*!
* \ingroup bmi160ApiOffsets
* \page bmi160_api_bmi160_get_offsets bmi160_get_offsets
* \code
* int8_t bmi160_get_offsets(struct bmi160_offsets *offset, const struct bmi160_dev *dev);
* \endcode
* @details This API reads and stores the offset values of accel and gyro
*
* @param[in,out] offset : Structure instance of bmi160_offsets in which
* the offset values are read and stored
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*/
int8_t bmi160_get_offsets(struct bmi160_offsets* offset, const struct bmi160_dev* dev);
/*!
* \ingroup bmi160ApiOffsets
* \page bmi160_api_bmi160_set_offsets bmi160_set_offsets
* \code
* int8_t bmi160_set_offsets(const struct bmi160_foc_conf *foc_conf,
* const struct bmi160_offsets *offset,
* struct bmi160_dev const *dev);
* \endcode
* @details This API writes the offset values of accel and gyro to
* the sensor but these values will be reset on POR or soft reset.
*
* @param[in] foc_conf : Structure instance of bmi160_foc_conf which
* has the FOC configuration
* @param[in] offset : Structure instance in which user updates offset
* values which are to be written in the sensor
* @param[in] dev : Structure instance of bmi160_dev.
*
* @note Offsets can be set by user like offset->off_acc_x = 10;
* where 1LSB = 3.9mg and for gyro 1LSB = 0.061degrees/second
*
* @note BMI160 offset values for xyz axes of accel should be within range of
* BMI160_ACCEL_MIN_OFFSET (-128) to BMI160_ACCEL_MAX_OFFSET (127)
*
* @note BMI160 offset values for xyz axes of gyro should be within range of
* BMI160_GYRO_MIN_OFFSET (-512) to BMI160_GYRO_MAX_OFFSET (511)
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*/
int8_t bmi160_set_offsets(
const struct bmi160_foc_conf* foc_conf,
const struct bmi160_offsets* offset,
struct bmi160_dev const* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiNVM NVM
* @brief Write image registers values to NVM
*/
/*!
* \ingroup bmi160ApiNVM
* \page bmi160_api_bmi160_update_nvm bmi160_update_nvm
* \code
* int8_t bmi160_update_nvm(struct bmi160_dev const *dev);
* \endcode
* @details This API writes the image registers values to NVM which is
* stored even after POR or soft reset
*
* @param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*/
int8_t bmi160_update_nvm(struct bmi160_dev const* dev);
/**
* \ingroup bmi160
* \defgroup bmi160ApiInts Interrupt status
* @brief Read interrupt status from the sensor
*/
/*!
* \ingroup bmi160ApiInts
* \page bmi160_api_bmi160_get_int_status bmi160_get_int_status
* \code
* int8_t bmi160_get_int_status(enum bmi160_int_status_sel int_status_sel,
* union bmi160_int_status *int_status,
* struct bmi160_dev const *dev);
* \endcode
* @details This API gets the interrupt status from the sensor.
*
* @param[in] int_status_sel : Enum variable to select either individual or all the
* interrupt status bits.
* @param[in] int_status : pointer variable to get the interrupt status
* from the sensor.
* param[in] dev : Structure instance of bmi160_dev.
*
* @return Result of API execution status
* @retval 0 -> Success
* @retval Any non zero value -> Fail
*/
int8_t bmi160_get_int_status(
enum bmi160_int_status_sel int_status_sel,
union bmi160_int_status* int_status,
struct bmi160_dev const* dev);
/*************************** C++ guard macro *****************************/
#ifdef __cplusplus
}
#endif
#endif /* BMI160_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
#include "imu.h"
#include <furi_hal.h>
bool bmi160_begin();
int bmi160_read(double* vec);
bool lsm6ds3trc_begin();
void lsm6ds3trc_end();
int lsm6ds3trc_read(double* vec);
bool imu_begin() {
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
bool ret = bmi160_begin(); // lsm6ds3trc_begin();
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return ret;
}
void imu_end() {
// furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
// lsm6ds3trc_end();
// furi_hal_i2c_release(&furi_hal_i2c_handle_external);
}
int imu_read(double* vec) {
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
int ret = bmi160_read(vec); // lsm6ds3trc_read(vec);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return ret;
}

View File

@ -0,0 +1,18 @@
#pragma once
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
#define ACC_DATA_READY (1 << 0)
#define GYR_DATA_READY (1 << 1)
bool imu_begin();
void imu_end();
int imu_read(double* vec);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,88 @@
#include "bmi160.h"
#include <furi_hal.h>
#include "imu.h"
#define TAG "BMI160"
#define BMI160_DEV_ADDR (0x69 << 1)
static const double DEG_TO_RAD = 0.017453292519943295769236907684886;
static const double G = 9.81;
struct bmi160_dev bmi160dev;
struct bmi160_sensor_data bmi160_accel;
struct bmi160_sensor_data bmi160_gyro;
int8_t bmi160_write_i2c(uint8_t dev_addr, uint8_t reg_addr, uint8_t* data, uint16_t len) {
if(furi_hal_i2c_write_mem(&furi_hal_i2c_handle_external, dev_addr, reg_addr, data, len, 50))
return BMI160_OK;
return BMI160_E_COM_FAIL;
}
int8_t bmi160_read_i2c(uint8_t dev_addr, uint8_t reg_addr, uint8_t* read_data, uint16_t len) {
if(furi_hal_i2c_read_mem(&furi_hal_i2c_handle_external, dev_addr, reg_addr, read_data, len, 50))
return BMI160_OK;
return BMI160_E_COM_FAIL;
}
bool bmi160_begin() {
FURI_LOG_I(TAG, "Init BMI160");
if(!furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, BMI160_DEV_ADDR, 50)) {
FURI_LOG_E(TAG, "Device not ready!");
return false;
}
FURI_LOG_I(TAG, "Device ready!");
bmi160dev.id = BMI160_DEV_ADDR;
bmi160dev.intf = BMI160_I2C_INTF;
bmi160dev.read = bmi160_read_i2c;
bmi160dev.write = bmi160_write_i2c;
bmi160dev.delay_ms = furi_delay_ms;
if(bmi160_init(&bmi160dev) != BMI160_OK) {
FURI_LOG_E(TAG, "Initialization failure!");
FURI_LOG_E(TAG, "Chip ID 0x%X", bmi160dev.chip_id);
return false;
}
bmi160dev.accel_cfg.odr = BMI160_ACCEL_ODR_400HZ;
bmi160dev.accel_cfg.range = BMI160_ACCEL_RANGE_4G;
bmi160dev.accel_cfg.bw = BMI160_ACCEL_BW_NORMAL_AVG4;
bmi160dev.accel_cfg.power = BMI160_ACCEL_NORMAL_MODE;
bmi160dev.gyro_cfg.odr = BMI160_GYRO_ODR_400HZ;
bmi160dev.gyro_cfg.range = BMI160_GYRO_RANGE_2000_DPS;
bmi160dev.gyro_cfg.bw = BMI160_GYRO_BW_NORMAL_MODE;
bmi160dev.gyro_cfg.power = BMI160_GYRO_NORMAL_MODE;
if(bmi160_set_sens_conf(&bmi160dev) != BMI160_OK) {
FURI_LOG_E(TAG, "Initialization failure!");
FURI_LOG_E(TAG, "Chip ID 0x%X", bmi160dev.chip_id);
return false;
}
FURI_LOG_I(TAG, "Initialization success!");
FURI_LOG_I(TAG, "Chip ID 0x%X", bmi160dev.chip_id);
return true;
}
int bmi160_read(double* vec) {
if(bmi160_get_sensor_data(
(BMI160_ACCEL_SEL | BMI160_GYRO_SEL), &bmi160_accel, &bmi160_gyro, &bmi160dev) !=
BMI160_OK) {
return 0;
}
vec[0] = ((double)bmi160_accel.x * 4 / 32768) * G;
vec[1] = ((double)bmi160_accel.y * 4 / 32768) * G;
vec[2] = ((double)bmi160_accel.z * 4 / 32768) * G;
vec[3] = ((double)bmi160_gyro.x * 2000 / 32768) * DEG_TO_RAD;
vec[4] = ((double)bmi160_gyro.y * 2000 / 32768) * DEG_TO_RAD;
vec[5] = ((double)bmi160_gyro.z * 2000 / 32768) * DEG_TO_RAD;
return ACC_DATA_READY | GYR_DATA_READY;
}

View File

@ -0,0 +1,94 @@
#include "lsm6ds3tr_c_reg.h"
#include <furi_hal.h>
#include "imu.h"
#define TAG "LSM6DS3TR-C"
#define LSM6DS3_ADDRESS (0x6A << 1)
static const double DEG_TO_RAD = 0.017453292519943295769236907684886;
stmdev_ctx_t lsm6ds3trc_ctx;
int32_t lsm6ds3trc_write_i2c(void* handle, uint8_t reg_addr, const uint8_t* data, uint16_t len) {
if(furi_hal_i2c_write_mem(handle, LSM6DS3_ADDRESS, reg_addr, (uint8_t*)data, len, 50))
return 0;
return -1;
}
int32_t lsm6ds3trc_read_i2c(void* handle, uint8_t reg_addr, uint8_t* read_data, uint16_t len) {
if(furi_hal_i2c_read_mem(handle, LSM6DS3_ADDRESS, reg_addr, read_data, len, 50)) return 0;
return -1;
}
bool lsm6ds3trc_begin() {
FURI_LOG_I(TAG, "Init LSM6DS3TR-C");
if(!furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, LSM6DS3_ADDRESS, 50)) {
FURI_LOG_E(TAG, "Not ready");
return false;
}
lsm6ds3trc_ctx.write_reg = lsm6ds3trc_write_i2c;
lsm6ds3trc_ctx.read_reg = lsm6ds3trc_read_i2c;
lsm6ds3trc_ctx.mdelay = furi_delay_ms;
lsm6ds3trc_ctx.handle = &furi_hal_i2c_handle_external;
uint8_t whoami;
lsm6ds3tr_c_device_id_get(&lsm6ds3trc_ctx, &whoami);
if(whoami != LSM6DS3TR_C_ID) {
FURI_LOG_I(TAG, "Unknown model: %x", (int)whoami);
return false;
}
lsm6ds3tr_c_reset_set(&lsm6ds3trc_ctx, PROPERTY_ENABLE);
uint8_t rst = PROPERTY_ENABLE;
while(rst) lsm6ds3tr_c_reset_get(&lsm6ds3trc_ctx, &rst);
lsm6ds3tr_c_block_data_update_set(&lsm6ds3trc_ctx, PROPERTY_ENABLE);
lsm6ds3tr_c_fifo_mode_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_BYPASS_MODE);
lsm6ds3tr_c_xl_data_rate_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_XL_ODR_104Hz);
lsm6ds3tr_c_xl_full_scale_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_4g);
lsm6ds3tr_c_xl_lp1_bandwidth_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_XL_LP1_ODR_DIV_4);
lsm6ds3tr_c_gy_data_rate_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_GY_ODR_104Hz);
lsm6ds3tr_c_gy_full_scale_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_2000dps);
lsm6ds3tr_c_gy_power_mode_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_GY_HIGH_PERFORMANCE);
lsm6ds3tr_c_gy_band_pass_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_LP2_ONLY);
FURI_LOG_I(TAG, "Init OK");
return true;
}
void lsm6ds3trc_end() {
lsm6ds3tr_c_xl_data_rate_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_XL_ODR_OFF);
lsm6ds3tr_c_gy_data_rate_set(&lsm6ds3trc_ctx, LSM6DS3TR_C_GY_ODR_OFF);
}
int lsm6ds3trc_read(double* vec) {
int ret = 0;
int16_t data[3];
lsm6ds3tr_c_reg_t reg;
lsm6ds3tr_c_status_reg_get(&lsm6ds3trc_ctx, &reg.status_reg);
if(reg.status_reg.xlda) {
lsm6ds3tr_c_acceleration_raw_get(&lsm6ds3trc_ctx, data);
vec[2] = (double)lsm6ds3tr_c_from_fs2g_to_mg(data[0]) / 1000;
vec[0] = (double)lsm6ds3tr_c_from_fs2g_to_mg(data[1]) / 1000;
vec[1] = (double)lsm6ds3tr_c_from_fs2g_to_mg(data[2]) / 1000;
ret |= ACC_DATA_READY;
}
if(reg.status_reg.gda) {
lsm6ds3tr_c_angular_rate_raw_get(&lsm6ds3trc_ctx, data);
vec[5] = (double)lsm6ds3tr_c_from_fs2000dps_to_mdps(data[0]) * DEG_TO_RAD / 1000;
vec[3] = (double)lsm6ds3tr_c_from_fs2000dps_to_mdps(data[1]) * DEG_TO_RAD / 1000;
vec[4] = (double)lsm6ds3tr_c_from_fs2000dps_to_mdps(data[2]) * DEG_TO_RAD / 1000;
ret |= GYR_DATA_READY;
}
return ret;
}

View File

@ -0,0 +1,189 @@
#include "main_loop.h"
#include <furi.h>
#include <furi_hal.h>
#include "imu/imu.h"
#include "orientation_tracker.h"
#include "calibration_data.h"
#define TAG "tracker"
static const float CURSOR_SPEED = 1024.0 / (M_PI / 4);
static const float STABILIZE_BIAS = 16.0;
float g_yaw = 0;
float g_pitch = 0;
float g_dYaw = 0;
float g_dPitch = 0;
bool firstRead = true;
bool stabilize = true;
CalibrationData calibration;
cardboard::OrientationTracker tracker(10000000l); // 10 ms / 100 Hz
uint64_t ippms, ippms2;
static inline float clamp(float val)
{
while (val <= -M_PI) {
val += 2 * M_PI;
}
while (val >= M_PI) {
val -= 2 * M_PI;
}
return val;
}
static inline float highpass(float oldVal, float newVal)
{
if (!stabilize) {
return newVal;
}
float delta = clamp(oldVal - newVal);
float alpha = (float) std::max(0.0, 1 - std::pow(std::fabs(delta) * CURSOR_SPEED / STABILIZE_BIAS, 3.0));
return newVal + alpha * delta;
}
void sendCurrentState(MouseMoveCallback mouse_move, void *context)
{
float dX = g_dYaw * CURSOR_SPEED;
float dY = g_dPitch * CURSOR_SPEED;
// Scale the shift down to fit the protocol.
if (dX > 127) {
dY *= 127.0 / dX;
dX = 127;
}
if (dX < -127) {
dY *= -127.0 / dX;
dX = -127;
}
if (dY > 127) {
dX *= 127.0 / dY;
dY = 127;
}
if (dY < -127) {
dX *= -127.0 / dY;
dY = -127;
}
const int8_t x = (int8_t)std::floor(dX + 0.5);
const int8_t y = (int8_t)std::floor(dY + 0.5);
mouse_move(x, y, context);
// Only subtract the part of the error that was already sent.
if (x != 0) {
g_dYaw -= x / CURSOR_SPEED;
}
if (y != 0) {
g_dPitch -= y / CURSOR_SPEED;
}
}
void onOrientation(cardboard::Vector4& quaternion)
{
float q1 = quaternion[0]; // X * sin(T/2)
float q2 = quaternion[1]; // Y * sin(T/2)
float q3 = quaternion[2]; // Z * sin(T/2)
float q0 = quaternion[3]; // cos(T/2)
float yaw = std::atan2(2 * (q0 * q3 - q1 * q2), (1 - 2 * (q1 * q1 + q3 * q3)));
float pitch = std::asin(2 * (q0 * q1 + q2 * q3));
// float roll = std::atan2(2 * (q0 * q2 - q1 * q3), (1 - 2 * (q1 * q1 + q2 * q2)));
if (yaw == NAN || pitch == NAN) {
// NaN case, skip it
return;
}
if (firstRead) {
g_yaw = yaw;
g_pitch = pitch;
firstRead = false;
} else {
const float newYaw = highpass(g_yaw, yaw);
const float newPitch = highpass(g_pitch, pitch);
float dYaw = clamp(g_yaw - newYaw);
float dPitch = g_pitch - newPitch;
g_yaw = newYaw;
g_pitch = newPitch;
// Accumulate the error locally.
g_dYaw += dYaw;
g_dPitch += dPitch;
}
}
extern "C" {
void calibration_begin() {
calibration.reset();
FURI_LOG_I(TAG, "Calibrating");
}
bool calibration_step() {
if (calibration.isComplete())
return true;
double vec[6];
if (imu_read(vec) & GYR_DATA_READY) {
cardboard::Vector3 data(vec[3], vec[4], vec[5]);
furi_delay_ms(9); // Artificially limit to ~100Hz
return calibration.add(data);
}
return false;
}
void calibration_end() {
CalibrationMedian store;
cardboard::Vector3 median = calibration.getMedian();
store.x = median[0];
store.y = median[1];
store.z = median[2];
CALIBRATION_DATA_SAVE(&store);
}
void tracking_begin() {
CalibrationMedian store;
cardboard::Vector3 median = calibration.getMedian();
if (CALIBRATION_DATA_LOAD(&store)) {
median[0] = store.x;
median[1] = store.y;
median[2] = store.z;
}
ippms = furi_hal_cortex_instructions_per_microsecond();
ippms2 = ippms / 2;
tracker.SetCalibration(median);
tracker.Resume();
}
void tracking_step(MouseMoveCallback mouse_move, void *context) {
double vec[6];
int ret = imu_read(vec);
if (ret != 0) {
uint64_t t = (DWT->CYCCNT * 1000llu + ippms2) / ippms;
if (ret & ACC_DATA_READY) {
cardboard::AccelerometerData adata
= { .system_timestamp = t, .sensor_timestamp_ns = t,
.data = cardboard::Vector3(vec[0], vec[1], vec[2]) };
tracker.OnAccelerometerData(adata);
}
if (ret & GYR_DATA_READY) {
cardboard::GyroscopeData gdata
= { .system_timestamp = t, .sensor_timestamp_ns = t,
.data = cardboard::Vector3(vec[3], vec[4], vec[5]) };
cardboard::Vector4 pose = tracker.OnGyroscopeData(gdata);
onOrientation(pose);
sendCurrentState(mouse_move, context);
}
}
}
void tracking_end() {
tracker.Pause();
}
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef bool (*MouseMoveCallback)(int8_t x, int8_t y, void* context);
void calibration_begin();
bool calibration_step();
void calibration_end();
void tracking_begin();
void tracking_step(MouseMoveCallback mouse_move, void* context);
void tracking_end();
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,95 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "orientation_tracker.h"
#include "sensors/pose_prediction.h"
#include "util/logging.h"
#include "util/vector.h"
#include "util/vectorutils.h"
namespace cardboard {
OrientationTracker::OrientationTracker(const long sampling_period_ns)
: sampling_period_ns_(sampling_period_ns)
, calibration_(Vector3::Zero())
, is_tracking_(false)
, sensor_fusion_(new SensorFusionEkf())
, latest_gyroscope_data_({ 0, 0, Vector3::Zero() })
{
sensor_fusion_->SetBiasEstimationEnabled(/*kGyroBiasEstimationEnabled*/ true);
}
void OrientationTracker::SetCalibration(const Vector3& calibration) {
calibration_ = calibration;
}
void OrientationTracker::Pause()
{
if (!is_tracking_) {
return;
}
// Create a gyro event with zero velocity. This effectively stops the prediction.
GyroscopeData event = latest_gyroscope_data_;
event.data = Vector3::Zero();
OnGyroscopeData(event);
is_tracking_ = false;
}
void OrientationTracker::Resume() { is_tracking_ = true; }
Vector4 OrientationTracker::GetPose(int64_t timestamp_ns) const
{
Rotation predicted_rotation;
const PoseState pose_state = sensor_fusion_->GetLatestPoseState();
if (sensor_fusion_->IsFullyInitialized()) {
predicted_rotation = pose_state.sensor_from_start_rotation;
} else {
CARDBOARD_LOGI("Tracker not fully initialized yet. Using pose prediction only.");
predicted_rotation = pose_prediction::PredictPose(timestamp_ns, pose_state);
}
return (-predicted_rotation).GetQuaternion();
}
void OrientationTracker::OnAccelerometerData(const AccelerometerData& event)
{
if (!is_tracking_) {
return;
}
sensor_fusion_->ProcessAccelerometerSample(event);
}
Vector4 OrientationTracker::OnGyroscopeData(const GyroscopeData& event)
{
if (!is_tracking_) {
return Vector4();
}
const GyroscopeData data = { .system_timestamp = event.system_timestamp,
.sensor_timestamp_ns = event.sensor_timestamp_ns,
.data = event.data - calibration_ };
latest_gyroscope_data_ = data;
sensor_fusion_->ProcessGyroscopeSample(data);
return OrientationTracker::GetPose(data.sensor_timestamp_ns + sampling_period_ns_);
}
} // namespace cardboard

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#pragma once
#include <array>
#include <memory>
#include <mutex> // NOLINT
#include "sensors/accelerometer_data.h"
#include "sensors/gyroscope_data.h"
#include "sensors/sensor_fusion_ekf.h"
#include "util/rotation.h"
namespace cardboard {
// OrientationTracker encapsulates pose tracking by connecting sensors
// to SensorFusion.
// This pose tracker reports poses in display space.
class OrientationTracker {
public:
OrientationTracker(const long sampling_period_ns);
void SetCalibration(const Vector3& calibration);
// Pauses tracking and sensors.
void Pause();
// Resumes tracking ans sensors.
void Resume();
// Gets the predicted pose for a given timestamp.
Vector4 GetPose(int64_t timestamp_ns) const;
// Function called when receiving AccelerometerData.
//
// @param event sensor event.
void OnAccelerometerData(const AccelerometerData& event);
// Function called when receiving GyroscopeData.
//
// @param event sensor event.
Vector4 OnGyroscopeData(const GyroscopeData& event);
private:
long sampling_period_ns_;
Vector3 calibration_;
std::atomic<bool> is_tracking_;
// Sensor Fusion object that stores the internal state of the filter.
std::unique_ptr<SensorFusionEkf> sensor_fusion_;
// Latest gyroscope data.
GyroscopeData latest_gyroscope_data_;
};
} // namespace cardboard

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_ACCELEROMETER_DATA_H_
#define CARDBOARD_SDK_SENSORS_ACCELEROMETER_DATA_H_
#include "../util/vector.h"
namespace cardboard {
struct AccelerometerData {
// System wall time.
uint64_t system_timestamp;
// Sensor clock time in nanoseconds.
uint64_t sensor_timestamp_ns;
// Acceleration force along the x,y,z axes in m/s^2. This follows android
// specification
// (https://developer.android.com/guide/topics/sensors/sensors_overview.html#sensors-coords).
Vector3 data;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_ACCELEROMETER_DATA_H_

View File

@ -0,0 +1,313 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "gyroscope_bias_estimator.h"
#include <algorithm>
#include <chrono> // NOLINT
#include "../util/rotation.h"
#include "../util/vector.h"
namespace {
// Cutoff frequencies in Hertz applied to our various signals, and their
// corresponding filters.
const float kAccelerometerLowPassCutOffFrequencyHz = 1.0f;
const float kRotationVelocityBasedAccelerometerLowPassCutOffFrequencyHz = 0.15f;
const float kGyroscopeLowPassCutOffFrequencyHz = 1.0f;
const float kGyroscopeBiasLowPassCutOffFrequencyHz = 0.15f;
// Note that MEMS IMU are not that precise.
const double kEpsilon = 1.0e-8;
// Size of the filtering window for the mean and median filter. The larger the
// windows the larger the filter delay.
const int kFilterWindowSize = 5;
// Threshold used to compare rotation computed from the accelerometer and the
// gyroscope bias.
const double kRatioBetweenGyroBiasAndAccel = 1.5;
// The minimum sum of weights we need to acquire before returning a bias
// estimation.
const float kMinSumOfWeightsGyroBiasThreshold = 25.0f;
// Amount of change in m/s^3 we allow on the smoothed accelerometer values to
// consider the phone static.
const double kAccelerometerDeltaStaticThreshold = 0.5;
// Amount of change in radians/s^2 we allow on the smoothed gyroscope values to
// consider the phone static.
const double kGyroscopeDeltaStaticThreshold = 0.03;
// If the gyroscope value is above this threshold, don't update the gyroscope
// bias estimation. This threshold is applied to the magnitude of gyroscope
// vectors in radians/s.
const float kGyroscopeForBiasThreshold = 0.30f;
// Used to monitor if accelerometer and gyroscope have been static for a few
// frames.
const int kStaticFrameDetectionThreshold = 50;
// Minimum time step between sensor updates.
const double kMinTimestep = 1; // std::chrono::nanoseconds(1);
} // namespace
namespace cardboard {
// A helper class to keep track of whether some signal can be considered static
// over specified number of frames.
class GyroscopeBiasEstimator::IsStaticCounter {
public:
// Initializes a counter with the number of consecutive frames we require
// the signal to be static before IsRecentlyStatic returns true.
//
// @param min_static_frames_threshold number of consecutive frames we
// require the signal to be static before IsRecentlyStatic returns true.
explicit IsStaticCounter(int min_static_frames_threshold)
: min_static_frames_threshold_(min_static_frames_threshold)
, consecutive_static_frames_(0)
{
}
// Specifies whether the current frame is considered static.
//
// @param is_static static flag for current frame.
void AppendFrame(bool is_static)
{
if (is_static) {
++consecutive_static_frames_;
} else {
consecutive_static_frames_ = 0;
}
}
// Returns if static movement is assumed.
bool IsRecentlyStatic() const
{
return consecutive_static_frames_ >= min_static_frames_threshold_;
}
// Resets counter.
void Reset() { consecutive_static_frames_ = 0; }
private:
const int min_static_frames_threshold_;
int consecutive_static_frames_;
};
GyroscopeBiasEstimator::GyroscopeBiasEstimator()
: accelerometer_lowpass_filter_(kAccelerometerLowPassCutOffFrequencyHz)
, simulated_gyroscope_from_accelerometer_lowpass_filter_(
kRotationVelocityBasedAccelerometerLowPassCutOffFrequencyHz)
, gyroscope_lowpass_filter_(kGyroscopeLowPassCutOffFrequencyHz)
, gyroscope_bias_lowpass_filter_(kGyroscopeBiasLowPassCutOffFrequencyHz)
, accelerometer_static_counter_(new IsStaticCounter(kStaticFrameDetectionThreshold))
, gyroscope_static_counter_(new IsStaticCounter(kStaticFrameDetectionThreshold))
, current_accumulated_weights_gyroscope_bias_(0.f)
, mean_filter_(kFilterWindowSize)
, median_filter_(kFilterWindowSize)
, last_mean_filtered_accelerometer_value_({ 0, 0, 0 })
{
Reset();
}
GyroscopeBiasEstimator::~GyroscopeBiasEstimator() { }
void GyroscopeBiasEstimator::Reset()
{
accelerometer_lowpass_filter_.Reset();
gyroscope_lowpass_filter_.Reset();
gyroscope_bias_lowpass_filter_.Reset();
accelerometer_static_counter_->Reset();
gyroscope_static_counter_->Reset();
}
void GyroscopeBiasEstimator::ProcessGyroscope(
const Vector3& gyroscope_sample, uint64_t timestamp_ns)
{
// Update gyroscope and gyroscope delta low-pass filters.
gyroscope_lowpass_filter_.AddSample(gyroscope_sample, timestamp_ns);
const auto smoothed_gyroscope_delta
= gyroscope_sample - gyroscope_lowpass_filter_.GetFilteredData();
gyroscope_static_counter_->AppendFrame(
Length(smoothed_gyroscope_delta) < kGyroscopeDeltaStaticThreshold);
// Only update the bias if the gyroscope and accelerometer signals have been
// relatively static recently.
if (gyroscope_static_counter_->IsRecentlyStatic()
&& accelerometer_static_counter_->IsRecentlyStatic()) {
// Reset static counter when updating the bias fails.
if (!UpdateGyroscopeBias(gyroscope_sample, timestamp_ns)) {
// Bias update fails because of large motion, thus reset the static
// counter.
gyroscope_static_counter_->AppendFrame(false);
}
} else {
// Reset weights, if not static.
current_accumulated_weights_gyroscope_bias_ = 0;
}
}
void GyroscopeBiasEstimator::ProcessAccelerometer(
const Vector3& accelerometer_sample, uint64_t timestamp_ns)
{
// Get current state of the filter.
const uint64_t previous_accel_timestamp_ns
= accelerometer_lowpass_filter_.GetMostRecentTimestampNs();
const bool is_low_pass_filter_init = accelerometer_lowpass_filter_.IsInitialized();
// Update accel and accel delta low-pass filters.
accelerometer_lowpass_filter_.AddSample(accelerometer_sample, timestamp_ns);
const auto smoothed_accelerometer_delta
= accelerometer_sample - accelerometer_lowpass_filter_.GetFilteredData();
accelerometer_static_counter_->AppendFrame(
Length(smoothed_accelerometer_delta) < kAccelerometerDeltaStaticThreshold);
// Rotation from accel cannot be differentiated with only one sample.
if (!is_low_pass_filter_init) {
simulated_gyroscope_from_accelerometer_lowpass_filter_.AddSample({ 0, 0, 0 }, timestamp_ns);
return;
}
// No need to update the simulated gyroscope at this point because the motion
// is too large.
if (!accelerometer_static_counter_->IsRecentlyStatic()) {
return;
}
median_filter_.AddSample(accelerometer_lowpass_filter_.GetFilteredData());
// This processing can only be started if the buffer is fully initialized.
if (!median_filter_.IsValid()) {
mean_filter_.AddSample(accelerometer_lowpass_filter_.GetFilteredData());
// Update the last filtered accelerometer value.
last_mean_filtered_accelerometer_value_ = accelerometer_lowpass_filter_.GetFilteredData();
return;
}
mean_filter_.AddSample(median_filter_.GetFilteredData());
// Compute a mock gyroscope value from accelerometer.
const int64_t diff = timestamp_ns - previous_accel_timestamp_ns;
const double timestep = static_cast<double>(diff);
simulated_gyroscope_from_accelerometer_lowpass_filter_.AddSample(
ComputeAngularVelocityFromLatestAccelerometer(timestep), timestamp_ns);
last_mean_filtered_accelerometer_value_ = mean_filter_.GetFilteredData();
}
Vector3 GyroscopeBiasEstimator::ComputeAngularVelocityFromLatestAccelerometer(double timestep) const
{
if (timestep < kMinTimestep) {
return { 0, 0, 0 };
}
const auto mean_of_median = mean_filter_.GetFilteredData();
// Compute an incremental rotation between the last state and the current
// state.
//
// Note that we switch to double precision here because of precision problem
// with small rotation.
const auto incremental_rotation = Rotation::RotateInto(
Vector3(last_mean_filtered_accelerometer_value_[0],
last_mean_filtered_accelerometer_value_[1], last_mean_filtered_accelerometer_value_[2]),
Vector3(mean_of_median[0], mean_of_median[1], mean_of_median[2]));
// We use axis angle here because this is how gyroscope values are stored.
Vector3 incremental_rotation_axis;
double incremental_rotation_angle;
incremental_rotation.GetAxisAndAngle(&incremental_rotation_axis, &incremental_rotation_angle);
incremental_rotation_axis *= incremental_rotation_angle / timestep;
return { static_cast<float>(incremental_rotation_axis[0]),
static_cast<float>(incremental_rotation_axis[1]),
static_cast<float>(incremental_rotation_axis[2]) };
}
bool GyroscopeBiasEstimator::UpdateGyroscopeBias(
const Vector3& gyroscope_sample, uint64_t timestamp_ns)
{
// Gyroscope values that are too big are potentially dangerous as they could
// originate from slow and steady head rotations.
//
// Therefore we compute an update weight which:
// * favors gyroscope values that are closer to 0
// * is set to zero if gyroscope values are greater than a threshold.
//
// This way, the gyroscope bias estimation converges faster if the phone is
// flat on a table, as opposed to held up somewhat stationary in the user's
// hands.
// If magnitude is too big, don't update the filter at all so that we don't
// artificially increase the number of samples accumulated by the filter.
const float gyroscope_sample_norm2 = Length(gyroscope_sample);
if (gyroscope_sample_norm2 >= kGyroscopeForBiasThreshold) {
return false;
}
float update_weight
= std::max(0.0f, 1 - gyroscope_sample_norm2 / kGyroscopeForBiasThreshold);
update_weight *= update_weight;
gyroscope_bias_lowpass_filter_.AddWeightedSample(
gyroscope_lowpass_filter_.GetFilteredData(), timestamp_ns, update_weight);
// This counter is only partially valid as the low pass filter drops large
// samples.
current_accumulated_weights_gyroscope_bias_ += update_weight;
return true;
}
Vector3 GyroscopeBiasEstimator::GetGyroscopeBias() const
{
return gyroscope_bias_lowpass_filter_.GetFilteredData();
}
bool GyroscopeBiasEstimator::IsCurrentEstimateValid() const
{
// Remove any bias component along the gravity because they cannot be
// evaluated from accelerometer.
const auto current_gravity_dir = Normalized(last_mean_filtered_accelerometer_value_);
const auto gyro_bias_lowpass = gyroscope_bias_lowpass_filter_.GetFilteredData();
const auto off_gravity_gyro_bias
= gyro_bias_lowpass - current_gravity_dir * Dot(gyro_bias_lowpass, current_gravity_dir);
// Checks that the current bias estimate is not correlated with the
// rotation computed from accelerometer.
const auto gyro_from_accel
= simulated_gyroscope_from_accelerometer_lowpass_filter_.GetFilteredData();
const bool isGyroscopeBiasCorrelatedWithSimulatedGyro
= (Length(gyro_from_accel) * kRatioBetweenGyroBiasAndAccel
> (Length(off_gravity_gyro_bias) + kEpsilon));
const bool hasEnoughSamples
= current_accumulated_weights_gyroscope_bias_ > kMinSumOfWeightsGyroBiasThreshold;
const bool areCountersStatic = gyroscope_static_counter_->IsRecentlyStatic()
&& accelerometer_static_counter_->IsRecentlyStatic();
const bool isStatic
= hasEnoughSamples && areCountersStatic && !isGyroscopeBiasCorrelatedWithSimulatedGyro;
return isStatic;
}
} // namespace cardboard

View File

@ -0,0 +1,134 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_GYROSCOPE_BIAS_ESTIMATOR_H_
#define CARDBOARD_SDK_SENSORS_GYROSCOPE_BIAS_ESTIMATOR_H_
#include <chrono> // NOLINT
#include <cstdint>
#include <list>
#include <memory>
#include <vector>
#include "lowpass_filter.h"
#include "mean_filter.h"
#include "median_filter.h"
#include "../util/vector.h"
namespace cardboard {
// Class that attempts to estimate the gyroscope's bias.
// Its main idea is that it averages the gyroscope values when the phone is
// considered stationary.
// Usage: A client should call the ProcessGyroscope and ProcessAccelerometer
// methods for every accelerometer and gyroscope sensor sample. This class
// expects these calls to be frequent, i.e., at least at 10 Hz. The client can
// then call GetGyroBias to retrieve the current estimate of the gyroscope bias.
// For best results, the fastest available delay option should be used when
// registering to sensors. Note that this class is not thread-safe.
//
// The filtering applied to the accelerometer to estimate a rotation
// from it follows :
// Baptiste Delporte, Laurent Perroton, Thierry Grandpierre, Jacques Trichet.
// Accelerometer and Magnetometer Based Gyroscope Emulation on Smart Sensor
// for a Virtual Reality Application. Sensor and Transducers Journal, 2012.
//
// which is a combination of a IIR filter, a median and a mean filter.
class GyroscopeBiasEstimator {
public:
GyroscopeBiasEstimator();
virtual ~GyroscopeBiasEstimator();
// Updates the estimator with a gyroscope event.
//
// @param gyroscope_sample the angular speed around the x, y, z axis in
// radians/sec.
// @param timestamp_ns the nanosecond at which the event occurred. Only
// guaranteed to be comparable with timestamps from other PocessGyroscope
// invocations.
virtual void ProcessGyroscope(const Vector3& gyroscope_sample, uint64_t timestamp_ns);
// Processes accelerometer samples to estimate if device is
// stable or not.
//
// First we filter the accelerometer. This is done with 3 filters.
// - A IIR low-pass filter
// - A median filter
// - A mean filter.
// Then a rotation is computed between consecutive filtered accelerometer
// samples.
// Finally this is converted to a velocity to emulate a gyroscope.
//
// @param accelerometer_sample the acceleration (including gravity) on the x,
// y, z axis in meters/s^2.
// @param timestamp_ns the nanosecond at which the event occurred. Only
// guaranteed to be comparable with timestamps from other
// ProcessAccelerometer invocations.
virtual void ProcessAccelerometer(const Vector3& accelerometer_sample, uint64_t timestamp_ns);
// Returns the estimated gyroscope bias.
//
// @return Estimated gyroscope bias. A vector with zeros is returned if no
// estimate has been computed.
virtual Vector3 GetGyroscopeBias() const;
// Resets the estimator state.
void Reset();
// Returns true if the current estimate returned by GetGyroscopeBias is
// correct. The device (measured using the sensors) has to be static for this
// function to return true.
virtual bool IsCurrentEstimateValid() const;
private:
// A helper class to keep track of whether some signal can be considered
// static over specified number of frames.
class IsStaticCounter;
// Updates gyroscope bias estimation.
//
// @return false if the current sample is too large.
bool UpdateGyroscopeBias(const Vector3& gyroscope_sample, uint64_t timestamp_ns);
// Returns device angular velocity (rad/s) from the latest accelerometer data.
//
// @param timestep in seconds between the last two samples.
// @return rotation velocity from latest accelerometer. This can be
// interpreted as an gyroscope.
Vector3 ComputeAngularVelocityFromLatestAccelerometer(double timestep) const;
LowpassFilter accelerometer_lowpass_filter_;
LowpassFilter simulated_gyroscope_from_accelerometer_lowpass_filter_;
LowpassFilter gyroscope_lowpass_filter_;
LowpassFilter gyroscope_bias_lowpass_filter_;
std::unique_ptr<IsStaticCounter> accelerometer_static_counter_;
std::unique_ptr<IsStaticCounter> gyroscope_static_counter_;
// Sum of the weight of sample used for gyroscope filtering.
float current_accumulated_weights_gyroscope_bias_;
// Set of filters for accelerometer data to estimate a rotation
// based only on accelerometer.
MeanFilter mean_filter_;
MedianFilter median_filter_;
// Last computed filter accelerometer value used for finite differences.
Vector3 last_mean_filtered_accelerometer_value_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_GYROSCOPE_BIAS_ESTIMATOR_H_

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_GYROSCOPE_DATA_H_
#define CARDBOARD_SDK_SENSORS_GYROSCOPE_DATA_H_
#include "../util/vector.h"
namespace cardboard {
struct GyroscopeData {
// System wall time.
uint64_t system_timestamp;
// Sensor clock time in nanoseconds.
uint64_t sensor_timestamp_ns;
// Rate of rotation around the x,y,z axes in rad/s. This follows android
// specification
// (https://developer.android.com/guide/topics/sensors/sensors_overview.html#sensors-coords).
Vector3 data;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_GYROSCOPE_DATA_H_

View File

@ -0,0 +1,84 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "lowpass_filter.h"
#include <cmath>
namespace {
const double kSecondsFromNanoseconds = 1.0e-9;
// Minimum time step between sensor updates. This corresponds to 1000 Hz.
const double kMinTimestepS = 0.001f;
// Maximum time step between sensor updates. This corresponds to 1 Hz.
const double kMaxTimestepS = 1.00f;
} // namespace
namespace cardboard {
LowpassFilter::LowpassFilter(double cutoff_freq_hz)
: cutoff_time_constant_(1 / (2 * (double)M_PI * cutoff_freq_hz))
, initialized_(false)
{
Reset();
}
void LowpassFilter::AddSample(const Vector3& sample, uint64_t timestamp_ns)
{
AddWeightedSample(sample, timestamp_ns, 1.0);
}
void LowpassFilter::AddWeightedSample(const Vector3& sample, uint64_t timestamp_ns, double weight)
{
if (!initialized_) {
// Initialize filter state
filtered_data_ = { sample[0], sample[1], sample[2] };
timestamp_most_recent_update_ns_ = timestamp_ns;
initialized_ = true;
return;
}
if (timestamp_ns < timestamp_most_recent_update_ns_) {
timestamp_most_recent_update_ns_ = timestamp_ns;
return;
}
const double delta_s = static_cast<double>(timestamp_ns - timestamp_most_recent_update_ns_)
* kSecondsFromNanoseconds;
if (delta_s <= kMinTimestepS || delta_s > kMaxTimestepS) {
timestamp_most_recent_update_ns_ = timestamp_ns;
return;
}
const double weighted_delta_secs = weight * delta_s;
const double alpha = weighted_delta_secs / (cutoff_time_constant_ + weighted_delta_secs);
for (int i = 0; i < 3; ++i) {
filtered_data_[i] = (1 - alpha) * filtered_data_[i] + alpha * sample[i];
}
timestamp_most_recent_update_ns_ = timestamp_ns;
}
void LowpassFilter::Reset()
{
initialized_ = false;
filtered_data_ = { 0, 0, 0 };
}
} // namespace cardboard

View File

@ -0,0 +1,81 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_LOWPASS_FILTER_H_
#define CARDBOARD_SDK_SENSORS_LOWPASS_FILTER_H_
#include <array>
#include <memory>
#include "../util/vector.h"
namespace cardboard {
// Implements an IIR, first order, low pass filter over vectors of the given
// dimension = 3.
// See http://en.wikipedia.org/wiki/Low-pass_filter
class LowpassFilter {
public:
// Initializes a filter with the given cutoff frequency in Hz.
explicit LowpassFilter(double cutoff_freq_hz);
// Updates the filter with the given sample. Note that samples with
// non-monotonic timestamps and successive samples with a time steps below 1
// ms or above 1 s are ignored.
//
// @param sample current sample data.
// @param timestamp_ns timestamp associated to this sample in nanoseconds.
void AddSample(const Vector3& sample, uint64_t timestamp_ns);
// Updates the filter with the given weighted sample.
//
// @param sample current sample data.
// @param timestamp_ns timestamp associated to this sample in nanoseconds.
// @param weight typically a [0, 1] weight factor used when applying a new
// sample. A weight of 1 corresponds to calling AddSample. A weight of 0
// makes the update no-op. The first initial sample is not affected by
// this.
void AddWeightedSample(const Vector3& sample, uint64_t timestamp_ns, double weight);
// Returns the filtered value. A vector with zeros is returned if no samples
// have been added.
Vector3 GetFilteredData() const {
return filtered_data_;
}
// Returns the most recent update timestamp in ns.
uint64_t GetMostRecentTimestampNs() const {
return timestamp_most_recent_update_ns_;
}
// Returns true when the filter is initialized.
bool IsInitialized() const {
return initialized_;
}
// Resets filter state.
void Reset();
private:
const double cutoff_time_constant_;
uint64_t timestamp_most_recent_update_ns_;
bool initialized_;
Vector3 filtered_data_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_LOWPASS_FILTER_H_

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "mean_filter.h"
namespace cardboard {
MeanFilter::MeanFilter(size_t filter_size)
: filter_size_(filter_size)
{
}
void MeanFilter::AddSample(const Vector3& sample)
{
buffer_.push_back(sample);
if (buffer_.size() > filter_size_) {
buffer_.pop_front();
}
}
bool MeanFilter::IsValid() const { return buffer_.size() == filter_size_; }
Vector3 MeanFilter::GetFilteredData() const
{
// Compute mean of the samples stored in buffer_.
Vector3 mean = Vector3::Zero();
for (auto sample : buffer_) {
mean += sample;
}
return mean / static_cast<double>(filter_size_);
}
} // namespace cardboard

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_MEAN_FILTER_H_
#define CARDBOARD_SDK_SENSORS_MEAN_FILTER_H_
#include <deque>
#include "../util/vector.h"
namespace cardboard {
// Fixed window FIFO mean filter for vectors of the given dimension.
class MeanFilter {
public:
// Create a mean filter of size filter_size.
// @param filter_size size of the internal filter.
explicit MeanFilter(size_t filter_size);
// Add sample to buffer_ if buffer_ is full it drop the oldest sample.
void AddSample(const Vector3& sample);
// Returns true if buffer has filter_size_ sample, false otherwise.
bool IsValid() const;
// Returns the mean of values stored in the internal buffer.
Vector3 GetFilteredData() const;
private:
const size_t filter_size_;
std::deque<Vector3> buffer_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_MEAN_FILTER_H_

View File

@ -0,0 +1,69 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "median_filter.h"
#include <algorithm>
#include <vector>
#include "../util/vector.h"
#include "../util/vectorutils.h"
namespace cardboard {
MedianFilter::MedianFilter(size_t filter_size)
: filter_size_(filter_size)
{
}
void MedianFilter::AddSample(const Vector3& sample)
{
buffer_.push_back(sample);
norms_.push_back(Length(sample));
if (buffer_.size() > filter_size_) {
buffer_.pop_front();
norms_.pop_front();
}
}
bool MedianFilter::IsValid() const { return buffer_.size() == filter_size_; }
Vector3 MedianFilter::GetFilteredData() const
{
std::vector<float> norms(norms_.begin(), norms_.end());
// Get median of value of the norms.
std::nth_element(norms.begin(), norms.begin() + filter_size_ / 2, norms.end());
const float median_norm = norms[filter_size_ / 2];
// Get median value based on their norm.
auto median_it = buffer_.begin();
for (const auto norm : norms_) {
if (norm == median_norm) {
break;
}
++median_it;
}
return *median_it;
}
void MedianFilter::Reset()
{
buffer_.clear();
norms_.clear();
}
} // namespace cardboard

View File

@ -0,0 +1,53 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_MEDIAN_FILTER_H_
#define CARDBOARD_SDK_SENSORS_MEDIAN_FILTER_H_
#include <deque>
#include "../util/vector.h"
namespace cardboard {
// Fixed window FIFO median filter for vectors of the given dimension = 3.
class MedianFilter {
public:
// Creates a median filter of size filter_size.
// @param filter_size size of the internal filter.
explicit MedianFilter(size_t filter_size);
// Adds sample to buffer_ if buffer_ is full it drops the oldest sample.
void AddSample(const Vector3& sample);
// Returns true if buffer has filter_size_ sample, false otherwise.
bool IsValid() const;
// Returns the median of values store in the internal buffer.
Vector3 GetFilteredData() const;
// Resets the filter, removing all samples that have been added.
void Reset();
private:
const size_t filter_size_;
std::deque<Vector3> buffer_;
// Contains norms of the elements stored in buffer_.
std::deque<float> norms_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_MEDIAN_FILTER_H_

View File

@ -0,0 +1,71 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "pose_prediction.h"
#include <chrono> // NOLINT
#include "../util/logging.h"
#include "../util/vectorutils.h"
namespace cardboard {
namespace {
const double kEpsilon = 1.0e-15;
} // namespace
namespace pose_prediction {
Rotation GetRotationFromGyroscope(const Vector3& gyroscope_value, double timestep_s)
{
const double velocity = Length(gyroscope_value);
// When there is no rotation data return an identity rotation.
if (velocity < kEpsilon) {
CARDBOARD_LOGI("PosePrediction::GetRotationFromGyroscope: Velocity really small, "
"returning identity rotation.");
return Rotation::Identity();
}
// Since the gyroscope_value is a start from sensor transformation we need to
// invert it to have a sensor from start transformation, hence the minus sign.
// For more info:
// http://developer.android.com/guide/topics/sensors/sensors_motion.html#sensors-motion-gyro
return Rotation::FromAxisAndAngle(gyroscope_value / velocity, -timestep_s * velocity);
}
Rotation PredictPose(int64_t requested_pose_timestamp, const PoseState& current_state)
{
// Subtracting unsigned numbers is bad when the result is negative.
const int64_t diff = requested_pose_timestamp - current_state.timestamp;
const double timestep_s = diff * 1.0e-9;
const Rotation update = GetRotationFromGyroscope(
current_state.sensor_from_start_rotation_velocity, timestep_s);
return update * current_state.sensor_from_start_rotation;
}
Rotation PredictPoseInv(int64_t requested_pose_timestamp, const PoseState& current_state)
{
// Subtracting unsigned numbers is bad when the result is negative.
const int64_t diff = requested_pose_timestamp - current_state.timestamp;
const double timestep_s = diff * 1.0e-9;
const Rotation update = GetRotationFromGyroscope(
current_state.sensor_from_start_rotation_velocity, timestep_s);
return current_state.sensor_from_start_rotation * (-update);
}
} // namespace pose_prediction
} // namespace cardboard

View File

@ -0,0 +1,55 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_POSE_PREDICTION_H_
#define CARDBOARD_SDK_SENSORS_POSE_PREDICTION_H_
#include <cstdint>
#include "pose_state.h"
#include "../util/rotation.h"
namespace cardboard {
namespace pose_prediction {
// Returns a rotation matrix based on the integration of the gyroscope_value
// over the timestep_s in seconds.
// TODO(pfg): Document the space better here.
//
// @param gyroscope_value gyroscope sensor values.
// @param timestep_s integration period in seconds.
// @return Integration of the gyroscope value the rotation is from Start to
// Sensor Space.
Rotation GetRotationFromGyroscope(const Vector3& gyroscope_value, double timestep_s);
// Gets a predicted pose for a given time in the future (e.g. rendering time)
// based on a linear prediction model. This uses the system current state
// (position, velocity, etc) from the past to extrapolate a position in the
// future.
//
// @param requested_pose_timestamp time at which you want the pose.
// @param current_state current state that stores the pose and linear model at a
// given time prior to requested_pose_timestamp_ns.
// @return pose from Start to Sensor Space.
Rotation PredictPose(int64_t requested_pose_timestamp, const PoseState& current_state);
// Equivalent to PredictPose, but for use with poses relative to Start Space
// rather than sensor space.
Rotation PredictPoseInv(int64_t requested_pose_timestamp, const PoseState& current_state);
} // namespace pose_prediction
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_POSE_PREDICTION_H_

View File

@ -0,0 +1,56 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_POSE_STATE_H_
#define CARDBOARD_SDK_SENSORS_POSE_STATE_H_
#include "../util/rotation.h"
#include "../util/vector.h"
namespace cardboard {
enum {
kPoseStateFlagInvalid = 1U << 0,
kPoseStateFlagInitializing = 1U << 1,
kPoseStateFlagHas6DoF = 1U << 2,
};
// Stores a head pose pose plus derivatives. This can be used for prediction.
struct PoseState {
// System wall time.
int64_t timestamp;
// Rotation from Sensor Space to Start Space.
Rotation sensor_from_start_rotation;
// First derivative of the rotation.
Vector3 sensor_from_start_rotation_velocity;
// Current gyroscope bias in rad/s.
Vector3 bias;
// The position of the headset.
Vector3 position = Vector3(0, 0, 0);
// In the same coordinate frame as the position.
Vector3 velocity = Vector3(0, 0, 0);
// Flags indicating the status of the pose.
uint64_t flags = 0U;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_POSE_STATE_H_

View File

@ -0,0 +1,333 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "sensor_fusion_ekf.h"
#include <algorithm>
#include <cmath>
#include "accelerometer_data.h"
#include "gyroscope_data.h"
#include "pose_prediction.h"
#include "../util/matrixutils.h"
namespace cardboard {
namespace {
const double kFiniteDifferencingEpsilon = 1.0e-7;
const double kEpsilon = 1.0e-15;
// Default gyroscope frequency. This corresponds to 100 Hz.
const double kDefaultGyroscopeTimestep_s = 0.01f;
// Maximum time between gyroscope before we start limiting the integration.
const double kMaximumGyroscopeSampleDelay_s = 0.04f;
// Compute a first-order exponential moving average of changes in accel norm per
// frame.
const double kSmoothingFactor = 0.5;
// Minimum and maximum values used for accelerometer noise covariance matrix.
// The smaller the sigma value, the more weight is given to the accelerometer
// signal.
const double kMinAccelNoiseSigma = 0.75;
const double kMaxAccelNoiseSigma = 7.0;
// Initial value for the diagonal elements of the different covariance matrices.
const double kInitialStateCovarianceValue = 25.0;
const double kInitialProcessCovarianceValue = 1.0;
// Maximum accelerometer norm change allowed before capping it covariance to a
// large value.
const double kMaxAccelNormChange = 0.15;
// Timestep IIR filtering coefficient.
const double kTimestepFilterCoeff = 0.95;
// Minimum number of sample for timestep filtering.
const int kTimestepFilterMinSamples = 10;
// Z direction in start space.
const Vector3 kCanonicalZDirection(0.0, 0.0, 1.0);
// Computes an axis-angle rotation from the input vector.
// angle = norm(a)
// axis = a.normalized()
// If norm(a) == 0, it returns an identity rotation.
static inline Rotation RotationFromVector(const Vector3& a)
{
const double norm_a = Length(a);
if (norm_a < kEpsilon) {
return Rotation::Identity();
}
return Rotation::FromAxisAndAngle(a / norm_a, norm_a);
}
} // namespace
SensorFusionEkf::SensorFusionEkf()
: execute_reset_with_next_accelerometer_sample_(false)
, bias_estimation_enabled_(true)
, gyroscope_bias_estimate_({ 0, 0, 0 })
{
ResetState();
}
void SensorFusionEkf::Reset() { execute_reset_with_next_accelerometer_sample_ = true; }
void SensorFusionEkf::ResetState()
{
current_state_.sensor_from_start_rotation = Rotation::Identity();
current_state_.sensor_from_start_rotation_velocity = Vector3::Zero();
current_gyroscope_sensor_timestamp_ns_ = 0;
current_accelerometer_sensor_timestamp_ns_ = 0;
state_covariance_ = Matrix3x3::Identity() * kInitialStateCovarianceValue;
process_covariance_ = Matrix3x3::Identity() * kInitialProcessCovarianceValue;
accelerometer_measurement_covariance_
= Matrix3x3::Identity() * kMinAccelNoiseSigma * kMinAccelNoiseSigma;
innovation_covariance_ = Matrix3x3::Identity();
accelerometer_measurement_jacobian_ = Matrix3x3::Zero();
kalman_gain_ = Matrix3x3::Zero();
innovation_ = Vector3::Zero();
accelerometer_measurement_ = Vector3::Zero();
prediction_ = Vector3::Zero();
control_input_ = Vector3::Zero();
state_update_ = Vector3::Zero();
moving_average_accelerometer_norm_change_ = 0.0;
is_timestep_filter_initialized_ = false;
is_gyroscope_filter_valid_ = false;
is_aligned_with_gravity_ = false;
// Reset biases.
gyroscope_bias_estimator_.Reset();
gyroscope_bias_estimate_ = { 0, 0, 0 };
}
// Here I am doing something wrong relative to time stamps. The state timestamps
// always correspond to the gyrostamps because it would require additional
// extrapolation if I wanted to do otherwise.
PoseState SensorFusionEkf::GetLatestPoseState() const { return current_state_; }
void SensorFusionEkf::ProcessGyroscopeSample(const GyroscopeData& sample)
{
// Don't accept gyroscope sample when waiting for a reset.
if (execute_reset_with_next_accelerometer_sample_) {
return;
}
// Discard outdated samples.
if (current_gyroscope_sensor_timestamp_ns_ >= sample.sensor_timestamp_ns) {
current_gyroscope_sensor_timestamp_ns_ = sample.sensor_timestamp_ns;
return;
}
// Checks that we received at least one gyroscope sample in the past.
if (current_gyroscope_sensor_timestamp_ns_ != 0) {
double current_timestep_s = std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::nanoseconds(
sample.sensor_timestamp_ns - current_gyroscope_sensor_timestamp_ns_))
.count();
if (current_timestep_s > kMaximumGyroscopeSampleDelay_s) {
if (is_gyroscope_filter_valid_) {
// Replaces the delta timestamp by the filtered estimates of the delta time.
current_timestep_s = filtered_gyroscope_timestep_s_;
} else {
current_timestep_s = kDefaultGyroscopeTimestep_s;
}
} else {
FilterGyroscopeTimestep(current_timestep_s);
}
if (bias_estimation_enabled_) {
gyroscope_bias_estimator_.ProcessGyroscope(sample.data, sample.sensor_timestamp_ns);
if (gyroscope_bias_estimator_.IsCurrentEstimateValid()) {
// As soon as the device is considered to be static, the bias estimator
// should have a precise estimate of the gyroscope bias.
gyroscope_bias_estimate_ = gyroscope_bias_estimator_.GetGyroscopeBias();
}
}
// Only integrate after receiving an accelerometer sample.
if (is_aligned_with_gravity_) {
const Rotation rotation_from_gyroscope = pose_prediction::GetRotationFromGyroscope(
{ sample.data[0] - gyroscope_bias_estimate_[0],
sample.data[1] - gyroscope_bias_estimate_[1],
sample.data[2] - gyroscope_bias_estimate_[2] },
current_timestep_s);
current_state_.sensor_from_start_rotation
= rotation_from_gyroscope * current_state_.sensor_from_start_rotation;
UpdateStateCovariance(RotationMatrixNH(rotation_from_gyroscope));
state_covariance_ = state_covariance_
+ ((current_timestep_s * current_timestep_s) * process_covariance_);
}
}
// Saves gyroscope event for future prediction.
current_state_.timestamp = sample.system_timestamp;
current_gyroscope_sensor_timestamp_ns_ = sample.sensor_timestamp_ns;
current_state_.sensor_from_start_rotation_velocity.Set(
sample.data[0] - gyroscope_bias_estimate_[0], sample.data[1] - gyroscope_bias_estimate_[1],
sample.data[2] - gyroscope_bias_estimate_[2]);
}
Vector3 SensorFusionEkf::ComputeInnovation(const Rotation& pose)
{
const Vector3 predicted_down_direction = pose * kCanonicalZDirection;
const Rotation rotation
= Rotation::RotateInto(predicted_down_direction, accelerometer_measurement_);
Vector3 axis;
double angle;
rotation.GetAxisAndAngle(&axis, &angle);
return axis * angle;
}
void SensorFusionEkf::ComputeMeasurementJacobian()
{
for (int dof = 0; dof < 3; dof++) {
Vector3 delta = Vector3::Zero();
delta[dof] = kFiniteDifferencingEpsilon;
const Rotation epsilon_rotation = RotationFromVector(delta);
const Vector3 delta_rotation
= ComputeInnovation(epsilon_rotation * current_state_.sensor_from_start_rotation);
const Vector3 col = (innovation_ - delta_rotation) / kFiniteDifferencingEpsilon;
accelerometer_measurement_jacobian_(0, dof) = col[0];
accelerometer_measurement_jacobian_(1, dof) = col[1];
accelerometer_measurement_jacobian_(2, dof) = col[2];
}
}
void SensorFusionEkf::ProcessAccelerometerSample(const AccelerometerData& sample)
{
// Discard outdated samples.
if (current_accelerometer_sensor_timestamp_ns_ >= sample.sensor_timestamp_ns) {
current_accelerometer_sensor_timestamp_ns_ = sample.sensor_timestamp_ns;
return;
}
// Call reset state if required.
if (execute_reset_with_next_accelerometer_sample_.exchange(false)) {
ResetState();
}
accelerometer_measurement_.Set(sample.data[0], sample.data[1], sample.data[2]);
current_accelerometer_sensor_timestamp_ns_ = sample.sensor_timestamp_ns;
if (bias_estimation_enabled_) {
gyroscope_bias_estimator_.ProcessAccelerometer(sample.data, sample.sensor_timestamp_ns);
}
if (!is_aligned_with_gravity_) {
// This is the first accelerometer measurement so it initializes the
// orientation estimate.
current_state_.sensor_from_start_rotation
= Rotation::RotateInto(kCanonicalZDirection, accelerometer_measurement_);
is_aligned_with_gravity_ = true;
previous_accelerometer_norm_ = Length(accelerometer_measurement_);
return;
}
UpdateMeasurementCovariance();
innovation_ = ComputeInnovation(current_state_.sensor_from_start_rotation);
ComputeMeasurementJacobian();
// S = H * P * H' + R
innovation_covariance_ = accelerometer_measurement_jacobian_ * state_covariance_
* Transpose(accelerometer_measurement_jacobian_)
+ accelerometer_measurement_covariance_;
// K = P * H' * S^-1
kalman_gain_ = state_covariance_ * Transpose(accelerometer_measurement_jacobian_)
* Inverse(innovation_covariance_);
// x_update = K*nu
state_update_ = kalman_gain_ * innovation_;
// P = (I - K * H) * P;
state_covariance_ = (Matrix3x3::Identity() - kalman_gain_ * accelerometer_measurement_jacobian_)
* state_covariance_;
// Updates pose and associate covariance matrix.
const Rotation rotation_from_state_update = RotationFromVector(state_update_);
current_state_.sensor_from_start_rotation
= rotation_from_state_update * current_state_.sensor_from_start_rotation;
UpdateStateCovariance(RotationMatrixNH(rotation_from_state_update));
}
void SensorFusionEkf::UpdateStateCovariance(const Matrix3x3& motion_update)
{
state_covariance_ = motion_update * state_covariance_ * Transpose(motion_update);
}
void SensorFusionEkf::FilterGyroscopeTimestep(double gyroscope_timestep_s)
{
if (!is_timestep_filter_initialized_) {
// Initializes the filter.
filtered_gyroscope_timestep_s_ = gyroscope_timestep_s;
num_gyroscope_timestep_samples_ = 1;
is_timestep_filter_initialized_ = true;
return;
}
// Computes the IIR filter response.
filtered_gyroscope_timestep_s_ = kTimestepFilterCoeff * filtered_gyroscope_timestep_s_
+ (1 - kTimestepFilterCoeff) * gyroscope_timestep_s;
++num_gyroscope_timestep_samples_;
if (num_gyroscope_timestep_samples_ > kTimestepFilterMinSamples) {
is_gyroscope_filter_valid_ = true;
}
}
void SensorFusionEkf::UpdateMeasurementCovariance()
{
const double current_accelerometer_norm = Length(accelerometer_measurement_);
// Norm change between current and previous accel readings.
const double current_accelerometer_norm_change
= std::abs(current_accelerometer_norm - previous_accelerometer_norm_);
previous_accelerometer_norm_ = current_accelerometer_norm;
moving_average_accelerometer_norm_change_ = kSmoothingFactor * current_accelerometer_norm_change
+ (1 - kSmoothingFactor) * moving_average_accelerometer_norm_change_;
// If we hit the accel norm change threshold, we use the maximum noise sigma
// for the accel covariance. For anything below that, we use a linear
// combination between min and max sigma values.
const double norm_change_ratio
= moving_average_accelerometer_norm_change_ / kMaxAccelNormChange;
const double accelerometer_noise_sigma = std::min(kMaxAccelNoiseSigma,
kMinAccelNoiseSigma + norm_change_ratio * (kMaxAccelNoiseSigma - kMinAccelNoiseSigma));
// Updates the accel covariance matrix with the new sigma value.
accelerometer_measurement_covariance_
= Matrix3x3::Identity() * accelerometer_noise_sigma * accelerometer_noise_sigma;
}
bool SensorFusionEkf::IsBiasEstimationEnabled() const { return bias_estimation_enabled_; }
void SensorFusionEkf::SetBiasEstimationEnabled(bool enable)
{
if (bias_estimation_enabled_ != enable) {
bias_estimation_enabled_ = enable;
gyroscope_bias_estimate_ = { 0, 0, 0 };
gyroscope_bias_estimator_.Reset();
}
}
} // namespace cardboard

View File

@ -0,0 +1,188 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_SENSORS_SENSOR_FUSION_EKF_H_
#define CARDBOARD_SDK_SENSORS_SENSOR_FUSION_EKF_H_
#include <array>
#include <atomic>
#include <cstdint>
#include "accelerometer_data.h"
#include "gyroscope_bias_estimator.h"
#include "gyroscope_data.h"
#include "pose_state.h"
#include "../util/matrix_3x3.h"
#include "../util/rotation.h"
#include "../util/vector.h"
namespace cardboard {
// Sensor fusion class that implements an Extended Kalman Filter (EKF) to
// estimate a 3D rotation from a gyroscope and an accelerometer.
// This system only has one state, the pose. It does not estimate any velocity
// or acceleration.
//
// To learn more about Kalman filtering one can read this article which is a
// good introduction: https://en.wikipedia.org/wiki/Kalman_filter
class SensorFusionEkf {
public:
SensorFusionEkf();
// Resets the state of the sensor fusion. It sets the velocity for
// prediction to zero. The reset will happen with the next
// accelerometer sample. Gyroscope sample will be discarded until a new
// accelerometer sample arrives.
void Reset();
// Gets the PoseState representing the latest pose and derivatives at a
// particular timestamp as estimated by SensorFusion.
PoseState GetLatestPoseState() const;
// Processes one gyroscope sample event. This updates the pose of the system
// and the prediction model. The gyroscope data is assumed to be in axis angle
// form. Angle = ||v|| and Axis = v / ||v||, with v = [v_x, v_y, v_z]^T.
//
// @param sample gyroscope sample data.
void ProcessGyroscopeSample(const GyroscopeData& sample);
// Processes one accelerometer sample event. This updates the pose of the
// system. If the Accelerometer norm changes too much between sample it is not
// trusted as much.
//
// @param sample accelerometer sample data.
void ProcessAccelerometerSample(const AccelerometerData& sample);
// Enables or disables the drift correction by estimating the gyroscope bias.
//
// @param enable Enable drift correction.
void SetBiasEstimationEnabled(bool enable);
// Returns a boolean that indicates if bias estimation is enabled or disabled.
//
// @return true if bias estimation is enabled, false otherwise.
bool IsBiasEstimationEnabled() const;
// Returns the current gyroscope bias estimate from GyroscopeBiasEstimator.
Vector3 GetGyroscopeBias() const {
return {
gyroscope_bias_estimate_[0], gyroscope_bias_estimate_[1], gyroscope_bias_estimate_[2]};
}
// Returns true after receiving the first accelerometer measurement.
bool IsFullyInitialized() const {
return is_aligned_with_gravity_;
}
private:
// Estimates the average timestep between gyroscope event.
void FilterGyroscopeTimestep(double gyroscope_timestep);
// Updates the state covariance with an incremental motion. It changes the
// space of the quadric.
void UpdateStateCovariance(const Matrix3x3& motion_update);
// Computes the innovation vector of the Kalman based on the input pose.
// It uses the latest measurement vector (i.e. accelerometer data), which must
// be set prior to calling this function.
Vector3 ComputeInnovation(const Rotation& pose);
// This computes the measurement_jacobian_ via numerical differentiation based
// on the current value of sensor_from_start_rotation_.
void ComputeMeasurementJacobian();
// Updates the accelerometer covariance matrix.
//
// This looks at the norm of recent accelerometer readings. If it has changed
// significantly, it means the phone receives additional acceleration than
// just gravity, and so the down vector information gravity signal is noisier.
void UpdateMeasurementCovariance();
// Reset all internal states. This is not thread safe. Lock should be acquired
// outside of it. This function is called in ProcessAccelerometerSample.
void ResetState();
// Current transformation from Sensor Space to Start Space.
// x_sensor = sensor_from_start_rotation_ * x_start;
PoseState current_state_;
// Filtering of the gyroscope timestep started?
bool is_timestep_filter_initialized_;
// Filtered gyroscope timestep valid?
bool is_gyroscope_filter_valid_;
// Sensor fusion currently aligned with gravity? After initialization
// it will requires a couple of accelerometer data for the system to get
// aligned.
std::atomic<bool> is_aligned_with_gravity_;
// Covariance of Kalman filter state (P in common formulation).
Matrix3x3 state_covariance_;
// Covariance of the process noise (Q in common formulation).
Matrix3x3 process_covariance_;
// Covariance of the accelerometer measurement (R in common formulation).
Matrix3x3 accelerometer_measurement_covariance_;
// Covariance of innovation (S in common formulation).
Matrix3x3 innovation_covariance_;
// Jacobian of the measurements (H in common formulation).
Matrix3x3 accelerometer_measurement_jacobian_;
// Gain of the Kalman filter (K in common formulation).
Matrix3x3 kalman_gain_;
// Parameter update a.k.a. innovation vector. (\nu in common formulation).
Vector3 innovation_;
// Measurement vector (z in common formulation).
Vector3 accelerometer_measurement_;
// Current prediction vector (g in common formulation).
Vector3 prediction_;
// Control input, currently this is only the gyroscope data (\mu in common
// formulation).
Vector3 control_input_;
// Update of the state vector. (x in common formulation).
Vector3 state_update_;
// Sensor time of the last gyroscope processed event.
uint64_t current_gyroscope_sensor_timestamp_ns_;
// Sensor time of the last accelerometer processed event.
uint64_t current_accelerometer_sensor_timestamp_ns_;
// Estimates of the timestep between gyroscope event in seconds.
double filtered_gyroscope_timestep_s_;
// Number of timestep samples processed so far by the filter.
uint32_t num_gyroscope_timestep_samples_;
// Norm of the accelerometer for the previous measurement.
double previous_accelerometer_norm_;
// Moving average of the accelerometer norm changes. It is computed for every
// sensor datum.
double moving_average_accelerometer_norm_change_;
// Flag indicating if a state reset should be executed with the next
// accelerometer sample.
std::atomic<bool> execute_reset_with_next_accelerometer_sample_;
// Flag indicating if bias estimation is enabled (enabled by default).
std::atomic<bool> bias_estimation_enabled_;
// Bias estimator and static device detector.
GyroscopeBiasEstimator gyroscope_bias_estimator_;
// Current bias estimate_;
Vector3 gyroscope_bias_estimate_;
SensorFusionEkf(const SensorFusionEkf&) = delete;
SensorFusionEkf& operator=(const SensorFusionEkf&) = delete;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_SENSORS_SENSOR_FUSION_EKF_H_

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_LOGGING_H_
#define CARDBOARD_SDK_UTIL_LOGGING_H_
#include <furi.h>
#include <furi_hal.h>
#if defined(__ANDROID__)
#include <android/log.h>
// Uncomment these to enable debug logging from native code
#define CARDBOARD_LOGI(...) // __android_log_print(ANDROID_LOG_INFO, "CardboardSDK", __VA_ARGS__)
#define CARDBOARD_LOGE(...) // __android_log_print(ANDROID_LOG_ERROR, "CardboardSDK", __VA_ARGS__)
#else
#define CARDBOARD_LOGI(...) // FURI_LOG_I("CardboardSDK", __VA_ARGS__)
#define CARDBOARD_LOGE(...) // FURI_LOG_E("CardboardSDK", __VA_ARGS__)
#endif
#endif // CARDBOARD_SDK_UTIL_LOGGING_H_

View File

@ -0,0 +1,121 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "matrix_3x3.h"
namespace cardboard {
Matrix3x3::Matrix3x3(double m00, double m01, double m02, double m10, double m11, double m12,
double m20, double m21, double m22)
: elem_ { { { m00, m01, m02 }, { m10, m11, m12 }, { m20, m21, m22 } } }
{
}
Matrix3x3::Matrix3x3()
{
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
elem_[row][col] = 0;
}
}
Matrix3x3 Matrix3x3::Zero()
{
Matrix3x3 result;
return result;
}
Matrix3x3 Matrix3x3::Identity()
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
result.elem_[row][row] = 1;
}
return result;
}
void Matrix3x3::MultiplyScalar(double s)
{
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
elem_[row][col] *= s;
}
}
Matrix3x3 Matrix3x3::Negation() const
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result.elem_[row][col] = -elem_[row][col];
}
return result;
}
Matrix3x3 Matrix3x3::Scale(const Matrix3x3& m, double s)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result.elem_[row][col] = m.elem_[row][col] * s;
}
return result;
}
Matrix3x3 Matrix3x3::Addition(const Matrix3x3& lhs, const Matrix3x3& rhs)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result.elem_[row][col] = lhs.elem_[row][col] + rhs.elem_[row][col];
}
return result;
}
Matrix3x3 Matrix3x3::Subtraction(const Matrix3x3& lhs, const Matrix3x3& rhs)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result.elem_[row][col] = lhs.elem_[row][col] - rhs.elem_[row][col];
}
return result;
}
Matrix3x3 Matrix3x3::Product(const Matrix3x3& m0, const Matrix3x3& m1)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
result.elem_[row][col] = 0;
for (int i = 0; i < 3; ++i)
result.elem_[row][col] += m0.elem_[row][i] * m1.elem_[i][col];
}
}
return result;
}
bool Matrix3x3::AreEqual(const Matrix3x3& m0, const Matrix3x3& m1)
{
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
if (m0.elem_[row][col] != m1.elem_[row][col])
return false;
}
}
return true;
}
} // namespace cardboard

View File

@ -0,0 +1,138 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_MATRIX_3X3_H_
#define CARDBOARD_SDK_UTIL_MATRIX_3X3_H_
#include <array>
#include <cstring> // For memcpy().
#include <istream> // NOLINT
#include <ostream> // NOLINT
namespace cardboard {
// The Matrix3x3 class defines a square 3-dimensional matrix. Elements are
// stored in row-major order.
// TODO(b/135461889): Make this class consistent with Matrix4x4.
class Matrix3x3 {
public:
// The default constructor zero-initializes all elements.
Matrix3x3();
// Dimension-specific constructors that are passed individual element values.
Matrix3x3(
double m00,
double m01,
double m02,
double m10,
double m11,
double m12,
double m20,
double m21,
double m22);
// Constructor that reads elements from a linear array of the correct size.
explicit Matrix3x3(const double array[3 * 3]);
// Returns a Matrix3x3 containing all zeroes.
static Matrix3x3 Zero();
// Returns an identity Matrix3x3.
static Matrix3x3 Identity();
// Mutable element accessors.
double& operator()(int row, int col) {
return elem_[row][col];
}
std::array<double, 3>& operator[](int row) {
return elem_[row];
}
// Read-only element accessors.
const double& operator()(int row, int col) const {
return elem_[row][col];
}
const std::array<double, 3>& operator[](int row) const {
return elem_[row];
}
// Return a pointer to the data for interfacing with libraries.
double* Data() {
return &elem_[0][0];
}
const double* Data() const {
return &elem_[0][0];
}
// Self-modifying multiplication operators.
void operator*=(double s) {
MultiplyScalar(s);
}
void operator*=(const Matrix3x3& m) {
*this = Product(*this, m);
}
// Unary operators.
Matrix3x3 operator-() const {
return Negation();
}
// Binary scale operators.
friend Matrix3x3 operator*(const Matrix3x3& m, double s) {
return Scale(m, s);
}
friend Matrix3x3 operator*(double s, const Matrix3x3& m) {
return Scale(m, s);
}
// Binary matrix addition.
friend Matrix3x3 operator+(const Matrix3x3& lhs, const Matrix3x3& rhs) {
return Addition(lhs, rhs);
}
// Binary matrix subtraction.
friend Matrix3x3 operator-(const Matrix3x3& lhs, const Matrix3x3& rhs) {
return Subtraction(lhs, rhs);
}
// Binary multiplication operator.
friend Matrix3x3 operator*(const Matrix3x3& m0, const Matrix3x3& m1) {
return Product(m0, m1);
}
// Exact equality and inequality comparisons.
friend bool operator==(const Matrix3x3& m0, const Matrix3x3& m1) {
return AreEqual(m0, m1);
}
friend bool operator!=(const Matrix3x3& m0, const Matrix3x3& m1) {
return !AreEqual(m0, m1);
}
private:
// These private functions implement most of the operators.
void MultiplyScalar(double s);
Matrix3x3 Negation() const;
static Matrix3x3 Addition(const Matrix3x3& lhs, const Matrix3x3& rhs);
static Matrix3x3 Subtraction(const Matrix3x3& lhs, const Matrix3x3& rhs);
static Matrix3x3 Scale(const Matrix3x3& m, double s);
static Matrix3x3 Product(const Matrix3x3& m0, const Matrix3x3& m1);
static bool AreEqual(const Matrix3x3& m0, const Matrix3x3& m1);
std::array<std::array<double, 3>, 3> elem_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_MATRIX_3X3_H_

View File

@ -0,0 +1,87 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "matrix_4x4.h"
#include <algorithm>
#include <cmath>
#include <cstring>
namespace cardboard {
Matrix4x4 Matrix4x4::Identity()
{
Matrix4x4 ret;
for (int j = 0; j < 4; ++j) {
for (int i = 0; i < 4; ++i) {
ret.m[j][i] = (i == j) ? 1 : 0;
}
}
return ret;
}
Matrix4x4 Matrix4x4::Zeros()
{
Matrix4x4 ret;
for (int j = 0; j < 4; ++j) {
for (int i = 0; i < 4; ++i) {
ret.m[j][i] = 0;
}
}
return ret;
}
Matrix4x4 Matrix4x4::Translation(float x, float y, float z)
{
Matrix4x4 ret = Matrix4x4::Identity();
ret.m[3][0] = x;
ret.m[3][1] = y;
ret.m[3][2] = z;
return ret;
}
Matrix4x4 Matrix4x4::Perspective(const std::array<float, 4>& fov, float zNear, float zFar)
{
Matrix4x4 ret = Matrix4x4::Zeros();
const float xLeft = -std::tan(fov[0] * M_PI / 180.0f) * zNear;
const float xRight = std::tan(fov[1] * M_PI / 180.0f) * zNear;
const float yBottom = -std::tan(fov[2] * M_PI / 180.0f) * zNear;
const float yTop = std::tan(fov[3] * M_PI / 180.0f) * zNear;
const float X = (2 * zNear) / (xRight - xLeft);
const float Y = (2 * zNear) / (yTop - yBottom);
const float A = (xRight + xLeft) / (xRight - xLeft);
const float B = (yTop + yBottom) / (yTop - yBottom);
const float C = (zNear + zFar) / (zNear - zFar);
const float D = (2 * zNear * zFar) / (zNear - zFar);
ret.m[0][0] = X;
ret.m[2][0] = A;
ret.m[1][1] = Y;
ret.m[2][1] = B;
ret.m[2][2] = C;
ret.m[3][2] = D;
ret.m[2][3] = -1;
return ret;
}
void Matrix4x4::ToArray(float* array) const { std::memcpy(array, &m[0][0], 16 * sizeof(float)); }
} // namespace cardboard

View File

@ -0,0 +1,37 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_MATRIX_4X4_H_
#define CARDBOARD_SDK_UTIL_MATRIX_4X4_H_
#include <array>
namespace cardboard {
class Matrix4x4 {
public:
static Matrix4x4 Identity();
static Matrix4x4 Zeros();
static Matrix4x4 Translation(float x, float y, float z);
static Matrix4x4 Perspective(const std::array<float, 4>& fov, float zNear, float zFar);
void ToArray(float* array) const;
private:
std::array<std::array<float, 4>, 4> m;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_MATRIX4X4_H_

View File

@ -0,0 +1,148 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "matrixutils.h"
#include "vectorutils.h"
namespace cardboard {
namespace {
// Returns true if the cofactor for a given row and column should be negated.
static bool IsCofactorNegated(int row, int col)
{
// Negated iff (row + col) is odd.
return ((row + col) & 1) != 0;
}
static double CofactorElement3(const Matrix3x3& m, int row, int col)
{
static const int index[3][2] = { { 1, 2 }, { 0, 2 }, { 0, 1 } };
const int i0 = index[row][0];
const int i1 = index[row][1];
const int j0 = index[col][0];
const int j1 = index[col][1];
const double cofactor = m(i0, j0) * m(i1, j1) - m(i0, j1) * m(i1, j0);
return IsCofactorNegated(row, col) ? -cofactor : cofactor;
}
// Multiplies a matrix and some type of column vector to
// produce another column vector of the same type.
Vector3 MultiplyMatrixAndVector(const Matrix3x3& m, const Vector3& v)
{
Vector3 result = Vector3::Zero();
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result[row] += m(row, col) * v[col];
}
return result;
}
// Sets the upper 3x3 of a Matrix to represent a 3D rotation.
void RotationMatrix3x3(const Rotation& r, Matrix3x3* matrix)
{
//
// Given a quaternion (a,b,c,d) where d is the scalar part, the 3x3 rotation
// matrix is:
//
// a^2 - b^2 - c^2 + d^2 2ab - 2cd 2ac + 2bd
// 2ab + 2cd -a^2 + b^2 - c^2 + d^2 2bc - 2ad
// 2ac - 2bd 2bc + 2ad -a^2 - b^2 + c^2 + d^2
//
const Vector<4>& quat = r.GetQuaternion();
const double aa = quat[0] * quat[0];
const double bb = quat[1] * quat[1];
const double cc = quat[2] * quat[2];
const double dd = quat[3] * quat[3];
const double ab = quat[0] * quat[1];
const double ac = quat[0] * quat[2];
const double bc = quat[1] * quat[2];
const double ad = quat[0] * quat[3];
const double bd = quat[1] * quat[3];
const double cd = quat[2] * quat[3];
Matrix3x3& m = *matrix;
m[0][0] = aa - bb - cc + dd;
m[0][1] = 2 * ab - 2 * cd;
m[0][2] = 2 * ac + 2 * bd;
m[1][0] = 2 * ab + 2 * cd;
m[1][1] = -aa + bb - cc + dd;
m[1][2] = 2 * bc - 2 * ad;
m[2][0] = 2 * ac - 2 * bd;
m[2][1] = 2 * bc + 2 * ad;
m[2][2] = -aa - bb + cc + dd;
}
} // anonymous namespace
Vector3 operator*(const Matrix3x3& m, const Vector3& v) { return MultiplyMatrixAndVector(m, v); }
Matrix3x3 CofactorMatrix(const Matrix3x3& m)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result(row, col) = CofactorElement3(m, row, col);
}
return result;
}
Matrix3x3 AdjugateWithDeterminant(const Matrix3x3& m, double* determinant)
{
const Matrix3x3 cofactor_matrix = CofactorMatrix(m);
if (determinant) {
*determinant = m(0, 0) * cofactor_matrix(0, 0) + m(0, 1) * cofactor_matrix(0, 1)
+ m(0, 2) * cofactor_matrix(0, 2);
}
return Transpose(cofactor_matrix);
}
// Returns the transpose of a matrix.
Matrix3x3 Transpose(const Matrix3x3& m)
{
Matrix3x3 result;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col)
result(row, col) = m(col, row);
}
return result;
}
Matrix3x3 InverseWithDeterminant(const Matrix3x3& m, double* determinant)
{
// The inverse is the adjugate divided by the determinant.
double det;
Matrix3x3 adjugate = AdjugateWithDeterminant(m, &det);
if (determinant)
*determinant = det;
if (det == 0)
return Matrix3x3::Zero();
else
return adjugate * (1 / det);
}
Matrix3x3 Inverse(const Matrix3x3& m) { return InverseWithDeterminant(m, nullptr); }
Matrix3x3 RotationMatrixNH(const Rotation& r)
{
Matrix3x3 m;
RotationMatrix3x3(r, &m);
return m;
}
} // namespace cardboard

View File

@ -0,0 +1,65 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_MATRIXUTILS_H_
#define CARDBOARD_SDK_UTIL_MATRIXUTILS_H_
//
// This file contains operators and free functions that define generic Matrix
// operations.
//
#include "matrix_3x3.h"
#include "rotation.h"
#include "vector.h"
namespace cardboard {
// Returns the transpose of a matrix.
Matrix3x3 Transpose(const Matrix3x3& m);
// Multiplies a Matrix and a column Vector of the same Dimension to produce
// another column Vector.
Vector3 operator*(const Matrix3x3& m, const Vector3& v);
// Returns the determinant of the matrix. This function is defined for all the
// typedef'ed Matrix types.
double Determinant(const Matrix3x3& m);
// Returns the adjugate of the matrix, which is defined as the transpose of the
// cofactor matrix. This function is defined for all the typedef'ed Matrix
// types. The determinant of the matrix is computed as a side effect, so it is
// returned in the determinant parameter if it is not null.
Matrix3x3 AdjugateWithDeterminant(const Matrix3x3& m, double* determinant);
// Returns the inverse of the matrix. This function is defined for all the
// typedef'ed Matrix types. The determinant of the matrix is computed as a
// side effect, so it is returned in the determinant parameter if it is not
// null. If the determinant is 0, the returned matrix has all zeroes.
Matrix3x3 InverseWithDeterminant(const Matrix3x3& m, double* determinant);
// Returns the inverse of the matrix. This function is defined for all the
// typedef'ed Matrix types. If the determinant of the matrix is 0, the returned
// matrix has all zeroes.
Matrix3x3 Inverse(const Matrix3x3& m);
// Returns a 3x3 Matrix representing a 3D rotation. This creates a Matrix that
// does not work with homogeneous coordinates, so the function name ends in
// "NH".
Matrix3x3 RotationMatrixNH(const Rotation& r);
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_MATRIXUTILS_H_

View File

@ -0,0 +1,117 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "rotation.h"
#include <cmath>
#include <limits>
#include "vectorutils.h"
namespace cardboard {
void Rotation::SetAxisAndAngle(const VectorType& axis, double angle)
{
VectorType unit_axis = axis;
if (!Normalize(&unit_axis)) {
*this = Identity();
} else {
double a = angle / 2;
const double s = sin(a);
const VectorType v(unit_axis * s);
SetQuaternion(QuaternionType(v[0], v[1], v[2], cos(a)));
}
}
Rotation Rotation::FromRotationMatrix(const Matrix3x3& mat)
{
static const double kOne = 1.0;
static const double kFour = 4.0;
const double d0 = mat(0, 0), d1 = mat(1, 1), d2 = mat(2, 2);
const double ww = kOne + d0 + d1 + d2;
const double xx = kOne + d0 - d1 - d2;
const double yy = kOne - d0 + d1 - d2;
const double zz = kOne - d0 - d1 + d2;
const double max = std::max(ww, std::max(xx, std::max(yy, zz)));
if (ww == max) {
const double w4 = sqrt(ww * kFour);
return Rotation::FromQuaternion(QuaternionType((mat(2, 1) - mat(1, 2)) / w4,
(mat(0, 2) - mat(2, 0)) / w4, (mat(1, 0) - mat(0, 1)) / w4, w4 / kFour));
}
if (xx == max) {
const double x4 = sqrt(xx * kFour);
return Rotation::FromQuaternion(QuaternionType(x4 / kFour, (mat(0, 1) + mat(1, 0)) / x4,
(mat(0, 2) + mat(2, 0)) / x4, (mat(2, 1) - mat(1, 2)) / x4));
}
if (yy == max) {
const double y4 = sqrt(yy * kFour);
return Rotation::FromQuaternion(QuaternionType((mat(0, 1) + mat(1, 0)) / y4, y4 / kFour,
(mat(1, 2) + mat(2, 1)) / y4, (mat(0, 2) - mat(2, 0)) / y4));
}
// zz is the largest component.
const double z4 = sqrt(zz * kFour);
return Rotation::FromQuaternion(QuaternionType((mat(0, 2) + mat(2, 0)) / z4,
(mat(1, 2) + mat(2, 1)) / z4, z4 / kFour, (mat(1, 0) - mat(0, 1)) / z4));
}
void Rotation::GetAxisAndAngle(VectorType* axis, double* angle) const
{
VectorType vec(quat_[0], quat_[1], quat_[2]);
if (Normalize(&vec)) {
*angle = 2 * acos(quat_[3]);
*axis = vec;
} else {
*axis = VectorType(1, 0, 0);
*angle = 0.0;
}
}
Rotation Rotation::RotateInto(const VectorType& from, const VectorType& to)
{
static const double kTolerance = std::numeric_limits<double>::epsilon() * 100;
// Directly build the quaternion using the following technique:
// http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final
const double norm_u_norm_v = sqrt(LengthSquared(from) * LengthSquared(to));
double real_part = norm_u_norm_v + Dot(from, to);
VectorType w;
if (real_part < kTolerance * norm_u_norm_v) {
// If |from| and |to| are exactly opposite, rotate 180 degrees around an
// arbitrary orthogonal axis. Axis normalization can happen later, when we
// normalize the quaternion.
real_part = 0.0;
w = (abs(from[0]) > abs(from[2])) ? VectorType(-from[1], from[0], 0)
: VectorType(0, -from[2], from[1]);
} else {
// Otherwise, build the quaternion the standard way.
w = Cross(from, to);
}
// Build and return a normalized quaternion.
// Note that Rotation::FromQuaternion automatically performs normalization.
return Rotation::FromQuaternion(QuaternionType(w[0], w[1], w[2], real_part));
}
Rotation::VectorType Rotation::operator*(const Rotation::VectorType& v) const
{
return ApplyToVector(v);
}
} // namespace cardboard

View File

@ -0,0 +1,156 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_ROTATION_H_
#define CARDBOARD_SDK_UTIL_ROTATION_H_
#include "matrix_3x3.h"
#include "vector.h"
#include "vectorutils.h"
namespace cardboard {
// The Rotation class represents a rotation around a 3-dimensional axis. It
// uses normalized quaternions internally to make the math robust.
class Rotation {
public:
// Convenience typedefs for vector of the correct type.
typedef Vector<3> VectorType;
typedef Vector<4> QuaternionType;
// The default constructor creates an identity Rotation, which has no effect.
Rotation() {
quat_.Set(0, 0, 0, 1);
}
// Returns an identity Rotation, which has no effect.
static Rotation Identity() {
return Rotation();
}
// Sets the Rotation from a quaternion (4D vector), which is first normalized.
void SetQuaternion(const QuaternionType& quaternion) {
quat_ = Normalized(quaternion);
}
// Returns the Rotation as a normalized quaternion (4D vector).
const QuaternionType& GetQuaternion() const {
return quat_;
}
// Sets the Rotation to rotate by the given angle around the given axis,
// following the right-hand rule. The axis does not need to be unit
// length. If it is zero length, this results in an identity Rotation.
void SetAxisAndAngle(const VectorType& axis, double angle);
// Returns the right-hand rule axis and angle corresponding to the
// Rotation. If the Rotation is the identity rotation, this returns the +X
// axis and an angle of 0.
void GetAxisAndAngle(VectorType* axis, double* angle) const;
// Convenience function that constructs and returns a Rotation given an axis
// and angle.
static Rotation FromAxisAndAngle(const VectorType& axis, double angle) {
Rotation r;
r.SetAxisAndAngle(axis, angle);
return r;
}
// Convenience function that constructs and returns a Rotation given a
// quaternion.
static Rotation FromQuaternion(const QuaternionType& quat) {
Rotation r;
r.SetQuaternion(quat);
return r;
}
// Convenience function that constructs and returns a Rotation given a
// rotation matrix R with $R^\top R = I && det(R) = 1$.
static Rotation FromRotationMatrix(const Matrix3x3& mat);
// Convenience function that constructs and returns a Rotation given Euler
// angles that are applied in the order of rotate-Z by roll, rotate-X by
// pitch, rotate-Y by yaw (same as GetRollPitchYaw).
static Rotation FromRollPitchYaw(double roll, double pitch, double yaw) {
VectorType x(1, 0, 0), y(0, 1, 0), z(0, 0, 1);
return FromAxisAndAngle(z, roll) * (FromAxisAndAngle(x, pitch) * FromAxisAndAngle(y, yaw));
}
// Convenience function that constructs and returns a Rotation given Euler
// angles that are applied in the order of rotate-Y by yaw, rotate-X by
// pitch, rotate-Z by roll (same as GetYawPitchRoll).
static Rotation FromYawPitchRoll(double yaw, double pitch, double roll) {
VectorType x(1, 0, 0), y(0, 1, 0), z(0, 0, 1);
return FromAxisAndAngle(y, yaw) * (FromAxisAndAngle(x, pitch) * FromAxisAndAngle(z, roll));
}
// Constructs and returns a Rotation that rotates one vector to another along
// the shortest arc. This returns an identity rotation if either vector has
// zero length.
static Rotation RotateInto(const VectorType& from, const VectorType& to);
// The negation operator returns the inverse rotation.
friend Rotation operator-(const Rotation& r) {
// Because we store normalized quaternions, the inverse is found by
// negating the vector part.
return Rotation(-r.quat_[0], -r.quat_[1], -r.quat_[2], r.quat_[3]);
}
// Appends a rotation to this one.
Rotation& operator*=(const Rotation& r) {
const QuaternionType& qr = r.quat_;
QuaternionType& qt = quat_;
SetQuaternion(QuaternionType(
qr[3] * qt[0] + qr[0] * qt[3] + qr[2] * qt[1] - qr[1] * qt[2],
qr[3] * qt[1] + qr[1] * qt[3] + qr[0] * qt[2] - qr[2] * qt[0],
qr[3] * qt[2] + qr[2] * qt[3] + qr[1] * qt[0] - qr[0] * qt[1],
qr[3] * qt[3] - qr[0] * qt[0] - qr[1] * qt[1] - qr[2] * qt[2]));
return *this;
}
// Binary multiplication operator - returns a composite Rotation.
friend const Rotation operator*(const Rotation& r0, const Rotation& r1) {
Rotation r = r0;
r *= r1;
return r;
}
// Multiply a Rotation and a Vector to get a Vector.
VectorType operator*(const VectorType& v) const;
private:
// Private constructor that builds a Rotation from quaternion components.
Rotation(double q0, double q1, double q2, double q3)
: quat_(q0, q1, q2, q3) {
}
// Applies a Rotation to a Vector to rotate the Vector. Method borrowed from:
// http://blog.molecular-matters.com/2013/05/24/a-faster-quaternion-vector-multiplication/
VectorType ApplyToVector(const VectorType& v) const {
VectorType im(quat_[0], quat_[1], quat_[2]);
VectorType temp = 2.0 * Cross(im, v);
return v + quat_[3] * temp + Cross(im, temp);
}
// The rotation represented as a normalized quaternion. (Unit quaternions are
// required for constructing rotation matrices, so it makes sense to always
// store them that way.) The vector part is in the first 3 elements, and the
// scalar part is in the last element.
QuaternionType quat_;
};
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_ROTATION_H_

View File

@ -0,0 +1,251 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_VECTOR_H_
#define CARDBOARD_SDK_UTIL_VECTOR_H_
#include <array>
namespace cardboard {
// Geometric N-dimensional Vector class.
template <int Dimension>
class Vector {
public:
// The default constructor zero-initializes all elements.
Vector();
// Dimension-specific constructors that are passed individual element values.
constexpr Vector(double e0, double e1, double e2);
constexpr Vector(double e0, double e1, double e2, double e3);
// Constructor for a Vector of dimension N from a Vector of dimension N-1 and
// a scalar of the correct type, assuming N is at least 2.
// constexpr Vector(const Vector<Dimension - 1>& v, double s);
void Set(double e0, double e1, double e2); // Only when Dimension == 3.
void Set(double e0, double e1, double e2,
double e3); // Only when Dimension == 4.
// Mutable element accessor.
double& operator[](int index) {
return elem_[index];
}
// Element accessor.
double operator[](int index) const {
return elem_[index];
}
// Returns a Vector containing all zeroes.
static Vector Zero();
// Self-modifying operators.
void operator+=(const Vector& v) {
Add(v);
}
void operator-=(const Vector& v) {
Subtract(v);
}
void operator*=(double s) {
Multiply(s);
}
void operator/=(double s) {
Divide(s);
}
// Unary negation operator.
Vector operator-() const {
return Negation();
}
// Binary operators.
friend Vector operator+(const Vector& v0, const Vector& v1) {
return Sum(v0, v1);
}
friend Vector operator-(const Vector& v0, const Vector& v1) {
return Difference(v0, v1);
}
friend Vector operator*(const Vector& v, double s) {
return Scale(v, s);
}
friend Vector operator*(double s, const Vector& v) {
return Scale(v, s);
}
friend Vector operator*(const Vector& v, const Vector& s) {
return Product(v, s);
}
friend Vector operator/(const Vector& v, double s) {
return Divide(v, s);
}
// Self-modifying addition.
void Add(const Vector& v);
// Self-modifying subtraction.
void Subtract(const Vector& v);
// Self-modifying multiplication by a scalar.
void Multiply(double s);
// Self-modifying division by a scalar.
void Divide(double s);
// Unary negation.
Vector Negation() const;
// Binary component-wise multiplication.
static Vector Product(const Vector& v0, const Vector& v1);
// Binary component-wise addition.
static Vector Sum(const Vector& v0, const Vector& v1);
// Binary component-wise subtraction.
static Vector Difference(const Vector& v0, const Vector& v1);
// Binary multiplication by a scalar.
static Vector Scale(const Vector& v, double s);
// Binary division by a scalar.
static Vector Divide(const Vector& v, double s);
private:
std::array<double, Dimension> elem_;
};
//------------------------------------------------------------------------------
template <int Dimension>
Vector<Dimension>::Vector() {
for(int i = 0; i < Dimension; i++) {
elem_[i] = 0;
}
}
template <int Dimension>
constexpr Vector<Dimension>::Vector(double e0, double e1, double e2)
: elem_{e0, e1, e2} {
}
template <int Dimension>
constexpr Vector<Dimension>::Vector(double e0, double e1, double e2, double e3)
: elem_{e0, e1, e2, e3} {
}
/*
template <>
constexpr Vector<4>::Vector(const Vector<3>& v, double s)
: elem_{v[0], v[1], v[2], s} {}
*/
template <int Dimension>
void Vector<Dimension>::Set(double e0, double e1, double e2) {
elem_[0] = e0;
elem_[1] = e1;
elem_[2] = e2;
}
template <int Dimension>
void Vector<Dimension>::Set(double e0, double e1, double e2, double e3) {
elem_[0] = e0;
elem_[1] = e1;
elem_[2] = e2;
elem_[3] = e3;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Zero() {
Vector<Dimension> v;
return v;
}
template <int Dimension>
void Vector<Dimension>::Add(const Vector& v) {
for(int i = 0; i < Dimension; i++) {
elem_[i] += v[i];
}
}
template <int Dimension>
void Vector<Dimension>::Subtract(const Vector& v) {
for(int i = 0; i < Dimension; i++) {
elem_[i] -= v[i];
}
}
template <int Dimension>
void Vector<Dimension>::Multiply(double s) {
for(int i = 0; i < Dimension; i++) {
elem_[i] *= s;
}
}
template <int Dimension>
void Vector<Dimension>::Divide(double s) {
for(int i = 0; i < Dimension; i++) {
elem_[i] /= s;
}
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Negation() const {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = -elem_[i];
}
return ret;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Product(const Vector& v0, const Vector& v1) {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = v0[i] * v1[i];
}
return ret;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Sum(const Vector& v0, const Vector& v1) {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = v0[i] + v1[i];
}
return ret;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Difference(const Vector& v0, const Vector& v1) {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = v0[i] - v1[i];
}
return ret;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Scale(const Vector& v, double s) {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = v[i] * s;
}
return ret;
}
template <int Dimension>
Vector<Dimension> Vector<Dimension>::Divide(const Vector& v, double s) {
Vector<Dimension> ret;
for(int i = 0; i < Dimension; i++) {
ret.elem_[i] = v[i] / s;
}
return ret;
}
typedef Vector<3> Vector3;
typedef Vector<4> Vector4;
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_VECTOR_H_

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#include "vectorutils.h"
namespace cardboard {
// Returns the dot (inner) product of two Vectors.
double Dot(const Vector<3>& v0, const Vector<3>& v1)
{
return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
}
// Returns the dot (inner) product of two Vectors.
double Dot(const Vector<4>& v0, const Vector<4>& v1)
{
return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2] + v0[3] * v1[3];
}
// Returns the 3-dimensional cross product of 2 Vectors. Note that this is
// defined only for 3-dimensional Vectors.
Vector<3> Cross(const Vector<3>& v0, const Vector<3>& v1)
{
return Vector<3>(v0[1] * v1[2] - v0[2] * v1[1], v0[2] * v1[0] - v0[0] * v1[2],
v0[0] * v1[1] - v0[1] * v1[0]);
}
} // namespace cardboard

View File

@ -0,0 +1,76 @@
/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* 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.
*/
#ifndef CARDBOARD_SDK_UTIL_VECTORUTILS_H_
#define CARDBOARD_SDK_UTIL_VECTORUTILS_H_
//
// This file contains free functions that operate on Vector instances.
//
#include <cmath>
#include "vector.h"
namespace cardboard {
// Returns the dot (inner) product of two Vectors.
double Dot(const Vector<3>& v0, const Vector<3>& v1);
// Returns the dot (inner) product of two Vectors.
double Dot(const Vector<4>& v0, const Vector<4>& v1);
// Returns the 3-dimensional cross product of 2 Vectors. Note that this is
// defined only for 3-dimensional Vectors.
Vector<3> Cross(const Vector<3>& v0, const Vector<3>& v1);
// Returns the square of the length of a Vector.
template <int Dimension>
double LengthSquared(const Vector<Dimension>& v) {
return Dot(v, v);
}
// Returns the geometric length of a Vector.
template <int Dimension>
double Length(const Vector<Dimension>& v) {
return sqrt(LengthSquared(v));
}
// the Vector untouched and returns false.
template <int Dimension>
bool Normalize(Vector<Dimension>* v) {
const double len = Length(*v);
if(len == 0) {
return false;
} else {
(*v) /= len;
return true;
}
}
// Returns a unit-length version of a Vector. If the given Vector has no
// length, this returns a Zero() Vector.
template <int Dimension>
Vector<Dimension> Normalized(const Vector<Dimension>& v) {
Vector<Dimension> result = v;
if(Normalize(&result))
return result;
else
return Vector<Dimension>::Zero();
}
} // namespace cardboard
#endif // CARDBOARD_SDK_UTIL_VECTORUTILS_H_

View File

@ -0,0 +1,310 @@
#include "bt_mouse.h"
#include "../tracking/main_loop.h"
#include <furi.h>
#include <furi_hal_bt.h>
#include <furi_hal_bt_hid.h>
#include <furi_hal_usb_hid.h>
#include <bt/bt_service/bt.h>
#include <gui/elements.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
typedef struct ButtonEvent {
int8_t button;
bool state;
} ButtonEvent;
#define BTN_EVT_QUEUE_SIZE 32
struct BtMouse {
View* view;
ViewDispatcher* view_dispatcher;
Bt* bt;
NotificationApp* notifications;
FuriMutex* mutex;
FuriThread* thread;
bool connected;
// Current mouse state
uint8_t btn;
int dx;
int dy;
int wheel;
// Circular buffer;
// (qhead == qtail) means either empty or overflow.
// We'll ignore overflow and treat it as empty.
int qhead;
int qtail;
ButtonEvent queue[BTN_EVT_QUEUE_SIZE];
};
#define BT_MOUSE_FLAG_INPUT_EVENT (1UL << 0)
#define BT_MOUSE_FLAG_KILL_THREAD (1UL << 1)
#define BT_MOUSE_FLAG_ALL (BT_MOUSE_FLAG_INPUT_EVENT | BT_MOUSE_FLAG_KILL_THREAD)
#define MOUSE_SCROLL 2
static void bt_mouse_notify_event(BtMouse* bt_mouse) {
FuriThreadId thread_id = furi_thread_get_id(bt_mouse->thread);
furi_assert(thread_id);
furi_thread_flags_set(thread_id, BT_MOUSE_FLAG_INPUT_EVENT);
}
static void bt_mouse_draw_callback(Canvas* canvas, void* context) {
UNUSED(context);
canvas_clear(canvas);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, 10, "Bluetooth Mouse mode");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 0, 63, "Hold [back] to exit");
}
static void bt_mouse_button_state(BtMouse* bt_mouse, int8_t button, bool state) {
ButtonEvent event;
event.button = button;
event.state = state;
if(bt_mouse->connected) {
furi_mutex_acquire(bt_mouse->mutex, FuriWaitForever);
bt_mouse->queue[bt_mouse->qtail++] = event;
bt_mouse->qtail %= BTN_EVT_QUEUE_SIZE;
furi_mutex_release(bt_mouse->mutex);
bt_mouse_notify_event(bt_mouse);
}
}
static void bt_mouse_process(BtMouse* bt_mouse, InputEvent* event) {
with_view_model(
bt_mouse->view,
void* model,
{
UNUSED(model);
if(event->key == InputKeyUp) {
if(event->type == InputTypePress) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_LEFT, true);
} else if(event->type == InputTypeRelease) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_LEFT, false);
}
} else if(event->key == InputKeyDown) {
if(event->type == InputTypePress) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_RIGHT, true);
} else if(event->type == InputTypeRelease) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_RIGHT, false);
}
} else if(event->key == InputKeyOk) {
if(event->type == InputTypePress) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_WHEEL, true);
} else if(event->type == InputTypeRelease) {
bt_mouse_button_state(bt_mouse, HID_MOUSE_BTN_WHEEL, false);
}
} else if(event->key == InputKeyRight) {
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
bt_mouse->wheel = MOUSE_SCROLL;
}
} else if(event->key == InputKeyLeft) {
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
bt_mouse->wheel = -MOUSE_SCROLL;
}
}
},
true);
}
static bool bt_mouse_input_callback(InputEvent* event, void* context) {
furi_assert(context);
BtMouse* bt_mouse = context;
bool consumed = false;
if(event->type == InputTypeLong && event->key == InputKeyBack) {
furi_hal_bt_hid_mouse_release_all();
} else {
bt_mouse_process(bt_mouse, event);
consumed = true;
}
return consumed;
}
void bt_mouse_connection_status_changed_callback(BtStatus status, void* context) {
furi_assert(context);
BtMouse* bt_mouse = context;
bt_mouse->connected = (status == BtStatusConnected);
if(bt_mouse->connected) {
notification_internal_message(bt_mouse->notifications, &sequence_set_blue_255);
tracking_begin();
view_dispatcher_send_custom_event(bt_mouse->view_dispatcher, 0);
} else {
tracking_end();
notification_internal_message(bt_mouse->notifications, &sequence_reset_blue);
}
//with_view_model(
// bt_mouse->view, void * model, { model->connected = connected; }, true);
}
bool bt_mouse_move(int8_t dx, int8_t dy, void* context) {
furi_assert(context);
BtMouse* bt_mouse = context;
if(bt_mouse->connected) {
furi_mutex_acquire(bt_mouse->mutex, FuriWaitForever);
bt_mouse->dx += dx;
bt_mouse->dy += dy;
furi_mutex_release(bt_mouse->mutex);
bt_mouse_notify_event(bt_mouse);
}
return true;
}
void bt_mouse_enter_callback(void* context) {
furi_assert(context);
BtMouse* bt_mouse = context;
bt_mouse->bt = furi_record_open(RECORD_BT);
bt_mouse->notifications = furi_record_open(RECORD_NOTIFICATION);
bt_set_status_changed_callback(
bt_mouse->bt, bt_mouse_connection_status_changed_callback, bt_mouse);
furi_assert(bt_set_profile(bt_mouse->bt, BtProfileHidKeyboard));
furi_hal_bt_start_advertising();
}
bool bt_mouse_custom_callback(uint32_t event, void* context) {
UNUSED(event);
furi_assert(context);
BtMouse* bt_mouse = context;
tracking_step(bt_mouse_move, context);
furi_delay_ms(3); // Magic! Removing this will break the buttons
view_dispatcher_send_custom_event(bt_mouse->view_dispatcher, 0);
return true;
}
void bt_mouse_exit_callback(void* context) {
furi_assert(context);
BtMouse* bt_mouse = context;
tracking_end();
notification_internal_message(bt_mouse->notifications, &sequence_reset_blue);
furi_hal_bt_stop_advertising();
bt_set_profile(bt_mouse->bt, BtProfileSerial);
furi_record_close(RECORD_NOTIFICATION);
bt_mouse->notifications = NULL;
furi_record_close(RECORD_BT);
bt_mouse->bt = NULL;
}
static int8_t clamp(int t) {
if(t < -128) {
return -128;
} else if(t > 127) {
return 127;
}
return t;
}
static int32_t bt_mouse_thread_callback(void* context) {
furi_assert(context);
BtMouse* bt_mouse = (BtMouse*)context;
while(1) {
uint32_t flags =
furi_thread_flags_wait(BT_MOUSE_FLAG_ALL, FuriFlagWaitAny, FuriWaitForever);
if(flags & BT_MOUSE_FLAG_KILL_THREAD) {
break;
}
if(flags & BT_MOUSE_FLAG_INPUT_EVENT) {
furi_mutex_acquire(bt_mouse->mutex, FuriWaitForever);
ButtonEvent event;
bool send_buttons = false;
if(bt_mouse->qhead != bt_mouse->qtail) {
event = bt_mouse->queue[bt_mouse->qhead++];
bt_mouse->qhead %= BTN_EVT_QUEUE_SIZE;
send_buttons = true;
}
int8_t dx = clamp(bt_mouse->dx);
bt_mouse->dx -= dx;
int8_t dy = clamp(bt_mouse->dy);
bt_mouse->dy -= dy;
int8_t wheel = clamp(bt_mouse->wheel);
bt_mouse->wheel -= wheel;
furi_mutex_release(bt_mouse->mutex);
if(bt_mouse->connected && send_buttons) {
if(event.state) {
furi_hal_bt_hid_mouse_press(event.button);
} else {
furi_hal_bt_hid_mouse_release(event.button);
}
}
if(bt_mouse->connected && (dx != 0 || dy != 0)) {
furi_hal_bt_hid_mouse_move(dx, dy);
}
if(bt_mouse->connected && wheel != 0) {
furi_hal_bt_hid_mouse_scroll(wheel);
}
}
}
return 0;
}
void bt_mouse_thread_start(BtMouse* bt_mouse) {
furi_assert(bt_mouse);
bt_mouse->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
bt_mouse->thread = furi_thread_alloc();
furi_thread_set_name(bt_mouse->thread, "BtSender");
furi_thread_set_stack_size(bt_mouse->thread, 1024);
furi_thread_set_context(bt_mouse->thread, bt_mouse);
furi_thread_set_callback(bt_mouse->thread, bt_mouse_thread_callback);
furi_thread_start(bt_mouse->thread);
}
void bt_mouse_thread_stop(BtMouse* bt_mouse) {
furi_assert(bt_mouse);
FuriThreadId thread_id = furi_thread_get_id(bt_mouse->thread);
furi_assert(thread_id);
furi_thread_flags_set(thread_id, BT_MOUSE_FLAG_KILL_THREAD);
furi_thread_join(bt_mouse->thread);
furi_thread_free(bt_mouse->thread);
furi_mutex_free(bt_mouse->mutex);
}
BtMouse* bt_mouse_alloc(ViewDispatcher* view_dispatcher) {
BtMouse* bt_mouse = malloc(sizeof(BtMouse));
memset(bt_mouse, 0, sizeof(BtMouse));
bt_mouse->view = view_alloc();
bt_mouse->view_dispatcher = view_dispatcher;
view_set_context(bt_mouse->view, bt_mouse);
view_set_draw_callback(bt_mouse->view, bt_mouse_draw_callback);
view_set_input_callback(bt_mouse->view, bt_mouse_input_callback);
view_set_enter_callback(bt_mouse->view, bt_mouse_enter_callback);
view_set_custom_callback(bt_mouse->view, bt_mouse_custom_callback);
view_set_exit_callback(bt_mouse->view, bt_mouse_exit_callback);
bt_mouse_thread_start(bt_mouse);
return bt_mouse;
}
void bt_mouse_free(BtMouse* bt_mouse) {
furi_assert(bt_mouse);
bt_mouse_thread_stop(bt_mouse);
view_free(bt_mouse->view);
free(bt_mouse);
}
View* bt_mouse_get_view(BtMouse* bt_mouse) {
furi_assert(bt_mouse);
return bt_mouse->view;
}

View File

@ -0,0 +1,14 @@
#pragma once
#include <gui/view.h>
#include <gui/view_dispatcher.h>
typedef struct BtMouse BtMouse;
BtMouse* bt_mouse_alloc(ViewDispatcher* view_dispatcher);
void bt_mouse_free(BtMouse* bt_mouse);
View* bt_mouse_get_view(BtMouse* bt_mouse);
void bt_mouse_set_connected_status(BtMouse* bt_mouse, bool connected);

View File

@ -0,0 +1,69 @@
#include "calibration.h"
#include "../tracking/main_loop.h"
#include "../air_mouse.h"
#include <furi.h>
#include <gui/elements.h>
struct Calibration {
View* view;
ViewDispatcher* view_dispatcher;
};
static void calibration_draw_callback(Canvas* canvas, void* context) {
UNUSED(context);
canvas_clear(canvas);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, 10, "Calibrating...");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 0, 63, "Please wait");
}
void calibration_enter_callback(void* context) {
furi_assert(context);
Calibration* calibration = context;
calibration_begin();
view_dispatcher_send_custom_event(calibration->view_dispatcher, 0);
}
bool calibration_custom_callback(uint32_t event, void* context) {
UNUSED(event);
furi_assert(context);
Calibration* calibration = context;
if(calibration_step()) {
view_dispatcher_switch_to_view(calibration->view_dispatcher, AirMouseViewSubmenu);
} else {
view_dispatcher_send_custom_event(calibration->view_dispatcher, 0);
}
return true;
}
void calibration_exit_callback(void* context) {
furi_assert(context);
calibration_end();
}
Calibration* calibration_alloc(ViewDispatcher* view_dispatcher) {
Calibration* calibration = malloc(sizeof(Calibration));
calibration->view = view_alloc();
calibration->view_dispatcher = view_dispatcher;
view_set_context(calibration->view, calibration);
view_set_draw_callback(calibration->view, calibration_draw_callback);
view_set_enter_callback(calibration->view, calibration_enter_callback);
view_set_custom_callback(calibration->view, calibration_custom_callback);
view_set_exit_callback(calibration->view, calibration_exit_callback);
return calibration;
}
void calibration_free(Calibration* calibration) {
furi_assert(calibration);
view_free(calibration->view);
free(calibration);
}
View* calibration_get_view(Calibration* calibration) {
furi_assert(calibration);
return calibration->view;
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <gui/view.h>
#include <gui/view_dispatcher.h>
typedef struct Calibration Calibration;
Calibration* calibration_alloc(ViewDispatcher* view_dispatcher);
void calibration_free(Calibration* calibration);
View* calibration_get_view(Calibration* calibration);

View File

@ -0,0 +1,139 @@
#include "usb_mouse.h"
#include "../tracking/main_loop.h"
#include <furi.h>
#include <furi_hal_usb.h>
#include <furi_hal_usb_hid.h>
#include <gui/elements.h>
struct UsbMouse {
View* view;
ViewDispatcher* view_dispatcher;
FuriHalUsbInterface* usb_mode_prev;
};
static void usb_mouse_draw_callback(Canvas* canvas, void* context) {
UNUSED(context);
canvas_clear(canvas);
canvas_set_font(canvas, FontPrimary);
canvas_draw_str(canvas, 0, 10, "USB Mouse mode");
canvas_set_font(canvas, FontSecondary);
canvas_draw_str(canvas, 0, 63, "Hold [back] to exit");
}
#define MOUSE_SCROLL 2
static void usb_mouse_process(UsbMouse* usb_mouse, InputEvent* event) {
with_view_model(
usb_mouse->view,
void* model,
{
UNUSED(model);
if(event->key == InputKeyUp) {
if(event->type == InputTypePress) {
furi_hal_hid_mouse_press(HID_MOUSE_BTN_LEFT);
} else if(event->type == InputTypeRelease) {
furi_hal_hid_mouse_release(HID_MOUSE_BTN_LEFT);
}
} else if(event->key == InputKeyDown) {
if(event->type == InputTypePress) {
furi_hal_hid_mouse_press(HID_MOUSE_BTN_RIGHT);
} else if(event->type == InputTypeRelease) {
furi_hal_hid_mouse_release(HID_MOUSE_BTN_RIGHT);
}
} else if(event->key == InputKeyOk) {
if(event->type == InputTypePress) {
furi_hal_hid_mouse_press(HID_MOUSE_BTN_WHEEL);
} else if(event->type == InputTypeRelease) {
furi_hal_hid_mouse_release(HID_MOUSE_BTN_WHEEL);
}
} else if(event->key == InputKeyRight) {
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
furi_hal_hid_mouse_scroll(MOUSE_SCROLL);
}
} else if(event->key == InputKeyLeft) {
if(event->type == InputTypePress || event->type == InputTypeRepeat) {
furi_hal_hid_mouse_scroll(-MOUSE_SCROLL);
}
}
},
true);
}
static bool usb_mouse_input_callback(InputEvent* event, void* context) {
furi_assert(context);
UsbMouse* usb_mouse = context;
bool consumed = false;
if(event->type == InputTypeLong && event->key == InputKeyBack) {
// furi_hal_hid_mouse_release_all();
} else {
usb_mouse_process(usb_mouse, event);
consumed = true;
}
return consumed;
}
void usb_mouse_enter_callback(void* context) {
furi_assert(context);
UsbMouse* usb_mouse = context;
usb_mouse->usb_mode_prev = furi_hal_usb_get_config();
furi_hal_usb_unlock();
furi_check(furi_hal_usb_set_config(&usb_hid, NULL) == true);
tracking_begin();
view_dispatcher_send_custom_event(usb_mouse->view_dispatcher, 0);
}
bool usb_mouse_move(int8_t dx, int8_t dy, void* context) {
UNUSED(context);
return furi_hal_hid_mouse_move(dx, dy);
}
bool usb_mouse_custom_callback(uint32_t event, void* context) {
UNUSED(event);
furi_assert(context);
UsbMouse* usb_mouse = context;
tracking_step(usb_mouse_move, context);
furi_delay_ms(3); // Magic! Removing this will break the buttons
view_dispatcher_send_custom_event(usb_mouse->view_dispatcher, 0);
return true;
}
void usb_mouse_exit_callback(void* context) {
furi_assert(context);
UsbMouse* usb_mouse = context;
tracking_end();
furi_hal_usb_set_config(usb_mouse->usb_mode_prev, NULL);
}
UsbMouse* usb_mouse_alloc(ViewDispatcher* view_dispatcher) {
UsbMouse* usb_mouse = malloc(sizeof(UsbMouse));
usb_mouse->view = view_alloc();
usb_mouse->view_dispatcher = view_dispatcher;
view_set_context(usb_mouse->view, usb_mouse);
view_set_draw_callback(usb_mouse->view, usb_mouse_draw_callback);
view_set_input_callback(usb_mouse->view, usb_mouse_input_callback);
view_set_enter_callback(usb_mouse->view, usb_mouse_enter_callback);
view_set_custom_callback(usb_mouse->view, usb_mouse_custom_callback);
view_set_exit_callback(usb_mouse->view, usb_mouse_exit_callback);
return usb_mouse;
}
void usb_mouse_free(UsbMouse* usb_mouse) {
furi_assert(usb_mouse);
view_free(usb_mouse->view);
free(usb_mouse);
}
View* usb_mouse_get_view(UsbMouse* usb_mouse) {
furi_assert(usb_mouse);
return usb_mouse->view;
}

View File

@ -0,0 +1,12 @@
#pragma once
#include <gui/view.h>
#include <gui/view_dispatcher.h>
typedef struct UsbMouse UsbMouse;
UsbMouse* usb_mouse_alloc(ViewDispatcher* view_dispatcher);
void usb_mouse_free(UsbMouse* usb_mouse);
View* usb_mouse_get_view(UsbMouse* usb_mouse);

View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2023 Alan Tsui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,71 @@
<p align="center">
<h1 align="center">Barcode Generator</h1>
<p align="center">
A barcode generator for the Flipper Zero that supports **UPC-A**, **EAN-8**, **EAN-13**, **Code-39**, and **Code-128**[1]
</p>
## Table of Contents
- [Table of Contents](#table-of-contents)
- [Installing](#installing)
- [Usage](#usage)
- [Creating a barcode](#creating-a-barcode)
- [Editing a barcode](#editing-a-barcode)
- [Deleting a barcode](#deleting-a-barcode)
- [Viewing a barcode](#viewing-a-barcode)
- [Screenshots](#screenshots)
- [Credits](#credits)
## Installing
1) Download the `.zip` file from the release section
2) Extract/unzip the `.zip` file onto your computer
3) Open qFlipper and go to the file manager
4) Navigate to the `apps` folder
5) Drag & Drop the `.fap` file into the `apps` folder
6) Navigate back to the root folder and create the folder `app_data`, if not already there
7) Navigate into `app_data` and create another folder called `barcode_data`
8) Navigate into `barcode_data`
9) Drag & Drop the encoding txts (`code39_encodings.txt` & `code128_encodings.txt`) into the `barcode_data` folder
## Usage
### Creating a barcode
1) To create a barcode click on `Create Barcode`
2) Next select your type using the left and right arrows
3) Enter your filename and then your barcode data
4) Click save
### Editing a barcode
1) To edit a barcode click on `Edit Barcode`
2) Next select the barcode file you want to edit
3) Edit the type, name, or data
4) Click save
### Deleting a barcode
1) To delete a barcode click on `Edit Barcode`
2) Next select the barcode file you want to delete
3) Scroll all the way to the bottom
4) Click delete
### Viewing a barcode
1) To view a barcode click on `Load Barcode`
2) Next select the barcode file you want to view
## Screenshots
![Barcode Create Screen](screenshots/Creating%20Barcode.png "Barcode Create Screen")
![Flipper Code-128 Barcode](screenshots/Flipper%20Barcode.png "Flipper Code-128 Barcode")
![Flipper Box EAN-13 Barcode](screenshots/Flipper%20Box%20Barcode.png "Flipper Box EAN-13 Barcode")
## Credits
[Kingal1337](https://github.com/Kingal1337) - Developer
[@teeebor](https://github.com/teeebor) - Menu Code Snippet
[1] - Only supports Set B and only the characters from 0-94

View File

@ -0,0 +1,11 @@
App(
appid="barcode_app",
name="Barcode",
apptype=FlipperAppType.EXTERNAL,
entry_point="barcode_main",
requires=["gui", "storage"],
stack_size=2 * 1024,
fap_category="Misc_Extra",
fap_icon="images/barcode_10.png",
fap_icon_assets="images",
)

View File

@ -0,0 +1,342 @@
#include "barcode_app.h"
#include "barcode_app_icons.h"
/**
* Opens a file browser dialog and returns the filepath of the selected file
*
* @param folder the folder to view when the browser opens
* @param file_path a string pointer for the file_path when a file is selected,
* file_path will be the folder path is nothing is selected
* @returns true if a file is selected
*/
static bool select_file(const char* folder, FuriString* file_path) {
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogsFileBrowserOptions browser_options;
dialog_file_browser_set_basic_options(&browser_options, BARCODE_EXTENSION, &I_barcode_10);
browser_options.base_path = DEFAULT_USER_BARCODES;
furi_string_set(file_path, folder);
bool res = dialog_file_browser_show(dialogs, file_path, file_path, &browser_options);
furi_record_close(RECORD_DIALOGS);
return res;
}
/**
* Reads the data from a file and stores them in the FuriStrings raw_type and raw_data
*/
ErrorCode read_raw_data(FuriString* file_path, FuriString* raw_type, FuriString* raw_data) {
//Open Storage
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
ErrorCode reason = OKCode;
if(!flipper_format_file_open_existing(ff, furi_string_get_cstr(file_path))) {
FURI_LOG_E(TAG, "Could not open file %s", furi_string_get_cstr(file_path));
reason = FileOpening;
} else {
if(!flipper_format_read_string(ff, "Type", raw_type)) {
FURI_LOG_E(TAG, "Could not read \"Type\" string");
reason = InvalidFileData;
}
if(!flipper_format_read_string(ff, "Data", raw_data)) {
FURI_LOG_E(TAG, "Could not read \"Data\" string");
reason = InvalidFileData;
}
}
//Close Storage
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
return reason;
}
/**
* Gets the file name from a file path
* @param file_path the file path
* @param file_name the FuriString to store the file name
* @param remove_extension true if the extension should be removed, otherwise false
*/
bool get_file_name_from_path(FuriString* file_path, FuriString* file_name, bool remove_extension) {
if(file_path == NULL || file_name == NULL) {
return false;
}
int slash_index = furi_string_search_rchar(file_path, '/', 0);
if(slash_index == FURI_STRING_FAILURE || slash_index >= (furi_string_size(file_path) - 1)) {
return false;
}
furi_string_set(file_name, file_path);
furi_string_right(file_name, slash_index + 1);
if(remove_extension) {
int ext_index = furi_string_search_rchar(file_name, '.', 0);
if(ext_index != FURI_STRING_FAILURE && ext_index < (furi_string_size(file_path))) {
furi_string_left(file_name, ext_index);
}
}
return true;
}
/**
* Creates the barcode folder
*/
void init_folder() {
Storage* storage = furi_record_open(RECORD_STORAGE);
FURI_LOG_I(TAG, "Creating barcodes folder");
if(storage_simply_mkdir(storage, DEFAULT_USER_BARCODES)) {
FURI_LOG_I(TAG, "Barcodes folder successfully created!");
} else {
FURI_LOG_I(TAG, "Barcodes folder already exists.");
}
furi_record_close(RECORD_STORAGE);
}
void select_barcode_item(BarcodeApp* app) {
FuriString* file_path = furi_string_alloc();
FuriString* raw_type = furi_string_alloc();
FuriString* raw_data = furi_string_alloc();
//this determines if the data was read correctly or if the
bool loaded_success = true;
ErrorCode reason = OKCode;
bool file_selected = select_file(DEFAULT_USER_BARCODES, file_path);
if(file_selected) {
FURI_LOG_I(TAG, "The file selected is %s", furi_string_get_cstr(file_path));
Barcode* barcode = app->barcode_view;
reason = read_raw_data(file_path, raw_type, raw_data);
if(reason != OKCode) {
loaded_success = false;
FURI_LOG_E(TAG, "Could not read data correctly");
}
//Free the data from the previous barcode
barcode_free_model(barcode);
with_view_model(
barcode->view,
BarcodeModel * model,
{
model->file_path = furi_string_alloc_set(file_path);
model->data = malloc(sizeof(BarcodeData));
model->data->valid = loaded_success;
if(loaded_success) {
model->data->raw_data = furi_string_alloc_set(raw_data);
model->data->correct_data = furi_string_alloc();
model->data->type_obj = get_type(raw_type);
barcode_loader(model->data);
} else {
model->data->reason = reason;
}
},
true);
view_dispatcher_switch_to_view(app->view_dispatcher, BarcodeView);
}
furi_string_free(raw_type);
furi_string_free(raw_data);
furi_string_free(file_path);
}
void edit_barcode_item(BarcodeApp* app) {
FuriString* file_path = furi_string_alloc();
FuriString* file_name = furi_string_alloc();
FuriString* raw_type = furi_string_alloc();
FuriString* raw_data = furi_string_alloc();
//this determines if the data was read correctly or if the
ErrorCode reason = OKCode;
bool file_selected = select_file(DEFAULT_USER_BARCODES, file_path);
if(file_selected) {
FURI_LOG_I(TAG, "The file selected is %s", furi_string_get_cstr(file_path));
CreateView* create_view_object = app->create_view;
reason = read_raw_data(file_path, raw_type, raw_data);
if(reason != OKCode) {
FURI_LOG_E(TAG, "Could not read data correctly");
with_view_model(
app->message_view->view,
MessageViewModel * model,
{ model->message = get_error_code_message(reason); },
true);
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, MessageErrorView);
} else {
BarcodeTypeObj* type_obj = get_type(raw_type);
if(type_obj->type == UNKNOWN) {
type_obj = barcode_type_objs[0];
}
get_file_name_from_path(file_path, file_name, true);
create_view_free_model(create_view_object);
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
model->selected_menu_item = 0;
model->barcode_type = type_obj;
model->file_path = furi_string_alloc_set(file_path);
model->file_name = furi_string_alloc_set(file_name);
model->barcode_data = furi_string_alloc_set(raw_data);
model->mode = EditMode;
},
true);
view_dispatcher_switch_to_view(app->view_dispatcher, CreateBarcodeView);
}
}
furi_string_free(raw_type);
furi_string_free(raw_data);
furi_string_free(file_name);
furi_string_free(file_path);
}
void create_barcode_item(BarcodeApp* app) {
CreateView* create_view_object = app->create_view;
create_view_free_model(create_view_object);
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
model->selected_menu_item = 0;
model->barcode_type = barcode_type_objs[0];
model->file_path = furi_string_alloc();
model->file_name = furi_string_alloc();
model->barcode_data = furi_string_alloc();
model->mode = NewMode;
},
true);
view_dispatcher_switch_to_view(app->view_dispatcher, CreateBarcodeView);
}
void submenu_callback(void* context, uint32_t index) {
furi_assert(context);
BarcodeApp* app = context;
if(index == SelectBarcodeItem) {
select_barcode_item(app);
} else if(index == EditBarcodeItem) {
edit_barcode_item(app);
} else if(index == CreateBarcodeItem) {
create_barcode_item(app);
}
}
uint32_t main_menu_callback(void* context) {
UNUSED(context);
return MainMenuView;
}
uint32_t exit_callback(void* context) {
UNUSED(context);
return VIEW_NONE;
}
void free_app(BarcodeApp* app) {
FURI_LOG_I(TAG, "Freeing Data");
init_folder();
free_types();
view_dispatcher_remove_view(app->view_dispatcher, TextInputView);
text_input_free(app->text_input);
view_dispatcher_remove_view(app->view_dispatcher, MessageErrorView);
message_view_free(app->message_view);
view_dispatcher_remove_view(app->view_dispatcher, MainMenuView);
submenu_free(app->main_menu);
view_dispatcher_remove_view(app->view_dispatcher, CreateBarcodeView);
create_view_free(app->create_view);
view_dispatcher_remove_view(app->view_dispatcher, BarcodeView);
barcode_free(app->barcode_view);
//free the dispatcher
view_dispatcher_free(app->view_dispatcher);
furi_message_queue_free(app->event_queue);
furi_record_close(RECORD_GUI);
app->gui = NULL;
free(app);
}
int32_t barcode_main(void* p) {
UNUSED(p);
BarcodeApp* app = malloc(sizeof(BarcodeApp));
init_types();
app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
// Register view port in GUI
app->gui = furi_record_open(RECORD_GUI);
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_enable_queue(app->view_dispatcher);
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
app->main_menu = submenu_alloc();
submenu_add_item(app->main_menu, "Load Barcode", SelectBarcodeItem, submenu_callback, app);
view_set_previous_callback(submenu_get_view(app->main_menu), exit_callback);
view_dispatcher_add_view(app->view_dispatcher, MainMenuView, submenu_get_view(app->main_menu));
submenu_add_item(app->main_menu, "Edit Barcode", EditBarcodeItem, submenu_callback, app);
/*****************************
* Creating Text Input View
******************************/
app->text_input = text_input_alloc();
view_dispatcher_add_view(
app->view_dispatcher, TextInputView, text_input_get_view(app->text_input));
/*****************************
* Creating Message View
******************************/
app->message_view = message_view_allocate(app);
view_dispatcher_add_view(
app->view_dispatcher, MessageErrorView, message_get_view(app->message_view));
/*****************************
* Creating Create View
******************************/
app->create_view = create_view_allocate(app);
submenu_add_item(app->main_menu, "Create Barcode", CreateBarcodeItem, submenu_callback, app);
view_set_previous_callback(create_get_view(app->create_view), main_menu_callback);
view_dispatcher_add_view(
app->view_dispatcher, CreateBarcodeView, create_get_view(app->create_view));
/*****************************
* Creating Barcode View
******************************/
app->barcode_view = barcode_view_allocate(app);
view_set_previous_callback(barcode_get_view(app->barcode_view), main_menu_callback);
view_dispatcher_add_view(
app->view_dispatcher, BarcodeView, barcode_get_view(app->barcode_view));
//switch view to submenu and run dispatcher
view_dispatcher_switch_to_view(app->view_dispatcher, MainMenuView);
view_dispatcher_run(app->view_dispatcher);
free_app(app);
return 0;
}

View File

@ -0,0 +1,87 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <input/input.h>
#include <dialogs/dialogs.h>
#include <gui/view_dispatcher.h>
#include <gui/modules/submenu.h>
#include <gui/modules/text_input.h>
#include <gui/modules/text_input.h>
#include <flipper_format/flipper_format.h>
#include "barcode_utils.h"
#define TAG "BARCODE"
#define VERSION "1.0"
#define FILE_VERSION "1"
#define TEXT_BUFFER_SIZE 128
#define BARCODE_HEIGHT 50
#define BARCODE_Y_START 3
#define APPS_DATA EXT_PATH("apps_data")
//the folder where the encodings are located
#define BARCODE_DATA_FILE_DIR_PATH APPS_DATA "/barcode_data"
//the folder where the code 39 encoding table is located
#define CODE39_DICT_FILE_PATH BARCODE_DATA_FILE_DIR_PATH "/code39_encodings.txt"
//the folder where the code 128 encoding table is located
#define CODE128_DICT_FILE_PATH BARCODE_DATA_FILE_DIR_PATH "/code128_encodings.txt"
//the folder where the user stores their barcodes
#define DEFAULT_USER_BARCODES EXT_PATH("barcodes")
//The extension barcode files use
#define BARCODE_EXTENSION ".barcode"
#define BARCODE_EXTENSION_LENGTH 8
#include "views/barcode_view.h"
#include "views/create_view.h"
#include "views/message_view.h"
#include "barcode_validator.h"
typedef struct BarcodeApp BarcodeApp;
struct BarcodeApp {
Submenu* main_menu;
ViewDispatcher* view_dispatcher;
Gui* gui;
FuriMessageQueue* event_queue;
CreateView* create_view;
Barcode* barcode_view;
MessageView* message_view;
TextInput* text_input;
};
enum SubmenuItems {
SelectBarcodeItem,
EditBarcodeItem,
CreateBarcodeItem
};
enum Views {
TextInputView,
MessageErrorView,
MainMenuView,
CreateBarcodeView,
BarcodeView
};
void submenu_callback(void* context, uint32_t index);
uint32_t main_menu_callback(void* context);
uint32_t exit_callback(void* context);
int32_t barcode_main(void* p);

View File

@ -0,0 +1,125 @@
#include "barcode_utils.h"
BarcodeTypeObj* barcode_type_objs[NUMBER_OF_BARCODE_TYPES] = {NULL};
void init_types() {
BarcodeTypeObj* upc_a = malloc(sizeof(BarcodeTypeObj));
upc_a->name = "UPC-A";
upc_a->type = UPCA;
upc_a->min_digits = 11;
upc_a->max_digits = 12;
upc_a->start_pos = 16;
barcode_type_objs[UPCA] = upc_a;
BarcodeTypeObj* ean_8 = malloc(sizeof(BarcodeTypeObj));
ean_8->name = "EAN-8";
ean_8->type = EAN8;
ean_8->min_digits = 7;
ean_8->max_digits = 8;
ean_8->start_pos = 32;
barcode_type_objs[EAN8] = ean_8;
BarcodeTypeObj* ean_13 = malloc(sizeof(BarcodeTypeObj));
ean_13->name = "EAN-13";
ean_13->type = EAN13;
ean_13->min_digits = 12;
ean_13->max_digits = 13;
ean_13->start_pos = 16;
barcode_type_objs[EAN13] = ean_13;
BarcodeTypeObj* code_39 = malloc(sizeof(BarcodeTypeObj));
code_39->name = "CODE-39";
code_39->type = CODE39;
code_39->min_digits = 1;
code_39->max_digits = -1;
code_39->start_pos = 0;
barcode_type_objs[CODE39] = code_39;
BarcodeTypeObj* code_128 = malloc(sizeof(BarcodeTypeObj));
code_128->name = "CODE-128";
code_128->type = CODE128;
code_128->min_digits = 1;
code_128->max_digits = -1;
code_128->start_pos = 0;
barcode_type_objs[CODE128] = code_128;
BarcodeTypeObj* unknown = malloc(sizeof(BarcodeTypeObj));
unknown->name = "Unknown";
unknown->type = UNKNOWN;
unknown->min_digits = 0;
unknown->max_digits = 0;
unknown->start_pos = 0;
barcode_type_objs[UNKNOWN] = unknown;
}
void free_types() {
for(int i = 0; i < NUMBER_OF_BARCODE_TYPES; i++) {
free(barcode_type_objs[i]);
}
}
BarcodeTypeObj* get_type(FuriString* type_string) {
if(furi_string_cmp_str(type_string, "UPC-A") == 0) {
return barcode_type_objs[UPCA];
}
if(furi_string_cmp_str(type_string, "EAN-8") == 0) {
return barcode_type_objs[EAN8];
}
if(furi_string_cmp_str(type_string, "EAN-13") == 0) {
return barcode_type_objs[EAN13];
}
if(furi_string_cmp_str(type_string, "CODE-39") == 0) {
return barcode_type_objs[CODE39];
}
if(furi_string_cmp_str(type_string, "CODE-128") == 0) {
return barcode_type_objs[CODE128];
}
return barcode_type_objs[UNKNOWN];
}
const char* get_error_code_name(ErrorCode error_code) {
switch(error_code) {
case WrongNumberOfDigits:
return "Wrong Number Of Digits";
case InvalidCharacters:
return "Invalid Characters";
case UnsupportedType:
return "Unsupported Type";
case FileOpening:
return "File Opening Error";
case InvalidFileData:
return "Invalid File Data";
case MissingEncodingTable:
return "Missing Encoding Table";
case EncodingTableError:
return "Encoding Table Error";
case OKCode:
return "OK";
default:
return "Unknown Code";
};
}
const char* get_error_code_message(ErrorCode error_code) {
switch(error_code) {
case WrongNumberOfDigits:
return "Wrong # of characters";
case InvalidCharacters:
return "Invalid characters";
case UnsupportedType:
return "Unsupported barcode type";
case FileOpening:
return "Could not open file";
case InvalidFileData:
return "Invalid file data";
case MissingEncodingTable:
return "Missing encoding table";
case EncodingTableError:
return "Encoding table error";
case OKCode:
return "OK";
default:
return "Could not read barcode data";
};
}

View File

@ -0,0 +1,53 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#define NUMBER_OF_BARCODE_TYPES 6
typedef enum {
WrongNumberOfDigits, //There is too many or too few digits in the barcode
InvalidCharacters, //The barcode contains invalid characters
UnsupportedType, //the barcode type is not supported
FileOpening, //A problem occurred when opening the barcode data file
InvalidFileData, //One of the key in the file doesn't exist or there is a typo
MissingEncodingTable, //The encoding table txt for the barcode type is missing
EncodingTableError, //Something is wrong with the encoding table, probably missing data or typo
OKCode
} ErrorCode;
typedef enum {
UPCA,
EAN8,
EAN13,
CODE39,
CODE128,
UNKNOWN
} BarcodeType;
typedef struct {
char* name; //The name of the barcode type
BarcodeType type; //The barcode type enum
int min_digits; //the minimum number of digits
int max_digits; //the maximum number of digits
int start_pos; //where to start drawing the barcode, set to -1 to dynamically draw barcode
} BarcodeTypeObj;
typedef struct {
BarcodeTypeObj* type_obj;
int check_digit; //A place to store the check digit
FuriString* raw_data; //the data directly from the file
FuriString* correct_data; //the corrected/processed data
bool valid; //true if the raw data is correctly formatted, such as correct num of digits, valid characters, etc.
ErrorCode reason; //the reason why this barcode is invalid
} BarcodeData;
//All available barcode types
extern BarcodeTypeObj* barcode_type_objs[NUMBER_OF_BARCODE_TYPES];
void init_types();
void free_types();
BarcodeTypeObj* get_type(FuriString* type_string);
const char* get_error_code_name(ErrorCode error_code);
const char* get_error_code_message(ErrorCode error_code);

View File

@ -0,0 +1,344 @@
#include "barcode_validator.h"
void barcode_loader(BarcodeData* barcode_data) {
switch(barcode_data->type_obj->type) {
case UPCA:
case EAN8:
case EAN13:
ean_upc_loader(barcode_data);
break;
case CODE39:
code_39_loader(barcode_data);
break;
case CODE128:
code_128_loader(barcode_data);
break;
case UNKNOWN:
barcode_data->reason = UnsupportedType;
barcode_data->valid = false;
default:
break;
}
}
/**
* Calculates the check digit of a barcode if they have one
* @param barcode_data the barcode data
* @returns a check digit or -1 for either an invalid
*/
int calculate_check_digit(BarcodeData* barcode_data) {
int check_digit = -1;
switch(barcode_data->type_obj->type) {
case UPCA:
case EAN8:
case EAN13:
check_digit = calculate_ean_upc_check_digit(barcode_data);
break;
case CODE39:
case CODE128:
case UNKNOWN:
default:
break;
}
return check_digit;
}
/**
* Calculates the check digit of barcode types UPC-A, EAN-8, & EAN-13
*/
int calculate_ean_upc_check_digit(BarcodeData* barcode_data) {
int check_digit = 0;
int odd = 0;
int even = 0;
int length = barcode_data->type_obj->min_digits;
//Get sum of odd digits
for(int i = 0; i < length; i += 2) {
odd += furi_string_get_char(barcode_data->raw_data, i) - '0';
}
//Get sum of even digits
for(int i = 1; i < length; i += 2) {
even += furi_string_get_char(barcode_data->raw_data, i) - '0';
}
if(barcode_data->type_obj->type == EAN13) {
check_digit = even * 3 + odd;
} else {
check_digit = odd * 3 + even;
}
check_digit = check_digit % 10;
return (10 - check_digit) % 10;
}
/**
* Loads and validates Barcode Types EAN-8, EAN-13, and UPC-A
* barcode_data and its strings should already be allocated;
*/
void ean_upc_loader(BarcodeData* barcode_data) {
int barcode_length = furi_string_size(barcode_data->raw_data);
int min_digits = barcode_data->type_obj->min_digits;
int max_digit = barcode_data->type_obj->max_digits;
//check the length of the barcode
if(barcode_length < min_digits || barcode_length > max_digit) {
barcode_data->reason = WrongNumberOfDigits;
barcode_data->valid = false;
return;
}
//checks if the barcode contains any characters that aren't a number
for(int i = 0; i < barcode_length; i++) {
char character = furi_string_get_char(barcode_data->raw_data, i);
int digit = character - '0'; //convert the number into an int (also the index)
if(digit < 0 || digit > 9) {
barcode_data->reason = InvalidCharacters;
barcode_data->valid = false;
return;
}
}
int check_digit = calculate_check_digit(barcode_data);
char check_digit_char = check_digit + '0';
barcode_data->check_digit = check_digit;
//if the barcode length is at max length then we will verify if the check digit is correct
if(barcode_length == max_digit) {
//append the raw_data to the correct data string
furi_string_cat(barcode_data->correct_data, barcode_data->raw_data);
//append the check digit to the correct data string
furi_string_set_char(barcode_data->correct_data, min_digits, check_digit_char);
}
//if the barcode length is at min length, we will calculate the check digit
if(barcode_length == min_digits) {
//append the raw_data to the correct data string
furi_string_cat(barcode_data->correct_data, barcode_data->raw_data);
//append the check digit to the correct data string
furi_string_push_back(barcode_data->correct_data, check_digit_char);
}
}
void code_39_loader(BarcodeData* barcode_data) {
int barcode_length = furi_string_size(barcode_data->raw_data);
int min_digits = barcode_data->type_obj->min_digits;
int max_digit = barcode_data->type_obj->max_digits;
//check the length of the barcode, must contain atleast a character,
//this can have as many characters as it wants, it might not fit on the screen
if(barcode_length < min_digits) {
barcode_data->reason = WrongNumberOfDigits;
barcode_data->valid = false;
return;
}
FuriString* barcode_bits = furi_string_alloc();
FuriString* temp_string = furi_string_alloc();
//add starting and ending *
if(!furi_string_start_with(barcode_data->raw_data, "*")) {
furi_string_push_back(temp_string, '*');
furi_string_cat(temp_string, barcode_data->raw_data);
furi_string_set(barcode_data->raw_data, temp_string);
}
if(!furi_string_end_with(barcode_data->raw_data, "*")) {
furi_string_push_back(barcode_data->raw_data, '*');
}
furi_string_free(temp_string);
barcode_length = furi_string_size(barcode_data->raw_data);
//Open Storage
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
if(!flipper_format_file_open_existing(ff, CODE39_DICT_FILE_PATH)) {
FURI_LOG_E(TAG, "Could not open file %s", CODE39_DICT_FILE_PATH);
barcode_data->reason = MissingEncodingTable;
barcode_data->valid = false;
} else {
FuriString* char_bits = furi_string_alloc();
for(int i = 0; i < barcode_length; i++) {
char barcode_char = toupper(furi_string_get_char(barcode_data->raw_data, i));
//convert a char into a string so it used in flipper_format_read_string
char current_character[2];
snprintf(current_character, 2, "%c", barcode_char);
if(!flipper_format_read_string(ff, current_character, char_bits)) {
FURI_LOG_E(TAG, "Could not read \"%c\" string", barcode_char);
barcode_data->reason = InvalidCharacters;
barcode_data->valid = false;
break;
} else {
FURI_LOG_I(
TAG, "\"%c\" string: %s", barcode_char, furi_string_get_cstr(char_bits));
furi_string_cat(barcode_bits, char_bits);
}
flipper_format_rewind(ff);
}
furi_string_free(char_bits);
}
//Close Storage
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
furi_string_cat(barcode_data->correct_data, barcode_bits);
furi_string_free(barcode_bits);
}
/**
* Loads a code 128 barcode
*
* Only supports character set B
*/
void code_128_loader(BarcodeData* barcode_data) {
int barcode_length = furi_string_size(barcode_data->raw_data);
//the start code for character set B
int start_code_value = 104;
//The bits for the start code
const char* start_code_bits = "11010010000";
//The bits for the stop code
const char* stop_code_bits = "1100011101011";
int min_digits = barcode_data->type_obj->min_digits;
int max_digit = barcode_data->type_obj->max_digits;
/**
* A sum of all of the characters values
* Ex:
* Barcode Data : ABC
* A has a value of 33
* B has a value of 34
* C has a value of 35
*
* the checksum_adder would be (33 * 1) + (34 * 2) + (35 * 3) + 104 = 310
*
* Add 104 since we are using set B
*/
int checksum_adder = start_code_value;
/**
* Checksum digits is the number of characters it has read so far
* In the above example the checksum_digits would be 3
*/
int checksum_digits = 0;
//the calculated check digit
int final_check_digit = 0;
//check the length of the barcode, must contain atleast a character,
//this can have as many characters as it wants, it might not fit on the screen
if(barcode_length < min_digits) {
barcode_data->reason = WrongNumberOfDigits;
barcode_data->valid = false;
return;
}
//Open Storage
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* ff = flipper_format_file_alloc(storage);
FuriString* barcode_bits = furi_string_alloc();
//add the start code
furi_string_cat(barcode_bits, start_code_bits);
if(!flipper_format_file_open_existing(ff, CODE128_DICT_FILE_PATH)) {
FURI_LOG_E(TAG, "Could not open file %s", CODE128_DICT_FILE_PATH);
barcode_data->reason = MissingEncodingTable;
barcode_data->valid = false;
} else {
FuriString* value = furi_string_alloc();
FuriString* char_bits = furi_string_alloc();
for(int i = 0; i < barcode_length; i++) {
char barcode_char = furi_string_get_char(barcode_data->raw_data, i);
//convert a char into a string so it used in flipper_format_read_string
char current_character[2];
snprintf(current_character, 2, "%c", barcode_char);
//get the value of the character
if(!flipper_format_read_string(ff, current_character, value)) {
FURI_LOG_E(TAG, "Could not read \"%c\" string", barcode_char);
barcode_data->reason = InvalidCharacters;
barcode_data->valid = false;
break;
}
//using the value of the character, get the characters bits
if(!flipper_format_read_string(ff, furi_string_get_cstr(value), char_bits)) {
FURI_LOG_E(TAG, "Could not read \"%c\" string", barcode_char);
barcode_data->reason = EncodingTableError;
barcode_data->valid = false;
break;
} else {
//add the bits to the full barcode
furi_string_cat(barcode_bits, char_bits);
//calculate the checksum
checksum_digits += 1;
checksum_adder += (atoi(furi_string_get_cstr(value)) * checksum_digits);
FURI_LOG_D(
TAG,
"\"%c\" string: %s : %s : %d : %d : %d",
barcode_char,
furi_string_get_cstr(char_bits),
furi_string_get_cstr(value),
checksum_digits,
(atoi(furi_string_get_cstr(value)) * checksum_digits),
checksum_adder);
}
//bring the file pointer back to the beginning
flipper_format_rewind(ff);
}
//calculate the check digit and convert it into a c string for lookup in the encoding table
final_check_digit = checksum_adder % 103;
int length = snprintf(NULL, 0, "%d", final_check_digit);
char* final_check_digit_string = malloc(length + 1);
snprintf(final_check_digit_string, length + 1, "%d", final_check_digit);
//after the checksum has been calculated, add the bits to the full barcode
if(!flipper_format_read_string(ff, final_check_digit_string, char_bits)) {
FURI_LOG_E(TAG, "Could not read \"%s\" string", final_check_digit_string);
barcode_data->reason = EncodingTableError;
barcode_data->valid = false;
} else {
//add the check digit bits to the full barcode
furi_string_cat(barcode_bits, char_bits);
FURI_LOG_D(
TAG,
"\"%s\" string: %s",
final_check_digit_string,
furi_string_get_cstr(char_bits));
}
free(final_check_digit_string);
furi_string_free(value);
furi_string_free(char_bits);
}
//add the stop code
furi_string_cat(barcode_bits, stop_code_bits);
//Close Storage
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
furi_string_cat(barcode_data->correct_data, barcode_bits);
furi_string_free(barcode_bits);
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "barcode_app.h"
int calculate_check_digit(BarcodeData* barcode_data);
int calculate_ean_upc_check_digit(BarcodeData* barcode_data);
void ean_upc_loader(BarcodeData* barcode_data);
void upc_a_loader(BarcodeData* barcode_data);
void ean_8_loader(BarcodeData* barcode_data);
void ean_13_loader(BarcodeData* barcode_data);
void code_39_loader(BarcodeData* barcode_data);
void code_128_loader(BarcodeData* barcode_data);
void barcode_loader(BarcodeData* barcode_data);

View File

@ -0,0 +1,52 @@
#include "encodings.h"
const char EAN_13_STRUCTURE_CODES[10][6] = {
"LLLLLL",
"LLGLGG",
"LLGGLG",
"LLGGGL",
"LGLLGG",
"LGGLLG",
"LGGGLL",
"LGLGLG",
"LGLGGL",
"LGGLGL"};
const char UPC_EAN_L_CODES[10][8] = {
"0001101", // 0
"0011001", // 1
"0010011", // 2
"0111101", // 3
"0100011", // 4
"0110001", // 5
"0101111", // 6
"0111011", // 7
"0110111", // 8
"0001011" // 9
};
const char EAN_G_CODES[10][8] = {
"0100111", // 0
"0110011", // 1
"0011011", // 2
"0100001", // 3
"0011101", // 4
"0111001", // 5
"0000101", // 6
"0010001", // 7
"0001001", // 8
"0010111" // 9
};
const char UPC_EAN_R_CODES[10][8] = {
"1110010", // 0
"1100110", // 1
"1101100", // 2
"1000010", // 3
"1011100", // 4
"1001110", // 5
"1010000", // 6
"1000100", // 7
"1001000", // 8
"1110100" // 9
};

View File

@ -0,0 +1,6 @@
#pragma once
extern const char EAN_13_STRUCTURE_CODES[10][6];
extern const char UPC_EAN_L_CODES[10][8];
extern const char EAN_G_CODES[10][8];
extern const char UPC_EAN_R_CODES[10][8];

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,444 @@
#include "../barcode_app.h"
#include "barcode_view.h"
#include "../encodings.h"
/**
* @brief Draws a single bit from a barcode at a specified location
* @param canvas
* @param bit a 1 or a 0 to signify a bit of data
* @param x the top left x coordinate
* @param y the top left y coordinate
* @param width the width of the bit
* @param height the height of the bit
*/
static void draw_bit(Canvas* canvas, int bit, int x, int y, int width, int height) {
if(bit == 1) {
canvas_set_color(canvas, ColorBlack);
} else {
canvas_set_color(canvas, ColorWhite);
}
canvas_draw_box(canvas, x, y, width, height);
}
/**
*
*/
static void draw_error_str(Canvas* canvas, const char* error) {
canvas_clear(canvas);
canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignCenter, error);
}
/**
* @param bits a string of 1's and 0's
* @returns the x coordinate after the bits have been drawn, useful for drawing the next section of bits
*/
static int draw_bits(Canvas* canvas, const char* bits, int x, int y, int width, int height) {
int bits_length = strlen(bits);
for(int i = 0; i < bits_length; i++) {
char c = bits[i];
int num = c - '0';
draw_bit(canvas, num, x, y, width, height);
x += width;
}
return x;
}
/**
* Draws an EAN-8 type barcode, does not check if the barcode is valid
* @param canvas the canvas
* @param barcode_digits the digits in the barcode, must be 8 characters long
*/
static void draw_ean_8(Canvas* canvas, BarcodeData* barcode_data) {
FuriString* barcode_digits = barcode_data->correct_data;
BarcodeTypeObj* type_obj = barcode_data->type_obj;
int barcode_length = furi_string_size(barcode_digits);
int x = type_obj->start_pos;
int y = BARCODE_Y_START;
int width = 1;
int height = BARCODE_HEIGHT;
//the guard patterns for the beginning, center, ending
const char* end_bits = "101";
const char* center_bits = "01010";
//draw the starting guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
FuriString* code_part = furi_string_alloc();
//loop through each digit, find the encoding, and draw it
for(int i = 0; i < barcode_length; i++) {
char current_digit = furi_string_get_char(barcode_digits, i);
//the actual number and the index of the bits
int index = current_digit - '0';
//use the L-codes for the first 4 digits and the R-Codes for the last 4 digits
if(i <= 3) {
furi_string_set_str(code_part, UPC_EAN_L_CODES[index]);
} else {
furi_string_set_str(code_part, UPC_EAN_R_CODES[index]);
}
//convert the current_digit char into a string so it can be printed
char current_digit_string[2];
snprintf(current_digit_string, 2, "%c", current_digit);
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, x + 1, y + height + 8, current_digit_string);
//draw the bits of the barcode
x = draw_bits(canvas, furi_string_get_cstr(code_part), x, y, width, height);
//if the index has reached 3, that means 4 digits have been drawn and now draw the center guard pattern
if(i == 3) {
x = draw_bits(canvas, center_bits, x, y, width, height + 5);
}
}
furi_string_free(code_part);
//draw the ending guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
}
static void draw_ean_13(Canvas* canvas, BarcodeData* barcode_data) {
FuriString* barcode_digits = barcode_data->correct_data;
BarcodeTypeObj* type_obj = barcode_data->type_obj;
int barcode_length = furi_string_size(barcode_digits);
int x = type_obj->start_pos;
int y = BARCODE_Y_START;
int width = 1;
int height = BARCODE_HEIGHT;
//the guard patterns for the beginning, center, ending
const char* end_bits = "101";
const char* center_bits = "01010";
//draw the starting guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
FuriString* left_structure = furi_string_alloc();
FuriString* code_part = furi_string_alloc();
//loop through each digit, find the encoding, and draw it
for(int i = 0; i < barcode_length; i++) {
char current_digit = furi_string_get_char(barcode_digits, i);
int index = current_digit - '0';
if(i == 0) {
furi_string_set_str(left_structure, EAN_13_STRUCTURE_CODES[index]);
//convert the current_digit char into a string so it can be printed
char current_digit_string[2];
snprintf(current_digit_string, 2, "%c", current_digit);
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, x - 10, y + height + 8, current_digit_string);
continue;
} else {
//use the L-codes for the first 6 digits and the R-Codes for the last 6 digits
if(i <= 6) {
//get the encoding type at the current barcode bit position
char encoding_type = furi_string_get_char(left_structure, i - 1);
if(encoding_type == 'L') {
furi_string_set_str(code_part, UPC_EAN_L_CODES[index]);
} else {
furi_string_set_str(code_part, EAN_G_CODES[index]);
}
} else {
furi_string_set_str(code_part, UPC_EAN_R_CODES[index]);
}
//convert the current_digit char into a string so it can be printed
char current_digit_string[2];
snprintf(current_digit_string, 2, "%c", current_digit);
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, x + 1, y + height + 8, current_digit_string);
//draw the bits of the barcode
x = draw_bits(canvas, furi_string_get_cstr(code_part), x, y, width, height);
//if the index has reached 6, that means 6 digits have been drawn and we now draw the center guard pattern
if(i == 6) {
x = draw_bits(canvas, center_bits, x, y, width, height + 5);
}
}
}
furi_string_free(left_structure);
furi_string_free(code_part);
//draw the ending guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
}
/**
* Draw a UPC-A barcode
*/
static void draw_upc_a(Canvas* canvas, BarcodeData* barcode_data) {
FuriString* barcode_digits = barcode_data->correct_data;
BarcodeTypeObj* type_obj = barcode_data->type_obj;
int barcode_length = furi_string_size(barcode_digits);
int x = type_obj->start_pos;
int y = BARCODE_Y_START;
int width = 1;
int height = BARCODE_HEIGHT;
//the guard patterns for the beginning, center, ending
char* end_bits = "101";
char* center_bits = "01010";
//draw the starting guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
FuriString* code_part = furi_string_alloc();
//loop through each digit, find the encoding, and draw it
for(int i = 0; i < barcode_length; i++) {
char current_digit = furi_string_get_char(barcode_digits, i);
int index = current_digit - '0'; //convert the number into an int (also the index)
//use the L-codes for the first 6 digits and the R-Codes for the last 6 digits
if(i <= 5) {
furi_string_set_str(code_part, UPC_EAN_L_CODES[index]);
} else {
furi_string_set_str(code_part, UPC_EAN_R_CODES[index]);
}
//convert the current_digit char into a string so it can be printed
char current_digit_string[2];
snprintf(current_digit_string, 2, "%c", current_digit);
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
canvas_draw_str(canvas, x + 1, y + height + 8, current_digit_string);
//draw the bits of the barcode
x = draw_bits(canvas, furi_string_get_cstr(code_part), x, y, width, height);
//if the index has reached 6, that means 6 digits have been drawn and we now draw the center guard pattern
if(i == 5) {
x = draw_bits(canvas, center_bits, x, y, width, height + 5);
}
}
furi_string_free(code_part);
//draw the ending guard pattern
x = draw_bits(canvas, end_bits, x, y, width, height + 5);
}
static void draw_code_39(Canvas* canvas, BarcodeData* barcode_data) {
FuriString* raw_data = barcode_data->raw_data;
FuriString* barcode_digits = barcode_data->correct_data;
//BarcodeTypeObj* type_obj = barcode_data->type_obj;
int barcode_length = furi_string_size(barcode_digits);
int total_pixels = 0;
for(int i = 0; i < barcode_length; i++) {
//1 for wide, 0 for narrow
char wide_or_narrow = furi_string_get_char(barcode_digits, i);
int wn_digit = wide_or_narrow - '0'; //wide(1) or narrow(0) digit
if(wn_digit == 1) {
total_pixels += 3;
} else {
total_pixels += 1;
}
if((i + 1) % 9 == 0) {
total_pixels += 1;
}
}
int x = (128 - total_pixels) / 2;
int y = BARCODE_Y_START;
int width = 1;
int height = BARCODE_HEIGHT;
bool filled_in = true;
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
// canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignCenter, error);
canvas_draw_str_aligned(
canvas, 62, y + height + 8, AlignCenter, AlignBottom, furi_string_get_cstr(raw_data));
for(int i = 0; i < barcode_length; i++) {
//1 for wide, 0 for narrow
char wide_or_narrow = furi_string_get_char(barcode_digits, i);
int wn_digit = wide_or_narrow - '0'; //wide(1) or narrow(0) digit
if(filled_in) {
if(wn_digit == 1) {
x = draw_bits(canvas, "111", x, y, width, height);
} else {
x = draw_bits(canvas, "1", x, y, width, height);
}
filled_in = false;
} else {
if(wn_digit == 1) {
x = draw_bits(canvas, "000", x, y, width, height);
} else {
x = draw_bits(canvas, "0", x, y, width, height);
}
filled_in = true;
}
if((i + 1) % 9 == 0) {
x = draw_bits(canvas, "0", x, y, width, height);
filled_in = true;
}
}
}
static void draw_code_128(Canvas* canvas, BarcodeData* barcode_data) {
FuriString* raw_data = barcode_data->raw_data;
FuriString* barcode_digits = barcode_data->correct_data;
int barcode_length = furi_string_size(barcode_digits);
int x = (128 - barcode_length) / 2;
int y = BARCODE_Y_START;
int width = 1;
int height = BARCODE_HEIGHT;
x = draw_bits(canvas, furi_string_get_cstr(barcode_digits), x, y, width, height);
//set the canvas color to black to print the digit
canvas_set_color(canvas, ColorBlack);
// canvas_draw_str_aligned(canvas, 62, 30, AlignCenter, AlignCenter, error);
canvas_draw_str_aligned(
canvas, 62, y + height + 8, AlignCenter, AlignBottom, furi_string_get_cstr(raw_data));
}
static void barcode_draw_callback(Canvas* canvas, void* ctx) {
furi_assert(ctx);
BarcodeModel* barcode_model = ctx;
BarcodeData* data = barcode_model->data;
// const char* barcode_digits =;
canvas_clear(canvas);
if(data->valid) {
switch(data->type_obj->type) {
case UPCA:
draw_upc_a(canvas, data);
break;
case EAN8:
draw_ean_8(canvas, data);
break;
case EAN13:
draw_ean_13(canvas, data);
break;
case CODE39:
draw_code_39(canvas, data);
break;
case CODE128:
draw_code_128(canvas, data);
break;
case UNKNOWN:
default:
break;
}
} else {
switch(data->reason) {
case WrongNumberOfDigits:
draw_error_str(canvas, "Wrong # of characters");
break;
case InvalidCharacters:
draw_error_str(canvas, "Invalid characters");
break;
case UnsupportedType:
draw_error_str(canvas, "Unsupported barcode type");
break;
case FileOpening:
draw_error_str(canvas, "Could not open file");
break;
case InvalidFileData:
draw_error_str(canvas, "Invalid file data");
break;
case MissingEncodingTable:
draw_error_str(canvas, "Missing encoding table");
break;
case EncodingTableError:
draw_error_str(canvas, "Encoding table error");
break;
default:
draw_error_str(canvas, "Could not read barcode data");
break;
}
}
}
bool barcode_input_callback(InputEvent* input_event, void* ctx) {
UNUSED(ctx);
//furi_assert(ctx);
//Barcode* test_view_object = ctx;
if(input_event->key == InputKeyBack) {
return false;
} else {
return true;
}
}
Barcode* barcode_view_allocate(BarcodeApp* barcode_app) {
furi_assert(barcode_app);
Barcode* barcode = malloc(sizeof(Barcode));
barcode->view = view_alloc();
barcode->barcode_app = barcode_app;
view_set_context(barcode->view, barcode);
view_allocate_model(barcode->view, ViewModelTypeLocking, sizeof(BarcodeModel));
view_set_draw_callback(barcode->view, barcode_draw_callback);
view_set_input_callback(barcode->view, barcode_input_callback);
return barcode;
}
void barcode_free_model(Barcode* barcode) {
with_view_model(
barcode->view,
BarcodeModel * model,
{
if(model->file_path != NULL) {
furi_string_free(model->file_path);
}
if(model->data != NULL) {
if(model->data->raw_data != NULL) {
furi_string_free(model->data->raw_data);
}
if(model->data->correct_data != NULL) {
furi_string_free(model->data->correct_data);
}
free(model->data);
}
},
false);
}
void barcode_free(Barcode* barcode) {
furi_assert(barcode);
barcode_free_model(barcode);
view_free(barcode->view);
free(barcode);
}
View* barcode_get_view(Barcode* barcode) {
furi_assert(barcode);
return barcode->view;
}

View File

@ -0,0 +1,23 @@
#pragma once
#include <gui/view.h>
typedef struct BarcodeApp BarcodeApp;
typedef struct {
View* view;
BarcodeApp* barcode_app;
} Barcode;
typedef struct {
FuriString* file_path;
BarcodeData* data;
} BarcodeModel;
Barcode* barcode_view_allocate(BarcodeApp* barcode_app);
void barcode_free_model(Barcode* barcode);
void barcode_free(Barcode* barcode);
View* barcode_get_view(Barcode* barcode);

View File

@ -0,0 +1,493 @@
#include "../barcode_app.h"
#include "create_view.h"
#include <math.h>
#define LINE_HEIGHT 16
#define TEXT_PADDING 4
#define TOTAL_MENU_ITEMS 5
typedef enum {
TypeMenuItem,
FileNameMenuItem,
BarcodeDataMenuItem,
SaveMenuButton,
DeleteMenuButton
} MenuItems;
/**
* Took this function from blackjack
* @author @teeebor
*/
void draw_menu_item(
Canvas* const canvas,
const char* text,
const char* value,
int y,
bool left_caret,
bool right_caret,
bool selected) {
UNUSED(selected);
if(y < 0 || y >= 64) {
return;
}
if(selected) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 0, y, 123, LINE_HEIGHT);
canvas_set_color(canvas, ColorWhite);
}
canvas_draw_str_aligned(canvas, 4, y + TEXT_PADDING, AlignLeft, AlignTop, text);
if(left_caret) {
canvas_draw_str_aligned(canvas, 60, y + TEXT_PADDING, AlignLeft, AlignTop, "<");
}
canvas_draw_str_aligned(canvas, 90, y + TEXT_PADDING, AlignCenter, AlignTop, value);
if(right_caret) {
canvas_draw_str_aligned(canvas, 120, y + TEXT_PADDING, AlignRight, AlignTop, ">");
}
canvas_set_color(canvas, ColorBlack);
}
void draw_button(Canvas* const canvas, const char* text, int y, bool selected) {
if(selected) {
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 0, y, 123, LINE_HEIGHT);
canvas_set_color(canvas, ColorWhite);
}
canvas_draw_str_aligned(canvas, 64, y + TEXT_PADDING, AlignCenter, AlignTop, text);
canvas_set_color(canvas, ColorBlack);
}
static void app_draw_callback(Canvas* canvas, void* ctx) {
furi_assert(ctx);
CreateViewModel* create_view_model = ctx;
BarcodeTypeObj* type_obj = create_view_model->barcode_type;
if(create_view_model->barcode_type == NULL) {
return;
}
BarcodeType selected_type = type_obj->type;
int selected_menu_item = create_view_model->selected_menu_item;
int total_menu_items = create_view_model->mode == EditMode ? TOTAL_MENU_ITEMS :
TOTAL_MENU_ITEMS - 1;
int startY = 0;
//the menu items index that is/would be in view
//int current_last_menu_item = selected_menu_item + 3;
if(selected_menu_item > 1) {
int offset = 2;
if(selected_menu_item + offset > total_menu_items) {
offset = 3;
}
startY -= (LINE_HEIGHT * (selected_menu_item - offset));
}
//ensure that the scroll height is atleast 1
int scrollHeight = ceil(64.0 / total_menu_items);
int scrollPos = scrollHeight * selected_menu_item;
canvas_set_color(canvas, ColorBlack);
//draw the scroll bar box
canvas_draw_box(canvas, 125, scrollPos, 3, scrollHeight);
//draw the scroll bar track
canvas_draw_box(canvas, 126, 0, 1, 64);
draw_menu_item(
canvas,
"Type",
type_obj->name,
TypeMenuItem * LINE_HEIGHT + startY,
selected_type > 0,
selected_type < NUMBER_OF_BARCODE_TYPES - 2,
selected_menu_item == TypeMenuItem);
draw_menu_item(
canvas,
"Name",
furi_string_empty(create_view_model->file_name) ?
"--" :
furi_string_get_cstr(create_view_model->file_name),
FileNameMenuItem * LINE_HEIGHT + startY,
false,
false,
selected_menu_item == FileNameMenuItem);
draw_menu_item(
canvas,
"Data",
furi_string_empty(create_view_model->barcode_data) ?
"--" :
furi_string_get_cstr(create_view_model->barcode_data),
BarcodeDataMenuItem * LINE_HEIGHT + startY,
false,
false,
selected_menu_item == BarcodeDataMenuItem);
draw_button(
canvas,
"Save",
SaveMenuButton * LINE_HEIGHT + startY,
selected_menu_item == SaveMenuButton);
if(create_view_model->mode == EditMode) {
draw_button(
canvas,
"Delete",
DeleteMenuButton * LINE_HEIGHT + startY,
selected_menu_item == DeleteMenuButton);
}
}
void text_input_callback(void* ctx) {
CreateView* create_view_object = ctx;
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
if(create_view_object->setter == FileNameSetter) {
furi_string_set_str(model->file_name, create_view_object->input);
}
if(create_view_object->setter == BarcodeDataSetter) {
furi_string_set_str(model->barcode_data, create_view_object->input);
}
},
true);
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, CreateBarcodeView);
}
static bool app_input_callback(InputEvent* input_event, void* ctx) {
furi_assert(ctx);
if(input_event->key == InputKeyBack) {
return false;
}
CreateView* create_view_object = ctx;
//get the currently selected menu item from the model
int selected_menu_item = 0;
BarcodeTypeObj* barcode_type = NULL;
FuriString* file_name;
FuriString* barcode_data;
CreateMode mode;
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
selected_menu_item = model->selected_menu_item;
barcode_type = model->barcode_type;
file_name = model->file_name;
barcode_data = model->barcode_data;
mode = model->mode;
},
true);
int total_menu_items = mode == EditMode ? TOTAL_MENU_ITEMS : TOTAL_MENU_ITEMS - 1;
if(input_event->type == InputTypePress) {
if(input_event->key == InputKeyUp && selected_menu_item > 0) {
selected_menu_item--;
} else if(input_event->key == InputKeyDown && selected_menu_item < total_menu_items - 1) {
selected_menu_item++;
} else if(input_event->key == InputKeyLeft) {
if(selected_menu_item == TypeMenuItem && barcode_type != NULL) { //Select Barcode Type
if(barcode_type->type > 0) {
barcode_type = barcode_type_objs[barcode_type->type - 1];
}
}
} else if(input_event->key == InputKeyRight) {
if(selected_menu_item == TypeMenuItem && barcode_type != NULL) { //Select Barcode Type
if(barcode_type->type < NUMBER_OF_BARCODE_TYPES - 2) {
barcode_type = barcode_type_objs[barcode_type->type + 1];
}
}
} else if(input_event->key == InputKeyOk) {
if(selected_menu_item == FileNameMenuItem && barcode_type != NULL) {
create_view_object->setter = FileNameSetter;
snprintf(
create_view_object->input,
sizeof(create_view_object->input),
"%s",
furi_string_get_cstr(file_name));
text_input_set_result_callback(
create_view_object->barcode_app->text_input,
text_input_callback,
create_view_object,
create_view_object->input,
TEXT_BUFFER_SIZE - BARCODE_EXTENSION_LENGTH, //remove the barcode length
//clear default text
false);
text_input_set_header_text(
create_view_object->barcode_app->text_input, "File Name");
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, TextInputView);
}
if(selected_menu_item == BarcodeDataMenuItem && barcode_type != NULL) {
create_view_object->setter = BarcodeDataSetter;
snprintf(
create_view_object->input,
sizeof(create_view_object->input),
"%s",
furi_string_get_cstr(barcode_data));
text_input_set_result_callback(
create_view_object->barcode_app->text_input,
text_input_callback,
create_view_object,
create_view_object->input,
TEXT_BUFFER_SIZE,
//clear default text
false);
text_input_set_header_text(
create_view_object->barcode_app->text_input, "Barcode Data");
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, TextInputView);
}
if(selected_menu_item == SaveMenuButton && barcode_type != NULL) {
save_barcode(create_view_object);
}
if(selected_menu_item == DeleteMenuButton && barcode_type != NULL) {
if(mode == EditMode) {
remove_barcode(create_view_object);
} else if(mode == NewMode) {
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, MainMenuView);
}
}
}
}
//change the currently selected menu item
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
model->selected_menu_item = selected_menu_item;
model->barcode_type = barcode_type;
},
true);
return true;
}
CreateView* create_view_allocate(BarcodeApp* barcode_app) {
furi_assert(barcode_app);
CreateView* create_view_object = malloc(sizeof(CreateView));
create_view_object->view = view_alloc();
create_view_object->barcode_app = barcode_app;
view_set_context(create_view_object->view, create_view_object);
view_allocate_model(create_view_object->view, ViewModelTypeLocking, sizeof(CreateViewModel));
view_set_draw_callback(create_view_object->view, app_draw_callback);
view_set_input_callback(create_view_object->view, app_input_callback);
return create_view_object;
}
void create_view_free_model(CreateView* create_view_object) {
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
if(model->file_path != NULL) {
furi_string_free(model->file_path);
}
if(model->file_name != NULL) {
furi_string_free(model->file_name);
}
if(model->barcode_data != NULL) {
furi_string_free(model->barcode_data);
}
},
true);
}
void remove_barcode(CreateView* create_view_object) {
Storage* storage = furi_record_open(RECORD_STORAGE);
bool success = false;
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
FURI_LOG_I(TAG, "Attempting to remove file");
if(model->file_path != NULL) {
FURI_LOG_I(TAG, "Removing File: %s", furi_string_get_cstr(model->file_path));
if(storage_simply_remove(storage, furi_string_get_cstr(model->file_path))) {
FURI_LOG_I(
TAG,
"File: \"%s\" was successfully removed",
furi_string_get_cstr(model->file_path));
success = true;
} else {
FURI_LOG_E(TAG, "Unable to remove file!");
success = false;
}
} else {
FURI_LOG_E(TAG, "Could not remove barcode file");
success = false;
}
},
true);
furi_record_close(RECORD_STORAGE);
with_view_model(
create_view_object->barcode_app->message_view->view,
MessageViewModel * model,
{
if(success) {
model->message = "File Deleted";
} else {
model->message = "Could not delete file";
}
},
true);
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, MessageErrorView);
}
void save_barcode(CreateView* create_view_object) {
BarcodeTypeObj* barcode_type = NULL;
FuriString* file_path; //this may be empty
FuriString* file_name;
FuriString* barcode_data;
CreateMode mode;
with_view_model(
create_view_object->view,
CreateViewModel * model,
{
file_path = model->file_path;
file_name = model->file_name;
barcode_data = model->barcode_data;
barcode_type = model->barcode_type;
mode = model->mode;
},
true);
if(file_name == NULL || furi_string_empty(file_name)) {
FURI_LOG_E(TAG, "File Name cannot be empty");
return;
}
if(barcode_data == NULL || furi_string_empty(barcode_data)) {
FURI_LOG_E(TAG, "Barcode Data cannot be empty");
return;
}
if(barcode_type == NULL) {
FURI_LOG_E(TAG, "Type not defined");
return;
}
bool success = false;
FuriString* full_file_path = furi_string_alloc_set(DEFAULT_USER_BARCODES);
furi_string_push_back(full_file_path, '/');
furi_string_cat(full_file_path, file_name);
furi_string_cat_str(full_file_path, BARCODE_EXTENSION);
Storage* storage = furi_record_open(RECORD_STORAGE);
if(mode == EditMode) {
if(!furi_string_empty(file_path)) {
if(!furi_string_equal(file_path, full_file_path)) {
FS_Error error = storage_common_rename(
storage,
furi_string_get_cstr(file_path),
furi_string_get_cstr(full_file_path));
if(error != FSE_OK) {
FURI_LOG_E(TAG, "Rename error: %s", storage_error_get_desc(error));
} else {
FURI_LOG_I(TAG, "Rename Success");
}
}
}
}
FlipperFormat* ff = flipper_format_file_alloc(storage);
FURI_LOG_I(TAG, "Saving Barcode to: %s", furi_string_get_cstr(full_file_path));
bool file_opened_status = false;
if(mode == NewMode) {
file_opened_status =
flipper_format_file_open_new(ff, furi_string_get_cstr(full_file_path));
} else if(mode == EditMode) {
file_opened_status =
flipper_format_file_open_always(ff, furi_string_get_cstr(full_file_path));
}
if(file_opened_status) {
// Filetype: Barcode
// Version: 1
// # Types - UPC-A, EAN-8, EAN-13, CODE-39
// Type: CODE-39
// Data: AB
flipper_format_write_string_cstr(ff, "Filetype", "Barcode");
flipper_format_write_string_cstr(ff, "Version", FILE_VERSION);
flipper_format_write_comment_cstr(ff, "Types - UPC-A, EAN-8, EAN-13, CODE-39, CODE-128");
flipper_format_write_string_cstr(ff, "Type", barcode_type->name);
flipper_format_write_string_cstr(ff, "Data", furi_string_get_cstr(barcode_data));
success = true;
} else {
FURI_LOG_E(TAG, "Save error");
success = false;
}
furi_string_free(full_file_path);
flipper_format_free(ff);
furi_record_close(RECORD_STORAGE);
with_view_model(
create_view_object->barcode_app->message_view->view,
MessageViewModel * model,
{
if(success) {
model->message = "File Saved!";
} else {
model->message = "A saving error has occurred";
}
},
true);
view_dispatcher_switch_to_view(
create_view_object->barcode_app->view_dispatcher, MessageErrorView);
}
void create_view_free(CreateView* create_view_object) {
furi_assert(create_view_object);
create_view_free_model(create_view_object);
view_free(create_view_object->view);
free(create_view_object);
}
View* create_get_view(CreateView* create_view_object) {
furi_assert(create_view_object);
return create_view_object->view;
}

View File

@ -0,0 +1,46 @@
#pragma once
#include <gui/view.h>
typedef struct BarcodeApp BarcodeApp;
typedef enum {
FileNameSetter,
BarcodeDataSetter
} InputSetter; //what value to set for the text input view
typedef enum {
EditMode,
NewMode
} CreateMode;
typedef struct {
View* view;
BarcodeApp* barcode_app;
InputSetter setter;
char input[TEXT_BUFFER_SIZE];
} CreateView;
typedef struct {
int selected_menu_item;
CreateMode mode;
BarcodeTypeObj* barcode_type;
FuriString* file_path; //the current file that is opened
FuriString* file_name;
FuriString* barcode_data;
} CreateViewModel;
CreateView* create_view_allocate(BarcodeApp* barcode_app);
void remove_barcode(CreateView* create_view_object);
void save_barcode(CreateView* create_view_object);
void create_view_free_model(CreateView* create_view_object);
void create_view_free(CreateView* create_view_object);
View* create_get_view(CreateView* create_view_object);

View File

@ -0,0 +1,77 @@
#include "../barcode_app.h"
#include "message_view.h"
static void app_draw_callback(Canvas* canvas, void* ctx) {
furi_assert(ctx);
MessageViewModel* message_view_model = ctx;
canvas_clear(canvas);
if(message_view_model->message != NULL) {
canvas_draw_str_aligned(
canvas, 62, 30, AlignCenter, AlignCenter, message_view_model->message);
}
canvas_set_color(canvas, ColorBlack);
canvas_draw_box(canvas, 100, 52, 28, 12);
canvas_set_color(canvas, ColorWhite);
canvas_draw_str_aligned(canvas, 114, 58, AlignCenter, AlignCenter, "OK");
}
static bool app_input_callback(InputEvent* input_event, void* ctx) {
furi_assert(ctx);
MessageView* message_view_object = ctx;
if(input_event->key == InputKeyBack) {
view_dispatcher_switch_to_view(
message_view_object->barcode_app->view_dispatcher, MainMenuView);
}
if(input_event->type == InputTypeShort) {
if(input_event->key == InputKeyOk) {
view_dispatcher_switch_to_view(
message_view_object->barcode_app->view_dispatcher, MainMenuView);
}
}
return true;
}
MessageView* message_view_allocate(BarcodeApp* barcode_app) {
furi_assert(barcode_app);
MessageView* message_view_object = malloc(sizeof(MessageView));
message_view_object->view = view_alloc();
message_view_object->barcode_app = barcode_app;
view_set_context(message_view_object->view, message_view_object);
view_allocate_model(message_view_object->view, ViewModelTypeLocking, sizeof(MessageViewModel));
view_set_draw_callback(message_view_object->view, app_draw_callback);
view_set_input_callback(message_view_object->view, app_input_callback);
return message_view_object;
}
void message_view_free_model(MessageView* message_view_object) {
with_view_model(
message_view_object->view,
MessageViewModel * model,
{
},
true);
}
void message_view_free(MessageView* message_view_object) {
furi_assert(message_view_object);
message_view_free_model(message_view_object);
view_free(message_view_object->view);
free(message_view_object);
}
View* message_get_view(MessageView* message_view_object) {
furi_assert(message_view_object);
return message_view_object->view;
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <gui/view.h>
typedef struct BarcodeApp BarcodeApp;
typedef struct {
View* view;
BarcodeApp* barcode_app;
} MessageView;
typedef struct {
const char* message;
} MessageViewModel;
MessageView* message_view_allocate(BarcodeApp* barcode_app);
void message_view_free_model(MessageView* message_view_object);
void message_view_free(MessageView* message_view_object);
View* message_get_view(MessageView* message_view_object);

View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@ -0,0 +1,14 @@
# BPM Tapper
A BPM Tapper for the Flipper Zero.
![screenshot](img/screenshot.png)
Hit any button other than back repeatedly. Calculates based on the average of the last 8 inputs.
## Compiling
```
./fbt firmware_bpm_tapper
```

View File

@ -0,0 +1,13 @@
App(
appid="BPM_Tapper",
name="BPM Tapper",
apptype=FlipperAppType.EXTERNAL,
entry_point="bpm_tapper_app",
cdefines=["APP_BPM_TAPPER"],
requires=["gui"],
stack_size=2 * 1024,
fap_icon="bpm_10px.png",
fap_category="Music_Extra",
fap_icon_assets="icons",
order=15,
)

View File

@ -0,0 +1,262 @@
#include <furi.h>
#include <furi_hal.h>
#include <dialogs/dialogs.h>
#include <gui/gui.h>
#include <input/input.h>
#include <stdlib.h>
#include "BPM_Tapper_icons.h"
typedef enum {
EventTypeTick,
EventTypeKey,
} EventType;
typedef struct {
EventType type;
InputEvent input;
} PluginEvent;
//QUEUE
struct node {
int interval;
struct node* next;
};
typedef struct node node;
typedef struct {
int size;
int max_size;
node* front;
node* rear;
} queue;
static void init_queue(queue* q) {
q->size = 0;
q->max_size = 8;
q->front = NULL;
q->rear = NULL;
}
static void queue_remove(queue* q) {
node* tmp;
tmp = q->front;
q->front = q->front->next;
q->size--;
free(tmp);
}
static void queue_add(queue* q, int value) {
node* tmp = malloc(sizeof(node));
tmp->interval = value;
tmp->next = NULL;
if(q->size == q->max_size) {
queue_remove(q);
}
// check if empty
if(q->rear == NULL) {
q->front = tmp;
q->rear = tmp;
} else {
q->rear->next = tmp;
q->rear = tmp;
}
q->size++;
}
static float queue_avg(queue* q) {
float avg = 0.0;
if(q->size == 0) {
return avg;
} else {
node* tmp;
float sum = 0.0;
tmp = q->front;
while(tmp != NULL) {
sum = sum + tmp->interval;
tmp = tmp->next;
}
avg = sum / q->size;
FURI_LOG_D("BPM-Tapper", "Sum: %.2f Avg: %.2f", (double)sum, (double)avg);
return avg;
}
}
// TOO SLOW!
//uint64_t dolphin_state_timestamp() {
// FuriHalRtcDateTime datetime;
// furi_hal_rtc_get_datetime(&datetime);
// return furi_hal_rtc_datetime_to_timestamp(&datetime);
//}
//
typedef struct {
int taps;
double bpm;
uint32_t last_stamp;
uint32_t interval;
queue* tap_queue;
} BPMTapper;
static void show_hello() {
// BEGIN HELLO DIALOG
DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS);
DialogMessage* message = dialog_message_alloc();
const char* header_text = "BPM Tapper";
const char* message_text = "Tap center to start";
dialog_message_set_header(message, header_text, 63, 3, AlignCenter, AlignTop);
dialog_message_set_text(message, message_text, 0, 17, AlignLeft, AlignTop);
dialog_message_set_buttons(message, NULL, "Tap", NULL);
dialog_message_set_icon(message, &I_DolphinCommon_56x48, 72, 17);
dialog_message_show(dialogs, message);
dialog_message_free(message);
furi_record_close(RECORD_DIALOGS);
// END HELLO DIALOG
}
static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
furi_assert(event_queue);
PluginEvent event = {.type = EventTypeKey, .input = *input_event};
furi_message_queue_put(event_queue, &event, FuriWaitForever);
}
static void render_callback(Canvas* const canvas, void* ctx) {
FuriString* tempStr;
const BPMTapper* bpm_state = acquire_mutex((ValueMutex*)ctx, 25);
if(bpm_state == NULL) {
return;
}
// border
//canvas_draw_frame(canvas, 0, 0, 128, 64);
canvas_set_font(canvas, FontPrimary);
tempStr = furi_string_alloc();
furi_string_printf(tempStr, "Taps: %d", bpm_state->taps);
canvas_draw_str_aligned(canvas, 5, 10, AlignLeft, AlignBottom, furi_string_get_cstr(tempStr));
furi_string_reset(tempStr);
furi_string_printf(tempStr, "Queue: %d", bpm_state->tap_queue->size);
canvas_draw_str_aligned(canvas, 70, 10, AlignLeft, AlignBottom, furi_string_get_cstr(tempStr));
furi_string_reset(tempStr);
furi_string_printf(tempStr, "Interval: %ldms", bpm_state->interval);
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignBottom, furi_string_get_cstr(tempStr));
furi_string_reset(tempStr);
furi_string_printf(tempStr, "x2 %.2f /2 %.2f", bpm_state->bpm * 2, bpm_state->bpm / 2);
canvas_draw_str_aligned(
canvas, 64, 60, AlignCenter, AlignCenter, furi_string_get_cstr(tempStr));
furi_string_reset(tempStr);
furi_string_printf(tempStr, "%.2f", bpm_state->bpm);
canvas_set_font(canvas, FontBigNumbers);
canvas_draw_str_aligned(
canvas, 64, 40, AlignCenter, AlignCenter, furi_string_get_cstr(tempStr));
furi_string_reset(tempStr);
furi_string_free(tempStr);
release_mutex((ValueMutex*)ctx, bpm_state);
}
static void bpm_state_init(BPMTapper* const plugin_state) {
plugin_state->taps = 0;
plugin_state->bpm = 120.0;
plugin_state->last_stamp = 0; // furi_get_tick();
plugin_state->interval = 0;
queue* q;
q = malloc(sizeof(queue));
init_queue(q);
plugin_state->tap_queue = q;
}
int32_t bpm_tapper_app(void* p) {
UNUSED(p);
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
BPMTapper* bpm_state = malloc(sizeof(BPMTapper));
// setup
bpm_state_init(bpm_state);
ValueMutex state_mutex;
if(!init_mutex(&state_mutex, bpm_state, sizeof(bpm_state))) {
FURI_LOG_E("BPM-Tapper", "cannot create mutex\r\n");
free(bpm_state);
return 255;
}
show_hello();
// BEGIN IMPLEMENTATION
// Set system callbacks
ViewPort* view_port = view_port_alloc();
view_port_draw_callback_set(view_port, render_callback, &state_mutex);
view_port_input_callback_set(view_port, input_callback, event_queue);
// Open GUI and register view_port
Gui* gui = furi_record_open("gui");
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
PluginEvent event;
for(bool processing = true; processing;) {
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
BPMTapper* bpm_state = (BPMTapper*)acquire_mutex_block(&state_mutex);
if(event_status == FuriStatusOk) {
// press events
if(event.type == EventTypeKey) {
if(event.input.type == InputTypePress) {
switch(event.input.key) {
case InputKeyUp:
case InputKeyDown:
case InputKeyRight:
case InputKeyLeft:
case InputKeyOk:
bpm_state->taps++;
uint32_t new_stamp = furi_get_tick();
if(bpm_state->last_stamp == 0) {
bpm_state->last_stamp = new_stamp;
break;
}
bpm_state->interval = new_stamp - bpm_state->last_stamp;
bpm_state->last_stamp = new_stamp;
queue_add(bpm_state->tap_queue, bpm_state->interval);
float avg = queue_avg(bpm_state->tap_queue);
float bps = 1.0 / (avg / 1000.0);
bpm_state->bpm = bps * 60.0;
break;
case InputKeyBack:
// Exit the plugin
processing = false;
break;
default:
break;
}
}
}
} else {
FURI_LOG_D("BPM-Tapper", "FuriMessageQueue: event timeout");
// event timeout
}
view_port_update(view_port);
release_mutex(&state_mutex, bpm_state);
}
view_port_enabled_set(view_port, false);
gui_remove_view_port(gui, view_port);
furi_record_close("gui");
view_port_free(view_port);
furi_message_queue_free(event_queue);
delete_mutex(&state_mutex);
queue* q = bpm_state->tap_queue;
free(q);
free(bpm_state);
return 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,10 @@
# FlipperZeroBrainfuck
Brainfuck interpreter and editor for the F0.
Supports text inputs and outputs.
No protection against infinite loops or syntax errors.
Major limitation is that programs MUST terminate, or it will freeze at "RUNNING"
![Screenshot-20230117-202147](https://user-images.githubusercontent.com/16545187/213004616-8846e897-506e-4510-8012-fd2fe2bbe8a1.png)
![Screenshot-20230117-202208](https://user-images.githubusercontent.com/16545187/213004659-d74751d2-76c4-4a7b-a0f2-f58623478b95.png)

View File

@ -0,0 +1,14 @@
App(
appid="Brainfuck",
name="Brainfuck",
apptype=FlipperAppType.EXTERNAL,
entry_point="brainfuck_app",
requires=[
"storage",
"gui",
],
stack_size=8 * 1024,
fap_icon="bfico.png",
fap_category="Misc_Extra",
fap_icon_assets="icons",
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,137 @@
#include "brainfuck_i.h"
/*
Due to the lack of documentation on the flipper i copied the picopass app,
ripped its insides out and used its hollow corpse to build this app inside of.
i dont know how this stuff works and after 6 hours of trying to learn it, i dont care
*/
bool brainfuck_custom_event_callback(void* context, uint32_t event) {
furi_assert(context);
BFApp* brainfuck = context;
return scene_manager_handle_custom_event(brainfuck->scene_manager, event);
}
bool brainfuck_back_event_callback(void* context) {
furi_assert(context);
BFApp* brainfuck = context;
return scene_manager_handle_back_event(brainfuck->scene_manager);
}
BFApp* brainfuck_alloc() {
BFApp* brainfuck = malloc(sizeof(BFApp));
brainfuck->dataSize = 0;
brainfuck->view_dispatcher = view_dispatcher_alloc();
brainfuck->scene_manager = scene_manager_alloc(&brainfuck_scene_handlers, brainfuck);
view_dispatcher_enable_queue(brainfuck->view_dispatcher);
view_dispatcher_set_event_callback_context(brainfuck->view_dispatcher, brainfuck);
view_dispatcher_set_custom_event_callback(brainfuck->view_dispatcher, brainfuck_custom_event_callback);
view_dispatcher_set_navigation_event_callback(brainfuck->view_dispatcher, brainfuck_back_event_callback);
// Open GUI record
brainfuck->gui = furi_record_open(RECORD_GUI);
view_dispatcher_attach_to_gui(brainfuck->view_dispatcher, brainfuck->gui, ViewDispatcherTypeFullscreen);
// Open Notification record
brainfuck->notifications = furi_record_open(RECORD_NOTIFICATION);
// Submenu
brainfuck->submenu = submenu_alloc();
view_dispatcher_add_view(brainfuck->view_dispatcher, brainfuckViewMenu, submenu_get_view(brainfuck->submenu));
// Popup
brainfuck->popup = popup_alloc();
view_dispatcher_add_view(brainfuck->view_dispatcher, brainfuckViewPopup, popup_get_view(brainfuck->popup));
// Text Input
brainfuck->text_input = text_input_alloc();
view_dispatcher_add_view(brainfuck->view_dispatcher, brainfuckViewTextInput, text_input_get_view(brainfuck->text_input));
// Textbox
brainfuck->text_box = text_box_alloc();
view_dispatcher_add_view(brainfuck->view_dispatcher, brainfuckViewTextBox, text_box_get_view(brainfuck->text_box));
brainfuck->text_box_store = furi_string_alloc();
// Dev environment
brainfuck->BF_dev_env = bf_dev_env_alloc(brainfuck);
view_dispatcher_add_view(brainfuck->view_dispatcher, brainfuckViewDev, bf_dev_env_get_view(brainfuck->BF_dev_env));
// File path
brainfuck->BF_file_path = furi_string_alloc();
return brainfuck;
}
void brainfuck_free(BFApp* brainfuck) {
furi_assert(brainfuck);
// Submenu
view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewMenu);
submenu_free(brainfuck->submenu);
// Popup
view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewPopup);
popup_free(brainfuck->popup);
// TextInput
view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewTextInput);
text_input_free(brainfuck->text_input);
// TextBox
view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewTextBox);
text_box_free(brainfuck->text_box);
furi_string_free(brainfuck->text_box_store);
//dev env
view_dispatcher_remove_view(brainfuck->view_dispatcher, brainfuckViewDev);
bf_dev_env_free(brainfuck->BF_dev_env);
// View Dispatcher
view_dispatcher_free(brainfuck->view_dispatcher);
// Scene Manager
scene_manager_free(brainfuck->scene_manager);
// GUI
furi_record_close(RECORD_GUI);
brainfuck->gui = NULL;
// Notifications
furi_record_close(RECORD_NOTIFICATION);
brainfuck->notifications = NULL;
free(brainfuck);
}
void brainfuck_show_loading_popup(void* context, bool show) {
BFApp* brainfuck = context;
TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME);
if(show) {
// Raise timer priority so that animations can play
vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1);
view_dispatcher_switch_to_view(brainfuck->view_dispatcher, brainfuckViewLoading);
} else {
// Restore default timer priority
vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY);
}
}
int32_t brainfuck_app(void* p) {
UNUSED(p);
BFApp* brainfuck = brainfuck_alloc();
if(!brainfuck){ return 0; }
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, "/ext/brainfuck");
scene_manager_next_scene(brainfuck->scene_manager, brainfuckSceneStart);
view_dispatcher_run(brainfuck->view_dispatcher);
brainfuck_free(brainfuck);
return 0;
}

View File

@ -0,0 +1,3 @@
#pragma once
typedef struct BFApp BFApp;

View File

@ -0,0 +1,86 @@
#pragma once
typedef struct BFDevEnv BFDevEnv;
typedef struct BFExecEnv BFExecEnv;
typedef unsigned char byte;
#include "brainfuck.h"
#include "worker.h"
#include <furi.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <notification/notification_messages.h>
#include <gui/modules/submenu.h>
#include <gui/modules/popup.h>
#include <gui/modules/loading.h>
#include <gui/modules/text_input.h>
#include <gui/modules/widget.h>
#include <gui/modules/text_box.h>
#include <dialogs/dialogs.h>
#include <input/input.h>
#include "scenes/brainfuck_scene.h"
#include "views/bf_dev_env.h"
#include <storage/storage.h>
#include <lib/toolbox/path.h>
#include <Brainfuck_icons.h>
#include <storage/storage.h>
#include <stream/stream.h>
#include <stream/buffered_file_stream.h>
#include <toolbox/stream/file_stream.h>
#define BF_INST_BUFFER_SIZE 2048
#define BF_OUTPUT_SIZE 512
#define BF_STACK_INITIAL_SIZE 128
#define BF_INPUT_BUFFER_SIZE 64
#define BF_STACK_STEP_SIZE 32
enum brainfuckCustomEvent {
// Reserve first 100 events for button types and indexes, starting from 0
brainfuckCustomEventReserved = 100,
brainfuckCustomEventViewExit,
brainfuckCustomEventWorkerExit,
brainfuckCustomEventByteInputDone,
brainfuckCustomEventTextInputDone,
};
typedef enum {
EventTypeTick,
EventTypeKey,
} EventType;
struct BFApp {
ViewDispatcher* view_dispatcher;
Gui* gui;
NotificationApp* notifications;
SceneManager* scene_manager;
Submenu* submenu;
Popup* popup;
TextInput* text_input;
TextBox* text_box;
FuriString* text_box_store;
FuriString* BF_file_path;
BFDevEnv* BF_dev_env;
int dataSize;
char dataBuffer[BF_INST_BUFFER_SIZE];
char inputBuffer[BF_INPUT_BUFFER_SIZE];
};
typedef enum {
brainfuckViewMenu,
brainfuckViewPopup,
brainfuckViewLoading,
brainfuckViewTextInput,
brainfuckViewTextBox,
brainfuckViewWidget,
brainfuckViewDev,
brainfuckViewExec,
} brainfuckView;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,30 @@
#include "brainfuck_scene.h"
// Generate scene on_enter handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
void (*const brainfuck_on_enter_handlers[])(void*) = {
#include "brainfuck_scene_config.h"
};
#undef ADD_SCENE
// Generate scene on_event handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
bool (*const brainfuck_on_event_handlers[])(void* context, SceneManagerEvent event) = {
#include "brainfuck_scene_config.h"
};
#undef ADD_SCENE
// Generate scene on_exit handlers array
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
void (*const brainfuck_on_exit_handlers[])(void* context) = {
#include "brainfuck_scene_config.h"
};
#undef ADD_SCENE
// Initialize scene handlers configuration structure
const SceneManagerHandlers brainfuck_scene_handlers = {
.on_enter_handlers = brainfuck_on_enter_handlers,
.on_event_handlers = brainfuck_on_event_handlers,
.on_exit_handlers = brainfuck_on_exit_handlers,
.scene_num = brainfuckSceneNum,
};

Some files were not shown because too many files have changed in this diff Show More