Merge remote-tracking branch 'OFW/dev' into dev

This commit is contained in:
MX
2024-10-15 00:08:47 +03:00
parent 2f102e61a9
commit 4b9b1769f7
54 changed files with 1357 additions and 213 deletions

View File

@@ -221,6 +221,14 @@ App(
requires=["unit_tests"],
)
App(
appid="test_js",
sources=["tests/common/*.c", "tests/js/*.c"],
apptype=FlipperAppType.PLUGIN,
entry_point="get_api",
requires=["unit_tests", "js_app"],
)
App(
appid="test_strint",
sources=["tests/common/*.c", "tests/strint/*.c"],

View File

@@ -0,0 +1,4 @@
let tests = require("tests");
tests.assert_eq(1337, 1337);
tests.assert_eq("hello", "hello");

View File

@@ -0,0 +1,30 @@
let tests = require("tests");
let event_loop = require("event_loop");
let ext = {
i: 0,
received: false,
};
let queue = event_loop.queue(16);
event_loop.subscribe(queue.input, function (_, item, tests, ext) {
tests.assert_eq(123, item);
ext.received = true;
}, tests, ext);
event_loop.subscribe(event_loop.timer("periodic", 1), function (_, _item, queue, counter, ext) {
ext.i++;
queue.send(123);
if (counter === 10)
event_loop.stop();
return [queue, counter + 1, ext];
}, queue, 1, ext);
event_loop.subscribe(event_loop.timer("oneshot", 1000), function (_, _item, tests) {
tests.fail("event loop was not stopped");
}, tests);
event_loop.run();
tests.assert_eq(10, ext.i);
tests.assert_eq(true, ext.received);

View File

@@ -0,0 +1,34 @@
let tests = require("tests");
let math = require("math");
// math.EPSILON on Flipper Zero is 2.22044604925031308085e-16
// basics
tests.assert_float_close(5, math.abs(-5), math.EPSILON);
tests.assert_float_close(0.5, math.abs(-0.5), math.EPSILON);
tests.assert_float_close(5, math.abs(5), math.EPSILON);
tests.assert_float_close(0.5, math.abs(0.5), math.EPSILON);
tests.assert_float_close(3, math.cbrt(27), math.EPSILON);
tests.assert_float_close(6, math.ceil(5.3), math.EPSILON);
tests.assert_float_close(31, math.clz32(1), math.EPSILON);
tests.assert_float_close(5, math.floor(5.7), math.EPSILON);
tests.assert_float_close(5, math.max(3, 5), math.EPSILON);
tests.assert_float_close(3, math.min(3, 5), math.EPSILON);
tests.assert_float_close(-1, math.sign(-5), math.EPSILON);
tests.assert_float_close(5, math.trunc(5.7), math.EPSILON);
// trig
tests.assert_float_close(1.0471975511965976, math.acos(0.5), math.EPSILON);
tests.assert_float_close(1.3169578969248166, math.acosh(2), math.EPSILON);
tests.assert_float_close(0.5235987755982988, math.asin(0.5), math.EPSILON);
tests.assert_float_close(1.4436354751788103, math.asinh(2), math.EPSILON);
tests.assert_float_close(0.7853981633974483, math.atan(1), math.EPSILON);
tests.assert_float_close(0.7853981633974483, math.atan2(1, 1), math.EPSILON);
tests.assert_float_close(0.5493061443340549, math.atanh(0.5), math.EPSILON);
tests.assert_float_close(-1, math.cos(math.PI), math.EPSILON * 18); // Error 3.77475828372553223744e-15
tests.assert_float_close(1, math.sin(math.PI / 2), math.EPSILON * 4.5); // Error 9.99200722162640886381e-16
// powers
tests.assert_float_close(5, math.sqrt(25), math.EPSILON);
tests.assert_float_close(8, math.pow(2, 3), math.EPSILON);
tests.assert_float_close(2.718281828459045, math.exp(1), math.EPSILON * 2); // Error 4.44089209850062616169e-16

View File

@@ -0,0 +1,136 @@
let storage = require("storage");
let tests = require("tests");
let baseDir = "/ext/.tmp/unit_tests";
tests.assert_eq(true, storage.rmrf(baseDir));
tests.assert_eq(true, storage.makeDirectory(baseDir));
// write
let file = storage.openFile(baseDir + "/helloworld", "w", "create_always");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.write("Hello, World!"));
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());
// read
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.size());
tests.assert_eq("Hello, World!", file.read("ascii", 128));
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());
// seek
file = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.isOpen());
tests.assert_eq(13, file.size());
tests.assert_eq("Hello, World!", file.read("ascii", 128));
tests.assert_eq(true, file.seekAbsolute(1));
tests.assert_eq(true, file.seekRelative(2));
tests.assert_eq(3, file.tell());
tests.assert_eq(false, file.eof());
tests.assert_eq("lo, World!", file.read("ascii", 128));
tests.assert_eq(true, file.eof());
tests.assert_eq(true, file.close());
tests.assert_eq(false, file.isOpen());
// byte-level copy
let src = storage.openFile(baseDir + "/helloworld", "r", "open_existing");
let dst = storage.openFile(baseDir + "/helloworld2", "rw", "create_always");
tests.assert_eq(true, !!src);
tests.assert_eq(true, src.isOpen());
tests.assert_eq(true, !!dst);
tests.assert_eq(true, dst.isOpen());
tests.assert_eq(true, src.copyTo(dst, 10));
tests.assert_eq(true, dst.seekAbsolute(0));
tests.assert_eq("Hello, Wor", dst.read("ascii", 128));
tests.assert_eq(true, src.copyTo(dst, 3));
tests.assert_eq(true, dst.seekAbsolute(0));
tests.assert_eq("Hello, World!", dst.read("ascii", 128));
tests.assert_eq(true, src.eof());
tests.assert_eq(true, src.close());
tests.assert_eq(false, src.isOpen());
tests.assert_eq(true, dst.eof());
tests.assert_eq(true, dst.close());
tests.assert_eq(false, dst.isOpen());
// truncate
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld2"));
file = storage.openFile(baseDir + "/helloworld2", "w", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq(true, file.seekAbsolute(5));
tests.assert_eq(true, file.truncate());
tests.assert_eq(true, file.close());
file = storage.openFile(baseDir + "/helloworld2", "r", "open_existing");
tests.assert_eq(true, !!file);
tests.assert_eq("Hello", file.read("ascii", 128));
tests.assert_eq(true, file.close());
// existence
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld2"));
tests.assert_eq(false, storage.fileExists(baseDir + "/sus_amogus_123"));
tests.assert_eq(false, storage.directoryExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir));
tests.assert_eq(true, storage.directoryExists(baseDir));
tests.assert_eq(true, storage.fileOrDirExists(baseDir));
tests.assert_eq(true, storage.remove(baseDir + "/helloworld2"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld2"));
// stat
let stat = storage.stat(baseDir + "/helloworld");
tests.assert_eq(true, !!stat);
tests.assert_eq(baseDir + "/helloworld", stat.path);
tests.assert_eq(false, stat.isDirectory);
tests.assert_eq(13, stat.size);
// rename
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.rename(baseDir + "/helloworld", baseDir + "/helloworld123"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.rename(baseDir + "/helloworld123", baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
// copy
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(false, storage.fileExists(baseDir + "/helloworld123"));
tests.assert_eq(true, storage.copy(baseDir + "/helloworld", baseDir + "/helloworld123"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld"));
tests.assert_eq(true, storage.fileExists(baseDir + "/helloworld123"));
// next avail
tests.assert_eq("helloworld1", storage.nextAvailableFilename(baseDir, "helloworld", "", 20));
// fs info
let fsInfo = storage.fsInfo("/ext");
tests.assert_eq(true, !!fsInfo);
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace); // idk \(-_-)/
fsInfo = storage.fsInfo("/int");
tests.assert_eq(true, !!fsInfo);
tests.assert_eq(true, fsInfo.freeSpace < fsInfo.totalSpace);
// path operations
tests.assert_eq(true, storage.arePathsEqual("/ext/test", "/ext/Test"));
tests.assert_eq(false, storage.arePathsEqual("/ext/test", "/ext/Testttt"));
tests.assert_eq(true, storage.isSubpathOf("/ext/test", "/ext/test/sub"));
tests.assert_eq(false, storage.isSubpathOf("/ext/test/sub", "/ext/test"));
// dir
let entries = storage.readDirectory(baseDir);
tests.assert_eq(true, !!entries);
// FIXME: (-nofl) this test suite assumes that files are listed by
// `readDirectory` in the exact order that they were created, which is not
// something that is actually guaranteed.
// Possible solution: sort and compare the array.
tests.assert_eq("helloworld", entries[0].path);
tests.assert_eq("helloworld123", entries[1].path);
tests.assert_eq(true, storage.rmrf(baseDir));
tests.assert_eq(true, storage.makeDirectory(baseDir));

View File

@@ -0,0 +1,88 @@
#include "../test.h" // IWYU pragma: keep
#include <furi.h>
#include <furi_hal.h>
#include <furi_hal_random.h>
#include <storage/storage.h>
#include <applications/system/js_app/js_thread.h>
#include <stdint.h>
#define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js")
typedef enum {
JsTestsFinished = 1,
JsTestsError = 2,
} JsTestFlag;
typedef struct {
FuriEventFlag* event_flags;
FuriString* error_string;
} JsTestCallbackContext;
static void js_test_callback(JsThreadEvent event, const char* msg, void* param) {
JsTestCallbackContext* context = param;
if(event == JsThreadEventPrint) {
FURI_LOG_I("js_test", "%s", msg);
} else if(event == JsThreadEventError || event == JsThreadEventErrorTrace) {
context->error_string = furi_string_alloc_set_str(msg);
furi_event_flag_set(context->event_flags, JsTestsFinished | JsTestsError);
} else if(event == JsThreadEventDone) {
furi_event_flag_set(context->event_flags, JsTestsFinished);
}
}
static void js_test_run(const char* script_path) {
JsTestCallbackContext* context = malloc(sizeof(JsTestCallbackContext));
context->event_flags = furi_event_flag_alloc();
JsThread* thread = js_thread_run(script_path, js_test_callback, context);
uint32_t flags = furi_event_flag_wait(
context->event_flags, JsTestsFinished, FuriFlagWaitAny, FuriWaitForever);
if(flags & FuriFlagError) {
// getting the flags themselves should not fail
furi_crash();
}
FuriString* error_string = context->error_string;
js_thread_stop(thread);
furi_event_flag_free(context->event_flags);
free(context);
if(flags & JsTestsError) {
// memory leak: not freeing the FuriString if the tests fail,
// because mu_fail executes a return
//
// who cares tho?
mu_fail(furi_string_get_cstr(error_string));
}
}
MU_TEST(js_test_basic) {
js_test_run(JS_SCRIPT_PATH("basic"));
}
MU_TEST(js_test_math) {
js_test_run(JS_SCRIPT_PATH("math"));
}
MU_TEST(js_test_event_loop) {
js_test_run(JS_SCRIPT_PATH("event_loop"));
}
MU_TEST(js_test_storage) {
js_test_run(JS_SCRIPT_PATH("storage"));
}
MU_TEST_SUITE(test_js) {
MU_RUN_TEST(js_test_basic);
MU_RUN_TEST(js_test_math);
MU_RUN_TEST(js_test_event_loop);
MU_RUN_TEST(js_test_storage);
}
int run_minunit_test_js(void) {
MU_RUN_SUITE(test_js);
return MU_EXIT_CODE;
}
TEST_API_DEFINE(run_minunit_test_js)

View File

@@ -31,7 +31,7 @@ extern "C" {
#include <Windows.h>
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#define __func__ __FUNCTION__
#define __func__ __FUNCTION__ //-V1059
#endif
#elif defined(__unix__) || defined(__unix) || defined(unix) || \
@@ -56,7 +56,7 @@ extern "C" {
#endif
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
#define __func__ __extension__ __FUNCTION__
#define __func__ __extension__ __FUNCTION__ //-V1059
#endif
#else
@@ -102,6 +102,7 @@ void minunit_printf_warning(const char* format, ...);
MU__SAFE_BLOCK(minunit_setup = setup_fun; minunit_teardown = teardown_fun;)
/* Test runner */
//-V:MU_RUN_TEST:550
#define MU_RUN_TEST(test) \
MU__SAFE_BLOCK( \
if(minunit_real_timer == 0 && minunit_proc_timer == 0) { \

View File

@@ -7,7 +7,7 @@
#include <rpc/rpc_i.h>
#include <flipper.pb.h>
#include <core/event_loop.h>
#include <applications/system/js_app/js_thread.h>
static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)),
@@ -33,13 +33,9 @@ static constexpr auto unit_tests_api_table = sort(create_array_t<sym_entry>(
xQueueGenericSend,
BaseType_t,
(QueueHandle_t, const void* const, TickType_t, const BaseType_t)),
API_METHOD(furi_event_loop_alloc, FuriEventLoop*, (void)),
API_METHOD(furi_event_loop_free, void, (FuriEventLoop*)),
API_METHOD(
furi_event_loop_subscribe_message_queue,
void,
(FuriEventLoop*, FuriMessageQueue*, FuriEventLoopEvent, FuriEventLoopEventCallback, void*)),
API_METHOD(furi_event_loop_unsubscribe, void, (FuriEventLoop*, FuriEventLoopObject*)),
API_METHOD(furi_event_loop_run, void, (FuriEventLoop*)),
API_METHOD(furi_event_loop_stop, void, (FuriEventLoop*)),
js_thread_run,
JsThread*,
(const char* script_path, JsThreadCallback callback, void* context)),
API_METHOD(js_thread_stop, void, (JsThread * worker)),
API_VARIABLE(PB_Main_msg, PB_Main_msg_t)));

View File

@@ -272,6 +272,15 @@ App(
sources=["plugins/supported_cards/skylanders.c"],
)
App(
appid="hworld_parser",
apptype=FlipperAppType.PLUGIN,
entry_point="hworld_plugin_ep",
targets=["f7"],
requires=["nfc"],
sources=["plugins/supported_cards/hworld.c"],
)
App(
appid="nfc_cli",
targets=["f7"],

View File

@@ -0,0 +1,243 @@
// Flipper Zero parser for H World Hotel Key Cards
// H World operates around 10,000 hotels, most of which in mainland China
// Reverse engineering and parser written by @Torron (Github: @zinongli)
#include "nfc_supported_card_plugin.h"
#include <flipper_application.h>
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
#include <bit_lib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TAG "H World"
#define ROOM_SECTOR 1
#define VIP_SECTOR 5
#define ROOM_SECTOR_KEY_BLOCK 7
#define VIP_SECTOR_KEY_BLOCK 23
#define ACCESS_INFO_BLOCK 5
#define ROOM_NUM_DECIMAL_BLOCK 6
#define H_WORLD_YEAR_OFFSET 2000
typedef struct {
uint64_t a;
uint64_t b;
} MfClassicKeyPair;
static MfClassicKeyPair hworld_standard_keys[] = {
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 000
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 005
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 006
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 007
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 008
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 009
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 010
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 011
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 012
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
};
static MfClassicKeyPair hworld_vip_keys[] = {
{.a = 0x000000000000, .b = 0xFFFFFFFFFFFF}, // 000
{.a = 0x543071543071, .b = 0x5F01015F0101}, // 001
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 002
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 003
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 004
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 005
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 006
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 007
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 008
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 009
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 010
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 011
{.a = 0xFFFFFFFFFFFF, .b = 0x200510241234}, // 012
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 013
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 014
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, // 015
};
static bool hworld_verify(Nfc* nfc) {
bool verified = false;
do {
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(ROOM_SECTOR);
MfClassicKey standard_key = {0};
bit_lib_num_to_bytes_be(
hworld_standard_keys[ROOM_SECTOR].a, COUNT_OF(standard_key.data), standard_key.data);
MfClassicAuthContext auth_context;
MfClassicError standard_error = mf_classic_poller_sync_auth(
nfc, block_num, &standard_key, MfClassicKeyTypeA, &auth_context);
if(standard_error != MfClassicErrorNone) {
FURI_LOG_D(TAG, "Failed static key check for block %u", block_num);
break;
}
MfClassicKey vip_key = {0};
bit_lib_num_to_bytes_be(
hworld_vip_keys[VIP_SECTOR].b, COUNT_OF(vip_key.data), vip_key.data);
MfClassicError vip_error = mf_classic_poller_sync_auth(
nfc, block_num, &vip_key, MfClassicKeyTypeB, &auth_context);
if(vip_error == MfClassicErrorNone) {
FURI_LOG_D(TAG, "VIP card detected");
} else {
FURI_LOG_D(TAG, "Standard card detected");
}
verified = true;
} while(false);
return verified;
}
static bool hworld_read(Nfc* nfc, NfcDevice* device) {
furi_assert(nfc);
furi_assert(device);
bool is_read = false;
MfClassicData* data = mf_classic_alloc();
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
do {
MfClassicType type = MfClassicType1k;
MfClassicError standard_error = mf_classic_poller_sync_detect_type(nfc, &type);
MfClassicError vip_error = MfClassicErrorNotPresent;
if(standard_error != MfClassicErrorNone) break;
data->type = type;
MfClassicDeviceKeys standard_keys = {};
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
bit_lib_num_to_bytes_be(
hworld_standard_keys[i].a, sizeof(MfClassicKey), standard_keys.key_a[i].data);
FURI_BIT_SET(standard_keys.key_a_mask, i);
bit_lib_num_to_bytes_be(
hworld_standard_keys[i].b, sizeof(MfClassicKey), standard_keys.key_b[i].data);
FURI_BIT_SET(standard_keys.key_b_mask, i);
}
standard_error = mf_classic_poller_sync_read(nfc, &standard_keys, data);
if(standard_error == MfClassicErrorNone) {
FURI_LOG_I(TAG, "Standard card successfully read");
} else {
MfClassicDeviceKeys vip_keys = {};
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
bit_lib_num_to_bytes_be(
hworld_vip_keys[i].a, sizeof(MfClassicKey), vip_keys.key_a[i].data);
FURI_BIT_SET(vip_keys.key_a_mask, i);
bit_lib_num_to_bytes_be(
hworld_vip_keys[i].b, sizeof(MfClassicKey), vip_keys.key_b[i].data);
FURI_BIT_SET(vip_keys.key_b_mask, i);
}
vip_error = mf_classic_poller_sync_read(nfc, &vip_keys, data);
if(vip_error == MfClassicErrorNone) {
FURI_LOG_I(TAG, "VIP card successfully read");
} else {
break;
}
}
nfc_device_set_data(device, NfcProtocolMfClassic, data);
is_read = (standard_error == MfClassicErrorNone) | (vip_error == MfClassicErrorNone);
} while(false);
mf_classic_free(data);
return is_read;
}
bool hworld_parse(const NfcDevice* device, FuriString* parsed_data) {
furi_assert(device);
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
bool parsed = false;
do {
// Check card type
if(data->type != MfClassicType1k) break;
// Check static key for verificaiton
const uint8_t* data_room_sec_key_a_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[0];
const uint8_t* data_room_sec_key_b_ptr = &data->block[ROOM_SECTOR_KEY_BLOCK].data[10];
uint64_t data_room_sec_key_a = bit_lib_get_bits_64(data_room_sec_key_a_ptr, 0, 48);
uint64_t data_room_sec_key_b = bit_lib_get_bits_64(data_room_sec_key_b_ptr, 0, 48);
if((data_room_sec_key_a != hworld_standard_keys[ROOM_SECTOR].a) |
(data_room_sec_key_b != hworld_standard_keys[ROOM_SECTOR].b))
break;
// Check whether this card is VIP
const uint8_t* data_vip_sec_key_b_ptr = &data->block[VIP_SECTOR_KEY_BLOCK].data[10];
uint64_t data_vip_sec_key_b = bit_lib_get_bits_64(data_vip_sec_key_b_ptr, 0, 48);
bool is_hworld_vip = (data_vip_sec_key_b == hworld_vip_keys[VIP_SECTOR].b);
uint8_t room_floor = data->block[ACCESS_INFO_BLOCK].data[13];
uint8_t room_num = data->block[ACCESS_INFO_BLOCK].data[14];
// Check in date & time
uint16_t check_in_year = data->block[ACCESS_INFO_BLOCK].data[2] + H_WORLD_YEAR_OFFSET;
uint8_t check_in_month = data->block[ACCESS_INFO_BLOCK].data[3];
uint8_t check_in_day = data->block[ACCESS_INFO_BLOCK].data[4];
uint8_t check_in_hour = data->block[ACCESS_INFO_BLOCK].data[5];
uint8_t check_in_minute = data->block[ACCESS_INFO_BLOCK].data[6];
// Expire date & time
uint16_t expire_year = data->block[ACCESS_INFO_BLOCK].data[7] + H_WORLD_YEAR_OFFSET;
uint8_t expire_month = data->block[ACCESS_INFO_BLOCK].data[8];
uint8_t expire_day = data->block[ACCESS_INFO_BLOCK].data[9];
uint8_t expire_hour = data->block[ACCESS_INFO_BLOCK].data[10];
uint8_t expire_minute = data->block[ACCESS_INFO_BLOCK].data[11];
furi_string_cat_printf(parsed_data, "\e#H World Card\n");
furi_string_cat_printf(
parsed_data, "%s\n", is_hworld_vip ? "VIP card" : "Standard room key");
furi_string_cat_printf(parsed_data, "Room Num: %u%02u\n", room_floor, room_num);
furi_string_cat_printf(
parsed_data,
"Check-in Date: \n%04u-%02d-%02d\n%02d:%02d:00\n",
check_in_year,
check_in_month,
check_in_day,
check_in_hour,
check_in_minute);
furi_string_cat_printf(
parsed_data,
"Expiration Date: \n%04u-%02d-%02d\n%02d:%02d:00",
expire_year,
expire_month,
expire_day,
expire_hour,
expire_minute);
parsed = true;
} while(false);
return parsed;
}
/* Actual implementation of app<>plugin interface */
static const NfcSupportedCardsPlugin hworld_plugin = {
.protocol = NfcProtocolMfClassic,
.verify = hworld_verify,
.read = hworld_read,
.parse = hworld_parse,
};
/* Plugin descriptor to comply with basic plugin specification */
static const FlipperAppPluginDescriptor hworld_plugin_descriptor = {
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
.entry_point = &hworld_plugin,
};
/* Plugin entry point - must return a pointer to const descriptor */
const FlipperAppPluginDescriptor* hworld_plugin_ep(void) {
return &hworld_plugin_descriptor;
}

View File

@@ -298,15 +298,14 @@ void canvas_draw_xbm(
/** Draw rotated XBM bitmap
*
* @param canvas Canvas instance
* @param x x coordinate
* @param y y coordinate
* @param[in] width bitmap width
* @param[in] height bitmap height
* @param[in] rotation bitmap rotation
* @param bitmap pointer to XBM bitmap data
* @param canvas Canvas instance
* @param x x coordinate
* @param y y coordinate
* @param[in] width bitmap width
* @param[in] height bitmap height
* @param[in] rotation bitmap rotation
* @param bitmap_data pointer to XBM bitmap data
*/
void canvas_draw_xbm_ex(
Canvas* canvas,
int32_t x,

View File

@@ -65,6 +65,13 @@ void text_input_set_result_callback(
size_t text_buffer_size,
bool clear_default_text);
/**
* @brief Sets the minimum length of a TextInput
* @param [in] text_input TextInput
* @param [in] minimum_length Minimum input length
*/
void text_input_set_minimum_length(TextInput* text_input, size_t minimum_length);
void text_input_set_validator(
TextInput* text_input,
TextInputValidatorCallback callback,

View File

@@ -5,6 +5,12 @@
#define VIEW_DISPATCHER_QUEUE_LEN (16U)
ViewDispatcher* view_dispatcher_alloc(void) {
ViewDispatcher* dispatcher = view_dispatcher_alloc_ex(furi_event_loop_alloc());
dispatcher->is_event_loop_owned = true;
return dispatcher;
}
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop) {
ViewDispatcher* view_dispatcher = malloc(sizeof(ViewDispatcher));
view_dispatcher->view_port = view_port_alloc();
@@ -16,7 +22,7 @@ ViewDispatcher* view_dispatcher_alloc(void) {
ViewDict_init(view_dispatcher->views);
view_dispatcher->event_loop = furi_event_loop_alloc();
view_dispatcher->event_loop = loop;
view_dispatcher->input_queue =
furi_message_queue_alloc(VIEW_DISPATCHER_QUEUE_LEN, sizeof(InputEvent));
@@ -57,7 +63,7 @@ void view_dispatcher_free(ViewDispatcher* view_dispatcher) {
furi_message_queue_free(view_dispatcher->input_queue);
furi_message_queue_free(view_dispatcher->event_queue);
furi_event_loop_free(view_dispatcher->event_loop);
if(view_dispatcher->is_event_loop_owned) furi_event_loop_free(view_dispatcher->event_loop);
// Free dispatcher
free(view_dispatcher);
}
@@ -85,6 +91,7 @@ void view_dispatcher_set_tick_event_callback(
ViewDispatcherTickEventCallback callback,
uint32_t tick_period) {
furi_check(view_dispatcher);
furi_check(view_dispatcher->is_event_loop_owned);
view_dispatcher->tick_event_callback = callback;
view_dispatcher->tick_period = tick_period;
}
@@ -106,11 +113,12 @@ void view_dispatcher_run(ViewDispatcher* view_dispatcher) {
uint32_t tick_period = view_dispatcher->tick_period == 0 ? FuriWaitForever :
view_dispatcher->tick_period;
furi_event_loop_tick_set(
view_dispatcher->event_loop,
tick_period,
view_dispatcher_handle_tick_event,
view_dispatcher);
if(view_dispatcher->is_event_loop_owned)
furi_event_loop_tick_set(
view_dispatcher->event_loop,
tick_period,
view_dispatcher_handle_tick_event,
view_dispatcher);
furi_event_loop_run(view_dispatcher->event_loop);

View File

@@ -47,6 +47,15 @@ typedef void (*ViewDispatcherTickEventCallback)(void* context);
*/
ViewDispatcher* view_dispatcher_alloc(void);
/** Allocate ViewDispatcher instance with an externally owned event loop. If
* this constructor is used instead of `view_dispatcher_alloc`, the burden of
* freeing the event loop is placed on the caller.
*
* @param loop pointer to FuriEventLoop instance
* @return pointer to ViewDispatcher instance
*/
ViewDispatcher* view_dispatcher_alloc_ex(FuriEventLoop* loop);
/** Free ViewDispatcher instance
*
* @warning All added views MUST be removed using view_dispatcher_remove_view()
@@ -97,6 +106,10 @@ void view_dispatcher_set_navigation_event_callback(
/** Set tick event handler
*
* @warning Requires the event loop to be owned by the view dispatcher, i.e.
* it should have been instantiated with `view_dispatcher_alloc`, not
* `view_dispatcher_alloc_ex`.
*
* @param view_dispatcher ViewDispatcher instance
* @param callback ViewDispatcherTickEventCallback
* @param tick_period callback call period

View File

@@ -14,6 +14,7 @@
DICT_DEF2(ViewDict, uint32_t, M_DEFAULT_OPLIST, View*, M_PTR_OPLIST) // NOLINT
struct ViewDispatcher {
bool is_event_loop_owned;
FuriEventLoop* event_loop;
FuriMessageQueue* input_queue;
FuriMessageQueue* event_queue;

View File

@@ -377,7 +377,7 @@ void storage_common_resolve_path_and_ensure_app_directory(Storage* storage, Furi
* @param storage pointer to a storage API instance.
* @param source pointer to a zero-terminated string containing the source path.
* @param dest pointer to a zero-terminated string containing the destination path.
* @return FSE_OK if the migration was successfull completed, any other error code on failure.
* @return FSE_OK if the migration was successfully completed, any other error code on failure.
*/
FS_Error storage_common_migrate(Storage* storage, const char* source, const char* dest);
@@ -425,7 +425,7 @@ bool storage_common_is_subdir(Storage* storage, const char* parent, const char*
/******************* Error Functions *******************/
/**
* @brief Get the textual description of a numeric error identifer.
* @brief Get the textual description of a numeric error identifier.
*
* @param error_id numeric identifier of the error in question.
* @return pointer to a statically allocated zero-terminated string containing the respective error text.