Reorg, added more sources
@@ -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.
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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;
|
||||
* 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:
|
||||
|
||||

|
||||
|
||||
Expectation:
|
||||
|
||||

|
||||
|
||||
Reality:
|
||||
|
||||

|
||||
|
||||
|
||||
## Software
|
||||
|
||||
The code is based on the original Bosch [driver](https://github.com/BoschSensortec/BMI160_driver/) and an orientation tracking implementation from the [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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
App(
|
||||
appid="BMI160_Air_Mouse",
|
||||
name="[BMI160] Air Mouse",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="air_mouse_app",
|
||||
stack_size=10 * 1024,
|
||||
fap_icon="mouse_10px.png",
|
||||
fap_category="GPIO",
|
||||
)
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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]);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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, ®.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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -0,0 +1,303 @@
|
||||
#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_MOVE_SHORT 5
|
||||
#define MOUSE_MOVE_LONG 20
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,129 @@
|
||||
#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");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,12 @@
|
||||
App(
|
||||
appid="Arkanoid",
|
||||
name="Arkanoid",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="arkanoid_game_app",
|
||||
cdefines=["APP_ARKANOID_GAME"],
|
||||
requires=["gui"],
|
||||
stack_size=1 * 1024,
|
||||
order=20,
|
||||
fap_icon="arkanoid_10px.png",
|
||||
fap_category="Games",
|
||||
)
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,474 @@
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdlib.h>
|
||||
#include <gui/view.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#define TAG "Arkanoid"
|
||||
|
||||
#define FLIPPER_LCD_WIDTH 128
|
||||
#define FLIPPER_LCD_HEIGHT 64
|
||||
#define MAX_SPEED 3
|
||||
|
||||
typedef enum { EventTypeTick, EventTypeKey } EventType;
|
||||
|
||||
typedef struct {
|
||||
//Brick Bounds used in collision detection
|
||||
int leftBrick;
|
||||
int rightBrick;
|
||||
int topBrick;
|
||||
int bottomBrick;
|
||||
bool isHit[4][13]; //Array of if bricks are hit or not
|
||||
} BrickState;
|
||||
|
||||
typedef struct {
|
||||
int dx; //Initial movement of ball
|
||||
int dy; //Initial movement of ball
|
||||
int xb; //Balls starting possition
|
||||
int yb; //Balls starting possition
|
||||
bool released; //If the ball has been released by the player
|
||||
//Ball Bounds used in collision detection
|
||||
int leftBall;
|
||||
int rightBall;
|
||||
int topBall;
|
||||
int bottomBall;
|
||||
} BallState;
|
||||
|
||||
typedef struct {
|
||||
BallState ball_state;
|
||||
BrickState brick_state;
|
||||
NotificationApp* notify;
|
||||
unsigned int COLUMNS; //Columns of bricks
|
||||
unsigned int ROWS; //Rows of bricks
|
||||
bool initialDraw; //If the inital draw has happened
|
||||
int xPaddle; //X position of paddle
|
||||
char text[16]; //General string buffer
|
||||
bool bounced; //Used to fix double bounce glitch
|
||||
int lives; //Amount of lives
|
||||
int level; //Current level
|
||||
unsigned int score; //Score for the game
|
||||
unsigned int brickCount; //Amount of bricks hit
|
||||
int tick; //Tick counter
|
||||
bool gameStarted; // Did the game start?
|
||||
int speed; // Ball speed
|
||||
} ArkanoidState;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} GameEvent;
|
||||
|
||||
static const NotificationSequence sequence_short_sound = {
|
||||
&message_note_c5,
|
||||
&message_delay_50,
|
||||
&message_sound_off,
|
||||
NULL,
|
||||
};
|
||||
|
||||
// generate number in range [min,max)
|
||||
int rand_range(int min, int max) {
|
||||
return min + rand() % (max - min);
|
||||
}
|
||||
|
||||
void move_ball(Canvas* canvas, ArkanoidState* st) {
|
||||
st->tick++;
|
||||
|
||||
int current_speed = abs(st->speed - 1 - MAX_SPEED);
|
||||
if(st->tick % current_speed != 0 && st->tick % (current_speed + 1) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(st->ball_state.released) {
|
||||
//Move ball
|
||||
if(abs(st->ball_state.dx) == 2) {
|
||||
st->ball_state.xb += st->ball_state.dx / 2;
|
||||
// 2x speed is really 1.5 speed
|
||||
if((st->tick / current_speed) % 2 == 0) st->ball_state.xb += st->ball_state.dx / 2;
|
||||
} else {
|
||||
st->ball_state.xb += st->ball_state.dx;
|
||||
}
|
||||
st->ball_state.yb = st->ball_state.yb + st->ball_state.dy;
|
||||
|
||||
//Set bounds
|
||||
st->ball_state.leftBall = st->ball_state.xb;
|
||||
st->ball_state.rightBall = st->ball_state.xb + 2;
|
||||
st->ball_state.topBall = st->ball_state.yb;
|
||||
st->ball_state.bottomBall = st->ball_state.yb + 2;
|
||||
|
||||
//Bounce off top edge
|
||||
if(st->ball_state.yb <= 0) {
|
||||
st->ball_state.yb = 2;
|
||||
st->ball_state.dy = -st->ball_state.dy;
|
||||
}
|
||||
|
||||
//Lose a life if bottom edge hit
|
||||
if(st->ball_state.yb >= FLIPPER_LCD_HEIGHT) {
|
||||
canvas_draw_frame(canvas, st->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
||||
st->xPaddle = 54;
|
||||
st->ball_state.yb = 60;
|
||||
st->ball_state.released = false;
|
||||
st->lives--;
|
||||
st->gameStarted = false;
|
||||
|
||||
if(rand_range(0, 2) == 0) {
|
||||
st->ball_state.dx = 1;
|
||||
} else {
|
||||
st->ball_state.dx = -1;
|
||||
}
|
||||
}
|
||||
|
||||
//Bounce off left side
|
||||
if(st->ball_state.xb <= 0) {
|
||||
st->ball_state.xb = 2;
|
||||
st->ball_state.dx = -st->ball_state.dx;
|
||||
}
|
||||
|
||||
//Bounce off right side
|
||||
if(st->ball_state.xb >= FLIPPER_LCD_WIDTH - 2) {
|
||||
st->ball_state.xb = FLIPPER_LCD_WIDTH - 4;
|
||||
st->ball_state.dx = -st->ball_state.dx;
|
||||
// arduboy.tunes.tone(523, 250);
|
||||
}
|
||||
|
||||
//Bounce off paddle
|
||||
if(st->ball_state.xb + 1 >= st->xPaddle && st->ball_state.xb <= st->xPaddle + 12 &&
|
||||
st->ball_state.yb + 2 >= FLIPPER_LCD_HEIGHT - 1 &&
|
||||
st->ball_state.yb <= FLIPPER_LCD_HEIGHT) {
|
||||
st->ball_state.dy = -st->ball_state.dy;
|
||||
st->ball_state.dx =
|
||||
((st->ball_state.xb - (st->xPaddle + 6)) / 3); //Applies spin on the ball
|
||||
// prevent straight bounce, but not prevent roguuemaster from stealing
|
||||
if(st->ball_state.dx == 0) {
|
||||
st->ball_state.dx = (rand_range(0, 2) == 1) ? 1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
//Bounce off Bricks
|
||||
for(unsigned int row = 0; row < st->ROWS; row++) {
|
||||
for(unsigned int column = 0; column < st->COLUMNS; column++) {
|
||||
if(!st->brick_state.isHit[row][column]) {
|
||||
//Sets Brick bounds
|
||||
st->brick_state.leftBrick = 10 * column;
|
||||
st->brick_state.rightBrick = 10 * column + 10;
|
||||
st->brick_state.topBrick = 6 * row + 1;
|
||||
st->brick_state.bottomBrick = 6 * row + 7;
|
||||
|
||||
//If A collison has occured
|
||||
if(st->ball_state.topBall <= st->brick_state.bottomBrick &&
|
||||
st->ball_state.bottomBall >= st->brick_state.topBrick &&
|
||||
st->ball_state.leftBall <= st->brick_state.rightBrick &&
|
||||
st->ball_state.rightBall >= st->brick_state.leftBrick) {
|
||||
st->score += st->level;
|
||||
// Blink led when we hit some brick
|
||||
notification_message(st->notify, &sequence_short_sound);
|
||||
//notification_message(st->notify, &sequence_blink_white_100);
|
||||
|
||||
st->brickCount++;
|
||||
st->brick_state.isHit[row][column] = true;
|
||||
canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
|
||||
|
||||
//Vertical collision
|
||||
if(st->ball_state.bottomBall > st->brick_state.bottomBrick ||
|
||||
st->ball_state.topBall < st->brick_state.topBrick) {
|
||||
//Only bounce once each ball move
|
||||
if(!st->bounced) {
|
||||
st->ball_state.dy = -st->ball_state.dy;
|
||||
st->ball_state.yb += st->ball_state.dy;
|
||||
st->bounced = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Hoizontal collision
|
||||
if(st->ball_state.leftBall < st->brick_state.leftBrick ||
|
||||
st->ball_state.rightBall > st->brick_state.rightBrick) {
|
||||
//Only bounce once brick each ball move
|
||||
if(!st->bounced) {
|
||||
st->ball_state.dx = -st->ball_state.dx;
|
||||
st->ball_state.xb += st->ball_state.dx;
|
||||
st->bounced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Reset Bounce
|
||||
st->bounced = false;
|
||||
} else {
|
||||
//Ball follows paddle
|
||||
st->ball_state.xb = st->xPaddle + 5;
|
||||
}
|
||||
}
|
||||
|
||||
void draw_lives(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
||||
if(arkanoid_state->lives == 3) {
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
||||
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12);
|
||||
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 15);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 15);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 16);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 16);
|
||||
} else if(arkanoid_state->lives == 2) {
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
||||
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 11);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 11);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 12);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 12);
|
||||
} else {
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 7);
|
||||
canvas_draw_dot(canvas, 4, FLIPPER_LCD_HEIGHT - 8);
|
||||
canvas_draw_dot(canvas, 3, FLIPPER_LCD_HEIGHT - 8);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_score(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
||||
snprintf(arkanoid_state->text, sizeof(arkanoid_state->text), "%u", arkanoid_state->score);
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
FLIPPER_LCD_WIDTH - 2,
|
||||
FLIPPER_LCD_HEIGHT - 6,
|
||||
AlignRight,
|
||||
AlignBottom,
|
||||
arkanoid_state->text);
|
||||
}
|
||||
|
||||
void draw_ball(Canvas* canvas, ArkanoidState* ast) {
|
||||
canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb);
|
||||
canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb);
|
||||
canvas_draw_dot(canvas, ast->ball_state.xb, ast->ball_state.yb + 1);
|
||||
canvas_draw_dot(canvas, ast->ball_state.xb + 1, ast->ball_state.yb + 1);
|
||||
|
||||
move_ball(canvas, ast);
|
||||
}
|
||||
|
||||
void draw_paddle(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
||||
canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
||||
}
|
||||
|
||||
void reset_level(Canvas* canvas, ArkanoidState* arkanoid_state) {
|
||||
//Undraw paddle
|
||||
canvas_draw_frame(canvas, arkanoid_state->xPaddle, FLIPPER_LCD_HEIGHT - 1, 11, 1);
|
||||
|
||||
//Undraw ball
|
||||
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb);
|
||||
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb);
|
||||
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb, arkanoid_state->ball_state.yb + 1);
|
||||
canvas_draw_dot(canvas, arkanoid_state->ball_state.xb + 1, arkanoid_state->ball_state.yb + 1);
|
||||
|
||||
//Alter various variables to reset the game
|
||||
arkanoid_state->xPaddle = 54;
|
||||
arkanoid_state->ball_state.yb = 60;
|
||||
arkanoid_state->brickCount = 0;
|
||||
arkanoid_state->ball_state.released = false;
|
||||
|
||||
// Reset all brick hit states
|
||||
for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) {
|
||||
for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) {
|
||||
arkanoid_state->brick_state.isHit[row][column] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void arkanoid_state_init(ArkanoidState* arkanoid_state) {
|
||||
// Init notification
|
||||
arkanoid_state->notify = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
// Set the initial game state
|
||||
arkanoid_state->COLUMNS = 13;
|
||||
arkanoid_state->ROWS = 4;
|
||||
arkanoid_state->ball_state.dx = -1;
|
||||
arkanoid_state->ball_state.dy = -1;
|
||||
arkanoid_state->speed = 2;
|
||||
arkanoid_state->bounced = false;
|
||||
arkanoid_state->lives = 3;
|
||||
arkanoid_state->level = 1;
|
||||
arkanoid_state->score = 0;
|
||||
arkanoid_state->COLUMNS = 13;
|
||||
arkanoid_state->COLUMNS = 13;
|
||||
|
||||
// Reset initial state
|
||||
arkanoid_state->initialDraw = false;
|
||||
arkanoid_state->gameStarted = false;
|
||||
}
|
||||
|
||||
static void arkanoid_draw_callback(Canvas* const canvas, void* ctx) {
|
||||
ArkanoidState* arkanoid_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
if(arkanoid_state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Initial level draw
|
||||
if(!arkanoid_state->initialDraw) {
|
||||
arkanoid_state->initialDraw = true;
|
||||
|
||||
// Set default font for text
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
|
||||
//Draws the new level
|
||||
reset_level(canvas, arkanoid_state);
|
||||
}
|
||||
|
||||
//Draws new bricks and resets their values
|
||||
for(unsigned int row = 0; row < arkanoid_state->ROWS; row++) {
|
||||
for(unsigned int column = 0; column < arkanoid_state->COLUMNS; column++) {
|
||||
if(!arkanoid_state->brick_state.isHit[row][column]) {
|
||||
canvas_draw_frame(canvas, 10 * column, 2 + 6 * row, 8, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(arkanoid_state->lives > 0) {
|
||||
draw_paddle(canvas, arkanoid_state);
|
||||
draw_ball(canvas, arkanoid_state);
|
||||
draw_score(canvas, arkanoid_state);
|
||||
draw_lives(canvas, arkanoid_state);
|
||||
|
||||
if(arkanoid_state->brickCount == arkanoid_state->ROWS * arkanoid_state->COLUMNS) {
|
||||
arkanoid_state->level++;
|
||||
reset_level(canvas, arkanoid_state);
|
||||
}
|
||||
} else {
|
||||
reset_level(canvas, arkanoid_state);
|
||||
arkanoid_state->initialDraw = false;
|
||||
arkanoid_state->lives = 3;
|
||||
arkanoid_state->score = 0;
|
||||
}
|
||||
|
||||
release_mutex((ValueMutex*)ctx, arkanoid_state);
|
||||
}
|
||||
|
||||
static void arkanoid_input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
|
||||
GameEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
static void arkanoid_update_timer_callback(FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
|
||||
GameEvent event = {.type = EventTypeTick};
|
||||
furi_message_queue_put(event_queue, &event, 0);
|
||||
}
|
||||
|
||||
int32_t arkanoid_game_app(void* p) {
|
||||
UNUSED(p);
|
||||
int32_t return_code = 0;
|
||||
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(GameEvent));
|
||||
|
||||
ArkanoidState* arkanoid_state = malloc(sizeof(ArkanoidState));
|
||||
arkanoid_state_init(arkanoid_state);
|
||||
|
||||
ValueMutex state_mutex;
|
||||
if(!init_mutex(&state_mutex, arkanoid_state, sizeof(ArkanoidState))) {
|
||||
FURI_LOG_E(TAG, "Cannot create mutex\r\n");
|
||||
return_code = 255;
|
||||
goto free_and_exit;
|
||||
}
|
||||
|
||||
// Set system callbacks
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, arkanoid_draw_callback, &state_mutex);
|
||||
view_port_input_callback_set(view_port, arkanoid_input_callback, event_queue);
|
||||
|
||||
FuriTimer* timer =
|
||||
furi_timer_alloc(arkanoid_update_timer_callback, FuriTimerTypePeriodic, event_queue);
|
||||
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 22);
|
||||
|
||||
// Open GUI and register view_port
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
GameEvent event;
|
||||
for(bool processing = true; processing;) {
|
||||
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
|
||||
ArkanoidState* arkanoid_state = (ArkanoidState*)acquire_mutex_block(&state_mutex);
|
||||
|
||||
if(event_status == FuriStatusOk) {
|
||||
// Key events
|
||||
if(event.type == EventTypeKey) {
|
||||
if(event.input.type == InputTypePress || event.input.type == InputTypeLong ||
|
||||
event.input.type == InputTypeRepeat) {
|
||||
switch(event.input.key) {
|
||||
case InputKeyBack:
|
||||
processing = false;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
if(arkanoid_state->xPaddle < FLIPPER_LCD_WIDTH - 12) {
|
||||
arkanoid_state->xPaddle += 8;
|
||||
}
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
if(arkanoid_state->xPaddle > 0) {
|
||||
arkanoid_state->xPaddle -= 8;
|
||||
}
|
||||
break;
|
||||
case InputKeyUp:
|
||||
if(arkanoid_state->speed < MAX_SPEED) {
|
||||
arkanoid_state->speed++;
|
||||
}
|
||||
break;
|
||||
case InputKeyDown:
|
||||
if(arkanoid_state->speed > 1) {
|
||||
arkanoid_state->speed--;
|
||||
}
|
||||
break;
|
||||
case InputKeyOk:
|
||||
if(arkanoid_state->gameStarted == false) {
|
||||
//Release ball if FIRE pressed
|
||||
arkanoid_state->ball_state.released = true;
|
||||
|
||||
//Apply random direction to ball on release
|
||||
if(rand_range(0, 2) == 0) {
|
||||
arkanoid_state->ball_state.dx = 1;
|
||||
} else {
|
||||
arkanoid_state->ball_state.dx = -1;
|
||||
}
|
||||
|
||||
//Makes sure the ball heads upwards
|
||||
arkanoid_state->ball_state.dy = -1;
|
||||
//start the game flag
|
||||
arkanoid_state->gameStarted = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view_port_update(view_port);
|
||||
release_mutex(&state_mutex, arkanoid_state);
|
||||
}
|
||||
furi_timer_free(timer);
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
view_port_free(view_port);
|
||||
delete_mutex(&state_mutex);
|
||||
|
||||
free_and_exit:
|
||||
free(arkanoid_state);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
return return_code;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
App(
|
||||
appid="BadApple",
|
||||
name="Bad Apple",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="bad_apple_main",
|
||||
requires=["gui"],
|
||||
stack_size=1 * 1024,
|
||||
fap_icon="bad_apple_10px.png",
|
||||
fap_category="Misc"
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <stm32wbxx_ll_tim.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <storage/storage.h>
|
||||
#include <notification/notification.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#include "bad_apple.h"
|
||||
#include "video_player.h"
|
||||
|
||||
#define TAG "badapple"
|
||||
|
||||
typedef enum {
|
||||
BadAppleEventTypeInput,
|
||||
BadAppleEventTypeTick,
|
||||
} BadAppleEventType;
|
||||
|
||||
typedef struct {
|
||||
BadAppleEventType type;
|
||||
InputEvent* input;
|
||||
} BadAppleEvent;
|
||||
|
||||
// Screen is 128x64 px
|
||||
static void app_draw_callback(Canvas* canvas, void* ctx) {
|
||||
BadAppleCtx* inst = ctx;
|
||||
|
||||
canvas_clear(canvas);
|
||||
canvas_draw_xbm(canvas, VIDEO_X, VIDEO_Y, VIDEO_WIDTH, SCREEN_HEIGHT, inst->framebuffer);
|
||||
canvas_draw_box(canvas, 0, 0, VIDEO_X, SCREEN_HEIGHT);
|
||||
canvas_draw_box(
|
||||
canvas, VIDEO_X + VIDEO_WIDTH, 0, SCREEN_WIDTH - VIDEO_WIDTH - VIDEO_X, SCREEN_HEIGHT);
|
||||
}
|
||||
|
||||
static void app_input_callback(InputEvent* input_event, void* ctx) {
|
||||
furi_assert(ctx);
|
||||
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
BadAppleEvent event = {.type = BadAppleEventTypeInput, .input = input_event};
|
||||
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
BadAppleCtx* bad_apple_ctx_alloc(void) {
|
||||
BadAppleCtx* inst = malloc(sizeof(BadAppleCtx));
|
||||
memset(inst, 0, sizeof(BadAppleCtx));
|
||||
|
||||
if(inst) {
|
||||
inst->storage = furi_record_open(RECORD_STORAGE);
|
||||
inst->video_file = storage_file_alloc(inst->storage);
|
||||
inst->file_buffer_offset = sizeof(inst->file_buffer);
|
||||
}
|
||||
|
||||
return inst;
|
||||
}
|
||||
|
||||
void bad_apple_ctx_free(BadAppleCtx* inst) {
|
||||
if(inst) {
|
||||
storage_file_free(inst->video_file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
free(inst);
|
||||
}
|
||||
}
|
||||
|
||||
void bad_apple_load_next_video_chunk(BadAppleCtx* inst) {
|
||||
size_t bytes_to_read = sizeof(inst->file_buffer);
|
||||
uint8_t* buf_ptr = inst->file_buffer;
|
||||
while(bytes_to_read > 0) {
|
||||
uint16_t curr_bytes_to_read = bytes_to_read > (UINT16_MAX / 2 + 1) ? (UINT16_MAX / 2 + 1) :
|
||||
bytes_to_read;
|
||||
uint16_t read = storage_file_read(inst->video_file, buf_ptr, curr_bytes_to_read);
|
||||
bytes_to_read -= read;
|
||||
buf_ptr += read;
|
||||
if(read == 0) break;
|
||||
}
|
||||
inst->file_buffer_offset = 0;
|
||||
}
|
||||
|
||||
uint8_t bad_apple_read_byte(BadAppleCtx* inst) {
|
||||
if(inst->file_buffer_offset >= sizeof(inst->file_buffer)) {
|
||||
bad_apple_load_next_video_chunk(inst);
|
||||
}
|
||||
return inst->file_buffer[inst->file_buffer_offset++];
|
||||
}
|
||||
|
||||
void bad_apple_timer_isr(void* ctx) {
|
||||
FuriMessageQueue* event_queue = ctx;
|
||||
BadAppleEvent event = {.type = BadAppleEventTypeTick};
|
||||
furi_message_queue_put(event_queue, &event, 0);
|
||||
LL_TIM_ClearFlag_UPDATE(TIM2);
|
||||
}
|
||||
|
||||
void bad_apple_timer_setup(BadAppleCtx* inst, void* ctx) {
|
||||
UNUSED(inst);
|
||||
|
||||
LL_TIM_InitTypeDef tim_init = {
|
||||
.Prescaler = 63999,
|
||||
.CounterMode = LL_TIM_COUNTERMODE_UP,
|
||||
.Autoreload = 30,
|
||||
};
|
||||
|
||||
LL_TIM_Init(TIM2, &tim_init);
|
||||
LL_TIM_SetClockSource(TIM2, LL_TIM_CLOCKSOURCE_INTERNAL);
|
||||
LL_TIM_DisableCounter(TIM2);
|
||||
LL_TIM_SetCounter(TIM2, 0);
|
||||
furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, bad_apple_timer_isr, ctx);
|
||||
LL_TIM_EnableIT_UPDATE(TIM2);
|
||||
}
|
||||
|
||||
void bad_apple_timer_deinit(void) {
|
||||
LL_TIM_DisableCounter(TIM2);
|
||||
LL_TIM_DisableIT_UPDATE(TIM2);
|
||||
furi_hal_interrupt_set_isr(FuriHalInterruptIdTIM2, NULL, NULL);
|
||||
LL_TIM_DeInit(TIM2);
|
||||
}
|
||||
|
||||
int32_t bad_apple_main(void* p) {
|
||||
UNUSED(p);
|
||||
BadAppleCtx* ctx = bad_apple_ctx_alloc();
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(BadAppleEvent));
|
||||
|
||||
// Configure view port
|
||||
ViewPort* view_port = view_port_alloc();
|
||||
view_port_draw_callback_set(view_port, app_draw_callback, ctx);
|
||||
view_port_input_callback_set(view_port, app_input_callback, event_queue);
|
||||
|
||||
// Register view port in GUI
|
||||
Gui* gui = furi_record_open(RECORD_GUI);
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
|
||||
|
||||
// Frame rate: 32 FPS
|
||||
bad_apple_timer_setup(ctx, event_queue);
|
||||
|
||||
bool is_opened = storage_file_open(ctx->video_file, VIDEO_PATH, FSAM_READ, FSOM_OPEN_EXISTING);
|
||||
if(is_opened) {
|
||||
BadAppleEvent event;
|
||||
notification_message(notification, &sequence_display_backlight_enforce_on);
|
||||
|
||||
bool running = true;
|
||||
LL_TIM_EnableCounter(TIM2);
|
||||
while(running) {
|
||||
if(furi_message_queue_get(event_queue, &event, 100) == FuriStatusOk) {
|
||||
if(event.type == BadAppleEventTypeInput) {
|
||||
InputEvent* input_event = event.input;
|
||||
if(input_event->type == InputTypeLong) {
|
||||
if(input_event->key == InputKeyBack) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
} else if(event.type == BadAppleEventTypeTick) {
|
||||
// FURI_LOG_D(TAG, "Update frame");
|
||||
if(!vp_play_frame(ctx)) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
view_port_update(view_port);
|
||||
}
|
||||
LL_TIM_DisableCounter(TIM2);
|
||||
notification_message(notification, &sequence_display_backlight_enforce_auto);
|
||||
storage_file_close(ctx->video_file);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_NOTIFICATION);
|
||||
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
bad_apple_timer_deinit();
|
||||
bad_apple_ctx_free(ctx);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <storage/storage.h>
|
||||
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
#define VIDEO_WIDTH 104
|
||||
#define VIDEO_HEIGHT 80
|
||||
#define FILE_BUFFER_SIZE (1024 * 64)
|
||||
#define VIDEO_PATH EXT_PATH("apps_data/bad_apple/video.bin")
|
||||
#define VIDEO_X ((SCREEN_WIDTH - VIDEO_WIDTH) / 2)
|
||||
#define VIDEO_Y 0
|
||||
|
||||
typedef struct {
|
||||
uint8_t framebuffer[VIDEO_HEIGHT * VIDEO_WIDTH / 8];
|
||||
uint8_t file_buffer[FILE_BUFFER_SIZE];
|
||||
uint32_t file_buffer_offset;
|
||||
uint32_t frame_write_offset; // bit index
|
||||
Storage* storage;
|
||||
File* video_file;
|
||||
} BadAppleCtx;
|
||||
|
||||
uint8_t bad_apple_read_byte(BadAppleCtx* inst);
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,137 @@
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
#include "bad_apple.h"
|
||||
#include "video_player.h"
|
||||
|
||||
#define TAG "video_player"
|
||||
|
||||
static void vp_decode_rle(BadAppleCtx* ctx);
|
||||
static void vp_decode_delta(BadAppleCtx* ctx);
|
||||
|
||||
int vp_play_frame(BadAppleCtx* ctx) {
|
||||
// Check frame type
|
||||
// FURI_LOG_D(TAG, "Buffer offset: %04lx", ctx->file_buffer_offset);
|
||||
uint8_t b = bad_apple_read_byte(ctx);
|
||||
// FURI_LOG_D(TAG, "Frame byte: %02x", b);
|
||||
switch(b) {
|
||||
case 1: // PFrame (delta)
|
||||
vp_decode_delta(ctx);
|
||||
break;
|
||||
case 2: // IFrame (rle)
|
||||
vp_decode_rle(ctx);
|
||||
break;
|
||||
case 3: // DFrame (duplicate)
|
||||
break;
|
||||
case 4: // next page (not supported)
|
||||
case 5: // end
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline void vp_write_pixel(BadAppleCtx* ctx, bool color) {
|
||||
if(color) {
|
||||
// White
|
||||
ctx->framebuffer[ctx->frame_write_offset / 8] &= ~(1 << (ctx->frame_write_offset % 8));
|
||||
} else {
|
||||
// Black
|
||||
ctx->framebuffer[ctx->frame_write_offset / 8] |= (1 << (ctx->frame_write_offset % 8));
|
||||
}
|
||||
++ctx->frame_write_offset;
|
||||
}
|
||||
|
||||
static inline void vp_set_rect_fast(BadAppleCtx* ctx, int x, int y) {
|
||||
ctx->frame_write_offset = y * VIDEO_WIDTH + x;
|
||||
}
|
||||
|
||||
static void vp_decode_rle(BadAppleCtx* ctx) {
|
||||
int i = 0;
|
||||
int repeat_byte = bad_apple_read_byte(ctx);
|
||||
|
||||
// Set rect to video area
|
||||
vp_set_rect_fast(ctx, 0, 0);
|
||||
|
||||
while(i < VIDEO_WIDTH * VIDEO_HEIGHT) {
|
||||
int count;
|
||||
unsigned pixels;
|
||||
int j;
|
||||
int b = bad_apple_read_byte(ctx);
|
||||
|
||||
if(b == repeat_byte) {
|
||||
count = bad_apple_read_byte(ctx);
|
||||
if(count == 0) count = 256;
|
||||
pixels = bad_apple_read_byte(ctx);
|
||||
} else {
|
||||
count = 1;
|
||||
pixels = (unsigned)b;
|
||||
}
|
||||
|
||||
for(j = 0; j < count; ++j) {
|
||||
bool out_color;
|
||||
int k;
|
||||
unsigned loop_pixels = pixels;
|
||||
|
||||
for(k = 0; k < 8; ++k) {
|
||||
if(loop_pixels & 0x80)
|
||||
out_color = COLOR_WHITE;
|
||||
else
|
||||
out_color = COLOR_BLACK;
|
||||
|
||||
vp_write_pixel(ctx, out_color);
|
||||
loop_pixels <<= 1;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void vp_decode_delta(BadAppleCtx* ctx) {
|
||||
int frame_header[(VIDEO_HEIGHT + 7) / 8];
|
||||
uint i;
|
||||
int fh_byte = 0;
|
||||
int fh_index = 0;
|
||||
|
||||
for(i = 0; i < sizeof(frame_header) / sizeof(frame_header[0]); ++i) {
|
||||
frame_header[i] = bad_apple_read_byte(ctx);
|
||||
}
|
||||
|
||||
for(i = 0; i < VIDEO_HEIGHT; ++i) {
|
||||
if(i % 8 == 0) fh_byte = frame_header[fh_index++];
|
||||
|
||||
if(fh_byte & 0x80) {
|
||||
int j;
|
||||
int sl_byte = 0;
|
||||
|
||||
for(j = 0; j < VIDEO_WIDTH;) {
|
||||
if(j % (8 * 8) == 0) sl_byte = bad_apple_read_byte(ctx);
|
||||
|
||||
if(sl_byte & 0x80) {
|
||||
unsigned out_color;
|
||||
int k;
|
||||
unsigned pixel_group = bad_apple_read_byte(ctx);
|
||||
|
||||
// Note: this needs to be revised for screen width not multiple of 8
|
||||
vp_set_rect_fast(ctx, j, i);
|
||||
|
||||
for(k = 0; k < 8 && j < VIDEO_WIDTH; ++k, ++j) {
|
||||
if(pixel_group & 0x80)
|
||||
out_color = COLOR_WHITE;
|
||||
else
|
||||
out_color = COLOR_BLACK;
|
||||
|
||||
vp_write_pixel(ctx, out_color);
|
||||
pixel_group <<= 1;
|
||||
}
|
||||
} else {
|
||||
j += 8;
|
||||
}
|
||||
sl_byte <<= 1;
|
||||
}
|
||||
}
|
||||
fh_byte <<= 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#define COLOR_WHITE true
|
||||
#define COLOR_BLACK false
|
||||
|
||||
int vp_play_frame(BadAppleCtx* ctx);
|
||||
@@ -0,0 +1,15 @@
|
||||
App(
|
||||
appid="Barcode_Generator",
|
||||
name="Barcode Generator",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="barcode_generator_app",
|
||||
cdefines=["APP_BARCODE_GEN"],
|
||||
requires=[
|
||||
"gui",
|
||||
"dialogs",
|
||||
],
|
||||
stack_size=1 * 1024,
|
||||
order=250,
|
||||
fap_icon="barcode_10px.png",
|
||||
fap_category="Misc",
|
||||
)
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,355 @@
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "barcode_generator.h"
|
||||
|
||||
static BarcodeType* barcodeTypes[NUMBER_OF_BARCODE_TYPES];
|
||||
|
||||
void init_types() {
|
||||
BarcodeType* upcA = malloc(sizeof(BarcodeType));
|
||||
upcA->name = "UPC-A";
|
||||
upcA->numberOfDigits = 12;
|
||||
upcA->startPos = 19;
|
||||
barcodeTypes[0] = upcA;
|
||||
|
||||
BarcodeType* ean8 = malloc(sizeof(BarcodeType));
|
||||
ean8->name = "EAN-8";
|
||||
ean8->numberOfDigits = 8;
|
||||
ean8->startPos = 33;
|
||||
barcodeTypes[1] = ean8;
|
||||
}
|
||||
|
||||
void draw_digit(Canvas* canvas, int digit, bool rightHand, int startingPosition) {
|
||||
char digitStr[2];
|
||||
snprintf(digitStr, 2, "%u", digit);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_str(
|
||||
canvas, startingPosition, BARCODE_Y_START + BARCODE_HEIGHT + BARCODE_TEXT_OFFSET, digitStr);
|
||||
if(rightHand) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
for(int i = 0; i < 4; i++) {
|
||||
canvas_draw_box(
|
||||
canvas, startingPosition + count, BARCODE_Y_START, DIGITS[digit][i], BARCODE_HEIGHT);
|
||||
canvas_invert_color(canvas);
|
||||
count += DIGITS[digit][i];
|
||||
}
|
||||
}
|
||||
|
||||
int get_digit_position(int index, BarcodeType* type) {
|
||||
int pos = type->startPos + index * 7;
|
||||
if(index >= type->numberOfDigits / 2) {
|
||||
pos += 5;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
int get_menu_text_location(int index) {
|
||||
return 20 + 10 * index;
|
||||
}
|
||||
|
||||
int calculate_check_digit(PluginState* plugin_state, BarcodeType* type) {
|
||||
int checkDigit = 0;
|
||||
//add all odd positions. Confusing because 0index
|
||||
for(int i = 0; i < type->numberOfDigits - 1; i += 2) {
|
||||
checkDigit += plugin_state->barcodeNumeral[i];
|
||||
}
|
||||
|
||||
checkDigit = checkDigit * 3; //times 3
|
||||
|
||||
//add all even positions to above. Confusing because 0index
|
||||
for(int i = 1; i < type->numberOfDigits - 1; i += 2) {
|
||||
checkDigit += plugin_state->barcodeNumeral[i];
|
||||
}
|
||||
|
||||
checkDigit = checkDigit % 10; //mod 10
|
||||
|
||||
//if m = 0 then x12 = 0, otherwise x12 is 10 - m
|
||||
return (10 - checkDigit) % 10;
|
||||
}
|
||||
|
||||
static void render_callback(Canvas* const canvas, void* ctx) {
|
||||
PluginState* plugin_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
if(plugin_state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(plugin_state->mode == MenuMode) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_str_aligned(canvas, 64, 6, AlignCenter, AlignCenter, "MENU");
|
||||
canvas_draw_frame(canvas, 50, 0, 29, 11); //box around Menu
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, get_menu_text_location(0), AlignCenter, AlignCenter, "View");
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, get_menu_text_location(1), AlignCenter, AlignCenter, "Edit");
|
||||
canvas_draw_str_aligned(
|
||||
canvas, 64, get_menu_text_location(2), AlignCenter, AlignCenter, "Parity?");
|
||||
|
||||
canvas_draw_frame(canvas, 83, get_menu_text_location(2) - 3, 6, 6);
|
||||
if(plugin_state->doParityCalculation == true) {
|
||||
canvas_draw_box(canvas, 85, get_menu_text_location(2) - 1, 2, 2);
|
||||
}
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
64,
|
||||
get_menu_text_location(3),
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
(barcodeTypes[plugin_state->barcodeTypeIndex])->name);
|
||||
canvas_draw_disc(
|
||||
canvas, 40, get_menu_text_location(plugin_state->menuIndex) - 1, 2); //draw menu cursor
|
||||
} else {
|
||||
BarcodeType* type = barcodeTypes[plugin_state->barcodeTypeIndex];
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, type->startPos - 3, BARCODE_Y_START, 1, BARCODE_HEIGHT + 2);
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
(type->startPos - 1),
|
||||
BARCODE_Y_START,
|
||||
1,
|
||||
BARCODE_HEIGHT + 2); //start saftey
|
||||
|
||||
for(int index = 0; index < type->numberOfDigits; index++) {
|
||||
bool isOnRight = false;
|
||||
if(index >= type->numberOfDigits / 2) {
|
||||
isOnRight = true;
|
||||
}
|
||||
if((index == type->numberOfDigits - 1) &&
|
||||
(plugin_state->doParityCalculation)) { //calculate the check digit
|
||||
int checkDigit = calculate_check_digit(plugin_state, type);
|
||||
plugin_state->barcodeNumeral[type->numberOfDigits - 1] = checkDigit;
|
||||
}
|
||||
int digitPosition =
|
||||
get_digit_position(index, barcodeTypes[plugin_state->barcodeTypeIndex]);
|
||||
draw_digit(canvas, plugin_state->barcodeNumeral[index], isOnRight, digitPosition);
|
||||
}
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, 62, BARCODE_Y_START, 1, BARCODE_HEIGHT + 2);
|
||||
canvas_draw_box(canvas, 64, BARCODE_Y_START, 1, BARCODE_HEIGHT + 2);
|
||||
|
||||
if(plugin_state->mode == EditMode) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
get_digit_position(
|
||||
plugin_state->editingIndex, barcodeTypes[plugin_state->barcodeTypeIndex]) -
|
||||
1,
|
||||
63,
|
||||
7,
|
||||
1); //draw editing cursor
|
||||
}
|
||||
|
||||
int endSafetyPosition = get_digit_position(type->numberOfDigits - 1, type) + 7;
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, endSafetyPosition, BARCODE_Y_START, 1, BARCODE_HEIGHT + 2);
|
||||
canvas_draw_box(
|
||||
canvas,
|
||||
(endSafetyPosition + 2),
|
||||
BARCODE_Y_START,
|
||||
1,
|
||||
BARCODE_HEIGHT + 2); //end safety
|
||||
}
|
||||
|
||||
release_mutex((ValueMutex*)ctx, plugin_state);
|
||||
}
|
||||
|
||||
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 barcode_generator_state_init(PluginState* const plugin_state) {
|
||||
for(int i = 0; i < 12; ++i) {
|
||||
plugin_state->barcodeNumeral[i] = i % 10;
|
||||
}
|
||||
plugin_state->editingIndex = 0;
|
||||
plugin_state->mode = ViewMode;
|
||||
plugin_state->doParityCalculation = true;
|
||||
plugin_state->menuIndex = MENU_INDEX_VIEW;
|
||||
plugin_state->barcodeTypeIndex = 0;
|
||||
}
|
||||
|
||||
static bool handle_key_press_view(InputKey key, PluginState* plugin_state) {
|
||||
switch(key) {
|
||||
case InputKeyOk:
|
||||
case InputKeyBack:
|
||||
plugin_state->mode = MenuMode;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_key_press_edit(InputKey key, PluginState* plugin_state) {
|
||||
int barcodeMaxIndex = plugin_state->doParityCalculation ?
|
||||
barcodeTypes[plugin_state->barcodeTypeIndex]->numberOfDigits - 1 :
|
||||
barcodeTypes[plugin_state->barcodeTypeIndex]->numberOfDigits;
|
||||
|
||||
switch(key) {
|
||||
case InputKeyUp:
|
||||
plugin_state->barcodeNumeral[plugin_state->editingIndex] =
|
||||
(plugin_state->barcodeNumeral[plugin_state->editingIndex] + 1) % 10;
|
||||
break;
|
||||
|
||||
case InputKeyDown:
|
||||
plugin_state->barcodeNumeral[plugin_state->editingIndex] =
|
||||
(plugin_state->barcodeNumeral[plugin_state->editingIndex] == 0) ?
|
||||
9 :
|
||||
plugin_state->barcodeNumeral[plugin_state->editingIndex] - 1;
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
plugin_state->editingIndex = (plugin_state->editingIndex + 1) % barcodeMaxIndex;
|
||||
break;
|
||||
|
||||
case InputKeyLeft:
|
||||
plugin_state->editingIndex = (plugin_state->editingIndex == 0) ?
|
||||
barcodeMaxIndex - 1 :
|
||||
plugin_state->editingIndex - 1;
|
||||
break;
|
||||
|
||||
case InputKeyOk:
|
||||
case InputKeyBack:
|
||||
plugin_state->mode = MenuMode;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool handle_key_press_menu(InputKey key, PluginState* plugin_state) {
|
||||
switch(key) {
|
||||
case InputKeyUp:
|
||||
plugin_state->menuIndex = (plugin_state->menuIndex == MENU_INDEX_VIEW) ?
|
||||
MENU_INDEX_TYPE :
|
||||
plugin_state->menuIndex - 1;
|
||||
break;
|
||||
|
||||
case InputKeyDown:
|
||||
plugin_state->menuIndex = (plugin_state->menuIndex + 1) % 4;
|
||||
break;
|
||||
|
||||
case InputKeyRight:
|
||||
if(plugin_state->menuIndex == MENU_INDEX_TYPE) {
|
||||
plugin_state->barcodeTypeIndex =
|
||||
(plugin_state->barcodeTypeIndex == NUMBER_OF_BARCODE_TYPES - 1) ?
|
||||
0 :
|
||||
plugin_state->barcodeTypeIndex + 1;
|
||||
} else if(plugin_state->menuIndex == MENU_INDEX_PARITY) {
|
||||
plugin_state->doParityCalculation = !plugin_state->doParityCalculation;
|
||||
}
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
if(plugin_state->menuIndex == MENU_INDEX_TYPE) {
|
||||
plugin_state->barcodeTypeIndex = (plugin_state->barcodeTypeIndex == 0) ?
|
||||
NUMBER_OF_BARCODE_TYPES - 1 :
|
||||
plugin_state->barcodeTypeIndex - 1;
|
||||
} else if(plugin_state->menuIndex == MENU_INDEX_PARITY) {
|
||||
plugin_state->doParityCalculation = !plugin_state->doParityCalculation;
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyOk:
|
||||
if(plugin_state->menuIndex == MENU_INDEX_VIEW) {
|
||||
plugin_state->mode = ViewMode;
|
||||
} else if(plugin_state->menuIndex == MENU_INDEX_EDIT) {
|
||||
plugin_state->mode = EditMode;
|
||||
} else if(plugin_state->menuIndex == MENU_INDEX_PARITY) {
|
||||
plugin_state->doParityCalculation = !plugin_state->doParityCalculation;
|
||||
} else if(plugin_state->menuIndex == MENU_INDEX_TYPE) {
|
||||
plugin_state->barcodeTypeIndex =
|
||||
(plugin_state->barcodeTypeIndex == NUMBER_OF_BARCODE_TYPES - 1) ?
|
||||
0 :
|
||||
plugin_state->barcodeTypeIndex + 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case InputKeyBack:
|
||||
return false;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t barcode_generator_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
init_types();
|
||||
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PluginEvent));
|
||||
|
||||
PluginState* plugin_state = malloc(sizeof(PluginState));
|
||||
barcode_generator_state_init(plugin_state);
|
||||
ValueMutex state_mutex;
|
||||
if(!init_mutex(&state_mutex, plugin_state, sizeof(PluginState))) {
|
||||
FURI_LOG_E("barcode_generator", "cannot create mutex\r\n");
|
||||
furi_message_queue_free(event_queue);
|
||||
free(plugin_state);
|
||||
return 255;
|
||||
}
|
||||
|
||||
// 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(RECORD_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);
|
||||
PluginState* plugin_state = (PluginState*)acquire_mutex_block(&state_mutex);
|
||||
|
||||
if(event_status == FuriStatusOk) {
|
||||
// press events
|
||||
if(event.type == EventTypeKey &&
|
||||
((event.input.type == InputTypePress) || (event.input.type == InputTypeRepeat))) {
|
||||
switch(plugin_state->mode) {
|
||||
case ViewMode:
|
||||
processing = handle_key_press_view(event.input.key, plugin_state);
|
||||
break;
|
||||
case EditMode:
|
||||
processing = handle_key_press_edit(event.input.key, plugin_state);
|
||||
break;
|
||||
case MenuMode:
|
||||
processing = handle_key_press_menu(event.input.key, plugin_state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view_port_update(view_port);
|
||||
release_mutex(&state_mutex, plugin_state);
|
||||
}
|
||||
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
view_port_free(view_port);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#define BARCODE_HEIGHT 50
|
||||
#define BARCODE_Y_START 3
|
||||
#define BARCODE_TEXT_OFFSET 9
|
||||
#define NUMBER_OF_BARCODE_TYPES 2
|
||||
#define MENU_INDEX_VIEW 0
|
||||
#define MENU_INDEX_EDIT 1
|
||||
#define MENU_INDEX_PARITY 2
|
||||
#define MENU_INDEX_TYPE 3
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} PluginEvent;
|
||||
|
||||
typedef enum {
|
||||
ViewMode,
|
||||
EditMode,
|
||||
MenuMode,
|
||||
} Mode;
|
||||
|
||||
typedef struct {
|
||||
char* name;
|
||||
int numberOfDigits;
|
||||
int startPos;
|
||||
} BarcodeType;
|
||||
|
||||
typedef struct {
|
||||
int barcodeNumeral[12]; //The current barcode number
|
||||
int editingIndex; //The index of the editing symbol
|
||||
int menuIndex; //The index of the menu cursor
|
||||
Mode mode; //View, edit or menu
|
||||
bool doParityCalculation; //Should do parity check?
|
||||
int barcodeTypeIndex;
|
||||
} PluginState;
|
||||
|
||||
static const int DIGITS[10][4] = {
|
||||
{3, 2, 1, 1},
|
||||
{2, 2, 2, 1},
|
||||
{2, 1, 2, 2},
|
||||
{1, 4, 1, 1},
|
||||
{1, 1, 3, 2},
|
||||
{1, 2, 3, 1},
|
||||
{1, 1, 1, 4},
|
||||
{1, 3, 1, 2},
|
||||
{1, 2, 1, 3},
|
||||
{3, 1, 1, 2},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
App(
|
||||
appid="BlackJack",
|
||||
name="BlackJack",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="blackjack_app",
|
||||
cdefines=["APP_BLACKJACK"],
|
||||
requires=["gui","storage","canvas"],
|
||||
stack_size=2 * 1024,
|
||||
order=30,
|
||||
fap_icon="blackjack_10px.png",
|
||||
fap_category="Games",
|
||||
fap_icon_assets="assets"
|
||||
)
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 409 B |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,634 @@
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <stdlib.h>
|
||||
#include <dolphin/dolphin.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <gui/canvas_i.h>
|
||||
|
||||
#include <math.h>
|
||||
#include "util.h"
|
||||
#include "defines.h"
|
||||
#include "common/card.h"
|
||||
#include "common/dml.h"
|
||||
#include "common/queue.h"
|
||||
#include "util.h"
|
||||
#include "ui.h"
|
||||
|
||||
#include "BlackJack_icons.h"
|
||||
|
||||
#define DEALER_MAX 17
|
||||
|
||||
void start_round(GameState* game_state);
|
||||
|
||||
void init(GameState* game_state);
|
||||
|
||||
static void draw_ui(Canvas* const canvas, const GameState* game_state) {
|
||||
draw_money(canvas, game_state->player_score);
|
||||
|
||||
draw_score(canvas, true, hand_count(game_state->player_cards, game_state->player_card_count));
|
||||
|
||||
if(!game_state->queue_state.running && game_state->state == GameStatePlay) {
|
||||
render_menu(game_state->menu, canvas, 2, 47);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_callback(Canvas* const canvas, void* ctx) {
|
||||
const GameState* game_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
|
||||
if(game_state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(canvas, 0, 0, 128, 64);
|
||||
|
||||
if(game_state->state == GameStateStart) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_blackjack);
|
||||
}
|
||||
if(game_state->state == GameStateGameOver) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_endscreen);
|
||||
}
|
||||
|
||||
if(game_state->state == GameStatePlay || game_state->state == GameStateDealer) {
|
||||
if(game_state->state == GameStatePlay)
|
||||
draw_player_scene(canvas, game_state);
|
||||
else
|
||||
draw_dealer_scene(canvas, game_state);
|
||||
render_queue(&(game_state->queue_state), game_state, canvas);
|
||||
draw_ui(canvas, game_state);
|
||||
} else if(game_state->state == GameStateSettings) {
|
||||
settings_page(canvas, game_state);
|
||||
}
|
||||
|
||||
release_mutex((ValueMutex*)ctx, game_state);
|
||||
}
|
||||
|
||||
//region card draw
|
||||
Card draw_card(GameState* game_state) {
|
||||
Card c = game_state->deck.cards[game_state->deck.index];
|
||||
game_state->deck.index++;
|
||||
return c;
|
||||
}
|
||||
|
||||
void drawPlayerCard(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
Card c = draw_card(game_state);
|
||||
game_state->player_cards[game_state->player_card_count] = c;
|
||||
game_state->player_card_count++;
|
||||
if(game_state->player_score < game_state->settings.round_price || game_state->doubled) {
|
||||
set_menu_state(game_state->menu, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
void drawDealerCard(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
Card c = draw_card(game_state);
|
||||
game_state->dealer_cards[game_state->dealer_card_count] = c;
|
||||
game_state->dealer_card_count++;
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region queue callbacks
|
||||
void to_lose_state(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You lost");
|
||||
}
|
||||
|
||||
void to_bust_state(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Busted!");
|
||||
}
|
||||
|
||||
void to_draw_state(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Draw");
|
||||
}
|
||||
|
||||
void to_dealer_turn(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Dealers turn");
|
||||
}
|
||||
|
||||
void to_win_state(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "You win");
|
||||
}
|
||||
|
||||
void to_start(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
if(game_state->settings.message_duration == 0) return;
|
||||
popup_frame(canvas);
|
||||
elements_multiline_text_aligned(canvas, 64, 22, AlignCenter, AlignCenter, "Round started");
|
||||
}
|
||||
|
||||
void before_start(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->dealer_card_count = 0;
|
||||
game_state->player_card_count = 0;
|
||||
}
|
||||
|
||||
void start(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
start_round(game_state);
|
||||
}
|
||||
|
||||
void draw(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->player_score += game_state->bet;
|
||||
game_state->bet = 0;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
start,
|
||||
before_start,
|
||||
to_start,
|
||||
game_state->settings.message_duration);
|
||||
}
|
||||
|
||||
void game_over(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->state = GameStateGameOver;
|
||||
}
|
||||
|
||||
void lose(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->state = GameStatePlay;
|
||||
game_state->bet = 0;
|
||||
if(game_state->player_score >= game_state->settings.round_price) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
start,
|
||||
before_start,
|
||||
to_start,
|
||||
game_state->settings.message_duration);
|
||||
} else {
|
||||
enqueue(&(game_state->queue_state), game_state, game_over, NULL, NULL, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void win(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->state = GameStatePlay;
|
||||
game_state->player_score += game_state->bet * 2;
|
||||
game_state->bet = 0;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
start,
|
||||
before_start,
|
||||
to_start,
|
||||
game_state->settings.message_duration);
|
||||
}
|
||||
|
||||
void dealerTurn(void* ctx) {
|
||||
GameState* game_state = ctx;
|
||||
game_state->state = GameStateDealer;
|
||||
}
|
||||
|
||||
float animationTime(const GameState* game_state) {
|
||||
return (float)(furi_get_tick() - game_state->queue_state.start) /
|
||||
(float)(game_state->settings.animation_duration);
|
||||
}
|
||||
|
||||
void dealer_card_animation(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
float t = animationTime(game_state);
|
||||
|
||||
Card animatingCard = game_state->deck.cards[game_state->deck.index];
|
||||
if(game_state->dealer_card_count > 1) {
|
||||
Vector end = card_pos_at_index(game_state->dealer_card_count);
|
||||
draw_card_animation(animatingCard, (Vector){0, 64}, (Vector){0, 32}, end, t, true, canvas);
|
||||
} else {
|
||||
draw_card_animation(
|
||||
animatingCard,
|
||||
(Vector){32, -CARD_HEIGHT},
|
||||
(Vector){64, 32},
|
||||
(Vector){2, 2},
|
||||
t,
|
||||
false,
|
||||
canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void dealer_back_card_animation(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
float t = animationTime(game_state);
|
||||
|
||||
Vector currentPos =
|
||||
quadratic_2d((Vector){32, -CARD_HEIGHT}, (Vector){64, 32}, (Vector){13, 5}, t);
|
||||
draw_card_back_at(currentPos.x, currentPos.y, canvas);
|
||||
}
|
||||
|
||||
void player_card_animation(const void* ctx, Canvas* const canvas) {
|
||||
const GameState* game_state = ctx;
|
||||
float t = animationTime(game_state);
|
||||
|
||||
Card animatingCard = game_state->deck.cards[game_state->deck.index];
|
||||
Vector end = card_pos_at_index(game_state->player_card_count);
|
||||
|
||||
draw_card_animation(
|
||||
animatingCard, (Vector){32, -CARD_HEIGHT}, (Vector){0, 32}, end, t, true, canvas);
|
||||
}
|
||||
//endregion
|
||||
|
||||
void player_tick(GameState* game_state) {
|
||||
uint8_t score = hand_count(game_state->player_cards, game_state->player_card_count);
|
||||
if((game_state->doubled && score <= 21) || score == 21) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
dealerTurn,
|
||||
NULL,
|
||||
to_dealer_turn,
|
||||
game_state->settings.message_duration);
|
||||
} else if(score > 21) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
lose,
|
||||
NULL,
|
||||
to_bust_state,
|
||||
game_state->settings.message_duration);
|
||||
} else {
|
||||
if(game_state->selectDirection == DirectionUp ||
|
||||
game_state->selectDirection == DirectionDown) {
|
||||
move_menu(game_state->menu, game_state->selectDirection == DirectionUp ? -1 : 1);
|
||||
}
|
||||
|
||||
if(game_state->selectDirection == Select) {
|
||||
activate_menu(game_state->menu, game_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void dealer_tick(GameState* game_state) {
|
||||
uint8_t dealer_score = hand_count(game_state->dealer_cards, game_state->dealer_card_count);
|
||||
uint8_t player_score = hand_count(game_state->player_cards, game_state->player_card_count);
|
||||
|
||||
if(dealer_score >= DEALER_MAX) {
|
||||
if(dealer_score > 21 || dealer_score < player_score) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
win,
|
||||
NULL,
|
||||
to_win_state,
|
||||
game_state->settings.message_duration);
|
||||
} else if(dealer_score > player_score) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
lose,
|
||||
NULL,
|
||||
to_lose_state,
|
||||
game_state->settings.message_duration);
|
||||
} else if(dealer_score == player_score) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
draw,
|
||||
NULL,
|
||||
to_draw_state,
|
||||
game_state->settings.message_duration);
|
||||
}
|
||||
} else {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawDealerCard,
|
||||
NULL,
|
||||
dealer_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
}
|
||||
}
|
||||
|
||||
void settings_tick(GameState* game_state) {
|
||||
if(game_state->selectDirection == DirectionDown && game_state->selectedMenu < 4) {
|
||||
game_state->selectedMenu++;
|
||||
}
|
||||
if(game_state->selectDirection == DirectionUp && game_state->selectedMenu > 0) {
|
||||
game_state->selectedMenu--;
|
||||
}
|
||||
|
||||
if(game_state->selectDirection == DirectionLeft ||
|
||||
game_state->selectDirection == DirectionRight) {
|
||||
int nextScore = 0;
|
||||
switch(game_state->selectedMenu) {
|
||||
case 0:
|
||||
nextScore = game_state->settings.starting_money;
|
||||
if(game_state->selectDirection == DirectionLeft)
|
||||
nextScore -= 10;
|
||||
else
|
||||
nextScore += 10;
|
||||
if(nextScore >= (int)game_state->settings.round_price && nextScore < 400)
|
||||
game_state->settings.starting_money = nextScore;
|
||||
break;
|
||||
case 1:
|
||||
nextScore = game_state->settings.round_price;
|
||||
if(game_state->selectDirection == DirectionLeft)
|
||||
nextScore -= 10;
|
||||
else
|
||||
nextScore += 10;
|
||||
if(nextScore >= 5 && nextScore <= (int)game_state->settings.starting_money)
|
||||
game_state->settings.round_price = nextScore;
|
||||
break;
|
||||
case 2:
|
||||
nextScore = game_state->settings.animation_duration;
|
||||
if(game_state->selectDirection == DirectionLeft)
|
||||
nextScore -= 100;
|
||||
else
|
||||
nextScore += 100;
|
||||
if(nextScore >= 0 && nextScore < 2000)
|
||||
game_state->settings.animation_duration = nextScore;
|
||||
break;
|
||||
case 3:
|
||||
nextScore = game_state->settings.message_duration;
|
||||
if(game_state->selectDirection == DirectionLeft)
|
||||
nextScore -= 100;
|
||||
else
|
||||
nextScore += 100;
|
||||
if(nextScore >= 0 && nextScore < 2000)
|
||||
game_state->settings.message_duration = nextScore;
|
||||
break;
|
||||
case 4:
|
||||
game_state->settings.sound_effects = !game_state->settings.sound_effects;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void tick(GameState* game_state) {
|
||||
game_state->last_tick = furi_get_tick();
|
||||
bool queue_ran = run_queue(&(game_state->queue_state), game_state);
|
||||
|
||||
switch(game_state->state) {
|
||||
case GameStateGameOver:
|
||||
case GameStateStart:
|
||||
if(game_state->selectDirection == Select)
|
||||
init(game_state);
|
||||
else if(game_state->selectDirection == DirectionRight) {
|
||||
game_state->selectedMenu = 0;
|
||||
game_state->state = GameStateSettings;
|
||||
}
|
||||
break;
|
||||
case GameStatePlay:
|
||||
if(!game_state->started) {
|
||||
game_state->selectedMenu = 0;
|
||||
game_state->started = true;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawDealerCard,
|
||||
NULL,
|
||||
dealer_back_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawPlayerCard,
|
||||
NULL,
|
||||
player_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawDealerCard,
|
||||
NULL,
|
||||
dealer_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawPlayerCard,
|
||||
NULL,
|
||||
player_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
}
|
||||
if(!queue_ran) player_tick(game_state);
|
||||
break;
|
||||
case GameStateDealer:
|
||||
if(!queue_ran) dealer_tick(game_state);
|
||||
break;
|
||||
case GameStateSettings:
|
||||
settings_tick(game_state);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
game_state->selectDirection = None;
|
||||
}
|
||||
|
||||
void start_round(GameState* game_state) {
|
||||
game_state->menu->current_menu = 1;
|
||||
game_state->player_card_count = 0;
|
||||
game_state->dealer_card_count = 0;
|
||||
set_menu_state(game_state->menu, 0, true);
|
||||
game_state->menu->enabled = true;
|
||||
game_state->started = false;
|
||||
game_state->doubled = false;
|
||||
game_state->queue_state.running = true;
|
||||
shuffle_deck(&(game_state->deck));
|
||||
game_state->doubled = false;
|
||||
game_state->bet = game_state->settings.round_price;
|
||||
if(game_state->player_score < game_state->settings.round_price) {
|
||||
game_state->state = GameStateGameOver;
|
||||
} else {
|
||||
game_state->player_score -= game_state->settings.round_price;
|
||||
}
|
||||
game_state->state = GameStatePlay;
|
||||
}
|
||||
|
||||
void init(GameState* game_state) {
|
||||
set_menu_state(game_state->menu, 0, true);
|
||||
game_state->menu->enabled = true;
|
||||
game_state->menu->current_menu = 1;
|
||||
game_state->settings = load_settings();
|
||||
game_state->last_tick = 0;
|
||||
game_state->processing = true;
|
||||
game_state->selectedMenu = 0;
|
||||
game_state->player_score = game_state->settings.starting_money;
|
||||
generate_deck(&(game_state->deck), 6);
|
||||
start_round(game_state);
|
||||
}
|
||||
|
||||
static void input_callback(InputEvent* input_event, FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
AppEvent event = {.type = EventTypeKey, .input = *input_event};
|
||||
furi_message_queue_put(event_queue, &event, FuriWaitForever);
|
||||
}
|
||||
|
||||
static void update_timer_callback(FuriMessageQueue* event_queue) {
|
||||
furi_assert(event_queue);
|
||||
AppEvent event = {.type = EventTypeTick};
|
||||
furi_message_queue_put(event_queue, &event, 0);
|
||||
}
|
||||
|
||||
void doubleAction(void* state) {
|
||||
GameState* game_state = state;
|
||||
if(!game_state->doubled && game_state->player_score >= game_state->settings.round_price) {
|
||||
game_state->player_score -= game_state->settings.round_price;
|
||||
game_state->bet += game_state->settings.round_price;
|
||||
game_state->doubled = true;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawPlayerCard,
|
||||
NULL,
|
||||
player_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
game_state->player_cards[game_state->player_card_count] =
|
||||
game_state->deck.cards[game_state->deck.index];
|
||||
uint8_t score = hand_count(game_state->player_cards, game_state->player_card_count + 1);
|
||||
if(score > 21) {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
lose,
|
||||
NULL,
|
||||
to_bust_state,
|
||||
game_state->settings.message_duration);
|
||||
} else {
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
dealerTurn,
|
||||
NULL,
|
||||
to_dealer_turn,
|
||||
game_state->settings.message_duration);
|
||||
}
|
||||
set_menu_state(game_state->menu, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
void hitAction(void* state) {
|
||||
GameState* game_state = state;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
drawPlayerCard,
|
||||
NULL,
|
||||
player_card_animation,
|
||||
game_state->settings.animation_duration);
|
||||
}
|
||||
void stayAction(void* state) {
|
||||
GameState* game_state = state;
|
||||
enqueue(
|
||||
&(game_state->queue_state),
|
||||
game_state,
|
||||
dealerTurn,
|
||||
NULL,
|
||||
to_dealer_turn,
|
||||
game_state->settings.message_duration);
|
||||
}
|
||||
|
||||
int32_t blackjack_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
int32_t return_code = 0;
|
||||
|
||||
FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(AppEvent));
|
||||
|
||||
GameState* game_state = malloc(sizeof(GameState));
|
||||
game_state->menu = malloc(sizeof(Menu));
|
||||
game_state->menu->menu_width = 40;
|
||||
init(game_state);
|
||||
add_menu(game_state->menu, "Double", doubleAction);
|
||||
add_menu(game_state->menu, "Hit", hitAction);
|
||||
add_menu(game_state->menu, "Stay", stayAction);
|
||||
set_card_graphics(&I_card_graphics);
|
||||
|
||||
game_state->state = GameStateStart;
|
||||
|
||||
ValueMutex state_mutex;
|
||||
if(!init_mutex(&state_mutex, game_state, sizeof(GameState))) {
|
||||
FURI_LOG_E(APP_NAME, "cannot create mutex\r\n");
|
||||
return_code = 255;
|
||||
goto free_and_exit;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
FuriTimer* timer = furi_timer_alloc(update_timer_callback, FuriTimerTypePeriodic, event_queue);
|
||||
furi_timer_start(timer, furi_kernel_get_tick_frequency() / 25);
|
||||
|
||||
Gui* gui = furi_record_open("gui");
|
||||
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
AppEvent event;
|
||||
|
||||
for(bool processing = true; processing;) {
|
||||
FuriStatus event_status = furi_message_queue_get(event_queue, &event, 100);
|
||||
GameState* localstate = (GameState*)acquire_mutex_block(&state_mutex);
|
||||
if(event_status == FuriStatusOk) {
|
||||
if(event.type == EventTypeKey) {
|
||||
if(event.input.type == InputTypePress) {
|
||||
switch(event.input.key) {
|
||||
case InputKeyUp:
|
||||
localstate->selectDirection = DirectionUp;
|
||||
break;
|
||||
case InputKeyDown:
|
||||
localstate->selectDirection = DirectionDown;
|
||||
break;
|
||||
case InputKeyRight:
|
||||
localstate->selectDirection = DirectionRight;
|
||||
break;
|
||||
case InputKeyLeft:
|
||||
localstate->selectDirection = DirectionLeft;
|
||||
break;
|
||||
case InputKeyBack:
|
||||
if(localstate->state == GameStateSettings) {
|
||||
localstate->state = GameStateStart;
|
||||
save_settings(localstate->settings);
|
||||
} else
|
||||
processing = false;
|
||||
break;
|
||||
case InputKeyOk:
|
||||
localstate->selectDirection = Select;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(event.type == EventTypeTick) {
|
||||
tick(localstate);
|
||||
processing = localstate->processing;
|
||||
}
|
||||
} else {
|
||||
//FURI_LOG_D(APP_NAME, "osMessageQueue: event timeout");
|
||||
// event timeout
|
||||
}
|
||||
view_port_update(view_port);
|
||||
release_mutex(&state_mutex, localstate);
|
||||
}
|
||||
|
||||
furi_timer_free(timer);
|
||||
view_port_enabled_set(view_port, false);
|
||||
gui_remove_view_port(gui, view_port);
|
||||
furi_record_close(RECORD_GUI);
|
||||
view_port_free(view_port);
|
||||
delete_mutex(&state_mutex);
|
||||
|
||||
free_and_exit:
|
||||
free(game_state->deck.cards);
|
||||
free_menu(game_state->menu);
|
||||
queue_clear(&(game_state->queue_state));
|
||||
free(game_state);
|
||||
furi_message_queue_free(event_queue);
|
||||
|
||||
return return_code;
|
||||
}
|
||||
|
After Width: | Height: | Size: 119 B |
@@ -0,0 +1,353 @@
|
||||
#include "card.h"
|
||||
#include "dml.h"
|
||||
#include "ui.h"
|
||||
|
||||
#define CARD_DRAW_X_START 108
|
||||
#define CARD_DRAW_Y_START 38
|
||||
#define CARD_DRAW_X_SPACE 10
|
||||
#define CARD_DRAW_Y_SPACE 8
|
||||
#define CARD_DRAW_X_OFFSET 4
|
||||
#define CARD_DRAW_FIRST_ROW_LENGTH 7
|
||||
|
||||
uint8_t pips[4][3] = {
|
||||
{21, 10, 7}, //spades
|
||||
{7, 10, 7}, //hearts
|
||||
{0, 10, 7}, //diamonds
|
||||
{14, 10, 7}, //clubs
|
||||
};
|
||||
uint8_t letters[13][3] = {
|
||||
{0, 0, 5},
|
||||
{5, 0, 5},
|
||||
{10, 0, 5},
|
||||
{15, 0, 5},
|
||||
{20, 0, 5},
|
||||
{25, 0, 5},
|
||||
{30, 0, 5},
|
||||
{0, 5, 5},
|
||||
{5, 5, 5},
|
||||
{10, 5, 5},
|
||||
{15, 5, 5},
|
||||
{20, 5, 5},
|
||||
{25, 5, 5},
|
||||
};
|
||||
|
||||
//region Player card positions
|
||||
uint8_t playerCardPositions[22][4] = {
|
||||
//first row
|
||||
{108, 38},
|
||||
{98, 38},
|
||||
{88, 38},
|
||||
{78, 38},
|
||||
{68, 38},
|
||||
{58, 38},
|
||||
{48, 38},
|
||||
{38, 38},
|
||||
//second row
|
||||
{104, 26},
|
||||
{94, 26},
|
||||
{84, 26},
|
||||
{74, 26},
|
||||
{64, 26},
|
||||
{54, 26},
|
||||
{44, 26},
|
||||
//third row
|
||||
{99, 14},
|
||||
{89, 14},
|
||||
{79, 14},
|
||||
{69, 14},
|
||||
{59, 14},
|
||||
{49, 14},
|
||||
};
|
||||
//endregion
|
||||
Icon* card_graphics = NULL;
|
||||
|
||||
void set_card_graphics(const Icon* graphics) {
|
||||
card_graphics = (Icon*)graphics;
|
||||
}
|
||||
|
||||
void draw_card_at_colored(
|
||||
int8_t pos_x,
|
||||
int8_t pos_y,
|
||||
uint8_t pip,
|
||||
uint8_t character,
|
||||
bool inverted,
|
||||
Canvas* const canvas) {
|
||||
DrawMode primary = inverted ? Black : White;
|
||||
DrawMode secondary = inverted ? White : Black;
|
||||
draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, primary);
|
||||
draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
|
||||
|
||||
uint8_t* drawInfo = pips[pip];
|
||||
uint8_t px = drawInfo[0], py = drawInfo[1], s = drawInfo[2];
|
||||
|
||||
uint8_t left = pos_x + 2;
|
||||
uint8_t right = (pos_x + CARD_WIDTH - s - 2);
|
||||
uint8_t top = pos_y + 2;
|
||||
uint8_t bottom = (pos_y + CARD_HEIGHT - s - 2);
|
||||
|
||||
draw_icon_clip(canvas, card_graphics, right, top, px, py, s, s, secondary);
|
||||
draw_icon_clip_flipped(canvas, card_graphics, left, bottom, px, py, s, s, secondary);
|
||||
|
||||
drawInfo = letters[character];
|
||||
px = drawInfo[0], py = drawInfo[1], s = drawInfo[2];
|
||||
left = pos_x + 2;
|
||||
right = (pos_x + CARD_WIDTH - s - 2);
|
||||
top = pos_y + 2;
|
||||
bottom = (pos_y + CARD_HEIGHT - s - 2);
|
||||
|
||||
draw_icon_clip(canvas, card_graphics, left, top + 1, px, py, s, s, secondary);
|
||||
draw_icon_clip_flipped(canvas, card_graphics, right, bottom - 1, px, py, s, s, secondary);
|
||||
}
|
||||
|
||||
void draw_card_at(int8_t pos_x, int8_t pos_y, uint8_t pip, uint8_t character, Canvas* const canvas) {
|
||||
draw_card_at_colored(pos_x, pos_y, pip, character, false, canvas);
|
||||
}
|
||||
|
||||
void draw_deck(const Card* cards, uint8_t count, Canvas* const canvas) {
|
||||
for(int i = count - 1; i >= 0; i--) {
|
||||
draw_card_at(
|
||||
playerCardPositions[i][0],
|
||||
playerCardPositions[i][1],
|
||||
cards[i].pip,
|
||||
cards[i].character,
|
||||
canvas);
|
||||
}
|
||||
}
|
||||
|
||||
Vector card_pos_at_index(uint8_t index) {
|
||||
return (Vector){playerCardPositions[index][0], playerCardPositions[index][1]};
|
||||
}
|
||||
|
||||
void draw_card_back_at(int8_t pos_x, int8_t pos_y, Canvas* const canvas) {
|
||||
draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, White);
|
||||
draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
|
||||
|
||||
draw_icon_clip(canvas, card_graphics, pos_x + 1, pos_y + 1, 35, 0, 15, 21, Black);
|
||||
}
|
||||
|
||||
void generate_deck(Deck* deck_ptr, uint8_t deck_count) {
|
||||
uint16_t counter = 0;
|
||||
if(deck_ptr->cards != NULL) {
|
||||
free(deck_ptr->cards);
|
||||
}
|
||||
|
||||
deck_ptr->deck_count = deck_count;
|
||||
deck_ptr->card_count = deck_count * 52;
|
||||
deck_ptr->cards = malloc(sizeof(Card) * deck_ptr->card_count);
|
||||
|
||||
for(uint8_t deck = 0; deck < deck_count; deck++) {
|
||||
for(uint8_t pip = 0; pip < 4; pip++) {
|
||||
for(uint8_t label = 0; label < 13; label++) {
|
||||
deck_ptr->cards[counter] = (Card){pip, label, false, false};
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void shuffle_deck(Deck* deck_ptr) {
|
||||
srand(DWT->CYCCNT);
|
||||
deck_ptr->index = 0;
|
||||
int max = deck_ptr->deck_count * 52;
|
||||
for(int i = 0; i < max; i++) {
|
||||
int r = i + (rand() % (max - i));
|
||||
Card tmp = deck_ptr->cards[i];
|
||||
deck_ptr->cards[i] = deck_ptr->cards[r];
|
||||
deck_ptr->cards[r] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t hand_count(const Card* cards, uint8_t count) {
|
||||
uint8_t aceCount = 0;
|
||||
uint8_t score = 0;
|
||||
|
||||
for(uint8_t i = 0; i < count; i++) {
|
||||
if(cards[i].character == 12)
|
||||
aceCount++;
|
||||
else {
|
||||
if(cards[i].character > 8)
|
||||
score += 10;
|
||||
else
|
||||
score += cards[i].character + 2;
|
||||
}
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < aceCount; i++) {
|
||||
if((score + 11) <= 21)
|
||||
score += 11;
|
||||
else
|
||||
score++;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
void draw_card_animation(
|
||||
Card animatingCard,
|
||||
Vector from,
|
||||
Vector control,
|
||||
Vector to,
|
||||
float t,
|
||||
bool extra_margin,
|
||||
Canvas* const canvas) {
|
||||
float time = t;
|
||||
if(extra_margin) {
|
||||
time += 0.2;
|
||||
}
|
||||
|
||||
Vector currentPos = quadratic_2d(from, control, to, time);
|
||||
if(t > 1) {
|
||||
draw_card_at(
|
||||
currentPos.x, currentPos.y, animatingCard.pip, animatingCard.character, canvas);
|
||||
} else {
|
||||
if(t < 0.5)
|
||||
draw_card_back_at(currentPos.x, currentPos.y, canvas);
|
||||
else
|
||||
draw_card_at(
|
||||
currentPos.x, currentPos.y, animatingCard.pip, animatingCard.character, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void init_hand(Hand* hand_ptr, uint8_t count) {
|
||||
hand_ptr->cards = malloc(sizeof(Card) * count);
|
||||
hand_ptr->index = 0;
|
||||
hand_ptr->max = count;
|
||||
}
|
||||
|
||||
void free_hand(Hand* hand_ptr) {
|
||||
FURI_LOG_D("CARD", "Freeing hand");
|
||||
free(hand_ptr->cards);
|
||||
}
|
||||
|
||||
void add_to_hand(Hand* hand_ptr, Card card) {
|
||||
FURI_LOG_D("CARD", "Adding to hand");
|
||||
if(hand_ptr->index < hand_ptr->max) {
|
||||
hand_ptr->cards[hand_ptr->index] = card;
|
||||
hand_ptr->index++;
|
||||
}
|
||||
}
|
||||
|
||||
void draw_card_space(int16_t pos_x, int16_t pos_y, bool highlighted, Canvas* const canvas) {
|
||||
if(highlighted) {
|
||||
draw_rounded_box_frame(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
|
||||
draw_rounded_box_frame(
|
||||
canvas, pos_x + 2, pos_y + 2, CARD_WIDTH - 4, CARD_HEIGHT - 4, White);
|
||||
} else {
|
||||
draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Black);
|
||||
draw_rounded_box_frame(
|
||||
canvas, pos_x + 2, pos_y + 2, CARD_WIDTH - 4, CARD_HEIGHT - 4, White);
|
||||
}
|
||||
}
|
||||
|
||||
int first_non_flipped_card(Hand hand) {
|
||||
for(int i = 0; i < hand.index; i++) {
|
||||
if(!hand.cards[i].flipped) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return hand.index;
|
||||
}
|
||||
|
||||
void draw_hand_column(
|
||||
Hand hand,
|
||||
int16_t pos_x,
|
||||
int16_t pos_y,
|
||||
int8_t highlight,
|
||||
Canvas* const canvas) {
|
||||
if(hand.index == 0) {
|
||||
draw_card_space(pos_x, pos_y, highlight > 0, canvas);
|
||||
if(highlight == 0)
|
||||
draw_rounded_box(canvas, pos_x, pos_y, CARD_WIDTH, CARD_HEIGHT, Inverse);
|
||||
return;
|
||||
}
|
||||
|
||||
int loopEnd = hand.index;
|
||||
int hStart = max(loopEnd - 4, 0);
|
||||
int pos = 0;
|
||||
int first = first_non_flipped_card(hand);
|
||||
bool wastop = false;
|
||||
if(first >= 0 && first <= hStart && highlight != first) {
|
||||
if(first > 0) {
|
||||
draw_card_back_at(pos_x, pos_y + pos, canvas);
|
||||
pos += 4;
|
||||
hStart++;
|
||||
wastop = true;
|
||||
}
|
||||
draw_card_at_colored(
|
||||
pos_x, pos_y + pos, hand.cards[first].pip, hand.cards[first].character, false, canvas);
|
||||
pos += 8;
|
||||
hStart++;
|
||||
}
|
||||
if(hStart > highlight && highlight >= 0) {
|
||||
if(!wastop && first > 0) {
|
||||
draw_card_back_at(pos_x, pos_y + pos, canvas);
|
||||
pos += 4;
|
||||
hStart++;
|
||||
}
|
||||
draw_card_at_colored(
|
||||
pos_x,
|
||||
pos_y + pos,
|
||||
hand.cards[highlight].pip,
|
||||
hand.cards[highlight].character,
|
||||
true,
|
||||
canvas);
|
||||
pos += 8;
|
||||
hStart++;
|
||||
}
|
||||
for(int i = hStart; i < loopEnd; i++, pos += 4) {
|
||||
if(hand.cards[i].flipped) {
|
||||
draw_card_back_at(pos_x, pos_y + pos, canvas);
|
||||
if(i == highlight)
|
||||
draw_rounded_box(
|
||||
canvas, pos_x + 1, pos_y + pos + 1, CARD_WIDTH - 2, CARD_HEIGHT - 2, Inverse);
|
||||
} else {
|
||||
draw_card_at_colored(
|
||||
pos_x,
|
||||
pos_y + pos,
|
||||
hand.cards[i].pip,
|
||||
hand.cards[i].character,
|
||||
(i == highlight),
|
||||
canvas);
|
||||
if(i == highlight || i == first) pos += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Card remove_from_deck(uint16_t index, Deck* deck) {
|
||||
FURI_LOG_D("CARD", "Removing from deck");
|
||||
Card result = {0, 0, true, false};
|
||||
if(deck->card_count > 0) {
|
||||
deck->card_count--;
|
||||
for(int i = 0, curr_index = 0; i <= deck->card_count; i++) {
|
||||
if(i != index) {
|
||||
deck->cards[curr_index] = deck->cards[i];
|
||||
curr_index++;
|
||||
} else {
|
||||
result = deck->cards[i];
|
||||
}
|
||||
}
|
||||
if(deck->index >= 0) {
|
||||
deck->index--;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void extract_hand_region(Hand* hand, Hand* to, uint8_t start_index) {
|
||||
FURI_LOG_D("CARD", "Extracting hand region");
|
||||
if(start_index >= hand->index) return;
|
||||
|
||||
for(uint8_t i = start_index; i < hand->index; i++) {
|
||||
add_to_hand(to, hand->cards[i]);
|
||||
}
|
||||
hand->index = start_index;
|
||||
}
|
||||
|
||||
void add_hand_region(Hand* to, Hand* from) {
|
||||
FURI_LOG_D("CARD", "Adding hand region");
|
||||
if((to->index + from->index) <= to->max) {
|
||||
for(int i = 0; i < from->index; i++) {
|
||||
add_to_hand(to, from->cards[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include "dml.h"
|
||||
|
||||
#define CARD_HEIGHT 23
|
||||
#define CARD_HALF_HEIGHT 11
|
||||
#define CARD_WIDTH 17
|
||||
#define CARD_HALF_WIDTH 8
|
||||
|
||||
//region types
|
||||
typedef struct {
|
||||
uint8_t pip; //Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
|
||||
uint8_t character; //Card letter [0-12], 0 means 2, 12 is Ace
|
||||
bool disabled;
|
||||
bool flipped;
|
||||
} Card;
|
||||
|
||||
typedef struct {
|
||||
uint8_t deck_count; //Number of decks used
|
||||
Card* cards; //Cards in the deck
|
||||
int card_count;
|
||||
int index; //Card index (to know where we at in the deck)
|
||||
} Deck;
|
||||
|
||||
typedef struct {
|
||||
Card* cards; //Cards in the deck
|
||||
uint8_t index; //Current index
|
||||
uint8_t max; //How many cards we want to store
|
||||
} Hand;
|
||||
//endregion
|
||||
|
||||
void set_card_graphics(const Icon* graphics);
|
||||
|
||||
/**
|
||||
* Gets card coordinates at the index (range: 0-20).
|
||||
*
|
||||
* @param index Index to check 0-20
|
||||
* @return Position of the card
|
||||
*/
|
||||
Vector card_pos_at_index(uint8_t index);
|
||||
|
||||
/**
|
||||
* Draws card at a given coordinate (top-left corner)
|
||||
*
|
||||
* @param pos_x X position
|
||||
* @param pos_y Y position
|
||||
* @param pip Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
|
||||
* @param character Letter [0-12] 0 is 2, 12 is A
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void draw_card_at(int8_t pos_x, int8_t pos_y, uint8_t pip, uint8_t character, Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Draws card at a given coordinate (top-left corner)
|
||||
*
|
||||
* @param pos_x X position
|
||||
* @param pos_y Y position
|
||||
* @param pip Pip index 0:spades, 1:hearths, 2:diamonds, 3:clubs
|
||||
* @param character Letter [0-12] 0 is 2, 12 is A
|
||||
* @param inverted Invert colors
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void draw_card_at_colored(
|
||||
int8_t pos_x,
|
||||
int8_t pos_y,
|
||||
uint8_t pip,
|
||||
uint8_t character,
|
||||
bool inverted,
|
||||
Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Draws 'count' cards at the bottom right corner
|
||||
*
|
||||
* @param cards List of cards
|
||||
* @param count Count of cards
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void draw_deck(const Card* cards, uint8_t count, Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Draws card back at a given coordinate (top-left corner)
|
||||
*
|
||||
* @param pos_x X coordinate
|
||||
* @param pos_y Y coordinate
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void draw_card_back_at(int8_t pos_x, int8_t pos_y, Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Generates the deck
|
||||
*
|
||||
* @param deck_ptr Pointer to the deck
|
||||
* @param deck_count Number of decks
|
||||
*/
|
||||
void generate_deck(Deck* deck_ptr, uint8_t deck_count);
|
||||
|
||||
/**
|
||||
* Shuffles the deck
|
||||
*
|
||||
* @param deck_ptr Pointer to the deck
|
||||
*/
|
||||
void shuffle_deck(Deck* deck_ptr);
|
||||
|
||||
/**
|
||||
* Calculates the hand count for blackjack
|
||||
*
|
||||
* @param cards List of cards
|
||||
* @param count Count of cards
|
||||
* @return Hand value
|
||||
*/
|
||||
uint8_t hand_count(const Card* cards, uint8_t count);
|
||||
|
||||
/**
|
||||
* Draws card animation
|
||||
*
|
||||
* @param animatingCard Card to animate
|
||||
* @param from Starting position
|
||||
* @param control Quadratic lerp control point
|
||||
* @param to End point
|
||||
* @param t Current time (0-1)
|
||||
* @param extra_margin Use extra margin at the end (arrives 0.2 unit before the end so it can stay there a bit)
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void draw_card_animation(
|
||||
Card animatingCard,
|
||||
Vector from,
|
||||
Vector control,
|
||||
Vector to,
|
||||
float t,
|
||||
bool extra_margin,
|
||||
Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Init hand pointer
|
||||
* @param hand_ptr Pointer to hand
|
||||
* @param count Number of cards we want to store
|
||||
*/
|
||||
void init_hand(Hand* hand_ptr, uint8_t count);
|
||||
|
||||
/**
|
||||
* Free hand resources
|
||||
* @param hand_ptr Pointer to hand
|
||||
*/
|
||||
void free_hand(Hand* hand_ptr);
|
||||
|
||||
/**
|
||||
* Add card to hand
|
||||
* @param hand_ptr Pointer to hand
|
||||
* @param card Card to add
|
||||
*/
|
||||
void add_to_hand(Hand* hand_ptr, Card card);
|
||||
|
||||
/**
|
||||
* Draw card placement position at coordinate
|
||||
* @param pos_x X coordinate
|
||||
* @param pos_y Y coordinate
|
||||
* @param highlighted Apply highlight effect
|
||||
* @param canvas Canvas object
|
||||
*/
|
||||
void draw_card_space(int16_t pos_x, int16_t pos_y, bool highlighted, Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Draws a column of card, displaying the last [max_cards] cards on the list
|
||||
* @param hand Hand object
|
||||
* @param pos_x X coordinate to draw
|
||||
* @param pos_y Y coordinate to draw
|
||||
* @param highlight Index to highlight, negative means no highlight
|
||||
* @param canvas Canvas object
|
||||
*/
|
||||
void draw_hand_column(
|
||||
Hand hand,
|
||||
int16_t pos_x,
|
||||
int16_t pos_y,
|
||||
int8_t highlight,
|
||||
Canvas* const canvas);
|
||||
|
||||
/**
|
||||
* Removes a card from the deck (Be aware, if you remove the first item, the deck index will be at -1 so you have to handle that)
|
||||
* @param index Index to remove
|
||||
* @param deck Deck reference
|
||||
* @return The removed card
|
||||
*/
|
||||
Card remove_from_deck(uint16_t index, Deck* deck);
|
||||
|
||||
int first_non_flipped_card(Hand hand);
|
||||
|
||||
void extract_hand_region(Hand* hand, Hand* to, uint8_t start_index);
|
||||
|
||||
void add_hand_region(Hand* to, Hand* from);
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "dml.h"
|
||||
#include <math.h>
|
||||
|
||||
float lerp(float v0, float v1, float t) {
|
||||
if(t > 1) return v1;
|
||||
return (1 - t) * v0 + t * v1;
|
||||
}
|
||||
|
||||
Vector lerp_2d(Vector start, Vector end, float t) {
|
||||
return (Vector){
|
||||
lerp(start.x, end.x, t),
|
||||
lerp(start.y, end.y, t),
|
||||
};
|
||||
}
|
||||
|
||||
Vector quadratic_2d(Vector start, Vector control, Vector end, float t) {
|
||||
return lerp_2d(lerp_2d(start, control, t), lerp_2d(control, end, t), t);
|
||||
}
|
||||
|
||||
Vector vector_add(Vector a, Vector b) {
|
||||
return (Vector){a.x + b.x, a.y + b.y};
|
||||
}
|
||||
|
||||
Vector vector_sub(Vector a, Vector b) {
|
||||
return (Vector){a.x - b.x, a.y - b.y};
|
||||
}
|
||||
|
||||
Vector vector_mul_components(Vector a, Vector b) {
|
||||
return (Vector){a.x * b.x, a.y * b.y};
|
||||
}
|
||||
|
||||
Vector vector_div_components(Vector a, Vector b) {
|
||||
return (Vector){a.x / b.x, a.y / b.y};
|
||||
}
|
||||
|
||||
Vector vector_normalized(Vector a) {
|
||||
float length = vector_magnitude(a);
|
||||
return (Vector){a.x / length, a.y / length};
|
||||
}
|
||||
|
||||
float vector_magnitude(Vector a) {
|
||||
return sqrt(a.x * a.x + a.y * a.y);
|
||||
}
|
||||
|
||||
float vector_distance(Vector a, Vector b) {
|
||||
return vector_magnitude(vector_sub(a, b));
|
||||
}
|
||||
|
||||
float vector_dot(Vector a, Vector b) {
|
||||
Vector _a = vector_normalized(a);
|
||||
Vector _b = vector_normalized(b);
|
||||
return _a.x * _b.x + _a.y * _b.y;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// Doofy's Math library
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
} Vector;
|
||||
|
||||
#define min(a, b) ((a) < (b) ? (a) : (b))
|
||||
#define max(a, b) ((a) > (b) ? (a) : (b))
|
||||
#define abs(x) ((x) > 0 ? (x) : -(x))
|
||||
|
||||
/**
|
||||
* Lerp function
|
||||
*
|
||||
* @param v0 Start value
|
||||
* @param v1 End value
|
||||
* @param t Time (0-1 range)
|
||||
* @return Point between v0-v1 at a given time
|
||||
*/
|
||||
float lerp(float v0, float v1, float t);
|
||||
|
||||
/**
|
||||
* 2D lerp function
|
||||
*
|
||||
* @param start Start vector
|
||||
* @param end End vector
|
||||
* @param t Time (0-1 range)
|
||||
* @return 2d Vector between start and end at time
|
||||
*/
|
||||
Vector lerp_2d(Vector start, Vector end, float t);
|
||||
|
||||
/**
|
||||
* Quadratic lerp function
|
||||
*
|
||||
* @param start Start vector
|
||||
* @param control Control point
|
||||
* @param end End vector
|
||||
* @param t Time (0-1 range)
|
||||
* @return 2d Vector at time
|
||||
*/
|
||||
Vector quadratic_2d(Vector start, Vector control, Vector end, float t);
|
||||
|
||||
/**
|
||||
* Add vector components together
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return Resulting vector
|
||||
*/
|
||||
Vector vector_add(Vector a, Vector b);
|
||||
|
||||
/**
|
||||
* Subtract vector components together
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return Resulting vector
|
||||
*/
|
||||
Vector vector_sub(Vector a, Vector b);
|
||||
|
||||
/**
|
||||
* Multiplying vector components together
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return Resulting vector
|
||||
*/
|
||||
Vector vector_mul_components(Vector a, Vector b);
|
||||
|
||||
/**
|
||||
* Dividing vector components
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return Resulting vector
|
||||
*/
|
||||
Vector vector_div_components(Vector a, Vector b);
|
||||
|
||||
/**
|
||||
* Calculating Vector length
|
||||
*
|
||||
* @param a Direction vector
|
||||
* @return Length of the vector
|
||||
*/
|
||||
float vector_magnitude(Vector a);
|
||||
|
||||
/**
|
||||
* Get a normalized vector (length of 1)
|
||||
*
|
||||
* @param a Direction vector
|
||||
* @return Normalized vector
|
||||
*/
|
||||
Vector vector_normalized(Vector a);
|
||||
|
||||
/**
|
||||
* Calculate two vector's distance
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return Distance between vectors
|
||||
*/
|
||||
float vector_distance(Vector a, Vector b);
|
||||
|
||||
/**
|
||||
* Calculate the dot product of the vectors.
|
||||
* No need to normalize, it will do it
|
||||
*
|
||||
* @param a First vector
|
||||
* @param b Second vector
|
||||
* @return value from -1 to 1
|
||||
*/
|
||||
float vector_dot(Vector a, Vector b);
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "menu.h"
|
||||
|
||||
void add_menu(Menu* menu, const char* name, void (*callback)(void*)) {
|
||||
MenuItem* items = menu->items;
|
||||
|
||||
menu->items = malloc(sizeof(MenuItem) * (menu->menu_count + 1));
|
||||
for(uint8_t i = 0; i < menu->menu_count; i++) {
|
||||
menu->items[i] = items[i];
|
||||
}
|
||||
free(items);
|
||||
|
||||
menu->items[menu->menu_count] = (MenuItem){name, true, callback};
|
||||
menu->menu_count++;
|
||||
}
|
||||
|
||||
void free_menu(Menu* menu) {
|
||||
free(menu->items);
|
||||
free(menu);
|
||||
}
|
||||
|
||||
void set_menu_state(Menu* menu, uint8_t index, bool state) {
|
||||
if(menu->menu_count > index) {
|
||||
menu->items[index].enabled = state;
|
||||
}
|
||||
if(!state && menu->current_menu == index) move_menu(menu, 1);
|
||||
}
|
||||
|
||||
void move_menu(Menu* menu, int8_t direction) {
|
||||
if(!menu->enabled) return;
|
||||
int max = menu->menu_count;
|
||||
for(int8_t i = 0; i < max; i++) {
|
||||
FURI_LOG_D(
|
||||
"MENU",
|
||||
"Iteration %i, current %i, direction %i, state %i",
|
||||
i,
|
||||
menu->current_menu,
|
||||
direction,
|
||||
menu->items[menu->current_menu].enabled ? 1 : 0);
|
||||
if(direction < 0 && menu->current_menu == 0) {
|
||||
menu->current_menu = menu->menu_count - 1;
|
||||
} else {
|
||||
menu->current_menu = (menu->current_menu + direction) % menu->menu_count;
|
||||
}
|
||||
FURI_LOG_D(
|
||||
"MENU",
|
||||
"After process current %i, direction %i, state %i",
|
||||
menu->current_menu,
|
||||
direction,
|
||||
menu->items[menu->current_menu].enabled ? 1 : 0);
|
||||
if(menu->items[menu->current_menu].enabled) {
|
||||
FURI_LOG_D("MENU", "Next menu %i", menu->current_menu);
|
||||
return;
|
||||
}
|
||||
}
|
||||
FURI_LOG_D("MENU", "Not found, setting false");
|
||||
menu->enabled = false;
|
||||
}
|
||||
|
||||
void activate_menu(Menu* menu, void* state) {
|
||||
if(!menu->enabled) return;
|
||||
menu->items[menu->current_menu].callback(state);
|
||||
}
|
||||
|
||||
void render_menu(Menu* menu, Canvas* canvas, uint8_t pos_x, uint8_t pos_y) {
|
||||
if(!menu->enabled) return;
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_rbox(canvas, pos_x, pos_y, menu->menu_width + 2, 10, 2);
|
||||
|
||||
uint8_t w = pos_x + menu->menu_width;
|
||||
uint8_t h = pos_y + 10;
|
||||
uint8_t p1x = pos_x + 2;
|
||||
uint8_t p2x = pos_x + menu->menu_width - 2;
|
||||
uint8_t p1y = pos_y + 2;
|
||||
uint8_t p2y = pos_y + 8;
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_line(canvas, p1x, pos_y, p2x, pos_y);
|
||||
canvas_draw_line(canvas, p1x, h, p2x, h);
|
||||
canvas_draw_line(canvas, pos_x, p1y, pos_x, p2y);
|
||||
canvas_draw_line(canvas, w, p1y, w, p2y);
|
||||
canvas_draw_dot(canvas, pos_x + 1, pos_y + 1);
|
||||
canvas_draw_dot(canvas, w - 1, pos_y + 1);
|
||||
canvas_draw_dot(canvas, w - 1, h - 1);
|
||||
canvas_draw_dot(canvas, pos_x + 1, h - 1);
|
||||
|
||||
// canvas_draw_rbox(canvas, pos_x, pos_y, menu->menu_width + 2, 10, 2);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
canvas_draw_str_aligned(
|
||||
canvas,
|
||||
pos_x + menu->menu_width / 2,
|
||||
pos_y + 6,
|
||||
AlignCenter,
|
||||
AlignCenter,
|
||||
menu->items[menu->current_menu].name);
|
||||
//9*5
|
||||
int center = pos_x + menu->menu_width / 2;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
for(int8_t j = -i; j <= i; j++) {
|
||||
canvas_draw_dot(canvas, center + j, pos_y - 4 + i);
|
||||
canvas_draw_dot(canvas, center + j, pos_y + 14 - i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
|
||||
typedef struct {
|
||||
const char* name; //Name of the menu
|
||||
bool enabled; //Is the menu item enabled (it will not render, you cannot select it)
|
||||
|
||||
void (*callback)(
|
||||
void* state); //Callback for when the activate_menu is called while this menu is selected
|
||||
} MenuItem;
|
||||
|
||||
typedef struct {
|
||||
MenuItem* items; //list of menu items
|
||||
uint8_t menu_count; //count of menu items (do not change)
|
||||
uint8_t current_menu; //currently selected menu item
|
||||
uint8_t menu_width; //width of the menu
|
||||
bool enabled; //is the menu enabled (it will not render and accept events when disabled)
|
||||
} Menu;
|
||||
|
||||
/**
|
||||
* Cleans up the pointers used by the menu
|
||||
*
|
||||
* @param menu Pointer of the menu to clean up
|
||||
*/
|
||||
void free_menu(Menu* menu);
|
||||
|
||||
/**
|
||||
* Add a new menu item
|
||||
*
|
||||
* @param menu Pointer of the menu
|
||||
* @param name Name of the menu item
|
||||
* @param callback Callback called on activation
|
||||
*/
|
||||
void add_menu(Menu* menu, const char* name, void (*callback)(void*));
|
||||
|
||||
/**
|
||||
* Setting menu item to be enabled/disabled
|
||||
*
|
||||
* @param menu Pointer of the menu
|
||||
* @param index Menu index to set
|
||||
* @param state Enabled (true), Disabled(false)
|
||||
*/
|
||||
void set_menu_state(Menu* menu, uint8_t index, bool state);
|
||||
|
||||
/**
|
||||
* Moves selection up or down
|
||||
*
|
||||
* @param menu Pointer of the menu
|
||||
* @param direction Direction to move -1 down, 1 up
|
||||
*/
|
||||
void move_menu(Menu* menu, int8_t direction);
|
||||
|
||||
/**
|
||||
* Triggers the current menu callback
|
||||
*
|
||||
* @param menu Pointer of the menu
|
||||
* @param state Usually your application state
|
||||
*/
|
||||
void activate_menu(Menu* menu, void* state);
|
||||
|
||||
/**
|
||||
* Renders the menu at a coordinate (call it in your render function).
|
||||
*
|
||||
* Keep in mind that Flipper has a 128x64 pixel screen resolution and the coordinate
|
||||
* you give is the menu's rectangle top-left corner (arrows not included).
|
||||
* The rectangle height is 10 px, the arrows have a 4 pixel height. Space needed is 18px.
|
||||
* The width of the menu can be configured in the menu object.
|
||||
*
|
||||
*
|
||||
* @param menu Pointer of the menu
|
||||
* @param canvas Flippers Canvas pointer
|
||||
* @param pos_x X position to draw
|
||||
* @param pos_y Y position to draw
|
||||
*/
|
||||
void render_menu(Menu* menu, Canvas* canvas, uint8_t pos_x, uint8_t pos_y);
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "queue.h"
|
||||
|
||||
void render_queue(const QueueState* queue_state, const void* app_state, Canvas* const canvas) {
|
||||
if(queue_state->current != NULL && queue_state->current->render != NULL)
|
||||
((QueueItem*)queue_state->current)->render(app_state, canvas);
|
||||
}
|
||||
|
||||
bool run_queue(QueueState* queue_state, void* app_state) {
|
||||
if(queue_state->current != NULL) {
|
||||
queue_state->running = true;
|
||||
if((furi_get_tick() - queue_state->start) >= queue_state->current->duration)
|
||||
dequeue(queue_state, app_state);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void dequeue(QueueState* queue_state, void* app_state) {
|
||||
((QueueItem*)queue_state->current)->callback(app_state);
|
||||
QueueItem* f = queue_state->current;
|
||||
queue_state->current = f->next;
|
||||
free(f);
|
||||
if(queue_state->current != NULL) {
|
||||
if(queue_state->current->start != NULL) queue_state->current->start(app_state);
|
||||
queue_state->start = furi_get_tick();
|
||||
} else {
|
||||
queue_state->running = false;
|
||||
}
|
||||
}
|
||||
|
||||
void queue_clear(QueueState* queue_state) {
|
||||
queue_state->running = false;
|
||||
QueueItem* curr = queue_state->current;
|
||||
while(curr != NULL) {
|
||||
QueueItem* f = curr;
|
||||
curr = curr->next;
|
||||
free(f);
|
||||
}
|
||||
}
|
||||
|
||||
void enqueue(
|
||||
QueueState* queue_state,
|
||||
void* app_state,
|
||||
void (*done)(void* state),
|
||||
void (*start)(void* state),
|
||||
void (*render)(const void* state, Canvas* const canvas),
|
||||
uint32_t duration) {
|
||||
QueueItem* next;
|
||||
if(queue_state->current == NULL) {
|
||||
queue_state->start = furi_get_tick();
|
||||
queue_state->current = malloc(sizeof(QueueItem));
|
||||
next = queue_state->current;
|
||||
if(next->start != NULL) next->start(app_state);
|
||||
|
||||
} else {
|
||||
next = queue_state->current;
|
||||
while(next->next != NULL) {
|
||||
next = (QueueItem*)(next->next);
|
||||
}
|
||||
next->next = malloc(sizeof(QueueItem));
|
||||
next = next->next;
|
||||
}
|
||||
next->callback = done;
|
||||
next->render = render;
|
||||
next->start = start;
|
||||
next->duration = duration;
|
||||
next->next = NULL;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <furi.h>
|
||||
|
||||
typedef struct {
|
||||
void (*callback)(void* state); //Callback for when the item is dequeued
|
||||
void (*render)(
|
||||
const void* state,
|
||||
Canvas* const canvas); //Callback for the rendering loop while this item is running
|
||||
void (*start)(void* state); //Callback when this item is started running
|
||||
void* next; //Pointer to the next item
|
||||
uint32_t duration; //duration of the item
|
||||
} QueueItem;
|
||||
|
||||
typedef struct {
|
||||
unsigned int start; //current queue item start time
|
||||
QueueItem* current; //current queue item
|
||||
bool running; //is the queue running
|
||||
} QueueState;
|
||||
|
||||
/**
|
||||
* Enqueue a new item.
|
||||
*
|
||||
* @param queue_state The queue state pointer
|
||||
* @param app_state Your app state
|
||||
* @param done Callback for dequeue event
|
||||
* @param start Callback for when the item is activated
|
||||
* @param render Callback to render loop if needed
|
||||
* @param duration Length of the item
|
||||
*/
|
||||
void enqueue(
|
||||
QueueState* queue_state,
|
||||
void* app_state,
|
||||
void (*done)(void* state),
|
||||
void (*start)(void* state),
|
||||
void (*render)(const void* state, Canvas* const canvas),
|
||||
uint32_t duration);
|
||||
/**
|
||||
* Clears all queue items
|
||||
*
|
||||
* @param queue_state The queue state pointer
|
||||
*/
|
||||
void queue_clear(QueueState* queue_state);
|
||||
|
||||
/**
|
||||
* Dequeues the active queue item. Usually you don't need to call it directly.
|
||||
*
|
||||
* @param queue_state The queue state pointer
|
||||
* @param app_state Your application state
|
||||
*/
|
||||
void dequeue(QueueState* queue_state, void* app_state);
|
||||
|
||||
/**
|
||||
* Runs the queue logic (place it in your tick function)
|
||||
*
|
||||
* @param queue_state The queue state pointer
|
||||
* @param app_state Your application state
|
||||
* @return FALSE when there is nothing to run, TRUE otherwise
|
||||
*/
|
||||
bool run_queue(QueueState* queue_state, void* app_state);
|
||||
|
||||
/**
|
||||
* Calls the currently active queue items render callback (if there is any)
|
||||
*
|
||||
* @param queue_state The queue state pointer
|
||||
* @param app_state Your application state
|
||||
* @param canvas Pointer to Flipper's canvas object
|
||||
*/
|
||||
void render_queue(const QueueState* queue_state, const void* app_state, Canvas* const canvas);
|
||||
@@ -0,0 +1,257 @@
|
||||
#include "ui.h"
|
||||
#include <gui/canvas_i.h>
|
||||
#include <u8g2_glue.h>
|
||||
#include <gui/icon_animation_i.h>
|
||||
#include <gui/icon.h>
|
||||
#include <gui/icon_i.h>
|
||||
#include <furi_hal.h>
|
||||
|
||||
TileMap* tileMap;
|
||||
uint8_t tileMapCount = 0;
|
||||
|
||||
void ui_cleanup() {
|
||||
if(tileMap != NULL) {
|
||||
for(uint8_t i = 0; i < tileMapCount; i++) {
|
||||
if(tileMap[i].data != NULL) free(tileMap[i].data);
|
||||
}
|
||||
free(tileMap);
|
||||
}
|
||||
}
|
||||
|
||||
void add_new_tilemap(uint8_t* data, unsigned long iconId) {
|
||||
TileMap* old = tileMap;
|
||||
tileMapCount++;
|
||||
tileMap = malloc(sizeof(TileMap) * tileMapCount);
|
||||
if(tileMapCount > 1) {
|
||||
for(uint8_t i = 0; i < tileMapCount; i++) tileMap[i] = old[i];
|
||||
}
|
||||
tileMap[tileMapCount - 1] = (TileMap){data, iconId};
|
||||
}
|
||||
|
||||
uint8_t* get_tilemap(unsigned long icon_id) {
|
||||
for(uint8_t i = 0; i < tileMapCount; i++) {
|
||||
if(tileMap[i].iconId == icon_id) return tileMap[i].data;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint32_t pixel_index(uint8_t x, uint8_t y) {
|
||||
return y * SCREEN_WIDTH + x;
|
||||
}
|
||||
|
||||
bool in_screen(int16_t x, int16_t y) {
|
||||
return x >= 0 && x < SCREEN_WIDTH && y >= 0 && y < SCREEN_HEIGHT;
|
||||
}
|
||||
|
||||
unsigned flipBit(uint8_t x, uint8_t bit) {
|
||||
return x ^ (1 << bit);
|
||||
}
|
||||
|
||||
unsigned setBit(uint8_t x, uint8_t bit) {
|
||||
return x | (1 << bit);
|
||||
}
|
||||
|
||||
unsigned unsetBit(uint8_t x, uint8_t bit) {
|
||||
return x & ~(1 << bit);
|
||||
}
|
||||
|
||||
bool test_pixel(uint8_t* data, uint8_t x, uint8_t y, uint8_t w) {
|
||||
uint8_t current_bit = (y % 8);
|
||||
uint8_t current_row = ((y - current_bit) / 8);
|
||||
uint8_t current_value = data[current_row * w + x];
|
||||
return current_value & (1 << current_bit);
|
||||
}
|
||||
|
||||
uint8_t* get_buffer(Canvas* const canvas) {
|
||||
return canvas->fb.tile_buf_ptr;
|
||||
// return canvas_get_buffer(canvas);
|
||||
}
|
||||
uint8_t* make_buffer() {
|
||||
return malloc(sizeof(uint8_t) * 8 * 128);
|
||||
}
|
||||
void clone_buffer(uint8_t* canvas, uint8_t* data) {
|
||||
for(int i = 0; i < 1024; i++) {
|
||||
data[i] = canvas[i];
|
||||
}
|
||||
}
|
||||
|
||||
bool read_pixel(Canvas* const canvas, int16_t x, int16_t y) {
|
||||
if(in_screen(x, y)) {
|
||||
return test_pixel(get_buffer(canvas), x, y, SCREEN_WIDTH);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void set_pixel(Canvas* const canvas, int16_t x, int16_t y, DrawMode draw_mode) {
|
||||
if(in_screen(x, y)) {
|
||||
uint8_t current_bit = (y % 8);
|
||||
uint8_t current_row = ((y - current_bit) / 8);
|
||||
uint32_t i = pixel_index(x, current_row);
|
||||
uint8_t* buffer = get_buffer(canvas);
|
||||
|
||||
uint8_t current_value = buffer[i];
|
||||
if(draw_mode == Inverse) {
|
||||
buffer[i] = flipBit(current_value, current_bit);
|
||||
} else {
|
||||
if(draw_mode == White) {
|
||||
buffer[i] = unsetBit(current_value, current_bit);
|
||||
} else {
|
||||
buffer[i] = setBit(current_value, current_bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_line(
|
||||
Canvas* const canvas,
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
DrawMode draw_mode) {
|
||||
for(int16_t x = x2; x >= x1; x--) {
|
||||
for(int16_t y = y2; y >= y1; y--) {
|
||||
set_pixel(canvas, x, y, draw_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_rounded_box_frame(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode draw_mode) {
|
||||
int16_t xMinCorner = x + 1;
|
||||
int16_t xMax = x + w - 1;
|
||||
int16_t xMaxCorner = x + w - 2;
|
||||
int16_t yMinCorner = y + 1;
|
||||
int16_t yMax = y + h - 1;
|
||||
int16_t yMaxCorner = y + h - 2;
|
||||
draw_line(canvas, xMinCorner, y, xMaxCorner, y, draw_mode);
|
||||
draw_line(canvas, xMinCorner, yMax, xMaxCorner, yMax, draw_mode);
|
||||
draw_line(canvas, x, yMinCorner, x, yMaxCorner, draw_mode);
|
||||
draw_line(canvas, xMax, yMinCorner, xMax, yMaxCorner, draw_mode);
|
||||
}
|
||||
|
||||
void draw_rounded_box(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode draw_mode) {
|
||||
for(int16_t o = w - 2; o >= 1; o--) {
|
||||
for(int16_t p = h - 2; p >= 1; p--) {
|
||||
set_pixel(canvas, x + o, y + p, draw_mode);
|
||||
}
|
||||
}
|
||||
draw_rounded_box_frame(canvas, x, y, w, h, draw_mode);
|
||||
}
|
||||
|
||||
void invert_shape(Canvas* const canvas, uint8_t* data, int16_t x, int16_t y, uint8_t w, uint8_t h) {
|
||||
draw_pixels(canvas, data, x, y, w, h, Inverse);
|
||||
}
|
||||
|
||||
void draw_pixels(
|
||||
Canvas* const canvas,
|
||||
uint8_t* data,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode) {
|
||||
for(int8_t o = 0; o < w; o++) {
|
||||
for(int8_t p = 0; p < h; p++) {
|
||||
if(in_screen(o + x, p + y) && data[p * w + o] == 1)
|
||||
set_pixel(canvas, o + x, p + y, drawMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_rectangle(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode) {
|
||||
for(int8_t o = 0; o < w; o++) {
|
||||
for(int8_t p = 0; p < h; p++) {
|
||||
if(in_screen(o + x, p + y)) {
|
||||
set_pixel(canvas, o + x, p + y, drawMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void invert_rectangle(Canvas* const canvas, int16_t x, int16_t y, uint8_t w, uint8_t h) {
|
||||
draw_rectangle(canvas, x, y, w, h, Inverse);
|
||||
}
|
||||
|
||||
uint8_t* image_data(Canvas* const canvas, const Icon* icon) {
|
||||
uint8_t* data = malloc(sizeof(uint8_t) * 8 * 128);
|
||||
uint8_t* screen = canvas->fb.tile_buf_ptr;
|
||||
canvas->fb.tile_buf_ptr = data;
|
||||
canvas_draw_icon(canvas, 0, 0, icon);
|
||||
canvas->fb.tile_buf_ptr = screen;
|
||||
return data;
|
||||
}
|
||||
|
||||
uint8_t* getOrAddIconData(Canvas* const canvas, const Icon* icon) {
|
||||
uint8_t* icon_data = get_tilemap((unsigned long)icon);
|
||||
if(icon_data == NULL) {
|
||||
icon_data = image_data(canvas, icon);
|
||||
add_new_tilemap(icon_data, (unsigned long)icon);
|
||||
}
|
||||
return icon_data;
|
||||
}
|
||||
|
||||
void draw_icon_clip(
|
||||
Canvas* const canvas,
|
||||
const Icon* icon,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t left,
|
||||
uint8_t top,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode) {
|
||||
uint8_t* icon_data = getOrAddIconData(canvas, icon);
|
||||
|
||||
for(int i = 0; i < w; i++) {
|
||||
for(int j = 0; j < h; j++) {
|
||||
bool on = test_pixel(icon_data, left + i, top + j, SCREEN_WIDTH);
|
||||
if(drawMode == Filled) {
|
||||
set_pixel(canvas, x + i, y + j, on ? Black : White);
|
||||
} else if(on)
|
||||
set_pixel(canvas, x + i, y + j, drawMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_icon_clip_flipped(
|
||||
Canvas* const canvas,
|
||||
const Icon* icon,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t left,
|
||||
uint8_t top,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode) {
|
||||
uint8_t* icon_data = getOrAddIconData(canvas, icon);
|
||||
|
||||
for(int i = 0; i < w; i++) {
|
||||
for(int j = 0; j < h; j++) {
|
||||
bool on = test_pixel(icon_data, left + i, top + j, SCREEN_WIDTH);
|
||||
|
||||
if(drawMode == Filled) {
|
||||
set_pixel(canvas, x + w - i - 1, y + h - j - 1, on ? Black : White);
|
||||
} else if(on)
|
||||
set_pixel(canvas, x + w - i - 1, y + h - j - 1, drawMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/canvas.h>
|
||||
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
|
||||
typedef enum {
|
||||
Black,
|
||||
White,
|
||||
Inverse,
|
||||
Filled //Currently only for Icon clip drawing
|
||||
} DrawMode;
|
||||
|
||||
// size is the screen size
|
||||
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
unsigned long iconId;
|
||||
} TileMap;
|
||||
|
||||
bool test_pixel(uint8_t* data, uint8_t x, uint8_t y, uint8_t w);
|
||||
|
||||
uint8_t* image_data(Canvas* const canvas, const Icon* icon);
|
||||
|
||||
uint32_t pixel_index(uint8_t x, uint8_t y);
|
||||
|
||||
void draw_icon_clip(
|
||||
Canvas* const canvas,
|
||||
const Icon* icon,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t left,
|
||||
uint8_t top,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
void draw_icon_clip_flipped(
|
||||
Canvas* const canvas,
|
||||
const Icon* icon,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t left,
|
||||
uint8_t top,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
void draw_rounded_box(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
void draw_rounded_box_frame(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
void draw_rectangle(
|
||||
Canvas* const canvas,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
void invert_rectangle(Canvas* const canvas, int16_t x, int16_t y, uint8_t w, uint8_t h);
|
||||
|
||||
void invert_shape(Canvas* const canvas, uint8_t* data, int16_t x, int16_t y, uint8_t w, uint8_t h);
|
||||
|
||||
void draw_pixels(
|
||||
Canvas* const canvas,
|
||||
uint8_t* data,
|
||||
int16_t x,
|
||||
int16_t y,
|
||||
uint8_t w,
|
||||
uint8_t h,
|
||||
DrawMode drawMode);
|
||||
|
||||
bool read_pixel(Canvas* const canvas, int16_t x, int16_t y);
|
||||
|
||||
void set_pixel(Canvas* const canvas, int16_t x, int16_t y, DrawMode draw_mode);
|
||||
|
||||
void draw_line(
|
||||
Canvas* const canvas,
|
||||
int16_t x1,
|
||||
int16_t y1,
|
||||
int16_t x2,
|
||||
int16_t y2,
|
||||
DrawMode draw_mode);
|
||||
|
||||
bool in_screen(int16_t x, int16_t y);
|
||||
|
||||
void ui_cleanup();
|
||||
uint8_t* get_buffer(Canvas* const canvas);
|
||||
uint8_t* make_buffer();
|
||||
void clone_buffer(uint8_t* canvas, uint8_t* data);
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <input/input.h>
|
||||
#include <gui/elements.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#include <flipper_format/flipper_format_i.h>
|
||||
#include "common/card.h"
|
||||
#include "common/queue.h"
|
||||
#include "common/menu.h"
|
||||
|
||||
#define APP_NAME "Blackjack"
|
||||
|
||||
#define CONF_ANIMATION_DURATION "AnimationDuration"
|
||||
#define CONF_MESSAGE_DURATION "MessageDuration"
|
||||
#define CONF_STARTING_MONEY "StartingMoney"
|
||||
#define CONF_ROUND_PRICE "RoundPrice"
|
||||
#define CONF_SOUND_EFFECTS "SoundEffects"
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
typedef struct {
|
||||
uint32_t animation_duration;
|
||||
uint32_t message_duration;
|
||||
uint32_t starting_money;
|
||||
uint32_t round_price;
|
||||
bool sound_effects;
|
||||
} Settings;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} AppEvent;
|
||||
|
||||
typedef enum {
|
||||
GameStateGameOver,
|
||||
GameStateStart,
|
||||
GameStatePlay,
|
||||
GameStateSettings,
|
||||
GameStateDealer,
|
||||
} PlayState;
|
||||
|
||||
typedef enum {
|
||||
DirectionUp,
|
||||
DirectionDown,
|
||||
DirectionRight,
|
||||
DirectionLeft,
|
||||
Select,
|
||||
Back,
|
||||
None
|
||||
} Direction;
|
||||
|
||||
typedef struct {
|
||||
Card player_cards[21];
|
||||
Card dealer_cards[21];
|
||||
uint8_t player_card_count;
|
||||
uint8_t dealer_card_count;
|
||||
|
||||
Direction selectDirection;
|
||||
Settings settings;
|
||||
|
||||
uint32_t player_score;
|
||||
uint32_t bet;
|
||||
uint8_t selectedMenu;
|
||||
bool doubled;
|
||||
bool started;
|
||||
bool processing;
|
||||
Deck deck;
|
||||
PlayState state;
|
||||
QueueState queue_state;
|
||||
Menu* menu;
|
||||
unsigned int last_tick;
|
||||
} GameState;
|
||||
@@ -0,0 +1,186 @@
|
||||
#include <math.h>
|
||||
#include <notification/notification_messages.h>
|
||||
|
||||
#include "ui.h"
|
||||
|
||||
#define LINE_HEIGHT 16
|
||||
#define ITEM_PADDING 4
|
||||
|
||||
const char MoneyMul[4] = {'K', 'B', 'T', 'S'};
|
||||
|
||||
void draw_player_scene(Canvas* const canvas, const GameState* game_state) {
|
||||
int max_card = game_state->player_card_count;
|
||||
|
||||
if(max_card > 0) draw_deck((game_state->player_cards), max_card, canvas);
|
||||
|
||||
if(game_state->dealer_card_count > 0) draw_card_back_at(13, 5, canvas);
|
||||
|
||||
max_card = game_state->dealer_card_count;
|
||||
if(max_card > 1) {
|
||||
draw_card_at(
|
||||
2, 2, game_state->dealer_cards[1].pip, game_state->dealer_cards[1].character, canvas);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_dealer_scene(Canvas* const canvas, const GameState* game_state) {
|
||||
uint8_t max_card = game_state->dealer_card_count;
|
||||
draw_deck((game_state->dealer_cards), max_card, canvas);
|
||||
}
|
||||
|
||||
void popup_frame(Canvas* const canvas) {
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 32, 15, 66, 13);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(canvas, 32, 15, 66, 13);
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
}
|
||||
|
||||
void draw_play_menu(Canvas* const canvas, const GameState* game_state) {
|
||||
const char* menus[3] = {"Double", "Hit", "Stay"};
|
||||
for(uint8_t m = 0; m < 3; m++) {
|
||||
if(m == 0 &&
|
||||
(game_state->doubled || game_state->player_score < game_state->settings.round_price))
|
||||
continue;
|
||||
int y = m * 13 + 25;
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(game_state->selectedMenu == m) {
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, 1, y, 31, 12);
|
||||
} else {
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 1, y, 31, 12);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_frame(canvas, 1, y, 31, 12);
|
||||
}
|
||||
|
||||
if(game_state->selectedMenu == m)
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
else
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_str_aligned(canvas, 16, y + 6, AlignCenter, AlignCenter, menus[m]);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_screen(Canvas* const canvas, const bool* points) {
|
||||
for(uint8_t x = 0; x < 128; x++) {
|
||||
for(uint8_t y = 0; y < 64; y++) {
|
||||
if(points[y * 128 + x]) canvas_draw_dot(canvas, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_score(Canvas* const canvas, bool top, uint8_t amount) {
|
||||
char drawChar[20];
|
||||
snprintf(drawChar, sizeof(drawChar), "Player score: %i", amount);
|
||||
if(top)
|
||||
canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, drawChar);
|
||||
else
|
||||
canvas_draw_str_aligned(canvas, 64, 62, AlignCenter, AlignBottom, drawChar);
|
||||
}
|
||||
|
||||
void draw_money(Canvas* const canvas, uint32_t score) {
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
char drawChar[11];
|
||||
uint32_t currAmount = score;
|
||||
if(currAmount < 1000) {
|
||||
snprintf(drawChar, sizeof(drawChar), "$%lu", currAmount);
|
||||
} else {
|
||||
char c = 'K';
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
currAmount = currAmount / 1000;
|
||||
if(currAmount < 1000) {
|
||||
c = MoneyMul[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
snprintf(drawChar, sizeof(drawChar), "$%lu %c", currAmount, c);
|
||||
}
|
||||
canvas_draw_str_aligned(canvas, 126, 2, AlignRight, AlignTop, drawChar);
|
||||
}
|
||||
|
||||
void draw_menu(
|
||||
Canvas* const canvas,
|
||||
const char* text,
|
||||
const char* value,
|
||||
int8_t 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, 122, LINE_HEIGHT);
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
|
||||
canvas_draw_str_aligned(canvas, 4, y + ITEM_PADDING, AlignLeft, AlignTop, text);
|
||||
if(left_caret) canvas_draw_str_aligned(canvas, 80, y + ITEM_PADDING, AlignLeft, AlignTop, "<");
|
||||
canvas_draw_str_aligned(canvas, 100, y + ITEM_PADDING, AlignCenter, AlignTop, value);
|
||||
if(right_caret)
|
||||
canvas_draw_str_aligned(canvas, 120, y + ITEM_PADDING, AlignRight, AlignTop, ">");
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
void settings_page(Canvas* const canvas, const GameState* gameState) {
|
||||
char drawChar[10];
|
||||
int startY = 0;
|
||||
if(LINE_HEIGHT * (gameState->selectedMenu + 1) >= 64) {
|
||||
startY -= (LINE_HEIGHT * (gameState->selectedMenu + 1)) - 64;
|
||||
}
|
||||
|
||||
int scrollHeight = round(64 / 6.0) + ITEM_PADDING * 2;
|
||||
int scrollPos = 64 / (6.0 / (gameState->selectedMenu + 1)) - ITEM_PADDING * 2;
|
||||
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_box(canvas, 123, scrollPos, 4, scrollHeight);
|
||||
canvas_draw_box(canvas, 125, 0, 1, 64);
|
||||
|
||||
snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.starting_money);
|
||||
draw_menu(
|
||||
canvas,
|
||||
"Start money",
|
||||
drawChar,
|
||||
0 * LINE_HEIGHT + startY,
|
||||
gameState->settings.starting_money > gameState->settings.round_price,
|
||||
gameState->settings.starting_money < 400,
|
||||
gameState->selectedMenu == 0);
|
||||
snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.round_price);
|
||||
draw_menu(
|
||||
canvas,
|
||||
"Round price",
|
||||
drawChar,
|
||||
1 * LINE_HEIGHT + startY,
|
||||
gameState->settings.round_price > 10,
|
||||
gameState->settings.round_price < gameState->settings.starting_money,
|
||||
gameState->selectedMenu == 1);
|
||||
|
||||
snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.animation_duration);
|
||||
draw_menu(
|
||||
canvas,
|
||||
"Anim. length",
|
||||
drawChar,
|
||||
2 * LINE_HEIGHT + startY,
|
||||
gameState->settings.animation_duration > 0,
|
||||
gameState->settings.animation_duration < 2000,
|
||||
gameState->selectedMenu == 2);
|
||||
snprintf(drawChar, sizeof(drawChar), "%li", gameState->settings.message_duration);
|
||||
draw_menu(
|
||||
canvas,
|
||||
"Popup time",
|
||||
drawChar,
|
||||
3 * LINE_HEIGHT + startY,
|
||||
gameState->settings.message_duration > 0,
|
||||
gameState->settings.message_duration < 2000,
|
||||
gameState->selectedMenu == 3);
|
||||
// draw_menu(canvas, "Sound", gameState->settings.sound_effects ? "Yes" : "No",
|
||||
// 5 * LINE_HEIGHT + startY,
|
||||
// true,
|
||||
// true,
|
||||
// gameState->selectedMenu == 5
|
||||
// );
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "defines.h"
|
||||
#include <gui/gui.h>
|
||||
|
||||
void draw_player_scene(Canvas* const canvas, const GameState* game_state);
|
||||
|
||||
void draw_dealer_scene(Canvas* const canvas, const GameState* game_state);
|
||||
|
||||
void draw_play_menu(Canvas* const canvas, const GameState* game_state);
|
||||
|
||||
void draw_score(Canvas* const canvas, bool top, uint8_t amount);
|
||||
|
||||
void draw_money(Canvas* const canvas, uint32_t score);
|
||||
void settings_page(Canvas* const canvas, const GameState* gameState);
|
||||
|
||||
void popup_frame(Canvas* const canvas);
|
||||
void draw_screen(Canvas* const canvas, const bool* points);
|
||||
@@ -0,0 +1,123 @@
|
||||
#include <storage/storage.h>
|
||||
#include "util.h"
|
||||
|
||||
const char* CONFIG_FILE_PATH = EXT_PATH(".blackjack.settings");
|
||||
|
||||
void save_settings(Settings settings) {
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FURI_LOG_D(APP_NAME, "Saving config");
|
||||
if(flipper_format_file_open_existing(file, CONFIG_FILE_PATH)) {
|
||||
FURI_LOG_D(
|
||||
APP_NAME, "Saving %s: %ld", CONF_ANIMATION_DURATION, settings.animation_duration);
|
||||
flipper_format_update_uint32(
|
||||
file, CONF_ANIMATION_DURATION, &(settings.animation_duration), 1);
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_MESSAGE_DURATION, settings.message_duration);
|
||||
flipper_format_update_uint32(file, CONF_MESSAGE_DURATION, &(settings.message_duration), 1);
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_STARTING_MONEY, settings.starting_money);
|
||||
flipper_format_update_uint32(file, CONF_STARTING_MONEY, &(settings.starting_money), 1);
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Saving %s: %ld", CONF_ROUND_PRICE, settings.round_price);
|
||||
flipper_format_update_uint32(file, CONF_ROUND_PRICE, &(settings.round_price), 1);
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Saving %s: %i", CONF_SOUND_EFFECTS, settings.sound_effects ? 1 : 0);
|
||||
flipper_format_update_bool(file, CONF_SOUND_EFFECTS, &(settings.sound_effects), 1);
|
||||
FURI_LOG_D(APP_NAME, "Config saved");
|
||||
} else {
|
||||
FURI_LOG_E(APP_NAME, "Save error");
|
||||
}
|
||||
flipper_format_file_close(file);
|
||||
flipper_format_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
}
|
||||
|
||||
void save_settings_file(FlipperFormat* file, Settings* settings) {
|
||||
flipper_format_write_header_cstr(file, CONFIG_FILE_HEADER, CONFIG_FILE_VERSION);
|
||||
flipper_format_write_comment_cstr(file, "Card animation duration in ms");
|
||||
flipper_format_write_uint32(file, CONF_ANIMATION_DURATION, &(settings->animation_duration), 1);
|
||||
flipper_format_write_comment_cstr(file, "Popup message duration in ms");
|
||||
flipper_format_write_uint32(file, CONF_MESSAGE_DURATION, &(settings->message_duration), 1);
|
||||
flipper_format_write_comment_cstr(file, "Player's starting money");
|
||||
flipper_format_write_uint32(file, CONF_STARTING_MONEY, &(settings->starting_money), 1);
|
||||
flipper_format_write_comment_cstr(file, "Round price");
|
||||
flipper_format_write_uint32(file, CONF_ROUND_PRICE, &(settings->round_price), 1);
|
||||
flipper_format_write_comment_cstr(file, "Enable sound effects");
|
||||
flipper_format_write_bool(file, CONF_SOUND_EFFECTS, &(settings->sound_effects), 1);
|
||||
}
|
||||
|
||||
Settings load_settings() {
|
||||
Settings settings;
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Loading default settings");
|
||||
settings.animation_duration = 800;
|
||||
settings.message_duration = 1500;
|
||||
settings.starting_money = 200;
|
||||
settings.round_price = 10;
|
||||
settings.sound_effects = true;
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Opening storage");
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
FURI_LOG_D(APP_NAME, "Allocating file");
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
|
||||
FURI_LOG_D(APP_NAME, "Allocating string");
|
||||
FuriString* string_value;
|
||||
string_value = furi_string_alloc();
|
||||
|
||||
if(storage_common_stat(storage, CONFIG_FILE_PATH, NULL) != FSE_OK) {
|
||||
FURI_LOG_D(APP_NAME, "Config file %s not found, creating new one...", CONFIG_FILE_PATH);
|
||||
if(!flipper_format_file_open_new(file, CONFIG_FILE_PATH)) {
|
||||
FURI_LOG_E(APP_NAME, "Error creating new file %s", CONFIG_FILE_PATH);
|
||||
flipper_format_file_close(file);
|
||||
} else {
|
||||
save_settings_file(file, &settings);
|
||||
}
|
||||
} else {
|
||||
if(!flipper_format_file_open_existing(file, CONFIG_FILE_PATH)) {
|
||||
FURI_LOG_E(APP_NAME, "Error opening existing file %s", CONFIG_FILE_PATH);
|
||||
flipper_format_file_close(file);
|
||||
} else {
|
||||
uint32_t value;
|
||||
bool valueBool;
|
||||
FURI_LOG_D(APP_NAME, "Checking version");
|
||||
if(!flipper_format_read_header(file, string_value, &value)) {
|
||||
FURI_LOG_E(APP_NAME, "Config file mismatch");
|
||||
} else {
|
||||
FURI_LOG_D(APP_NAME, "Loading %s", CONF_ANIMATION_DURATION);
|
||||
if(flipper_format_read_uint32(file, CONF_ANIMATION_DURATION, &value, 1)) {
|
||||
settings.animation_duration = value;
|
||||
FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_ANIMATION_DURATION, value);
|
||||
}
|
||||
FURI_LOG_D(APP_NAME, "Loading %s", CONF_MESSAGE_DURATION);
|
||||
if(flipper_format_read_uint32(file, CONF_MESSAGE_DURATION, &value, 1)) {
|
||||
settings.message_duration = value;
|
||||
FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_MESSAGE_DURATION, value);
|
||||
}
|
||||
FURI_LOG_D(APP_NAME, "Loading %s", CONF_STARTING_MONEY);
|
||||
if(flipper_format_read_uint32(file, CONF_STARTING_MONEY, &value, 1)) {
|
||||
settings.starting_money = value;
|
||||
FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_STARTING_MONEY, value);
|
||||
}
|
||||
FURI_LOG_D(APP_NAME, "Loading %s", CONF_ROUND_PRICE);
|
||||
if(flipper_format_read_uint32(file, CONF_ROUND_PRICE, &value, 1)) {
|
||||
settings.round_price = value;
|
||||
FURI_LOG_D(APP_NAME, "Loaded %s: %ld", CONF_ROUND_PRICE, value);
|
||||
}
|
||||
FURI_LOG_D(APP_NAME, "Loading %s", CONF_SOUND_EFFECTS);
|
||||
if(flipper_format_read_bool(file, CONF_SOUND_EFFECTS, &valueBool, 1)) {
|
||||
settings.sound_effects = valueBool;
|
||||
FURI_LOG_D(APP_NAME, "Loaded %s: %i", CONF_ROUND_PRICE, valueBool ? 1 : 0);
|
||||
}
|
||||
}
|
||||
flipper_format_file_close(file);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_free(string_value);
|
||||
// flipper_format_file_close(file);
|
||||
flipper_format_free(file);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
return settings;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "defines.h"
|
||||
#define CONFIG_FILE_HEADER "Blackjack config file"
|
||||
#define CONFIG_FILE_VERSION 1
|
||||
|
||||
void save_settings(Settings settings);
|
||||
Settings load_settings();
|
||||
@@ -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>.
|
||||
@@ -0,0 +1,14 @@
|
||||
# BPM Tapper
|
||||
|
||||
A BPM Tapper for the Flipper Zero.
|
||||
|
||||

|
||||
|
||||
Hit any button other than back repeatedly. Calculates based on the average of the last 8 inputs.
|
||||
|
||||
## Compiling
|
||||
|
||||
```
|
||||
./fbt fap_bpm_tapper
|
||||
```
|
||||
|
||||
@@ -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_icon_assets="icons",
|
||||
fap_category="Music",
|
||||
order=35,
|
||||
)
|
||||
@@ -0,0 +1,261 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <dialogs/dialogs.h>
|
||||
#include <gui/gui.h>
|
||||
#include <input/input.h>
|
||||
#include <m-string.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) {
|
||||
string_t 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);
|
||||
|
||||
string_init(tempStr);
|
||||
|
||||
string_printf(tempStr, "Taps: %d", bpm_state->taps);
|
||||
canvas_draw_str_aligned(canvas, 5, 10, AlignLeft, AlignBottom, string_get_cstr(tempStr));
|
||||
string_reset(tempStr);
|
||||
|
||||
string_printf(tempStr, "Queue: %d", bpm_state->tap_queue->size);
|
||||
canvas_draw_str_aligned(canvas, 70, 10, AlignLeft, AlignBottom, string_get_cstr(tempStr));
|
||||
string_reset(tempStr);
|
||||
|
||||
string_printf(tempStr, "Interval: %dms", bpm_state->interval);
|
||||
canvas_draw_str_aligned(canvas, 5, 20, AlignLeft, AlignBottom, string_get_cstr(tempStr));
|
||||
string_reset(tempStr);
|
||||
|
||||
string_printf(tempStr, "x2 %.2f /2 %.2f", bpm_state->bpm * 2, bpm_state->bpm / 2);
|
||||
canvas_draw_str_aligned(canvas, 64, 60, AlignCenter, AlignCenter, string_get_cstr(tempStr));
|
||||
string_reset(tempStr);
|
||||
|
||||
string_printf(tempStr, "%.2f", bpm_state->bpm);
|
||||
canvas_set_font(canvas, FontBigNumbers);
|
||||
canvas_draw_str_aligned(canvas, 64, 40, AlignCenter, AlignCenter, string_get_cstr(tempStr));
|
||||
string_reset(tempStr);
|
||||
|
||||
string_clear(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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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>.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Caesar Cipher
|
||||
|
||||
A [caesar cipher](https://en.wikipedia.org/wiki/Caesar_cipher) encoder for the Flipper Zero device.
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
Start app, painfully input your ciphertext with the onscreen keyboard. Replace spaces with underscores. Hit "Save", scroll output.
|
||||
|
||||
## Compiling
|
||||
|
||||
```
|
||||
./fbt firmware_caesar_cipher
|
||||
```
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
App(
|
||||
appid="Caesar_Cipher",
|
||||
name="Caesar Cipher",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="caesar_cipher_app",
|
||||
cdefines=["APP_CAESAR_CIPHER"],
|
||||
requires=[
|
||||
"gui",
|
||||
],
|
||||
stack_size=2 * 1024,
|
||||
fap_icon="caesar_cipher_icon.png",
|
||||
fap_category="Misc",
|
||||
order=20,
|
||||
)
|
||||
@@ -0,0 +1,147 @@
|
||||
#include <furi.h>
|
||||
#include <input/input.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/modules/text_input.h>
|
||||
#include <gui/modules/text_box.h>
|
||||
|
||||
#define TEXT_BUFFER_SIZE 256
|
||||
|
||||
typedef enum {
|
||||
EventTypeTick,
|
||||
EventTypeKey,
|
||||
} EventType;
|
||||
|
||||
typedef struct {
|
||||
EventType type;
|
||||
InputEvent input;
|
||||
} PluginEvent;
|
||||
|
||||
typedef struct {
|
||||
ViewDispatcher* view_dispatcher;
|
||||
TextInput* text_input;
|
||||
TextBox* text_box;
|
||||
char input[TEXT_BUFFER_SIZE];
|
||||
char output[(TEXT_BUFFER_SIZE * 26) + (26)]; // linebreaks
|
||||
} CaesarState;
|
||||
|
||||
static void string_to_uppercase(char* input) {
|
||||
int i;
|
||||
for(i = 0; input[i] != '\0'; i++) {
|
||||
if(input[i] >= 'a' && input[i] <= 'z') {
|
||||
input[i] = input[i] - 32;
|
||||
} else {
|
||||
input[i] = input[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void build_output(char* input, char* output) {
|
||||
int out = 0;
|
||||
for(int rot = 1; rot < 26; rot++) {
|
||||
int in;
|
||||
for(in = 0; input[in] != '\0'; in++) {
|
||||
if(input[in] >= 'A' && input[in] <= 'Z') {
|
||||
output[out] = 65 + (((input[in] - 65) + rot) % 26);
|
||||
} else {
|
||||
output[out] = input[in];
|
||||
}
|
||||
out++;
|
||||
}
|
||||
output[out] = '\n';
|
||||
out++;
|
||||
}
|
||||
output[out] = '\0';
|
||||
}
|
||||
|
||||
static void text_input_callback(void* ctx) {
|
||||
CaesarState* caesar_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
FURI_LOG_D("caesar_cipher", "Input text: %s", caesar_state->input);
|
||||
// this is where we build the output.
|
||||
string_to_uppercase(caesar_state->input);
|
||||
FURI_LOG_D("caesar_cipher", "Upper text: %s", caesar_state->input);
|
||||
build_output(caesar_state->input, caesar_state->output);
|
||||
text_box_set_text(caesar_state->text_box, caesar_state->output);
|
||||
view_dispatcher_switch_to_view(caesar_state->view_dispatcher, 1);
|
||||
|
||||
release_mutex((ValueMutex*)ctx, caesar_state);
|
||||
}
|
||||
|
||||
static bool back_event_callback(void* ctx) {
|
||||
const CaesarState* caesar_state = acquire_mutex((ValueMutex*)ctx, 25);
|
||||
view_dispatcher_stop(caesar_state->view_dispatcher);
|
||||
release_mutex((ValueMutex*)ctx, caesar_state);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void caesar_cipher_state_init(CaesarState* const caesar_state) {
|
||||
caesar_state->view_dispatcher = view_dispatcher_alloc();
|
||||
caesar_state->text_input = text_input_alloc();
|
||||
caesar_state->text_box = text_box_alloc();
|
||||
text_box_set_font(caesar_state->text_box, TextBoxFontText);
|
||||
}
|
||||
|
||||
static void caesar_cipher_state_free(CaesarState* const caesar_state) {
|
||||
text_input_free(caesar_state->text_input);
|
||||
text_box_free(caesar_state->text_box);
|
||||
view_dispatcher_remove_view(caesar_state->view_dispatcher, 0);
|
||||
view_dispatcher_remove_view(caesar_state->view_dispatcher, 1);
|
||||
view_dispatcher_free(caesar_state->view_dispatcher);
|
||||
free(caesar_state);
|
||||
}
|
||||
|
||||
int32_t caesar_cipher_app() {
|
||||
CaesarState* caesar_state = malloc(sizeof(CaesarState));
|
||||
|
||||
FURI_LOG_D("caesar_cipher", "Running caesar_cipher_state_init");
|
||||
caesar_cipher_state_init(caesar_state);
|
||||
|
||||
ValueMutex state_mutex;
|
||||
if(!init_mutex(&state_mutex, caesar_state, sizeof(CaesarState))) {
|
||||
FURI_LOG_E("caesar_cipher", "cannot create mutex\r\n");
|
||||
free(caesar_state);
|
||||
return 255;
|
||||
}
|
||||
|
||||
FURI_LOG_D("caesar_cipher", "Assigning text input callback");
|
||||
text_input_set_result_callback(
|
||||
caesar_state->text_input,
|
||||
text_input_callback,
|
||||
&state_mutex,
|
||||
caesar_state->input,
|
||||
TEXT_BUFFER_SIZE,
|
||||
//clear default text
|
||||
true);
|
||||
text_input_set_header_text(caesar_state->text_input, "Input");
|
||||
|
||||
// Open GUI and register view_port
|
||||
Gui* gui = furi_record_open("gui");
|
||||
//gui_add_view_port(gui, view_port, GuiLayerFullscreen);
|
||||
|
||||
FURI_LOG_D("caesar_cipher", "Enabling view dispatcher queue");
|
||||
view_dispatcher_enable_queue(caesar_state->view_dispatcher);
|
||||
|
||||
FURI_LOG_D("caesar_cipher", "Adding text input view to dispatcher");
|
||||
view_dispatcher_add_view(
|
||||
caesar_state->view_dispatcher, 0, text_input_get_view(caesar_state->text_input));
|
||||
view_dispatcher_add_view(
|
||||
caesar_state->view_dispatcher, 1, text_box_get_view(caesar_state->text_box));
|
||||
FURI_LOG_D("caesar_cipher", "Attaching view dispatcher to GUI");
|
||||
view_dispatcher_attach_to_gui(
|
||||
caesar_state->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
|
||||
FURI_LOG_D("ceasar_cipher", "starting view dispatcher");
|
||||
view_dispatcher_set_navigation_event_callback(
|
||||
caesar_state->view_dispatcher, back_event_callback);
|
||||
view_dispatcher_set_event_callback_context(caesar_state->view_dispatcher, &state_mutex);
|
||||
view_dispatcher_switch_to_view(caesar_state->view_dispatcher, 0);
|
||||
view_dispatcher_run(caesar_state->view_dispatcher);
|
||||
|
||||
furi_record_close("gui");
|
||||
delete_mutex(&state_mutex);
|
||||
caesar_cipher_state_free(caesar_state);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
After Width: | Height: | Size: 172 B |