mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-06-11 19:33:30 -07:00
Merge remote-tracking branch 'ul/dev' into mntm-dev
This commit is contained in:
@@ -306,6 +306,33 @@ App(
|
||||
sources=["plugins/supported_cards/plantain.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="szppk_so_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="szppk_so_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/szppk_so.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="sk_tk_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="sk_tk_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/sk_tk.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="sev_tk_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
entry_point="sev_tk_plugin_ep",
|
||||
targets=["f7"],
|
||||
requires=["nfc"],
|
||||
sources=["plugins/supported_cards/sevppk_tk.c"],
|
||||
)
|
||||
|
||||
App(
|
||||
appid="two_cities_parser",
|
||||
apptype=FlipperAppType.PLUGIN,
|
||||
|
||||
@@ -109,9 +109,76 @@ static void nfc_scene_emulate_on_enter_iso15693_3(NfcApp* instance) {
|
||||
instance->listener, nfc_scene_emulate_listener_callback_iso15693_3, instance);
|
||||
}
|
||||
|
||||
static NfcCommand
|
||||
nfc_scene_write_poller_callback_iso15693_3(NfcGenericEvent event, void* context) {
|
||||
furi_assert(event.protocol == NfcProtocolIso15693_3);
|
||||
|
||||
NfcApp* instance = context;
|
||||
Iso15693_3Poller* poller = event.instance;
|
||||
const Iso15693_3PollerEvent* iso15693_3_event = event.event_data;
|
||||
NfcCommand command = NfcCommandContinue;
|
||||
|
||||
if(iso15693_3_event->type == Iso15693_3PollerEventTypeReady) {
|
||||
const Iso15693_3Data* write_data =
|
||||
nfc_device_get_data(instance->nfc_device, NfcProtocolIso15693_3);
|
||||
const Iso15693_3Data* card_data = iso15693_3_poller_get_data(poller);
|
||||
|
||||
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
|
||||
|
||||
if(card_data->system_info.block_count != write_data->system_info.block_count ||
|
||||
card_data->system_info.block_size != write_data->system_info.block_size) {
|
||||
furi_string_set(
|
||||
instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)");
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerFailure);
|
||||
} else {
|
||||
Iso15693_3Error error = Iso15693_3ErrorNone;
|
||||
const uint8_t block_size = write_data->system_info.block_size;
|
||||
const uint8_t* write_block_data =
|
||||
(const uint8_t*)simple_array_cget_data(write_data->block_data);
|
||||
const uint8_t* card_block_data =
|
||||
(const uint8_t*)simple_array_cget_data(card_data->block_data);
|
||||
for(uint16_t i = 0; i < write_data->system_info.block_count; i++) {
|
||||
if(iso15693_3_is_block_locked(write_data, i)) continue;
|
||||
// Skip blocks that already contain the correct data
|
||||
if(memcmp(
|
||||
write_block_data + i * block_size,
|
||||
card_block_data + i * block_size,
|
||||
block_size) == 0)
|
||||
continue;
|
||||
error = iso15693_3_poller_write_block(
|
||||
poller, write_block_data + i * block_size, i, block_size);
|
||||
if(error == Iso15693_3ErrorInternal) {
|
||||
// Block is locked on the target card, skip it
|
||||
error = Iso15693_3ErrorNone;
|
||||
continue;
|
||||
}
|
||||
if(error != Iso15693_3ErrorNone) break;
|
||||
}
|
||||
|
||||
if(error == Iso15693_3ErrorNone) {
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerSuccess);
|
||||
} else {
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerFailure);
|
||||
}
|
||||
}
|
||||
command = NfcCommandStop;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
static void nfc_scene_write_on_enter_iso15693_3(NfcApp* instance) {
|
||||
furi_string_set(instance->text_box_store, "Apply the\ntarget card now");
|
||||
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolIso15693_3);
|
||||
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_iso15693_3, instance);
|
||||
}
|
||||
|
||||
const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = {
|
||||
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureEditUid |
|
||||
NfcProtocolFeatureMoreInfo,
|
||||
NfcProtocolFeatureMoreInfo | NfcProtocolFeatureWrite,
|
||||
|
||||
.scene_info =
|
||||
{
|
||||
@@ -155,7 +222,7 @@ const NfcProtocolSupportBase nfc_protocol_support_iso15693_3 = {
|
||||
},
|
||||
.scene_write =
|
||||
{
|
||||
.on_enter = nfc_protocol_support_common_on_enter_empty,
|
||||
.on_enter = nfc_scene_write_on_enter_iso15693_3,
|
||||
.on_event = nfc_protocol_support_common_on_event_empty,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "../nfc_protocol_support_common.h"
|
||||
#include "../nfc_protocol_support_gui_common.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "SlixWrite"
|
||||
|
||||
static void nfc_scene_info_on_enter_slix(NfcApp* instance) {
|
||||
const NfcDevice* device = instance->nfc_device;
|
||||
const SlixData* data = nfc_device_get_data(device, NfcProtocolSlix);
|
||||
@@ -106,8 +110,104 @@ static void nfc_scene_emulate_on_enter_slix(NfcApp* instance) {
|
||||
nfc_listener_start(instance->listener, nfc_scene_emulate_listener_callback_slix, instance);
|
||||
}
|
||||
|
||||
static NfcCommand nfc_scene_write_poller_callback_slix(NfcGenericEvent event, void* context) {
|
||||
furi_assert(event.protocol == NfcProtocolSlix);
|
||||
|
||||
NfcApp* instance = context;
|
||||
SlixPoller* poller = event.instance;
|
||||
const SlixPollerEvent* slix_event = event.event_data;
|
||||
NfcCommand command = NfcCommandContinue;
|
||||
|
||||
if(slix_event->type == SlixPollerEventTypeReady) {
|
||||
const SlixData* write_data = nfc_device_get_data(instance->nfc_device, NfcProtocolSlix);
|
||||
const Iso15693_3Data* write_iso_data = slix_get_base_data(write_data);
|
||||
const Iso15693_3Data* card_iso_data =
|
||||
slix_get_base_data((const SlixData*)nfc_poller_get_data(instance->poller));
|
||||
|
||||
view_dispatcher_send_custom_event(instance->view_dispatcher, NfcCustomEventCardDetected);
|
||||
|
||||
if(card_iso_data->system_info.block_count != write_iso_data->system_info.block_count ||
|
||||
card_iso_data->system_info.block_size != write_iso_data->system_info.block_size) {
|
||||
furi_string_set(
|
||||
instance->text_box_store, "Incompatible card\n(block count/size\nmismatch)");
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerFailure);
|
||||
} else {
|
||||
SlixError error = SlixErrorNone;
|
||||
uint16_t failed_block = 0;
|
||||
const uint8_t block_size = write_iso_data->system_info.block_size;
|
||||
const uint8_t* write_data_ptr =
|
||||
(const uint8_t*)simple_array_cget_data(write_iso_data->block_data);
|
||||
const uint8_t* card_data_ptr =
|
||||
(const uint8_t*)simple_array_cget_data(card_iso_data->block_data);
|
||||
for(uint16_t i = 0; i < write_iso_data->system_info.block_count; i++) {
|
||||
if(iso15693_3_is_block_locked(write_iso_data, i)) continue;
|
||||
// Skip blocks that already contain the correct data
|
||||
if(memcmp(
|
||||
write_data_ptr + i * block_size,
|
||||
card_data_ptr + i * block_size,
|
||||
block_size) == 0)
|
||||
continue;
|
||||
error = slix_poller_write_block(
|
||||
poller, write_data_ptr + i * block_size, i, block_size);
|
||||
if(error == SlixErrorInternal) {
|
||||
// Block is locked on the target card, skip it
|
||||
error = SlixErrorNone;
|
||||
continue;
|
||||
}
|
||||
if(error != SlixErrorNone) {
|
||||
failed_block = i;
|
||||
FURI_LOG_E(TAG, "Write failed: block %u, error %d", failed_block, (int)error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(error == SlixErrorNone) {
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerSuccess);
|
||||
} else {
|
||||
const char* err_name;
|
||||
switch(error) {
|
||||
case SlixErrorTimeout:
|
||||
err_name = "Timeout";
|
||||
break;
|
||||
case SlixErrorFormat:
|
||||
err_name = "BadCRC";
|
||||
break;
|
||||
case SlixErrorNotSupported:
|
||||
err_name = "NotSupported";
|
||||
break;
|
||||
case SlixErrorInternal:
|
||||
err_name = "Locked";
|
||||
break;
|
||||
case SlixErrorWrongPassword:
|
||||
err_name = "WrongPwd";
|
||||
break;
|
||||
default:
|
||||
err_name = "Unknown";
|
||||
break;
|
||||
}
|
||||
furi_string_printf(
|
||||
instance->text_box_store, "Block %u: %s", failed_block, err_name);
|
||||
view_dispatcher_send_custom_event(
|
||||
instance->view_dispatcher, NfcCustomEventPollerFailure);
|
||||
}
|
||||
}
|
||||
command = NfcCommandStop;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
static void nfc_scene_write_on_enter_slix(NfcApp* instance) {
|
||||
furi_string_set(instance->text_box_store, "Apply the\ntarget card now");
|
||||
instance->poller = nfc_poller_alloc(instance->nfc, NfcProtocolSlix);
|
||||
nfc_poller_start(instance->poller, nfc_scene_write_poller_callback_slix, instance);
|
||||
}
|
||||
|
||||
const NfcProtocolSupportBase nfc_protocol_support_slix = {
|
||||
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo,
|
||||
.features = NfcProtocolFeatureEmulateFull | NfcProtocolFeatureMoreInfo |
|
||||
NfcProtocolFeatureWrite,
|
||||
|
||||
.scene_info =
|
||||
{
|
||||
@@ -151,7 +251,7 @@ const NfcProtocolSupportBase nfc_protocol_support_slix = {
|
||||
},
|
||||
.scene_write =
|
||||
{
|
||||
.on_enter = nfc_protocol_support_common_on_enter_empty,
|
||||
.on_enter = nfc_scene_write_on_enter_slix,
|
||||
.on_event = nfc_protocol_support_common_on_event_empty,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
|
||||
#include <flipper_application/flipper_application.h>
|
||||
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
|
||||
#define TAG "Plantain"
|
||||
|
||||
void from_minutes_to_datetime(uint32_t minutes, DateTime* datetime, uint16_t start_year) {
|
||||
uint32_t timestamp = minutes * 60;
|
||||
DateTime start_datetime = {0};
|
||||
start_datetime.year = start_year - 1;
|
||||
start_datetime.month = 12;
|
||||
start_datetime.day = 31;
|
||||
timestamp += datetime_datetime_to_timestamp(&start_datetime);
|
||||
datetime_timestamp_to_datetime(timestamp, datetime);
|
||||
}
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PLANTAIN_EPOCH_START 1262304000 //2010-01-01
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 101
|
||||
#define FIRST_TICKET_VALUE_BLOCK 104
|
||||
#define SECOND_PPK_TICKET_OFFSET 102
|
||||
#define SECOND_TICKET_VALUE_BLOCK 108
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
@@ -30,79 +28,549 @@ typedef struct {
|
||||
} PlantainCardConfig;
|
||||
|
||||
static const MfClassicKeyPair plantain_1k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b},
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7},
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb},
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //27
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2aaf3a1ba}, //28
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair plantain_4k_keys[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406},
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, {.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA},
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, {.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3},
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x3a8a139c20b4, .b = 0x8818a9c5d406}, //25
|
||||
{.a = 0xbaff3053b496, .b = 0x4b7cb25354d3}, //26
|
||||
{.a = 0x7413b599c4ea, .b = 0xb0a2AAF3A1BA}, //27
|
||||
{.a = 0x0ce7cd2cc72b, .b = 0xfa1fbb3f0f1f}, //28
|
||||
{.a = 0x0be5fac8b06a, .b = 0x6f95887a4fd3}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
|
||||
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
|
||||
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
|
||||
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
|
||||
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
|
||||
};
|
||||
|
||||
static const MfClassicKeyPair plantain_4k_keys_legacy[] = {
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, {.a = 0x77dabc9825e1, .b = 0x9764fec3154a},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, {.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3},
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, {.a = 0xc76bf71a2509, .b = 0x9ba241db3f56},
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, {.a = 0xffffffffffff, .b = 0xffffffffffff},
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, {.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a},
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, {.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3},
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, {.a = 0x0f318130ed18, .b = 0x0c420a20e056},
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, {.a = 0x9d993c5d4ef4, .b = 0x86120e488abf},
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, {.a = 0x46d78e850a7e, .b = 0xa470f8130991},
|
||||
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, {.a = 0x0f01ceff2742, .b = 0x6fec74559ca7},
|
||||
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, {.a = 0x9ea3387a63c1, .b = 0x437e59f57561},
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, {.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290},
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, {.a = 0xa3faa6daff67, .b = 0x7600e889adf9},
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, {.a = 0x22052b480d11, .b = 0xe19504c39461},
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, {.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a},
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, {.a = 0x7259fa0197c6, .b = 0x5583698df085},
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //0
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //1
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //2
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //3
|
||||
{.a = 0xe56ac127dd45, .b = 0x19fc84a3784b}, //4
|
||||
{.a = 0x77dabc9825e1, .b = 0x9764fec3154a}, //5
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //6
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //7
|
||||
{.a = 0x26973ea74321, .b = 0xd27058c6e2c7}, //8
|
||||
{.a = 0xeb0a8ff88ade, .b = 0x578a9ada41e3}, //9
|
||||
{.a = 0xea0fd73cb149, .b = 0x29c35fa068fb}, //10
|
||||
{.a = 0xc76bf71a2509, .b = 0x9ba241db3f56}, //11
|
||||
{.a = 0xacffffffffff, .b = 0x71f3a315ad26}, //12
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //13
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //14
|
||||
{.a = 0xffffffffffff, .b = 0xffffffffffff}, //15
|
||||
{.a = 0x72f96bdd3714, .b = 0x462225cd34cf}, //16
|
||||
{.a = 0x044ce1872bc3, .b = 0x8c90c70cff4a}, //17
|
||||
{.a = 0xbc2d1791dec1, .b = 0xca96a487de0b}, //18
|
||||
{.a = 0x8791b2ccb5c4, .b = 0xc956c3b80da3}, //19
|
||||
{.a = 0x8e26e45e7d65, .b = 0x8e65b3af7d22}, //20
|
||||
{.a = 0x0f318130ed18, .b = 0x0c420a20e056}, //21
|
||||
{.a = 0x045ceca15535, .b = 0x31bec3d9e510}, //22
|
||||
{.a = 0x9d993c5d4ef4, .b = 0x86120e488abf}, //23
|
||||
{.a = 0xc65d4eaa645b, .b = 0xb69d40d1a439}, //24
|
||||
{.a = 0x46d78e850a7e, .b = 0xa470f8130991}, //25
|
||||
{.a = 0x42e9b54e51ab, .b = 0x0231b86df52e}, //26
|
||||
{.a = 0x0f01ceff2742, .b = 0x6fec74559ca7}, //27
|
||||
{.a = 0xb81f2b0c2f66, .b = 0xa7e2d95f0003}, //28
|
||||
{.a = 0x9ea3387a63c1, .b = 0x437e59f57561}, //29
|
||||
{.a = 0x0eb23cc8110b, .b = 0x04dc35277635}, //30
|
||||
{.a = 0xbc4580b7f20b, .b = 0xd0a4131fb290}, //31
|
||||
{.a = 0x7a396f0d633d, .b = 0xad2bdc097023}, //32
|
||||
{.a = 0xa3faa6daff67, .b = 0x7600e889adf9}, //33
|
||||
{.a = 0xfd8705e721b0, .b = 0x296fc317a513}, //34
|
||||
{.a = 0x22052b480d11, .b = 0xe19504c39461}, //35
|
||||
{.a = 0xa7141147d430, .b = 0xff16014fefc7}, //36
|
||||
{.a = 0x8a8d88151a00, .b = 0x038b5f9b5a2a}, //37
|
||||
{.a = 0xb27addfb64b0, .b = 0x152fd0c420a7}, //38
|
||||
{.a = 0x7259fa0197c6, .b = 0x5583698df085}, //39
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t direction;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint64_t sys_n;
|
||||
uint8_t ppk_cnt;
|
||||
} PPKData;
|
||||
|
||||
typedef struct {
|
||||
uint64_t card_number;
|
||||
FuriString* card_number_str;
|
||||
uint32_t balance;
|
||||
uint8_t trips_metro;
|
||||
uint8_t trips_ground;
|
||||
uint32_t last_trip_data;
|
||||
DateTime last_trip_time;
|
||||
uint16_t validator;
|
||||
uint16_t fare;
|
||||
uint32_t last_payment_date_data;
|
||||
DateTime last_payment_date;
|
||||
uint16_t last_payment_amount;
|
||||
uint8_t keyset;
|
||||
|
||||
} PlantainData;
|
||||
// Function to map UIC codes to station names
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, PPKData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
|
||||
static void resolve_station_name(Storage* storage, PPKData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
// Function to extract plantain purse data from the card data
|
||||
static inline void extract_purse_data(
|
||||
const MfClassicData* data,
|
||||
PlantainData* purse,
|
||||
PPKData* ticket,
|
||||
uint8_t first_ppk_ticket_offset,
|
||||
uint8_t second_ppk_ticket_offset) {
|
||||
uint64_t card_number = 0;
|
||||
size_t uid_len = 0;
|
||||
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
|
||||
purse->card_number_str = furi_string_alloc();
|
||||
const uint8_t* temp_ptr = &uid[0];
|
||||
uint8_t card_number_tmp[uid_len];
|
||||
|
||||
if(uid_len == 4) {
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
card_number_tmp[i] = temp_ptr[3 - i];
|
||||
}
|
||||
} else if(uid_len == 7) {
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_tmp[i] = temp_ptr[6 - i];
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
for(size_t i = 0; i < uid_len; i++) {
|
||||
card_number = (card_number << 8) | card_number_tmp[i];
|
||||
}
|
||||
FuriString* card_number_s = furi_string_alloc();
|
||||
furi_string_cat_printf(card_number_s, "%lld", card_number);
|
||||
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
|
||||
for(uint8_t i = 0; i < 24; i += 4) {
|
||||
for(uint8_t j = 0; j < 4; j++) {
|
||||
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
|
||||
}
|
||||
furi_string_push_back(tmp_s, ' ');
|
||||
}
|
||||
|
||||
furi_string_set(purse->card_number_str, furi_string_get_cstr(tmp_s));
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
|
||||
if(data->type == MfClassicType1k) {
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++)
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
balance /= 100;
|
||||
purse->balance = balance;
|
||||
purse->trips_metro = data->block[21].data[0];
|
||||
purse->trips_ground = data->block[21].data[1];
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
|
||||
purse->fare = fare;
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
last_trip_timestamp = PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
|
||||
const uint32_t last_payment_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
|
||||
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
|
||||
|
||||
} else if(data->type == MfClassicType4k) {
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++)
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
balance /= 100;
|
||||
purse->balance = balance;
|
||||
purse->trips_metro = data->block[21].data[0];
|
||||
purse->trips_ground = data->block[21].data[1];
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_trip_data = (purse->last_trip_data << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
purse->validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
uint16_t fare = ((data->block[20].data[7] << 8) | data->block[20].data[6]) / 100;
|
||||
purse->fare = fare;
|
||||
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
purse->last_payment_date_data = (purse->last_payment_date_data << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
purse->last_payment_amount = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
uint32_t last_trip_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_trip_data * SECONDS_IN_A_MINUTE;
|
||||
const uint32_t last_payment_timestamp =
|
||||
PLANTAIN_EPOCH_START + purse->last_payment_date_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(last_trip_timestamp, &purse->last_trip_time);
|
||||
datetime_timestamp_to_datetime(last_payment_timestamp, &purse->last_payment_date);
|
||||
}
|
||||
|
||||
ticket->first_ticket_marker = data->block[first_ppk_ticket_offset].data[0];
|
||||
ticket->second_ticket_marker = data->block[second_ppk_ticket_offset].data[0];
|
||||
}
|
||||
// Function to extract PPK ticket data from the card data
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
PPKData* ticket,
|
||||
bool second_ticket) {
|
||||
const uint8_t* temp_ptr = &data->block[SECOND_TICKET_VALUE_BLOCK + 2].data[0];
|
||||
uint8_t sys_n_arr[6] = {0};
|
||||
|
||||
if(second_ticket == 0) {
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n_arr[i] = temp_ptr[7 - i];
|
||||
}
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
|
||||
ticket->direction = data->block[FIRST_PPK_TICKET_OFFSET].data[14];
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[105].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else if(second_ticket == 1) {
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n_arr[i] = temp_ptr[13 - i];
|
||||
}
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
|
||||
ticket->direction = data->block[SECOND_PPK_TICKET_OFFSET].data[14];
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[109].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
uint64_t sys_n = 0;
|
||||
for(size_t i = 0; i < 6; i++) {
|
||||
sys_n = (sys_n << 8) | sys_n_arr[i];
|
||||
}
|
||||
ticket->sys_n = sys_n;
|
||||
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
// Function to format and print plantain purse data
|
||||
static void printf_plantain_data(FuriString* parsed_data, PlantainData* purse) {
|
||||
furi_string_printf(parsed_data, "\e#Plantain Card");
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nNumber: %s\nBalance: %ld RUB\nMetro Trips: %d\nGround Trips: %d\nLast Trip: %02d.%02d.%04d %02d:%02d",
|
||||
furi_string_get_cstr(purse->card_number_str),
|
||||
purse->balance,
|
||||
purse->trips_metro,
|
||||
purse->trips_ground,
|
||||
purse->last_trip_time.day,
|
||||
purse->last_trip_time.month,
|
||||
purse->last_trip_time.year,
|
||||
purse->last_trip_time.hour,
|
||||
purse->last_trip_time.minute);
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValidator: %d\nFare: %d RUB\nRefilled on: %02d.%02d.%04d %02d:%02d\nAmount: %d RUB",
|
||||
purse->validator,
|
||||
purse->fare,
|
||||
purse->last_payment_date.day,
|
||||
purse->last_payment_date.month,
|
||||
purse->last_payment_date.year,
|
||||
purse->last_payment_date.hour,
|
||||
purse->last_payment_date.minute,
|
||||
purse->last_payment_amount);
|
||||
|
||||
if(purse->keyset == 1)
|
||||
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> YES");
|
||||
else
|
||||
furi_string_cat_printf(parsed_data, "\nPPK keys installed:> NO");
|
||||
|
||||
furi_string_free(purse->card_number_str);
|
||||
}
|
||||
|
||||
// Function to format and print PPK ticket data
|
||||
static void printf_ppk_data(FuriString* parsed_data, PPKData* ticket, bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\n\n\e#PPK Ticket:\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x33:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
|
||||
break;
|
||||
case 0x34:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
|
||||
break;
|
||||
case 0x35:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
|
||||
break;
|
||||
case 0x36:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type:> Unknown, 0x%02X", ticket->first_ticket_marker);
|
||||
}
|
||||
} else if(ticket_number == 1) {
|
||||
furi_string_cat_printf(parsed_data, "\n\nSecond PPK Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x33:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 1 ride");
|
||||
break;
|
||||
case 0x34:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Mon.-Fri.)");
|
||||
break;
|
||||
case 0x35:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Fri.-Mon.)");
|
||||
break;
|
||||
case 0x36:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 2 rides (Sat.-Mon.)");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type:> Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:> %s\nTo:> %s",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name));
|
||||
|
||||
if(ticket->valid_from.day == ticket->valid_till.day) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValid On: %02d-%02d-%04d",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year);
|
||||
} else {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
}
|
||||
|
||||
if(ticket->direction == 1) {
|
||||
furi_string_cat_printf(parsed_data, "\nDirection: One-way ->>");
|
||||
} else if(ticket->direction == 2) {
|
||||
furi_string_cat_printf(parsed_data, "\nDirection: Round-trip <<-->>");
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "\nRides left:> %02d", ticket->value_data);
|
||||
|
||||
if(ticket->current_status == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\nStatus:> TICKET IS READY\n");
|
||||
} else if(ticket->current_status == 0x80)
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute);
|
||||
else if(ticket->current_status == 0x1E)
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute);
|
||||
else
|
||||
furi_string_cat_printf(parsed_data, "\nStatus:> UNKNOWN (%02X)\n", ticket->current_status);
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "SYS N:> %lld\nPPK CNT:> %03d", ticket->sys_n, ticket->ppk_cnt);
|
||||
}
|
||||
//Function to select a keyset based on card type
|
||||
static bool plantain_get_card_config(PlantainCardConfig* config, MfClassicType type) {
|
||||
bool success = true;
|
||||
|
||||
if(type == MfClassicType1k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_1k_keys;
|
||||
|
||||
} else if(type == MfClassicType4k) {
|
||||
config->data_sector = 8;
|
||||
config->keys = plantain_4k_keys;
|
||||
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
@@ -114,11 +582,10 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, type)) break;
|
||||
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(cfg.data_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %lu", cfg.data_sector);
|
||||
|
||||
MfClassicKey key = {0};
|
||||
bit_lib_num_to_bytes_be(cfg.keys[cfg.data_sector].a, COUNT_OF(key.data), key.data);
|
||||
@@ -127,7 +594,6 @@ static bool plantain_verify_type(Nfc* nfc, MfClassicType type) {
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_context);
|
||||
if(error != MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Failed to read block %u: %d", block_num, error);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -157,7 +623,7 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
if(error != MfClassicErrorNone) break;
|
||||
|
||||
data->type = type;
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
const uint8_t legacy_check_sec_num = 26;
|
||||
@@ -171,21 +637,28 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
error = mf_classic_poller_sync_auth(
|
||||
nfc, legacy_check_block_num, &key, MfClassicKeyTypeA, NULL);
|
||||
if(error == MfClassicErrorNone) {
|
||||
FURI_LOG_D(TAG, "Legacy keys detected");
|
||||
cfg.keys = plantain_4k_keys_legacy;
|
||||
}
|
||||
|
||||
MfClassicDeviceKeys keys = {};
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
if(data->type == MfClassicType1k) {
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
} else {
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(cfg.keys[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error == MfClassicErrorNotPresent) {
|
||||
FURI_LOG_W(TAG, "Failed to read data");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -198,18 +671,22 @@ static bool plantain_read(Nfc* nfc, NfcDevice* device) {
|
||||
|
||||
return is_read;
|
||||
}
|
||||
|
||||
//Main parsing function
|
||||
static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
size_t uid_len = 0;
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
const uint8_t* uid = mf_classic_get_uid(data, &uid_len);
|
||||
|
||||
PPKData ticket = {0};
|
||||
ticket.departure_name = furi_string_alloc();
|
||||
ticket.destination_name = furi_string_alloc();
|
||||
ticket.trip_start_sta_name = furi_string_alloc();
|
||||
ticket.trip_end_sta_name = furi_string_alloc();
|
||||
PlantainData purse = {0};
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
// Verify card type
|
||||
PlantainCardConfig cfg = {};
|
||||
PlantainCardConfig cfg = {0};
|
||||
if(!plantain_get_card_config(&cfg, data->type)) break;
|
||||
|
||||
// Verify key
|
||||
@@ -220,167 +697,47 @@ static bool plantain_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
bit_lib_bytes_to_num_be(sec_tr->key_a.data, COUNT_OF(sec_tr->key_a.data));
|
||||
if(key != cfg.keys[cfg.data_sector].a) break;
|
||||
|
||||
furi_string_printf(parsed_data, "\e#Plantain card\n");
|
||||
if(data->block[107].data[10] == 0x02)
|
||||
purse.keyset = 0;
|
||||
else
|
||||
purse.keyset = 1;
|
||||
|
||||
const uint8_t* temp_ptr = &uid[0];
|
||||
|
||||
// UID is read from last to first byte
|
||||
uint8_t card_number_tmp[uid_len];
|
||||
|
||||
if(uid_len == 4) {
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
card_number_tmp[i] = temp_ptr[3 - i];
|
||||
}
|
||||
} else if(uid_len == 7) {
|
||||
for(size_t i = 0; i < 7; i++) {
|
||||
card_number_tmp[i] = temp_ptr[6 - i];
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
// Extract plantain purse data and fill PPK tickets markers
|
||||
extract_purse_data(
|
||||
data, &purse, &ticket, FIRST_PPK_TICKET_OFFSET, SECOND_PPK_TICKET_OFFSET);
|
||||
// Print plantain purse data
|
||||
printf_plantain_data(parsed_data, &purse);
|
||||
if(purse.card_number_str) furi_string_free(purse.card_number_str);
|
||||
// Extract and print PPK ticket data if present
|
||||
if(ticket.first_ticket_marker != 0) {
|
||||
extract_ppk_data(storage, data, &ticket, 0);
|
||||
printf_ppk_data(parsed_data, &ticket, false);
|
||||
}
|
||||
//UID is converted to a card number
|
||||
uint64_t card_number = 0;
|
||||
for(size_t i = 0; i < uid_len; i++) {
|
||||
card_number = (card_number << 8) | card_number_tmp[i];
|
||||
// Extract and print second PPK ticket data if present
|
||||
if(ticket.second_ticket_marker != 0) {
|
||||
PPKData second_ticket = {0};
|
||||
second_ticket.departure_name = furi_string_alloc();
|
||||
second_ticket.destination_name = furi_string_alloc();
|
||||
second_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
second_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &second_ticket, 1);
|
||||
|
||||
printf_ppk_data(parsed_data, &second_ticket, true);
|
||||
|
||||
furi_string_free(second_ticket.departure_name);
|
||||
furi_string_free(second_ticket.destination_name);
|
||||
furi_string_free(second_ticket.trip_start_sta_name);
|
||||
furi_string_free(second_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
// Print card number with 4-digit groups. "3" in "3078" denotes a ticket type "3 - full ticket", will differ on discounted cards.
|
||||
furi_string_cat_printf(parsed_data, "Number: ");
|
||||
FuriString* card_number_s = furi_string_alloc();
|
||||
furi_string_cat_printf(card_number_s, "%lld", card_number);
|
||||
FuriString* tmp_s = furi_string_alloc_set_str("9643 3078 ");
|
||||
for(uint8_t i = 0; i < 24; i += 4) {
|
||||
for(uint8_t j = 0; j < 4; j++) {
|
||||
furi_string_push_back(tmp_s, furi_string_get_char(card_number_s, i + j));
|
||||
}
|
||||
furi_string_push_back(tmp_s, ' ');
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "%s\n", furi_string_get_cstr(tmp_s));
|
||||
// this works for 2K Plantain
|
||||
if(data->type == MfClassicType1k) {
|
||||
//balance
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
|
||||
|
||||
//trips
|
||||
uint8_t trips_metro = data->block[21].data[0];
|
||||
uint8_t trips_ground = data->block[21].data[1];
|
||||
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
|
||||
//trip time
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
DateTime last_trip = {0};
|
||||
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Trip start: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_trip.day,
|
||||
last_trip.month,
|
||||
last_trip.year,
|
||||
last_trip.hour,
|
||||
last_trip.minute);
|
||||
//validator
|
||||
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
|
||||
//tariff
|
||||
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
|
||||
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
|
||||
//trips in metro
|
||||
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
|
||||
//trips on ground
|
||||
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
|
||||
//last payment
|
||||
uint32_t last_payment_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_payment_timestamp = (last_payment_timestamp << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
DateTime last_payment_date = {0};
|
||||
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Last pay: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_payment_date.day,
|
||||
last_payment_date.month,
|
||||
last_payment_date.year,
|
||||
last_payment_date.hour,
|
||||
last_payment_date.minute);
|
||||
//Last payment amount.
|
||||
uint16_t last_payment = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
//This is for 4K Plantains.
|
||||
} else if(data->type == MfClassicType4k) {
|
||||
//balance
|
||||
uint32_t balance = 0;
|
||||
for(uint8_t i = 0; i < 4; i++) {
|
||||
balance = (balance << 8) | data->block[16].data[3 - i];
|
||||
}
|
||||
furi_string_cat_printf(parsed_data, "Balance: %ld rub\n", balance / 100);
|
||||
|
||||
//trips
|
||||
uint8_t trips_metro = data->block[21].data[0];
|
||||
uint8_t trips_ground = data->block[21].data[1];
|
||||
furi_string_cat_printf(parsed_data, "Trips: %d\n", trips_metro + trips_ground);
|
||||
//trip time
|
||||
uint32_t last_trip_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_trip_timestamp = (last_trip_timestamp << 8) | data->block[21].data[4 - i];
|
||||
}
|
||||
DateTime last_trip = {0};
|
||||
from_minutes_to_datetime(last_trip_timestamp + 24 * 60, &last_trip, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Trip start: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_trip.day,
|
||||
last_trip.month,
|
||||
last_trip.year,
|
||||
last_trip.hour,
|
||||
last_trip.minute);
|
||||
//validator
|
||||
uint16_t validator = (data->block[20].data[5] << 8) | data->block[20].data[4];
|
||||
furi_string_cat_printf(parsed_data, "Validator: %d\n", validator);
|
||||
//tariff
|
||||
uint16_t fare = (data->block[20].data[7] << 8) | data->block[20].data[6];
|
||||
furi_string_cat_printf(parsed_data, "Tariff: %d rub\n", fare / 100);
|
||||
//trips in metro
|
||||
furi_string_cat_printf(parsed_data, "Trips (Metro): %d\n", trips_metro);
|
||||
//trips on ground
|
||||
furi_string_cat_printf(parsed_data, "Trips (Ground): %d\n", trips_ground);
|
||||
//last payment
|
||||
uint32_t last_payment_timestamp = 0;
|
||||
for(uint8_t i = 0; i < 3; i++) {
|
||||
last_payment_timestamp = (last_payment_timestamp << 8) |
|
||||
data->block[18].data[4 - i];
|
||||
}
|
||||
DateTime last_payment_date = {0};
|
||||
from_minutes_to_datetime(last_payment_timestamp + 24 * 60, &last_payment_date, 2010);
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Last pay: %02d.%02d.%04d %02d:%02d\n",
|
||||
last_payment_date.day,
|
||||
last_payment_date.month,
|
||||
last_payment_date.year,
|
||||
last_payment_date.hour,
|
||||
last_payment_date.minute);
|
||||
//Last payment amount
|
||||
uint16_t last_payment = ((data->block[18].data[10] << 16) |
|
||||
(data->block[18].data[9] << 8) | (data->block[18].data[8])) /
|
||||
100;
|
||||
furi_string_cat_printf(parsed_data, "Amount: %d rub", last_payment);
|
||||
furi_string_free(card_number_s);
|
||||
furi_string_free(tmp_s);
|
||||
}
|
||||
parsed = true;
|
||||
} while(false);
|
||||
furi_string_free(ticket.departure_name);
|
||||
furi_string_free(ticket.destination_name);
|
||||
furi_string_free(ticket.trip_start_sta_name);
|
||||
furi_string_free(ticket.trip_end_sta_name);
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair t_card_2k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //15
|
||||
|
||||
{.a = 0x061029A4A6A4, .b = 0xCF43E52FC23D}, //16
|
||||
{.a = 0xE09F2EC8229C, .b = 0x044D78B70F92}, //17
|
||||
{.a = 0x66843B4FF86E, .b = 0x4474EB131577}, //18
|
||||
{.a = 0x9AC9A9B5E809, .b = 0x558B8E45E568}, //19
|
||||
{.a = 0xC114DE1AD90F, .b = 0xA0A9033E2A09}, //20
|
||||
{.a = 0xF696EFFDD838, .b = 0xA38333EC3F72}, //21
|
||||
{.a = 0xAB50BDE9CD04, .b = 0x67991D678AED}, //22
|
||||
{.a = 0x9041DD7B236C, .b = 0x8A527C8CA237}, //23
|
||||
{.a = 0x7D40A5AA63D0, .b = 0x4A61C563DD8A}, //24
|
||||
{.a = 0x05D0520EC52B, .b = 0xB40BD14E6CB4}, //25
|
||||
{.a = 0x9354F1BFF80F, .b = 0xF99D40CBBA63}, //26
|
||||
{.a = 0x84132D9FD1AF, .b = 0x1BAA2563C14D}, //27
|
||||
{.a = 0x27A8E6436B01, .b = 0x4DB883715A5C}, //28
|
||||
{.a = 0x58B173568D26, .b = 0x2E4ADD66E35E}, //29
|
||||
{.a = 0x1BE71601E73C, .b = 0xB855F30C54FB}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xFFFFFFFFFFFF}, //31
|
||||
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sev_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SevPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SevPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1F:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool sev_tk_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(t_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool sev_tk_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(t_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(t_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool sev_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != t_card_2k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin sev_tk_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = sev_tk_verify,
|
||||
.read = sev_tk_read,
|
||||
.parse = sev_tk_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor sev_tk_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &sev_tk_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* sev_tk_plugin_ep(void) {
|
||||
return &sev_tk_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair t_card_4k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B1B3B6}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B5B2B4B9B0}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B6B1B3}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B3B8B5B7}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B2B8}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B4B5B3B8}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B8B1B7B8}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B5B1B8B1}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B6B4B8B7}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B9B2B4}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B8B0B6}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B9B5B5}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B7B1B0B0}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B8B3B2}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B1B0B4B6}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B4B2B5B4B8}, //15
|
||||
{.a = 0x684CE8377AB8, .b = 0x91888D728D9E}, //16
|
||||
{.a = 0xF462ED255B44, .b = 0x0D40620AC610}, //17
|
||||
{.a = 0xF1E8FD5B5C9F, .b = 0xABB5763550B0}, //18
|
||||
{.a = 0xE9E3265B45B1, .b = 0x11D848B26034}, //19
|
||||
{.a = 0xAEA3755DFA82, .b = 0x36D1BAC1395E}, //20
|
||||
{.a = 0x83F58B205854, .b = 0x967DAFCF2674}, //21
|
||||
{.a = 0x5CDF18E68A75, .b = 0x1689C175B14E}, //22
|
||||
{.a = 0x126DC25C5D53, .b = 0x346B03AF1FF3}, //23
|
||||
{.a = 0xF1B013C4495C, .b = 0x74CE14DBC71F}, //24
|
||||
{.a = 0xA5FE4FAD0269, .b = 0x0025FEA845E5}, //25
|
||||
{.a = 0x43080428049C, .b = 0x2E91BB6F511E}, //26
|
||||
{.a = 0x4F44A08C51BC, .b = 0xE44CC58DF833}, //27
|
||||
{.a = 0x3FEC92F652BE, .b = 0x942039006B83}, //28
|
||||
{.a = 0x3A3BE5B635FA, .b = 0xFC564425A9BA}, //29
|
||||
{.a = 0x78C9E1C688BB, .b = 0xA29E362B22F3}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //32
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //33
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //34
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //35
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //36
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //37
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //38
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //39
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sk_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1f%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1F%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1F%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1F%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SKPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SKPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d\n",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(parsed_data, "Status:> NOT USED\n");
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1F:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool sk_tk_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(t_card_4k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool sk_tk_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType4k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < mf_classic_get_total_sectors_num(data->type); i++) {
|
||||
bit_lib_num_to_bytes_be(t_card_4k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(t_card_4k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool sk_tk_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != t_card_4k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin sk_tk_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = sk_tk_verify,
|
||||
.read = sk_tk_read,
|
||||
.parse = sk_tk_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor sk_tk_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &sk_tk_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* sk_tk_plugin_ep(void) {
|
||||
return &sk_tk_plugin_descriptor;
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
//Based on parsers written by Leptoptilos and Assasinfil. Also, thanks to WillyJL (<me@willyjl.dev>) for help!
|
||||
|
||||
#include "nfc_supported_card_plugin.h"
|
||||
#include <flipper_application/flipper_application.h>
|
||||
#include <nfc/nfc_device.h>
|
||||
#include <bit_lib/bit_lib.h>
|
||||
#include <datetime.h>
|
||||
#include <nfc/protocols/mf_classic/mf_classic_poller_sync.h>
|
||||
#include <flipper_format/flipper_format.h>
|
||||
#define PPK_WHOLE_EPOCH_START 946684800 //2000-01-01
|
||||
#define PPK_CURRENT_EPOCH_START 1388534400 //1388530800 //2014-01-01
|
||||
#define SECONDS_IN_A_DAY 86400
|
||||
#define SECONDS_IN_A_MINUTE 60
|
||||
#define FIRST_PPK_TICKET_OFFSET 76
|
||||
#define FIRST_TICKET_VALUE_BLOCK 77
|
||||
#define SECOND_PPK_TICKET_OFFSET 88
|
||||
#define SECOND_TICKET_VALUE_BLOCK 89
|
||||
|
||||
typedef struct {
|
||||
uint64_t a;
|
||||
uint64_t b;
|
||||
} MfClassicKeyPair;
|
||||
|
||||
typedef struct {
|
||||
uint16_t departure_uic;
|
||||
uint16_t destination_uic;
|
||||
uint16_t trip_start_uic;
|
||||
uint16_t trip_end_uic;
|
||||
FuriString* departure_name;
|
||||
FuriString* destination_name;
|
||||
FuriString* trip_start_sta_name;
|
||||
FuriString* trip_end_sta_name;
|
||||
uint8_t value_data;
|
||||
uint8_t current_status;
|
||||
uint16_t valid_from_data;
|
||||
uint16_t valid_till_data;
|
||||
DateTime valid_from;
|
||||
DateTime valid_till;
|
||||
uint32_t tap_data;
|
||||
DateTime tap_time;
|
||||
uint8_t first_ticket_marker;
|
||||
uint8_t second_ticket_marker;
|
||||
uint8_t ppk_cnt;
|
||||
} TicketData;
|
||||
|
||||
static const MfClassicKeyPair so_card_2k[] = {
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B5B8B4B5}, //0
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B2B9B8B7}, //1
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B4B4B4B0}, //2
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B5B2B2B2}, //3
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B8B4B8B0B9}, //4
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B7B9B8B6}, //5
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B6B5B5B5B6}, //6
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B0B0B2B0}, //7
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B1B2B8B0}, //8
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B1B8B3B2}, //9
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B0B0B3B1B8}, //10
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B2B6B7B5B2}, //11
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B9B2B0B2B0}, //12
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B8B8B7B3}, //13
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B3B4B9B2B0}, //14
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B7B2B0B0B9}, //15
|
||||
{.a = 0xA94CAB611187, .b = 0x389109BD1D82}, //16
|
||||
{.a = 0xBF4280329F11, .b = 0x28D9EDD2096D}, //17
|
||||
{.a = 0xDE6BD90BD6B0, .b = 0x94866C16E9A4}, //18
|
||||
{.a = 0x2EA9493CAA7C, .b = 0x5068BCE2BC1C}, //19
|
||||
{.a = 0x15A41BA53F6C, .b = 0x3BD3CF43571C}, //20
|
||||
{.a = 0x1290FFD80DB5, .b = 0xD821B7916B7E}, //21
|
||||
{.a = 0x68C1A07E96A9, .b = 0x2B3323E75750}, //22
|
||||
{.a = 0xC699831AB307, .b = 0xCD7F7E9111F1}, //23
|
||||
{.a = 0x4E5884BF23E9, .b = 0x2287812A6AEE}, //24
|
||||
{.a = 0xC55212F716DC, .b = 0x594E368CCEFF}, //25
|
||||
{.a = 0x6EF127E674B1, .b = 0xDD21C8D3E0B9}, //26
|
||||
{.a = 0xFB79FAF4B55C, .b = 0xFE52B3B2A93B}, //27
|
||||
{.a = 0x6CF85CDFF647, .b = 0xCCAD7C41FC8A}, //28
|
||||
{.a = 0x591F6C130F91, .b = 0x2D2B734ECF91}, //29
|
||||
{.a = 0xEEB83529B79B, .b = 0xCB14E70EBA38}, //30
|
||||
{.a = 0xFFFFFFFFFFFF, .b = 0xB0B1B2B3B4B5}, //31
|
||||
};
|
||||
|
||||
static inline void sz_uic_to_sta(Storage* storage, const char* file_name, TicketData* ticket) {
|
||||
FlipperFormat* file = flipper_format_file_alloc(storage);
|
||||
FuriString* departure_uic = furi_string_alloc_printf("%04X", ticket->departure_uic);
|
||||
FuriString* destination_uic = furi_string_alloc_printf("%04X", ticket->destination_uic);
|
||||
FuriString* trip_start_uic = furi_string_alloc_printf("%04X", ticket->trip_start_uic);
|
||||
FuriString* trip_end_uic = furi_string_alloc_printf("%04X", ticket->trip_end_uic);
|
||||
|
||||
if(flipper_format_file_open_existing(file, file_name)) {
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(departure_uic), ticket->departure_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(destination_uic), ticket->destination_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_start_uic), ticket->trip_start_sta_name);
|
||||
flipper_format_rewind(file);
|
||||
flipper_format_read_string(
|
||||
file, furi_string_get_cstr(trip_end_uic), ticket->trip_end_sta_name);
|
||||
}
|
||||
|
||||
flipper_format_free(file);
|
||||
furi_string_free(departure_uic);
|
||||
furi_string_free(destination_uic);
|
||||
furi_string_free(trip_start_uic);
|
||||
furi_string_free(trip_end_uic);
|
||||
}
|
||||
// Function to resolve station names for a ticket, and if not found, set to "1E" + UIC code
|
||||
static void resolve_station_name(Storage* storage, TicketData* ticket) {
|
||||
sz_uic_to_sta(storage, EXT_PATH("nfc/assets/sz_id.nfc"), ticket);
|
||||
if(furi_string_utf8_length(ticket->departure_name) <= 2) {
|
||||
furi_string_printf(ticket->departure_name, "1E%04X", ticket->departure_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->destination_name) <= 2) {
|
||||
furi_string_printf(ticket->destination_name, "1E%04X", ticket->destination_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_start_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_start_sta_name, "1E%04X", ticket->trip_start_uic);
|
||||
}
|
||||
if(furi_string_utf8_length(ticket->trip_end_sta_name) <= 2) {
|
||||
furi_string_printf(ticket->trip_end_sta_name, "1E%04X", ticket->trip_end_uic);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void extract_ppk_data(
|
||||
Storage* storage,
|
||||
const MfClassicData* data,
|
||||
TicketData* ticket,
|
||||
bool ticket_number) {
|
||||
if(ticket_number == 0) {
|
||||
ticket->departure_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[FIRST_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[FIRST_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[FIRST_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[FIRST_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[FIRST_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
|
||||
} else {
|
||||
ticket->departure_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[6] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[5]);
|
||||
ticket->destination_uic = (data->block[SECOND_PPK_TICKET_OFFSET].data[9] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[8]);
|
||||
ticket->value_data = data->block[SECOND_TICKET_VALUE_BLOCK].data[0];
|
||||
ticket->current_status = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[8];
|
||||
ticket->valid_from_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[2] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[1]);
|
||||
ticket->valid_till_data = (data->block[SECOND_PPK_TICKET_OFFSET].data[4] << 8) |
|
||||
(data->block[SECOND_PPK_TICKET_OFFSET].data[3]);
|
||||
ticket->tap_data = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[2] << 16) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[1] << 8) |
|
||||
data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[0];
|
||||
ticket->ppk_cnt = data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[10];
|
||||
ticket->trip_start_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[4] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[3]);
|
||||
ticket->trip_end_uic = (data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[7] << 8) |
|
||||
(data->block[SECOND_TICKET_VALUE_BLOCK + 1].data[6]);
|
||||
}
|
||||
ticket->first_ticket_marker = data->block[FIRST_PPK_TICKET_OFFSET].data[0];
|
||||
ticket->second_ticket_marker = data->block[SECOND_PPK_TICKET_OFFSET].data[0];
|
||||
const uint32_t valid_from_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_from_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t valid_till_timestamp =
|
||||
PPK_WHOLE_EPOCH_START + ticket->valid_till_data * SECONDS_IN_A_DAY;
|
||||
const uint32_t tap_timestamp =
|
||||
PPK_CURRENT_EPOCH_START + ticket->tap_data * SECONDS_IN_A_MINUTE;
|
||||
datetime_timestamp_to_datetime(valid_from_timestamp, &ticket->valid_from);
|
||||
datetime_timestamp_to_datetime(valid_till_timestamp, &ticket->valid_till);
|
||||
datetime_timestamp_to_datetime(tap_timestamp, &ticket->tap_time);
|
||||
resolve_station_name(storage, ticket);
|
||||
}
|
||||
|
||||
static inline bool is_accompany_card(uint16_t departure_uic, uint16_t destination_uic) {
|
||||
return departure_uic == destination_uic;
|
||||
}
|
||||
|
||||
static void printf_accompany_card(TicketData* ticket, FuriString* parsed_data) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#SZPPK Accompany Card\nValid on: %02d-%02d-%04d\nStation: > %s\n",
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
furi_string_get_cstr(ticket->departure_name));
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> ENTERED STATION\nChecked in at:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1E:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> EXITED STATION\nChecked out at:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> UNKNOWN\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
printf_transport_card(FuriString* parsed_data, TicketData* ticket, bool ticket_number) {
|
||||
if(ticket->departure_uic == 0x0000) {
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\e#Unknown SZPPK Card\n NO TICKET DATA FOUND \n\nTHE TICKET IS NOT ISSUED\nOR LAYOUT IS UNKNOWN\n");
|
||||
return;
|
||||
} else {
|
||||
if(ticket_number == 0) {
|
||||
furi_string_cat_printf(parsed_data, "\e#SZPPK Transport Card\n");
|
||||
switch(ticket->first_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X\n", ticket->first_ticket_marker);
|
||||
}
|
||||
} else {
|
||||
furi_string_cat_printf(parsed_data, "\e#Second Ticket:\n");
|
||||
switch(ticket->second_ticket_marker) {
|
||||
case 0x02:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 5 days unlim.");
|
||||
break;
|
||||
case 0x06:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 10 rides");
|
||||
break;
|
||||
case 0x18:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 30 days unlim.");
|
||||
break;
|
||||
case 0x1B:
|
||||
furi_string_cat_printf(parsed_data, "Type:> 20 rides.");
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Type: Unknown, 0x%02X", ticket->second_ticket_marker);
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nFrom:>%s\nTo:>%s\nValid From: %02d-%02d-%04d\nValid thru: %02d-%02d-%04d",
|
||||
furi_string_get_cstr(ticket->departure_name),
|
||||
furi_string_get_cstr(ticket->destination_name),
|
||||
ticket->valid_from.day,
|
||||
ticket->valid_from.month,
|
||||
ticket->valid_from.year,
|
||||
ticket->valid_till.day,
|
||||
ticket->valid_till.month,
|
||||
ticket->valid_till.year);
|
||||
|
||||
if(ticket->value_data > 0) {
|
||||
furi_string_cat_printf(parsed_data, "Rides remain: %02d\n", ticket->value_data);
|
||||
}
|
||||
|
||||
switch(ticket->current_status) {
|
||||
case 0x00:
|
||||
furi_string_cat_printf(
|
||||
parsed_data, "Status:> NOT USED\nPPK CNT: %03d\n", ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x80:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> ENTERED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_start_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
case 0x1E:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"\nStatus:> EXITED STATION\nSta name:> %s\nLast pass on:> %02d-%02d-%04d\nPass time:> %02d:%02d\nPPK CNT: %03d\n",
|
||||
furi_string_get_cstr(ticket->trip_end_sta_name),
|
||||
ticket->tap_time.day,
|
||||
ticket->tap_time.month,
|
||||
ticket->tap_time.year,
|
||||
ticket->tap_time.hour,
|
||||
ticket->tap_time.minute,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
default:
|
||||
furi_string_cat_printf(
|
||||
parsed_data,
|
||||
"Status:> UNKNOWN (%02X)\nPPK CNT: %03d",
|
||||
ticket->current_status,
|
||||
ticket->ppk_cnt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool szppk_so_verify(Nfc* nfc) {
|
||||
const uint8_t verify_sector = 19;
|
||||
const uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
|
||||
MfClassicKey key = {};
|
||||
bit_lib_num_to_bytes_be(so_card_2k[verify_sector].a, COUNT_OF(key.data), key.data);
|
||||
|
||||
MfClassicAuthContext auth_ctx = {};
|
||||
MfClassicError error =
|
||||
mf_classic_poller_sync_auth(nfc, block_num, &key, MfClassicKeyTypeA, &auth_ctx);
|
||||
|
||||
return error == MfClassicErrorNone;
|
||||
}
|
||||
|
||||
static bool szppk_so_read(Nfc* nfc, NfcDevice* device) {
|
||||
furi_assert(nfc);
|
||||
furi_assert(device);
|
||||
|
||||
MfClassicData* data = mf_classic_alloc();
|
||||
nfc_device_copy_data(device, NfcProtocolMfClassic, data);
|
||||
|
||||
bool is_read = false;
|
||||
MfClassicType type = MfClassicType1k;
|
||||
MfClassicError error = mf_classic_poller_sync_detect_type(nfc, &type);
|
||||
|
||||
if(error == MfClassicErrorNone) {
|
||||
data->type = type;
|
||||
MfClassicDeviceKeys keys = {};
|
||||
|
||||
for(size_t i = 0; i < 32; i++) {
|
||||
bit_lib_num_to_bytes_be(so_card_2k[i].a, sizeof(MfClassicKey), keys.key_a[i].data);
|
||||
FURI_BIT_SET(keys.key_a_mask, i);
|
||||
bit_lib_num_to_bytes_be(so_card_2k[i].b, sizeof(MfClassicKey), keys.key_b[i].data);
|
||||
FURI_BIT_SET(keys.key_b_mask, i);
|
||||
}
|
||||
|
||||
error = mf_classic_poller_sync_read(nfc, &keys, data);
|
||||
if(error != MfClassicErrorNotPresent) {
|
||||
nfc_device_set_data(device, NfcProtocolMfClassic, data);
|
||||
is_read = (error == MfClassicErrorNone);
|
||||
}
|
||||
}
|
||||
|
||||
mf_classic_free(data);
|
||||
return is_read;
|
||||
}
|
||||
|
||||
static bool szppk_so_parse(const NfcDevice* device, FuriString* parsed_data) {
|
||||
furi_assert(device);
|
||||
|
||||
const MfClassicData* data = nfc_device_get_data(device, NfcProtocolMfClassic);
|
||||
bool parsed = false;
|
||||
|
||||
do {
|
||||
MfClassicSectorTrailer* sec_tr = mf_classic_get_sector_trailer_by_sector(data, 19);
|
||||
uint64_t key = bit_lib_bytes_to_num_be(sec_tr->key_a.data, 6);
|
||||
if(key != so_card_2k[19].a) break;
|
||||
|
||||
Storage* storage = furi_record_open(RECORD_STORAGE);
|
||||
TicketData primary_ticket = {0};
|
||||
primary_ticket.departure_name = furi_string_alloc();
|
||||
primary_ticket.destination_name = furi_string_alloc();
|
||||
primary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
primary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
extract_ppk_data(storage, data, &primary_ticket, 0);
|
||||
if(is_accompany_card(primary_ticket.departure_uic, primary_ticket.destination_uic)) {
|
||||
printf_accompany_card(&primary_ticket, parsed_data);
|
||||
} else {
|
||||
printf_transport_card(parsed_data, &primary_ticket, false);
|
||||
}
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
|
||||
if(primary_ticket.second_ticket_marker != 0) {
|
||||
TicketData secondary_ticket = {0};
|
||||
secondary_ticket.departure_name = furi_string_alloc();
|
||||
secondary_ticket.destination_name = furi_string_alloc();
|
||||
secondary_ticket.trip_start_sta_name = furi_string_alloc();
|
||||
secondary_ticket.trip_end_sta_name = furi_string_alloc();
|
||||
|
||||
extract_ppk_data(storage, data, &secondary_ticket, 1);
|
||||
printf_transport_card(parsed_data, &secondary_ticket, true);
|
||||
furi_string_free(primary_ticket.departure_name);
|
||||
furi_string_free(primary_ticket.destination_name);
|
||||
furi_string_free(primary_ticket.trip_start_sta_name);
|
||||
furi_string_free(primary_ticket.trip_end_sta_name);
|
||||
}
|
||||
|
||||
furi_record_close(RECORD_STORAGE);
|
||||
parsed = true;
|
||||
|
||||
} while(false);
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
static const NfcSupportedCardsPlugin szppk_so_plugin = {
|
||||
.protocol = NfcProtocolMfClassic,
|
||||
.verify = szppk_so_verify,
|
||||
.read = szppk_so_read,
|
||||
.parse = szppk_so_parse,
|
||||
};
|
||||
|
||||
static const FlipperAppPluginDescriptor szppk_so_plugin_descriptor = {
|
||||
.appid = NFC_SUPPORTED_CARD_PLUGIN_APP_ID,
|
||||
.ep_api_version = NFC_SUPPORTED_CARD_PLUGIN_API_VERSION,
|
||||
.entry_point = &szppk_so_plugin,
|
||||
};
|
||||
|
||||
const FlipperAppPluginDescriptor* szppk_so_plugin_ep(void) {
|
||||
return &szppk_so_plugin_descriptor;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ bool two_cities_verify(Nfc* nfc) {
|
||||
bool verified = false;
|
||||
|
||||
do {
|
||||
const uint8_t verify_sector = 4;
|
||||
const uint8_t verify_sector = 9;
|
||||
uint8_t block_num = mf_classic_get_first_block_num_of_sector(verify_sector);
|
||||
FURI_LOG_D(TAG, "Verifying sector %u", verify_sector);
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
9468: KULISHKI
|
||||
9469: KRASNOE
|
||||
968C: UGLICH
|
||||
97A7: 40 KM
|
||||
9C61: 29 KM
|
||||
AB91: YAROSLAVL-GL.
|
||||
AB92: YAROSLAVL-PASS
|
||||
AB93: PRIVOLZHYE
|
||||
AB94: FILINO
|
||||
ABD5: 300 KM
|
||||
ABFF: 4 KM
|
||||
AC4D: SKALINO
|
||||
AC4F: MARFINO
|
||||
AC50: PRECHISTOE
|
||||
AC51: MAKAROVO
|
||||
AC53: PURSHEVO
|
||||
AC54: RODIONOVO
|
||||
AC55: MASLOVO
|
||||
AC56: NEKOUZ
|
||||
AC57: SHESTIKHINO
|
||||
AC59: VOLGA
|
||||
AC5A: KOBOSTOVO
|
||||
AC5B: TIKHMENEVO
|
||||
AC5C: LOM
|
||||
AC5D: VAULOVO
|
||||
AC5E: CHEBAKOVO
|
||||
AC64: LYUTOVO
|
||||
AC65: TOSCHIKHA
|
||||
AC66: BURMAKINO
|
||||
AC68: PANTELEEVO
|
||||
AC69: PUTYATINO
|
||||
AC6A: PUCHKOVSKIY
|
||||
AC6B: UTKINO
|
||||
AC6F: KOZMODEMNSK
|
||||
AC70: KOROMYSLOVO
|
||||
AC71: SEMIBRATOVO
|
||||
AC72: ROSTOV-YARSLV.
|
||||
AC73: PETROVSK
|
||||
AC74: SIL'NITSY
|
||||
AC75: ITLAR'
|
||||
AC77: BEKLEMISHEVO
|
||||
AC78: RYAZANTSEVO
|
||||
AC79: SHUSHKOVO
|
||||
AC7A: BERENDEEVO
|
||||
AC8A: RYBINSK-PASS.
|
||||
ACA6: SEKSHA
|
||||
ACA7: LYUBIM
|
||||
ACA9: ZHAROK
|
||||
ACAA: SOT'
|
||||
ACAB: LUNKA
|
||||
ACD4: DANOLOV
|
||||
ACE9: DUNAIKA
|
||||
ACEA: LIPOVAYA GORA
|
||||
AD03: 155 KM
|
||||
AD04: 147 KM
|
||||
AD06: 187 KM
|
||||
AD07: 231 KM
|
||||
AD08: 259 KM
|
||||
AD09: 268 KM
|
||||
AD0A: 274 KM
|
||||
AD0B: DEPO
|
||||
AD0C: 310 KM
|
||||
AD0D: 322 KM
|
||||
AD0E: 331 KM
|
||||
AD0F: 343 KM
|
||||
AD10: 351 KM
|
||||
AD45: 361 KM
|
||||
AD46: 370 KM
|
||||
AD47: 378 KM
|
||||
AD48: 388 KM
|
||||
AD4A: 399 KM
|
||||
AD4B: 409 KM
|
||||
AD4C: 419 KM
|
||||
AEB1: KOCHENYATINO
|
||||
AEB3: BAGRIMOVO
|
||||
AEB4: REKA
|
||||
AEB5: SOSNOVTSY
|
||||
AEB9: DOGADTSEVO
|
||||
AEBD: KUDRYAVTSEVO
|
||||
AEBF: PROSVET
|
||||
AEC1: GAVROLOV YAM
|
||||
AEC3: LEVTSOVO
|
||||
AEC7: HOZHAEVO
|
||||
AECA: TSYBRINO
|
||||
AECB: ROKSHA
|
||||
AEF4: 69 KM
|
||||
AEF5: 56 KM
|
||||
AEF6: 323 KM
|
||||
AEFA: 287 KM
|
||||
AEFB: 296 KM
|
||||
AEFC: 305 KM
|
||||
AEFD: 310 KM
|
||||
AEFE: 318 KM
|
||||
AF0E: 265 KM
|
||||
AF0F: 285 KM
|
||||
B057: MAKAROVSKAYA
|
||||
B05A: USHAKOVO
|
||||
B05F: VAREGOVO
|
||||
B07A: 306 KM
|
||||
B07B: 296 KM
|
||||
B07C: YURINSKIY
|
||||
B07D: TOROPOVO
|
||||
B07E: PINYAGI
|
||||
B07F: CHIZHOVO
|
||||
B080: KLINTSEVO
|
||||
B081: TENINO
|
||||
B082: MOLOT
|
||||
B083: TELISCHEVO
|
||||
B084: SUHAREZH
|
||||
B086: 324 KM
|
||||
B087: POLYANKI
|
||||
B08A: DEBOLOVSKAYA
|
||||
B08B: 2 KM
|
||||
B090: RUSHA
|
||||
B0AC: KOTOROSL'
|
||||
B0D1: 403 KM
|
||||
B0D2: SHOLOSOVO
|
||||
B0D3: MURINSKOE
|
||||
B0D4: ONAN'JINO
|
||||
B0D5: L'NOZAVOD
|
||||
B0F3: 359 KM
|
||||
@@ -0,0 +1,85 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
8377: OP 1725 KM
|
||||
8350: STEKOL'NYI
|
||||
834F: SELHOZTECHNIKA
|
||||
834D: KANGLY
|
||||
834B: OP 1861 KM
|
||||
834A: OP 1856 KM
|
||||
8349: OP 1842 KM
|
||||
8348: OP 1832 KM
|
||||
8347: OP 1806 KM
|
||||
8344: OP 44 KM
|
||||
8343: OP 129 KM
|
||||
8288: SUVOROVSKAYA
|
||||
8188: NESKUCHNYI
|
||||
8183: OP 1741 KM
|
||||
8182: KOCHUBEEVSKII
|
||||
817E: SURKUL
|
||||
817D: OP 1823 KM
|
||||
8177: TERSKII
|
||||
8176: KRASNAYA
|
||||
8175: TEPLAYA RECHKA
|
||||
8165: DVORTSOVYI
|
||||
8164: PIONER
|
||||
8161: ORBEL'YANOVO
|
||||
815F: DZHEMUHA
|
||||
815D: ETOKA
|
||||
8158: OBIL'NYI
|
||||
8157: NINY
|
||||
813C: STAVROPOL
|
||||
8104: KARMALINOVSKII
|
||||
8084: KRASN.DEREVNIA
|
||||
807E: MIHAILOVSKAIA
|
||||
7FF8: ZMEIKA
|
||||
7FF6: MASHUK
|
||||
7FF5: LERMONTOVSKII
|
||||
7FF4: NOVOPIATIGORSK
|
||||
7FF3: SKACHKI
|
||||
7FF2: ZOLOTUSHKA
|
||||
7FF1: BELYI UGOL'
|
||||
7FF0: PODKUMOK
|
||||
7FEF: MINUTKA
|
||||
7FBA: PLAKSEIKA
|
||||
7FB9: MASLOV KUT
|
||||
7FB8: ZELENOKUMSK
|
||||
7FB7: KUMA
|
||||
7FB6: GEORGIEVSK
|
||||
7FA6: VIAZNIKI
|
||||
7FA4: YAGODKA
|
||||
7FA1: MAIAK
|
||||
7F9B: BEDENNOVSK
|
||||
7F6A: PALAGIADA
|
||||
7F69: RYZDVIANAIA
|
||||
7F68: PEREDOVAIA
|
||||
7F65: GRIGOROPOLISSK.
|
||||
7F5C: NEVINNOMYSSK.
|
||||
7F2B: INOZEMTSEVO
|
||||
7F2A: MIN.VODY
|
||||
7F1B: ZHELEZNOVODSK
|
||||
7F17: BESHTAU
|
||||
7EFD: RASSHEVATKA
|
||||
7EBC: PIATIGORSK
|
||||
7EB2: KISLOVODSK
|
||||
7EA5: ZOLSKII
|
||||
7EA4: VINOGRADNAIA
|
||||
7EA2: KUMAGORSK
|
||||
7E9F: NAGUTSKAIA
|
||||
7E9D: KURSAVKA
|
||||
7E9B: KIAN
|
||||
7E9A: ZELENCHUK
|
||||
7E96: BOGLOVSKAIA
|
||||
7E95: OVECHKA
|
||||
8162: KURSHAVA
|
||||
7F97: CHUKOTSKII
|
||||
83AE: DEBRI
|
||||
8306: DEGTIAREVSKI
|
||||
8185: NIVA
|
||||
8160: OP 1814 KM
|
||||
815B: CHISTAIA
|
||||
7FA5: BERMEDSKII
|
||||
7F93: IZOBILNAIA
|
||||
7F64: KRASNOKUBANSK.
|
||||
7ED0: ESSENTUKI
|
||||
7EA6: APOLLONSKAIA
|
||||
@@ -0,0 +1,151 @@
|
||||
Filetype: Flipper NFC resources
|
||||
Version: 1
|
||||
# staID: Name
|
||||
9426: LADOZH.VOKZ.
|
||||
9421: MOSKOV.VOKZ.
|
||||
9423: VITEBS.VOKZ
|
||||
9424: FINLND.VOKZ
|
||||
9425: BALT.VOKZ
|
||||
948B: IM.MOROZOVA
|
||||
94B4: TOSNO
|
||||
94B5: SABLINO
|
||||
94BE: GORY
|
||||
94C0: IZHORY
|
||||
94C1: RYBATSKOE
|
||||
24C2: OBUKHOVO
|
||||
94C3: SLAVYANKA
|
||||
94C4: KOLPINO
|
||||
94C9: MSHINSKAYA
|
||||
94CC: STROGANOVO
|
||||
94CD: SUI'DA
|
||||
94CE: GATCHINA BLT.
|
||||
94D4: VYRITSA
|
||||
94D5: PAVLOVSK
|
||||
94D6: TSARSKOE SELO
|
||||
94E2: KALISHCHE
|
||||
94E3: LEBYAZHYE
|
||||
94E4: BOLSH.IZHORA
|
||||
9507: BELOOSTROV
|
||||
9508: ZELENOGORSK
|
||||
9511: RUCH'I
|
||||
9512: TOKSOVO
|
||||
9513: OSEL'KI
|
||||
9514: PERI
|
||||
9515: GRUZINO
|
||||
9516: VASKELOVO
|
||||
9517: OREKHOVO
|
||||
9518: PETYAJARVI
|
||||
9519: LOSEVO
|
||||
951A: GROMOVO
|
||||
951F: KUZNECHNOE
|
||||
95C3: KAPITOLOVO
|
||||
969A: MGA
|
||||
969C: LUGA
|
||||
969D: TOLMACHEVO
|
||||
969E: SIVERSKAYA
|
||||
969F: GATCHINA VRSH.
|
||||
96A1: OREDEZH
|
||||
96A3: SLANTSY
|
||||
96A5: ORANIENBAUM 1
|
||||
96A8: VOLOSOVO
|
||||
96BD: TIKHVIN
|
||||
96C0: VOLKHOVSTROY 1
|
||||
96C1: VOLKHOVSTROY 2
|
||||
96CA: VYBORG
|
||||
96CB: SOSNOVO
|
||||
96CC: PRIOZYORSK
|
||||
970A: BOLOTISTOE
|
||||
971D: STAR.DEREVNYA
|
||||
9721: 54 KM
|
||||
9723: KOLOSKOVO(79KM)
|
||||
972C: PARAVOZNY MUZEI
|
||||
9741: BOROVAYA
|
||||
97BB: DETSKOSELSKAYA
|
||||
9809: KUPCHINO
|
||||
9810: PETROKREPOST'
|
||||
9811: ALEKSANDROVSK.F
|
||||
9817: BERNGARDOVKA
|
||||
981C: PROSPEKT SLAVY
|
||||
9826: SOSNOVAYA POL.
|
||||
9829: PUDOST'
|
||||
982B: LENINSKI' PR.
|
||||
982D: PAVLOVO-NA-NEVE
|
||||
9835: LANSKAYA
|
||||
9838: KANNELYARVI
|
||||
983A: MELN.RUCHEI
|
||||
983F: PISKARYOVKA
|
||||
9849: UDEL'NAYA
|
||||
9852: BRONKA
|
||||
9853: VSEVOLOZHSK.
|
||||
985D: FARFOROVSKAYA
|
||||
9867: RZHEVKA
|
||||
9868: KOBRALOVO
|
||||
986B: AEROPORT
|
||||
9871: BRONEVAYA
|
||||
987B: SESTRORETSK
|
||||
9880: TATIANINO
|
||||
988A: KUSHELEVKA
|
||||
9890: RAKH'YA
|
||||
9894: UL'YANKA
|
||||
9899: PESOCHNAYA
|
||||
989A: NAVALOCHNAYA
|
||||
989E: STREL'NA
|
||||
98A3: ST.PETERGOF
|
||||
98A5: KOLTUSHI
|
||||
98A8: KUZ'MOLOVO
|
||||
98AD: KRASN. SELO
|
||||
98AE: SHUSHARY
|
||||
98BC: GORELOVO
|
||||
98C1: ROSHCHINO
|
||||
98CB: LISII' NOS
|
||||
98D5: DACHNOYE
|
||||
98D6: PREDPORTOVAYA
|
||||
98E4: NOVAYA OKHTA
|
||||
98E9: REPINO
|
||||
98F3: LEVASHOVO
|
||||
98FD: NOV.DEREVNYA
|
||||
98FE: IZHORSK.ZAVOD
|
||||
9908: VAGANOVO
|
||||
990C: SHUVALOVO
|
||||
9911: DUNAI'
|
||||
9916: IRINOVKA
|
||||
991C: TARKHOVKA
|
||||
9923: PUPYSHEVO
|
||||
9925: TAI'TSY
|
||||
9926: LAVRIKI
|
||||
992A: LADOZHSK.OZ.
|
||||
992D: ROMANOVKA
|
||||
9936: RAZLIV
|
||||
9938: KIRILLOVSKOE
|
||||
9939: OL'GINO
|
||||
993D: METALLOSTROY
|
||||
993E: KAVGOLOVO
|
||||
9943: VOZD.PARK
|
||||
9944: POST KOVALEVO
|
||||
9949: LAPPELOVO
|
||||
994D: KORNEVO
|
||||
9952: SOLNECHNOYE
|
||||
9956: MYAGLOVO
|
||||
995B: OZERKI
|
||||
995C: KOMAROVO
|
||||
9969: PROBA
|
||||
996A: KIRPICH.ZAVOD
|
||||
9970: KURORT
|
||||
9975: 67 KM
|
||||
9976: USHKOVO
|
||||
9981: DUDERGOF
|
||||
9989: UNIVERSITETSK.
|
||||
9992: YAKHTENNAYA
|
||||
9996: KOVALEVO
|
||||
9997: DIBUNY
|
||||
99AF: MANUSHKINO
|
||||
99CF: PARGOLOVO
|
||||
99D4: LIGOVO
|
||||
9C09: DEVYATKINO
|
||||
9C58: NEV.DUBROVKA
|
||||
9C5A: ALEKSANDROVSK.
|
||||
9C5B: GORSKAYA
|
||||
9D1C: NOV.PETERGOF
|
||||
9FDC: BORISOVA GRIVA
|
||||
98B7: SERGIEVO
|
||||
9957: LAKHTA
|
||||
@@ -77,6 +77,7 @@ typedef enum {
|
||||
SetTypeFaacSLH_868,
|
||||
SetTypeFaacSLH_433,
|
||||
SetTypeBFTMitto,
|
||||
SetTypeErreka433,
|
||||
SetTypeSomfyTelis,
|
||||
SetTypeSomfyKeytis,
|
||||
SetTypeKingGatesStylo4k,
|
||||
@@ -117,6 +118,19 @@ typedef enum {
|
||||
SetTypeNovoferm_433_92,
|
||||
SetTypeHormannEcoStar_433_92,
|
||||
SetTypeCardinS449_433FM,
|
||||
SetTypePujol433,
|
||||
SetTypePujol_Vario433,
|
||||
SetTypeET_Blue433,
|
||||
SetTypeET_Blue_Mix433,
|
||||
SetTypeATA_PTX4_433,
|
||||
SetTypeSeav433,
|
||||
SetTypeWisniowski433,
|
||||
SetTypeFadini433,
|
||||
SetTypeMc_Garcia433,
|
||||
SetTypeClemsa_Mutancode433,
|
||||
SetTypeDoormatic433,
|
||||
SetTypeElvox433,
|
||||
SetTypeVerex433,
|
||||
SetTypeFAACRCXT_433_92,
|
||||
SetTypeFAACRCXT_868,
|
||||
SetTypeGeniusBravo433,
|
||||
|
||||
@@ -496,14 +496,25 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
|
||||
break;
|
||||
case SetTypeBFTMitto:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloqBFT,
|
||||
.type = GenKeeloqSeed,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq_bft.serial = key & 0x000FFFFF,
|
||||
.keeloq_bft.btn = 0x02,
|
||||
.keeloq_bft.cnt = 0x02,
|
||||
.keeloq_bft.seed = key & 0x000FFFFF,
|
||||
.keeloq_bft.manuf = "BFT"};
|
||||
.keeloq_seed.serial = key & 0x000FFFFF,
|
||||
.keeloq_seed.btn = 0x02,
|
||||
.keeloq_seed.cnt = 0x02,
|
||||
.keeloq_seed.seed = key & 0x000FFFFF,
|
||||
.keeloq_seed.manuf = "BFT"};
|
||||
break;
|
||||
case SetTypeErreka433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloqSeed,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq_seed.serial = key & 0x000FFFFF,
|
||||
.keeloq_seed.btn = 0x02,
|
||||
.keeloq_seed.cnt = 0x02,
|
||||
.keeloq_seed.seed = key & 0x000FFFFF,
|
||||
.keeloq_seed.manuf = "Erreka"};
|
||||
break;
|
||||
case SetTypeAlutechAT4N:
|
||||
gen_info = (GenInfo){
|
||||
@@ -698,6 +709,136 @@ void subghz_scene_set_type_fill_generation_infos(GenInfo* infos_dest, SetType ty
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Cardin_S449"};
|
||||
break;
|
||||
case SetTypePujol433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Pujol"};
|
||||
break;
|
||||
case SetTypeET_Blue433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x04,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "ET_Blue"};
|
||||
break;
|
||||
case SetTypeET_Blue_Mix433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x04,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "ET_Blue_Mix"};
|
||||
break;
|
||||
case SetTypeATA_PTX4_433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "ATA_PTX4"};
|
||||
break;
|
||||
case SetTypePujol_Vario433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Pujol_Vario"};
|
||||
break;
|
||||
case SetTypeSeav433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Seav"};
|
||||
break;
|
||||
case SetTypeWisniowski433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Wisniowski"};
|
||||
break;
|
||||
case SetTypeFadini433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Fadini"};
|
||||
break;
|
||||
case SetTypeMc_Garcia433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Mc_Garcia"};
|
||||
break;
|
||||
case SetTypeClemsa_Mutancode433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Clemsa_Mutancode"};
|
||||
break;
|
||||
case SetTypeDoormatic433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Doormatic"};
|
||||
break;
|
||||
case SetTypeElvox433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Elvox"};
|
||||
break;
|
||||
case SetTypeVerex433:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
.mod = "AM650",
|
||||
.freq = 433920000,
|
||||
.keeloq.serial = key & 0x00FFFFFF,
|
||||
.keeloq.btn = 0x02,
|
||||
.keeloq.cnt = 0x03,
|
||||
.keeloq.manuf = "Verex"};
|
||||
break;
|
||||
case SetTypeFAACRCXT_433_92:
|
||||
gen_info = (GenInfo){
|
||||
.type = GenKeeloq,
|
||||
|
||||
@@ -7,7 +7,7 @@ typedef enum {
|
||||
GenFaacSLH,
|
||||
GenKeeloq,
|
||||
GenCameAtomo,
|
||||
GenKeeloqBFT,
|
||||
GenKeeloqSeed,
|
||||
GenAlutechAt4n,
|
||||
GenSomfyTelis,
|
||||
GenSomfyKeytis,
|
||||
@@ -55,7 +55,7 @@ typedef struct {
|
||||
uint16_t cnt;
|
||||
uint32_t seed;
|
||||
const char* manuf;
|
||||
} keeloq_bft;
|
||||
} keeloq_seed;
|
||||
struct {
|
||||
uint32_t serial;
|
||||
uint8_t btn;
|
||||
|
||||
@@ -125,7 +125,7 @@ bool subghz_txrx_gen_keeloq_protocol( //TODO lead to a general appearance
|
||||
return res;
|
||||
}
|
||||
|
||||
bool subghz_txrx_gen_keeloq_bft_protocol(
|
||||
bool subghz_txrx_gen_keeloq_seed_protocol(
|
||||
void* context,
|
||||
const char* preset_name,
|
||||
uint32_t frequency,
|
||||
@@ -142,7 +142,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol(
|
||||
subghz_transmitter_alloc_init(txrx->environment, SUBGHZ_PROTOCOL_KEELOQ_NAME);
|
||||
subghz_txrx_set_preset(txrx, preset_name, frequency, NAN, NAN, NULL, 0);
|
||||
|
||||
if(txrx->transmitter && subghz_protocol_keeloq_bft_create_data(
|
||||
if(txrx->transmitter && subghz_protocol_keeloq_seed_create_data(
|
||||
subghz_transmitter_get_protocol_instance(txrx->transmitter),
|
||||
txrx->fff_data,
|
||||
serial,
|
||||
@@ -162,7 +162,7 @@ bool subghz_txrx_gen_keeloq_bft_protocol(
|
||||
|
||||
flipper_format_write_hex(txrx->fff_data, "Seed", seed_data, sizeof(uint32_t));
|
||||
|
||||
flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", "BFT");
|
||||
flipper_format_write_string_cstr(txrx->fff_data, "Manufacture", manufacture_name);
|
||||
}
|
||||
|
||||
subghz_transmitter_free(txrx->transmitter);
|
||||
|
||||
@@ -63,7 +63,7 @@ bool subghz_txrx_gen_keeloq_protocol(
|
||||
uint16_t cnt,
|
||||
const char* manufacture_name);
|
||||
|
||||
bool subghz_txrx_gen_keeloq_bft_protocol(
|
||||
bool subghz_txrx_gen_keeloq_seed_protocol(
|
||||
void* context,
|
||||
const char* preset_name,
|
||||
uint32_t frequency,
|
||||
|
||||
@@ -1,72 +1,120 @@
|
||||
Filetype: Flipper SubGhz Keystore File
|
||||
Version: 0
|
||||
Encryption: 1
|
||||
IV: 6E 6F 74 69 63 65 73 20 62 75 6C 67 65 4F 77 4F
|
||||
212F687B2D38E6E9066E95894E455A6AD3A8861CDDAFD09DBC2557506676332D
|
||||
2CFF25E9743E4588820D24998B5047E8019C6E200922BCB66830C7F722822A79
|
||||
74AA0EF345C27F583042C4EDDBB83D5446387D4E5B31DFD3F7B9D09F0662A0AA95BEB40BF37678954CAE0200B2898D22
|
||||
6998443F71D17938425CD231B8500E769976601315D79D673AA37E4F9BDCE50B
|
||||
3335691566FEDDDE5828BE0A5BBBD9AC013987A1134E68020D3F0F477E09BC71
|
||||
1B3A3CBB3D56783220BEAECEA15FFF5CD92AF44F63547F6F84F4533D2B3D820B
|
||||
A1ECFE74E714D0E1514D0C18903598E7FB7E7AF5C0165DEDE85DFAE05B26C0EC
|
||||
BA3F2EFBE7CB6671206EA90DA903FF48CCABEC9F393D55B9DB3B7E207A48C059
|
||||
CA91805C8ADF02501B41D6BF0C69D7C3DD6E218D4B4EB6BED2FDE1367256E544
|
||||
5465E78D877F2594AF40115723C8AC71D09F60ACCC8121CD3B6BA271BEA43B5C
|
||||
BB1D3DE0A048694FC86CA8E79BB7BFBB1EF91515C44E917FD5017676EE5C4B13
|
||||
48C08D8B2B42D025D2557CC650EDA3C5CC301CD71169CFE0044C07A95224109B
|
||||
3DAE6C5B94C237FF10CF82FAA58517DB4DD4E4D82FD81CFAAA4836C9B8460CE1
|
||||
645F8F5D213889BAB0F437497856A0CF402E3D4A3679258BB0BF2B768D1F1714
|
||||
AFC3C462698C795B1A407F27F499CD1757931BD676BA43E4B41E8EF2672EC4AE
|
||||
5A19F0D1CD87C40883D7BE51377CDAD94C977B4F6CFCCECED91D56867C9EC211
|
||||
716645BD4F5B18002417A92D6A2BD29D18087F7ADB549A90A2661EB4163EF190
|
||||
54B83F7F7F50AD4C3DC3498822240AEB7D9A43728704F14691A39C79FAC9B6A3
|
||||
FFDDCEFAFF3ADA3616B388684B1AD4987B83E459BA5037E377FE17CC7EDAA430
|
||||
9CC98590A371E580B164A5F1369945A6DB653797331EC898755E84BE93EDF3EF
|
||||
63697AD15064194A3986CC1F3111A09E3D86AC8D370A8EC4D22EDDA4B9661B63
|
||||
F7CE4FA63EA9D388AE4AB6A468768AE91E2E3CF6F1D9A5EA17845466A6801A8C
|
||||
38B2DE32CF36D68842CB31FD97394892B76FB4E81FCFB33164676BAB354939DE
|
||||
93192FA1D7E347AF9FC98EE49A8B02869527511D0B263FD3B08CCFE645A39F51
|
||||
3D5F832CF75D94B58B078B4EE0DFC1D987795CD49959517F391F9E9F09CA187C
|
||||
6CBCCCE0F8E6A833D5CA18E09E821A7F93B9FD7D912632E277B1721985F4B701
|
||||
5896050E9F4950CD7061ED4DD3BA9FC6013CA52F5C1B0493110FA3934116AC66
|
||||
F0D3FF283FBD7CFBFE697DBE752EDD5A1F124DCCAE3B629A146E0E85A7ED4E06
|
||||
6B6C7C365A5CABA2F1023ED1F97D182F0B9D575C64393272E19B8B809DF3C329
|
||||
1AF30761D85AE1C6DD4D78D4ECE79983C7F58ECE859709570262811CFCA8C97F
|
||||
FC86201D96F4AE36C63A1C00DA34AC14BFDE317383D19A534362E6149E860366
|
||||
54F55F34A27E1D690F56269D29C795105C8407A4E1A9ED431588F559948C115A
|
||||
4DD62ECD2FE8CBB783E3D7F0B1D95496D585405DE5AEB07C8A4E053B3AAAD808
|
||||
EDB356AFA869EFB987F099239449D6B9F1469EA6D8C9F0A6B4171F685E2853A1
|
||||
03C058A2C0DB83179E520F035BBAAB01673EFE4FDE717A0F5D1AB6B81BC9D033
|
||||
D2F33C08136AB161EED6121E721F8C479CED5F26A1B7DEEDEA8580314C232C5E
|
||||
51CB3275D6D7250CD88A446242590C3DFD36DF83F5E34379DE73601511CD24B9
|
||||
ED4EC928AC505CBEFA6ACB2901C8E2221FC5073624B0B78F8365DD0442EFBA10
|
||||
76F959BBF8A4EC4AC22F41F06719BCBA58F070CB65B282BFC2C99F6CC02E30DB
|
||||
0A44A7C5D4ECF2F08CBDF3FE947D45C6AC586D4558DBFB36719927F58EBB525B
|
||||
F096777DDC5BF671CBF1A9682EBEE8308AA1002F6CB6899D575A30CFACB30125C8378864C77922360ADD0AC486EFF7F4
|
||||
372863D5B6F6D7A671AB734A99403AF230DEBBBA435AB8F236B179960FAFDFC6
|
||||
6CC31C5E86CF6851D92FA973253B3B1FC9023B150980A216A63720CD74592EB7
|
||||
6643B2CA94415FE4A4E25A3086B8370172C53283360A7BD54B322BD889A5A831
|
||||
03209037BD7E96A16C69A4F0B6FEB3F2ABA1408353BE09C53485CB7C2362A490
|
||||
BE4619EC34F7FB8DFE53A8ABFDE6A831895B88D0AB1A9B4861BC7EC7F1121611
|
||||
41223F7AD8B0F28D08C1EEC2B96324F337F6428759032CAE58A04E8511BC4CBCE497EE1502F18BED5B5038E719F4F085
|
||||
1533D9B78F16803DFD8AC0C9C85ECEB6BABE9ADF83BF0B6692857BAA03843444064FCDBC89464D9E002AAFF53C65098B
|
||||
466CC36F82D1AF9BA0704789B2F8B249E79D4964E7E5CB74917760E7D30AB7FA
|
||||
E9673C0CF35137C179132DDCA57869E285FE6014BB32C5DD78E7549ED0076906
|
||||
A511A9999C777750FA348772A9E2881A75DEC94ED833AC282037B3E10913B150
|
||||
F20652B2CA0E0ADD34331D2462CD5AFACC4E01F24EF17C97CFF57089039AD7D8
|
||||
AFCB6518894626B9E8AFC7E7D9D0430D1D820969CE79B61B91E63D08D4995308D8403705AE4E25AB0F254D47FB03A1A2
|
||||
C43987548689A1DD9A65A3BE76F97A8470263ACAC0696B5882383AE97893B5D6
|
||||
7FB47AC9A16094C38D436F9932041A40F6B4644A633361B47C6F6E4A8F2E7C26
|
||||
BDD48C4689912872055999ECADCF93D152ACDEC34C6318F2FBBBA1A34C8CC166FCBA31AED11F95C973B21A646828CEEA
|
||||
1582E265DF46C8A9A019AF338104C267873FD3C2C7AF290263FEF921CC9926E76F37FA31881307C675CF37F8A5A236D7
|
||||
790D8AF6D9F5B634F953F5751378BA5412425B950C696549902EB6A9383630D0
|
||||
6F44FF0DB55651218C81B41153C9D77B1911C5BFA70650FB9326DD6B8A1479EE
|
||||
4B0891C1C24E004AB2869A858941C47346EF0789DFAEA1CD1B0E008A73AF2F7E
|
||||
7E9F990652FD6632612866B6ECA726EAE2112234EBCA59D0C1BF220B94E1F472
|
||||
9C08E9DAE977DD30E61E6634F51BFD654330C34E9FB3AA2E8856B428141D7164
|
||||
7C0E756C593FFBBE2609F63D142A0D53F99772EB0722616DFC7B203BA70AA5AE
|
||||
52C03D8984496DFF95503D843CA1980A6CA46AAE3390C6D4D38E277A0937429C185C118B98B6EBE0711F10F47F8D8F3F
|
||||
DE8A5BFD2AB351BCAD1D7817737946EF2445E3C974C9E11BF7AD058463F0E437
|
||||
D09ABB320F59A75C4B2EE8B52E45D52C46226DD8BC8339F53229578733928F1F
|
||||
C2E6F135E638C83363097EB79ED9A58E94A8E5F626E27083D1363E8BC61B55A4
|
||||
F06CC55446D0FCCCE5AD007949AE3FCA9C6FCB3C0CA1B5DE1BA65544B55326F1CF726E1F1E5CA90CA2B986930C9B9D83
|
||||
IV: 2E 2E 2E 4D 65 6F 77 20 4D 65 6F 77 20 4F 77 4F
|
||||
411F604B4F4487BEFB5285C7023822BE66D08DD0AAB0B659D58A35E68850BD85
|
||||
12BB80D35A9750D218BDE6D4BE4E5FD3B5CE20EDA464E1FA80A000D49FF567B4
|
||||
81B613E6CFD99287BCBF0957733704E61B4FFE9CBEED2DC721FBABFEB1A249CE91EEEAE285FC10E0A83A6E1BBEA7EB56
|
||||
421C18DF4D8280560EFADDBF0BB02F90942CC5B5B2986327D96F5446CC531BD0
|
||||
8BB08D9758943404ACE935B58640E78478C91140E5871D7D070B104DE470C3F2
|
||||
1E45CCF03C56DDA4403214A430330647D58D443F665794F5F04D9701C6F32AA2
|
||||
4429DDE801D7EB69ADD042A70F83AD6160E8504D6C28A9A4A3D7531140549343
|
||||
64BC31C7E98E5223E7D74B844CF4BA26594119D3E468188123D6056C52AF11E7
|
||||
F7133D37B1A05BCFB3267D7659B85598E3B2B23502C090C042F0AAEB84E4326D
|
||||
97FA5E05D70495342BACADA35918C339518DC3EC4AC94F94BC73A08BC539A80F
|
||||
45F22E4ED7559C875725F2DFCF2DFC0631DF1565A12F455F13C4A101A54E607B
|
||||
95C78F6417110955813DA8E4AFDCF118A1E19006B4D627FCD1B1688F16CE1933
|
||||
7B19D2DA8DEA82083E9F52CED668A0CD6B997AED26F40F6BED30EE7D0E3BD117
|
||||
A9B4AECEB0C40BFE38F06302769702C89CF530C16308D0DB7A74AF9B70D3034B
|
||||
D97B2D17062FD5264C88A2369ACA8BBA064B9B99D064C8831B12B1A23E51BEC1
|
||||
A26401DADE20E5D99F53703D17B6E247D169FE0073512A23F6EDB8B3FD9AA73E
|
||||
FFA8F754EC999FA789C333D7C2EEE62CE7ABEFEF9EDA191DDF9C0BEE59E30927
|
||||
82FB6C3FB0AD818834B4335855599741728466E3418EBB0C03382C01F2CFE517
|
||||
2D284BC94C8C3EAE99FB8060524540C1FA07866A648CD49B82B874DF2073691E
|
||||
5EFE887394C60DADCDBE4E25E323CAFDC8E0E5A435FE4941CE29274A319E2F4B
|
||||
8B3CD3C3310933B59C328892998E434CF34F5230B219B9F00EE888154F71D234
|
||||
FF176D5DD6D6C296498E40E30FFF2F8498760ED5609B2E3080661A29B1F3C7E7
|
||||
CFFD9A9B1210EE4B49849E00B8513400B5399BEDCC98D8C1CEFB5F062BA0F2D7
|
||||
99DACFB462669C3F2C87C09F2B3FB0AC8F9F26DEDF6D04CF0C306F7F91615FCF
|
||||
47A99EB39C8655BD8CB93D349478514E0ECA46C6463011B6B1F0A9B807DEF300
|
||||
CC2EDDB4A96311B0A3D4A55F485292CF7585E1CAE9D3D46ADBD40A3391396DFF
|
||||
82DE652C1008367C54C8C431DADC5026CCF1B300BB112D2F4B3366B319F61284
|
||||
71BBA17FDC6261801D17434538524D3B6EE42355B8ABF99C07DBD69F3C1031CE
|
||||
974DC30EDA9E1CA01C0532E82593D7D84E7FE282659EDAA8FC3474DA422D57B2
|
||||
6975A600597A196A21D8432881D95D977C168027D9FFAB273DE00A5AEE3BC5E1
|
||||
0B7EC247D17B8C80E151E9DD60B94D2D6CD4E7A03E6404FA044D6BC2A15CF214
|
||||
73D12A173C2B97A77231901E94B1C81EA1AF79BD1EA01F8F8E2A3B38177A84EC
|
||||
9D309CE4B225FC251019B1354E8AEB47EE1D6FA792FF0E14FAFB0C0944D79350
|
||||
816B8EB8CB5ADEAC80C9359F9BF538A6641A20AB8BE422AD65E50802DFF38CED
|
||||
1F41BBFFA9338DB4143D4D3CBA84637FB397EEA36D5D89D4A5D08B3FB8181540
|
||||
295359A1E73F5F94186DBFC13175A6DA08771483043408C145B0FB4A10716C86
|
||||
990E0B2ABE53757378385879D4D5BBCEF45A69F659963DB6813931973B69ABD6
|
||||
6509E69551E5D3E38349DC16CBABBD059DB80852ECA56F7902DEE11F280B14B6
|
||||
C8D22F8E7592087432BC4FF32D1810697406948F2C69C54BC5CA8F59BE65278F
|
||||
E53840FE4593AB7708C14E3E1A33D689160C018CF2CEC3A6FB309929BDA2A66F
|
||||
3CE77B617C5E133046704DAE55B5328C861CE02F223CE75E6CDFF296927D0012
|
||||
0CD25E6B29860621BED286C97675EC6515D9A93B7BCC5E8571460DF90A73F748
|
||||
C387DAC55E4B8F15BCB228CF58F857419C91BAF619D38D2700E7149BE56C9D37
|
||||
E1AEB1E36C5C0D30B858101BE459E29CE2A49470022495A8D4869D44826F7928
|
||||
173D2BB833DCB4291D25E43C9E85AA7900198BF92D6E97EE861BBF6CBE1D2147ADABA5DAEA976F6D2A2716B7E3E75DEC
|
||||
A388246FAB037912C9B1D2D25C144795B16E917BD2653693346E1A076718FC03
|
||||
4EA223311825F64F88AB509D69B708A2143A76A180ED1AB46C8F1AA1C4A9E5AF
|
||||
0CBBC26D7C458E415DCEECC9F73008B556BED1B77A3AA6BE8C557B46058180D9
|
||||
9DE0FFD924900E5B3FD4CCE2B69D7B20FF52EE1E747FFACC6A631056D7D2B4CE
|
||||
2C05D81B20DA46ED217BE759EFD95919D434FAC98B08405FB9604B61982E4141
|
||||
4EEBB881DBE7A97C9C043221B2038F8B919C144D33ACDCE0F9AB5A2D63363E07
|
||||
F19CDBD22DCC70D2A11AD7D6210FEA7601BA2093F1E9766C4F8452F1AD14046B
|
||||
AA165E848FB25FAF3AC74D1AD809BA2A45FE2E79090256BF9E77BDDCD7A4A4E2
|
||||
C63B40C559AE339DB491E1E5B39C0EB321EC3A3BD2441763A5B2DF7C2DF534F4
|
||||
DA1CEA4B84C64D744AC3BE1D29B1E752CCFB3CFAA56AEB85C1BED37293BEAF93
|
||||
C67E0C619482F59D63C6B097347A4C777EBC60D800DD586C10A0BCEC67628618
|
||||
0EDE32F4FB6ED09526381B638A7CA5869C7B20FE6E003311065398E3ED3E3474
|
||||
43714DC1C28BC0FD3DDD348E93169523D6BC4C8AC1F62545C44D405ED1CA4927
|
||||
B49A42DCCC1A76C1643AD96CAE981BE430BEC9CA372A115C02E87BECE4FFFAA6
|
||||
E66E1F05704B9BE33AC1A52D33B3CE1D373106B61C10252E56618F64FB2E665B
|
||||
A78DD00AF303F280CACF7DC5C91778096D77989AAE29EDDC3D772EA32A6A5CB1038196BB1C2394C3E09A706EB421447F
|
||||
7EC357770322CFB212E77C7A1557C46AF1048A87CB14AE0339FF18E031927DD9
|
||||
698F99BEC7BBDA2F06BA8F393235783C05F1E324CB89A2F9239A93C63A96D657
|
||||
5955B7E969ECD7E5DDAB6AC8C103CD1C03504A8DA71346452302887FD72ED9D4
|
||||
A6856ADA5AD21EAC6255C1A3166CBAFC8763DD6BB4F2AB42BCCB1B71D02B722C
|
||||
DB86290A20BD5A1CAFED7D06511096B32BAA7CAEDC49A53488542F3944B72531
|
||||
83B8E69B9A50EB7D3F51D4221C4002110AB129A5E32A5CF07763BF5A198EBC68978CCF097D4396AFD920DD5A23C4DE14
|
||||
95C4CA690A9214E66520F67150A42E5ADE143E9C8D0C5B9CFABDFC93D193FC3D
|
||||
A3B3E5E7B57F758FDB2062613DC7CF3990DAEF6F2F1EABCD169EFA85C1237F8D
|
||||
2FD52B5A55EA063680E6CD164F6CD50148E6CBF0DAD1250494CF0E536D39687D46876E1E2C0A9902F785C60273BB5026
|
||||
38F372E8F07F16FB50942FFAC6FC4D49601CF09A65C714AC0998EB1FE0DD8F5A
|
||||
94B863DA82DD1BD27D083690AB4B59D9B6236FCB1172B9480F2E11BC420FC46C3D554B78B2476C964379E028DC89BA29
|
||||
4D52AAF64319E435FDD046276964E7A819A3CCF9EEC576A0993FE6F8E29095BB
|
||||
F44CE71DE07CFAD16BEE0041832A532EA266557C640B4FAE6245252B47C6B8C05AFD9C6F305596ECF68BFE8829B9032D
|
||||
8520769CE953F5F02C132DE95141E8F78FF2C846B9F672970771BBFCB51386BF
|
||||
FAA8C71F036FD5A1BDCFCEC05E890BCCA8EB92E7AABF4C7D64D1EA3D25730DD0
|
||||
CDB89D5B8B73DB2013EDBB34D8444B37EBAA308345A9626E153FF0A9574FC4D6A2B9588BC20EC0D9B1498228F116D3F6
|
||||
13351D444888E92AD03EFFE3466EFD5719DA41FB5BA0805C09CF3E79B188223B
|
||||
B36B0913A714AB0DD7C0005426127B3A2C02DD3112E745064D7A5899DE26A128
|
||||
C3AB7BBA577D74FD581FDA6BA17C1D2E1EBD4A4AA5CB42852E59CEE91D81B3E263549755171B22A5FBF4A1156C8C0A7B
|
||||
18F579227253BAB1E3CA2E71331C6090286480D19BB1FEF242793D27BCA157D3
|
||||
BF613CE44B9BDA278D884D2F756FBE164A9736D28A52B0101611D61B643EB978
|
||||
9DE9087BD023D6F6ABDF4A345E85A1110D6B205CCC31DC7372126CCC5EC618EF
|
||||
764602226E608E5371E693185D57B7C4D6FE2CBDA379D986A251CC7286B283FEE35E07E5B32B93649D52D9CE4FFC90A7
|
||||
3AA3162A345891481F09C9231238AD644B6804643C5EED165B3B3F078E0FC216435AE2385B3AF4EB44C029D9A60FC5D9
|
||||
867DD29F637DDFBE24CA6AFDA94366D865D701CBCAA9E7074B6FB29592AE31933F230C6471C5A513D19560F5E0820BCB
|
||||
EDB4BC2F025C11311527B227E42726C3BB4A6CFF49898E2FFEB2097F0924E8EE
|
||||
D54BFE2E7C21D84AD967433C7329BBBD257A026BFC7866071C80396B45E764D6
|
||||
ED13A30EC29B22AB34D5EFE27C74C112ACD42CFFFD0695CE743FE2D0ECCCB35EF58222519E54289E7AD0624A11698986
|
||||
84AF1F78D666058274DE1896D706E6196B55B782C395216915C1023E1E44F4A2
|
||||
648F762C38B32B930029E5730609A46CF0BCB4117BEF41D36ED47BE8EB8DB2AC
|
||||
745FAFAFC79AD2DFBE3B03387EE5C3FC05EBF1A61A371E439B7FA977A1ACA3B0
|
||||
8C4FE20E0F71A9822FD32A866571D9ED6F8A1F393430AF5DB5FDBF4F7030165A
|
||||
CC54F712C0C63A6B207C9ACAD7943BDCCC0F8126449E82CA5681F88A0491625B
|
||||
9F1654268308E44DD6A9128AB1BF9B01D521D5BA9A98E5A77D35564C3678D9EAF2C8A2EC09FC3E5CD1A831FDB95486D0
|
||||
58E094157F881A156E0D4EBB79ABFED8D855A9BE1589471A8BDA6AA9F9C58F4B8BF6B326653B6CE28C45DDC3DE4CC4BE
|
||||
B3BE094F6E487FD211792E424BB5EB95FC11AC78832FE9EAFDB076867A9B4AA2
|
||||
9FC6BE151041C150003E5BA940ADABDD02A080A9F4EE61215A6162BB1C13B7D9
|
||||
7CFE8E8FE7E2218425F581F70AC6E043E9253774A80450113C836EBAF4EAF4A0
|
||||
8610A9694E586306A4078E5DD3C757870C21130D6F67593994568FA1E7B7F4CA
|
||||
774DDBA8E8AA78BFF82B1E9C8CC58873C73AD0B977ED4EDD465513F9D238AA7DB2BE64536F4950BDAA00F971551F0922
|
||||
DB103B1AB06E9115193F5DC6F0C2D806AE9518C91D7D72945B27BE86B2D16351
|
||||
86C4A3F2ACBFA942CF4FF3DC062A4786D9D005C05671E232D93DEBCF5C1D7B96
|
||||
909EB49451C1D7A1B6E01FFC55EED8DA3C5DB4AFEA5C347DD4E79642746C963E64113CEDECF951947C095B12390BA10F
|
||||
AD7CFDF6D5C93806F8E5321EC2B4E2C6C7B750874E046154EC76EC2678B6F0C938D2D99A6646663ABC730190C7108C1F
|
||||
F3DC34F33197A7D2DCBA4D25ACC6EAA0612723C92BFF226E7B3A73BD07B2EBEA
|
||||
B7074A0B79FE44FA94C3EC55C2C694ADD593C021DED43085CB5D69DBA7F6F6F1
|
||||
9C7B26FC47E16E41E8F1F560EBEE3B580E5F44C1A2DA0BA415C279530416F231
|
||||
EBED9733677546781E77DC9E6668AE3CA6D1B6C0E1446EE10EBEC8F8ABF5228C
|
||||
D21EB73FE8C693DD4C580A51DB46D5DF26D798A8A67095FF087B8F14907993C2
|
||||
CB9D919298CB3EF9458A962BDD79736F6FC00E209B389C0543D06687AB16ADE0
|
||||
EED58F3D2003F1F288718544B95F8F7728AB6D0A7D10E3BEFED3C7F4A55A6B7CCBF8EACE17F0FADCDA6B19DBA74A2324
|
||||
D4E83675B9B5BB81D1E5C17CB88225C514C6DD2852D22A8C4681F0683248B21E
|
||||
0949495177B2C21F42679921EA4C83CD7A9700FB34B13DDF3E61CE26162DE0AF
|
||||
2C38210508654DE730157BA138118793DDA2BCD7958EDD21B02796B689D0990C
|
||||
47C78252386879DB6F9F3516FBCA29BAD3EB5F837B881422D7D421A41CCB851EFEA209D371E6E81EF957441D31D71842
|
||||
|
||||
@@ -24,9 +24,9 @@ void subghz_scene_set_button_on_enter(void* context) {
|
||||
byte_ptr = &subghz->gen_info->keeloq.btn;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq.btn);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
byte_ptr = &subghz->gen_info->keeloq_bft.btn;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_bft.btn);
|
||||
case GenKeeloqSeed:
|
||||
byte_ptr = &subghz->gen_info->keeloq_seed.btn;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_seed.btn);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
byte_ptr = &subghz->gen_info->alutech_at_4n.btn;
|
||||
@@ -99,7 +99,7 @@ bool subghz_scene_set_button_on_event(void* context, SceneManagerEvent event) {
|
||||
switch(subghz->gen_info->type) {
|
||||
case GenFaacSLH:
|
||||
case GenKeeloq:
|
||||
case GenKeeloqBFT:
|
||||
case GenKeeloqSeed:
|
||||
case GenAlutechAt4n:
|
||||
case GenSomfyTelis:
|
||||
case GenKingGatesStylo4k:
|
||||
|
||||
@@ -30,9 +30,9 @@ void subghz_scene_set_counter_on_enter(void* context) {
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.cnt;
|
||||
byte_count = sizeof(subghz->gen_info->came_atomo.cnt);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.cnt;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_bft.cnt);
|
||||
case GenKeeloqSeed:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.cnt;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_seed.cnt);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.cnt;
|
||||
@@ -123,8 +123,8 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
|
||||
case GenCameAtomo:
|
||||
subghz->gen_info->came_atomo.cnt = __bswap16(subghz->gen_info->came_atomo.cnt);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
subghz->gen_info->keeloq_bft.cnt = __bswap16(subghz->gen_info->keeloq_bft.cnt);
|
||||
case GenKeeloqSeed:
|
||||
subghz->gen_info->keeloq_seed.cnt = __bswap16(subghz->gen_info->keeloq_seed.cnt);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
subghz->gen_info->alutech_at_4n.cnt =
|
||||
@@ -168,7 +168,7 @@ bool subghz_scene_set_counter_on_event(void* context, SceneManagerEvent event) {
|
||||
|
||||
switch(subghz->gen_info->type) {
|
||||
case GenFaacSLH:
|
||||
case GenKeeloqBFT:
|
||||
case GenKeeloqSeed:
|
||||
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSetSeed);
|
||||
return true;
|
||||
case GenKeeloq:
|
||||
|
||||
@@ -22,9 +22,9 @@ void subghz_scene_set_seed_on_enter(void* context) {
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->faac_slh.seed;
|
||||
byte_count = sizeof(subghz->gen_info->faac_slh.seed);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.seed;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_bft.seed);
|
||||
case GenKeeloqSeed:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.seed;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_seed.seed);
|
||||
break;
|
||||
// Not needed for these types
|
||||
case GenKeeloq:
|
||||
@@ -78,17 +78,17 @@ bool subghz_scene_set_seed_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->gen_info->faac_slh.seed,
|
||||
subghz->gen_info->faac_slh.manuf);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
subghz->gen_info->keeloq_bft.seed = __bswap32(subghz->gen_info->keeloq_bft.seed);
|
||||
generated_protocol = subghz_txrx_gen_keeloq_bft_protocol(
|
||||
case GenKeeloqSeed:
|
||||
subghz->gen_info->keeloq_seed.seed = __bswap32(subghz->gen_info->keeloq_seed.seed);
|
||||
generated_protocol = subghz_txrx_gen_keeloq_seed_protocol(
|
||||
subghz->txrx,
|
||||
subghz->gen_info->mod,
|
||||
subghz->gen_info->freq,
|
||||
subghz->gen_info->keeloq_bft.serial,
|
||||
subghz->gen_info->keeloq_bft.btn,
|
||||
subghz->gen_info->keeloq_bft.cnt,
|
||||
subghz->gen_info->keeloq_bft.seed,
|
||||
subghz->gen_info->keeloq_bft.manuf);
|
||||
subghz->gen_info->keeloq_seed.serial,
|
||||
subghz->gen_info->keeloq_seed.btn,
|
||||
subghz->gen_info->keeloq_seed.cnt,
|
||||
subghz->gen_info->keeloq_seed.seed,
|
||||
subghz->gen_info->keeloq_seed.manuf);
|
||||
break;
|
||||
// Not needed for these types
|
||||
case GenKeeloq:
|
||||
|
||||
@@ -30,9 +30,9 @@ void subghz_scene_set_serial_on_enter(void* context) {
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->came_atomo.serial;
|
||||
byte_count = sizeof(subghz->gen_info->came_atomo.serial);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_bft.serial;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_bft.serial);
|
||||
case GenKeeloqSeed:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->keeloq_seed.serial;
|
||||
byte_count = sizeof(subghz->gen_info->keeloq_seed.serial);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
byte_ptr = (uint8_t*)&subghz->gen_info->alutech_at_4n.serial;
|
||||
@@ -118,9 +118,9 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) {
|
||||
subghz->gen_info->came_atomo.serial =
|
||||
__bswap32(subghz->gen_info->came_atomo.serial);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
subghz->gen_info->keeloq_bft.serial =
|
||||
__bswap32(subghz->gen_info->keeloq_bft.serial);
|
||||
case GenKeeloqSeed:
|
||||
subghz->gen_info->keeloq_seed.serial =
|
||||
__bswap32(subghz->gen_info->keeloq_seed.serial);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
subghz->gen_info->alutech_at_4n.serial =
|
||||
@@ -172,7 +172,7 @@ bool subghz_scene_set_serial_on_event(void* context, SceneManagerEvent event) {
|
||||
switch(subghz->gen_info->type) {
|
||||
case GenFaacSLH:
|
||||
case GenKeeloq:
|
||||
case GenKeeloqBFT:
|
||||
case GenKeeloqSeed:
|
||||
case GenAlutechAt4n:
|
||||
case GenSomfyTelis:
|
||||
case GenSomfyKeytis:
|
||||
|
||||
@@ -15,6 +15,7 @@ static const char* submenu_names[SetTypeMAX] = {
|
||||
[SetTypeFaacSLH_868] = "FAAC SLH 868MHz",
|
||||
[SetTypeFaacSLH_433] = "FAAC SLH 433MHz",
|
||||
[SetTypeBFTMitto] = "BFT Mitto 433MHz",
|
||||
[SetTypeErreka433] = "Erreka 433MHz",
|
||||
[SetTypeSomfyTelis] = "Somfy Telis 433MHz",
|
||||
[SetTypeSomfyKeytis] = "Somfy Keytis 433MHz",
|
||||
[SetTypeANMotorsAT4] = "AN-Motors AT4 433MHz",
|
||||
@@ -56,9 +57,22 @@ static const char* submenu_names[SetTypeMAX] = {
|
||||
[SetTypeNovoferm_433_92] = "KL: Novoferm 433MHz",
|
||||
[SetTypeHormannEcoStar_433_92] = "KL: Hor. EcoStar 433MHz",
|
||||
[SetTypeCardinS449_433FM] = "KL: Cardin S449 433MHz",
|
||||
[SetTypePujol433] = "KL: Pujol 433MHz",
|
||||
[SetTypePujol_Vario433] = "KL: Pujol Vario 433MHz",
|
||||
[SetTypeET_Blue433] = "KL: ET Blue 433MHz",
|
||||
[SetTypeET_Blue_Mix433] = "KL: ET Blue Mix 433MHz",
|
||||
[SetTypeATA_PTX4_433] = "KL: ATA PTX4 433MHz",
|
||||
[SetTypeSeav433] = "KL: Seav 433MHz",
|
||||
[SetTypeWisniowski433] = "KL: Wisniowski 433MHz",
|
||||
[SetTypeFadini433] = "KL: Fadini 433MHz",
|
||||
[SetTypeMc_Garcia433] = "KL: Mc Garcia 433MHz",
|
||||
[SetTypeClemsa_Mutancode433] = "KL: Clm.Mutancode 433M.",
|
||||
[SetTypeDoormatic433] = "KL: Doormatic 433MHz",
|
||||
[SetTypeElvox433] = "KL: Elvox 433MHz",
|
||||
[SetTypeVerex433] = "KL: Verex 433MHz",
|
||||
[SetTypeFAACRCXT_433_92] = "KL: FAAC RC,XT 433MHz",
|
||||
[SetTypeFAACRCXT_868] = "KL: FAAC RC,XT 868MHz",
|
||||
[SetTypeGeniusBravo433] = "KL: Genius TX4RC 433MHz",
|
||||
[SetTypeGeniusBravo433] = "KL: Genius TX4RC 433M.",
|
||||
[SetTypeNiceMHouse_433_92] = "KL: Mhouse 433MHz",
|
||||
[SetTypeNiceSmilo_433_92] = "KL: Nice Smilo 433MHz",
|
||||
[SetTypeNiceFlorS_433_92] = "Nice FloR-S 433MHz",
|
||||
@@ -163,16 +177,16 @@ bool subghz_scene_set_type_generate_protocol_from_infos(SubGhz* subghz) {
|
||||
gen_info.came_atomo.serial,
|
||||
gen_info.came_atomo.cnt);
|
||||
break;
|
||||
case GenKeeloqBFT:
|
||||
generated_protocol = subghz_txrx_gen_keeloq_bft_protocol(
|
||||
case GenKeeloqSeed:
|
||||
generated_protocol = subghz_txrx_gen_keeloq_seed_protocol(
|
||||
subghz->txrx,
|
||||
gen_info.mod,
|
||||
gen_info.freq,
|
||||
gen_info.keeloq_bft.serial,
|
||||
gen_info.keeloq_bft.btn,
|
||||
gen_info.keeloq_bft.cnt,
|
||||
gen_info.keeloq_bft.seed,
|
||||
gen_info.keeloq_bft.manuf);
|
||||
gen_info.keeloq_seed.serial,
|
||||
gen_info.keeloq_seed.btn,
|
||||
gen_info.keeloq_seed.cnt,
|
||||
gen_info.keeloq_seed.seed,
|
||||
gen_info.keeloq_seed.manuf);
|
||||
break;
|
||||
case GenAlutechAt4n:
|
||||
generated_protocol = subghz_txrx_gen_alutech_at_4n_protocol(
|
||||
@@ -313,7 +327,7 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) {
|
||||
case GenFaacSLH: // Serial (u32), Button (u8), Counter (u32), Seed (u32)
|
||||
case GenKeeloq: // Serial (u32), Button (u8), Counter (u16)
|
||||
case GenCameAtomo: // Serial (u32), Counter (u16)
|
||||
case GenKeeloqBFT: // Serial (u32), Button (u8), Counter (u16), Seed (u32)
|
||||
case GenKeeloqSeed: // Serial (u32), Button (u8), Counter (u16), Seed (u32)
|
||||
case GenAlutechAt4n: // Serial (u32), Button (u8), Counter (u16)
|
||||
case GenSomfyTelis: // Serial (u32), Button (u8), Counter (u16)
|
||||
case GenSomfyKeytis: // Serial (u32), Button (u8), Counter (u16)
|
||||
|
||||
@@ -312,13 +312,6 @@ bool subghz_history_add_to_history(
|
||||
break;
|
||||
}
|
||||
furi_string_cat(instance->tmp_string, text);
|
||||
} else if(!strcmp(furi_string_get_cstr(instance->tmp_string), "Star Line")) {
|
||||
furi_string_set(instance->tmp_string, "SL ");
|
||||
if(!flipper_format_read_string(item->flipper_string, "Manufacture", text)) {
|
||||
FURI_LOG_E(TAG, "Missing Protocol");
|
||||
break;
|
||||
}
|
||||
furi_string_cat(instance->tmp_string, text);
|
||||
}
|
||||
if(!flipper_format_rewind(item->flipper_string)) {
|
||||
FURI_LOG_E(TAG, "Rewind error");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "canvas_i.h"
|
||||
#include "canvas.h"
|
||||
#include "icon_animation_i.h"
|
||||
|
||||
#include <furi.h>
|
||||
|
||||
@@ -85,6 +85,22 @@ typedef enum {
|
||||
/** Canvas anonymous structure */
|
||||
typedef struct Canvas Canvas;
|
||||
|
||||
/** Get canvas buffer.
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
*
|
||||
* @return pointer to buffer
|
||||
*/
|
||||
uint8_t* canvas_get_buffer(Canvas* canvas);
|
||||
|
||||
/** Get canvas buffer size.
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
*
|
||||
* @return size of canvas in bytes
|
||||
*/
|
||||
size_t canvas_get_buffer_size(const Canvas* canvas);
|
||||
|
||||
/** Reset canvas drawing tools configuration
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
|
||||
@@ -61,22 +61,6 @@ Canvas* canvas_init(void);
|
||||
*/
|
||||
void canvas_free(Canvas* canvas);
|
||||
|
||||
/** Get canvas buffer.
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
*
|
||||
* @return pointer to buffer
|
||||
*/
|
||||
uint8_t* canvas_get_buffer(Canvas* canvas);
|
||||
|
||||
/** Get canvas buffer size.
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
*
|
||||
* @return size of canvas in bytes
|
||||
*/
|
||||
size_t canvas_get_buffer_size(const Canvas* canvas);
|
||||
|
||||
/** Set drawing region relative to real screen buffer
|
||||
*
|
||||
* @param canvas Canvas instance
|
||||
|
||||
@@ -20,44 +20,15 @@ static const NotificationSequence sequence_note_c = {
|
||||
NULL,
|
||||
};
|
||||
|
||||
#define CONTRAST_COUNT 17
|
||||
#define CONTRAST_COUNT 29
|
||||
const char* const contrast_text[CONTRAST_COUNT] = {
|
||||
"-8",
|
||||
"-7",
|
||||
"-6",
|
||||
"-5",
|
||||
"-4",
|
||||
"-3",
|
||||
"-2",
|
||||
"-1",
|
||||
"0",
|
||||
"+1",
|
||||
"+2",
|
||||
"+3",
|
||||
"+4",
|
||||
"+5",
|
||||
"+6",
|
||||
"+7",
|
||||
"+8",
|
||||
"-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1",
|
||||
"0", "+1", "+2", "+3", "+4", "+5", "+6", "+7", "+8", "+9",
|
||||
"+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18",
|
||||
};
|
||||
const int32_t contrast_value[CONTRAST_COUNT] = {
|
||||
-8,
|
||||
-7,
|
||||
-6,
|
||||
-5,
|
||||
-4,
|
||||
-3,
|
||||
-2,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4,
|
||||
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
|
||||
};
|
||||
|
||||
#define BACKLIGHT_COUNT 21
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 335 B |
@@ -1,19 +1,60 @@
|
||||
#include "hid_ptt.h"
|
||||
#include "hid_ptt_menu.h"
|
||||
#include <gui/elements.h>
|
||||
#include <locale/locale.h>
|
||||
#include <notification/notification_messages.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <furi_hal_power.h>
|
||||
#include <furi_hal_rtc.h>
|
||||
#include "../hid.h"
|
||||
#include "../views.h"
|
||||
|
||||
#include "hid_icons.h"
|
||||
|
||||
#define TAG "HidPushToTalk"
|
||||
#define TAG "HidPushToTalk"
|
||||
#define HID_PTT_LEFT_HOLD_ANIM_STEP_MS 100U
|
||||
#define HID_PTT_LEFT_HOLD_ANIM_STEPS 5U
|
||||
|
||||
// Exact home status-bar Bluetooth icon pixels (from assets/icons/StatusBar).
|
||||
// Bitmap format for canvas_draw_bitmap(): row-major, 1-bit.
|
||||
// Full compressed frame data including leading 0x00 heatshrink marker,
|
||||
// copied verbatim from build/f7-firmware-D/assets/compiled/assets_icons.c
|
||||
static const uint8_t hid_ptt_bluetooth_connected_16x8_bits[] = {
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x16,
|
||||
0x60,
|
||||
0x4c,
|
||||
0x97,
|
||||
0x4c,
|
||||
0x97,
|
||||
0x16,
|
||||
0x60,
|
||||
0x0d,
|
||||
0x00,
|
||||
0x04,
|
||||
0x00,
|
||||
};
|
||||
static const uint8_t hid_ptt_bluetooth_idle_5x8_bits[] = {
|
||||
0x00,
|
||||
0x04,
|
||||
0x0d,
|
||||
0x16,
|
||||
0x0c,
|
||||
0x0c,
|
||||
0x16,
|
||||
0x0d,
|
||||
0x04,
|
||||
};
|
||||
|
||||
struct HidPushToTalk {
|
||||
View* view;
|
||||
Hid* hid;
|
||||
Widget* help;
|
||||
FuriTimer* left_hold_timer;
|
||||
};
|
||||
|
||||
typedef void (*PushToTalkActionCallback)(HidPushToTalk* hid_ptt);
|
||||
@@ -32,6 +73,7 @@ typedef struct {
|
||||
size_t osIndex;
|
||||
size_t appIndex;
|
||||
size_t window_position;
|
||||
uint8_t left_hold_progress;
|
||||
PushToTalkActionCallback callback_trigger_mute;
|
||||
PushToTalkActionCallback callback_trigger_camera;
|
||||
PushToTalkActionCallback callback_trigger_hand;
|
||||
@@ -59,6 +101,66 @@ enum HidPushToTalkAppIndex {
|
||||
HidPushToTalkAppIndexSize,
|
||||
};
|
||||
|
||||
static bool hid_ptt_is_zoom_app(size_t app_index) {
|
||||
return (app_index == HidPushToTalkAppIndexZoom) ||
|
||||
(app_index == HidPushToTalkAppIndexZoomGlobal);
|
||||
}
|
||||
|
||||
static void hid_ptt_left_hold_timer_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalk* hid_ptt = context;
|
||||
with_view_model(
|
||||
hid_ptt->view,
|
||||
HidPushToTalkModel * model,
|
||||
{
|
||||
if(model->left_pressed && hid_ptt_is_zoom_app(model->appIndex)) {
|
||||
if(model->left_hold_progress < HID_PTT_LEFT_HOLD_ANIM_STEPS) {
|
||||
model->left_hold_progress++;
|
||||
}
|
||||
} else {
|
||||
model->left_hold_progress = 0;
|
||||
furi_timer_stop(hid_ptt->left_hold_timer);
|
||||
}
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
static void hid_ptt_draw_zoom_enter_hint(
|
||||
Canvas* canvas,
|
||||
uint8_t x,
|
||||
uint8_t y,
|
||||
uint8_t progress,
|
||||
bool pressed) {
|
||||
const uint8_t width = 18;
|
||||
const uint8_t height = 10;
|
||||
const uint8_t inner_x = x + 1;
|
||||
const uint8_t inner_y = y + 1;
|
||||
const uint8_t inner_w = width - 2;
|
||||
const uint8_t inner_h = height - 1;
|
||||
|
||||
if(progress > 0) {
|
||||
const uint8_t fill_w = (inner_w * progress) / HID_PTT_LEFT_HOLD_ANIM_STEPS;
|
||||
if(fill_w > 0) {
|
||||
canvas_draw_box(canvas, inner_x, inner_y, fill_w, inner_h - 1);
|
||||
}
|
||||
}
|
||||
|
||||
for(uint8_t dot_x = x; dot_x < x + width; dot_x += 2) {
|
||||
canvas_draw_dot(canvas, dot_x, y + height - 1);
|
||||
}
|
||||
for(uint8_t dot_y = y + 1; dot_y < y + height; dot_y += 2) {
|
||||
canvas_draw_dot(canvas, x, dot_y);
|
||||
canvas_draw_dot(canvas, x + width - 1, dot_y);
|
||||
}
|
||||
|
||||
UNUSED(pressed);
|
||||
if(progress > 0) {
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
}
|
||||
canvas_draw_icon(canvas, x + 4, y + 1, &I_Enter_11x7);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
}
|
||||
|
||||
// meet, zoom
|
||||
static void hid_ptt_start_ptt_meet_zoom(HidPushToTalk* hid_ptt) {
|
||||
hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_SPACEBAR);
|
||||
@@ -128,9 +230,9 @@ static void hid_ptt_trigger_hand_zoom(HidPushToTalk* hid_ptt) {
|
||||
// zoom global macos
|
||||
static void hid_ptt_trigger_mute_macos_zoom_global(HidPushToTalk* hid_ptt) {
|
||||
hid_hal_keyboard_press(
|
||||
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M);
|
||||
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N);
|
||||
hid_hal_keyboard_release(
|
||||
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_M);
|
||||
hid_ptt->hid, KEY_MOD_LEFT_GUI | KEY_MOD_RIGHT_ALT | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_N);
|
||||
}
|
||||
|
||||
static void hid_ptt_trigger_camera_macos_zoom_global(HidPushToTalk* hid_ptt) {
|
||||
@@ -387,6 +489,97 @@ static void hid_ptt_trigger_mute_linux_gather(HidPushToTalk* hid_ptt) {
|
||||
hid_ptt->hid, KEY_MOD_LEFT_CTRL | KEY_MOD_LEFT_SHIFT | HID_KEYBOARD_A);
|
||||
}
|
||||
|
||||
static void hid_ptt_populate_help(HidPushToTalk* hid_ptt, uint32_t appIndex) {
|
||||
widget_reset(hid_ptt->help);
|
||||
char* app_specific_help = "";
|
||||
switch(appIndex) {
|
||||
case HidPushToTalkAppIndexGoogleMeet:
|
||||
app_specific_help =
|
||||
"Google Meet:\n"
|
||||
"This feature is off by default in your audio settings "
|
||||
"and may not work for Windows users who use their screen "
|
||||
"reader. In this situation, the spacebar performs a different action.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexGoogleMeetGlobal:
|
||||
app_specific_help = "Google Meet (Global):\n"
|
||||
"1. Install \"Google Meet - Global Shortcuts\" extension.\n"
|
||||
"2. Open chrome://extensions/shortcuts.\n"
|
||||
"3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n"
|
||||
"4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n"
|
||||
"5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexDiscord:
|
||||
app_specific_help = "Discord:\n"
|
||||
"1. Under App Settings, click Voice & Video. Under Input Mode, "
|
||||
"check the box next to Push to Talk.\n"
|
||||
"2. Scroll down to SHORTCUT, click Record Keybinder.\n"
|
||||
"3. Press PTT in the app to bind it."
|
||||
"4. Go to Keybinds and assign mute button.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexTeamSpeak:
|
||||
app_specific_help = "TeamSpeak:\n"
|
||||
"To make keys working bind them in TeamSpeak settings.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexTeams:
|
||||
app_specific_help =
|
||||
"Teams:\n"
|
||||
"Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexZoomGlobal:
|
||||
app_specific_help = "Zoom (Global):\n"
|
||||
"1. Go to Settings > Keyboard Shortcuts.\n"
|
||||
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
|
||||
"3. Press the Mute button in the app to bind it.\n"
|
||||
"4. Check global checkbox.\n"
|
||||
"5. Repeat for video and hand shortcuts.\n"
|
||||
"6. Long-press < to send Enter key.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexZoom:
|
||||
app_specific_help = "Zoom:\n"
|
||||
"1. Go to Settings > Keyboard Shortcuts.\n"
|
||||
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
|
||||
"3. Press the Mute button in the app to bind it.\n"
|
||||
"4. Repeat for video and hand shortcuts.\n"
|
||||
"5. Long-press < to send Enter key.\n\n";
|
||||
break;
|
||||
}
|
||||
char* left_button_help = "";
|
||||
if(appIndex == HidPushToTalkAppIndexZoom || appIndex == HidPushToTalkAppIndexZoomGlobal) {
|
||||
left_button_help = "Long-press < sends Enter.\n";
|
||||
}
|
||||
FuriString* msg = furi_string_alloc();
|
||||
furi_string_cat_printf(
|
||||
msg,
|
||||
"%sGeneral:\n"
|
||||
"To operate properly flipper microphone "
|
||||
"status must be in sync with your computer.\n"
|
||||
"Hold > to change mic status.\n"
|
||||
"%s"
|
||||
"Long-press OK in menu to open this help.\n"
|
||||
"Press BACK to switch mic on/off.\n"
|
||||
"Hold 'o' for PTT mode (mic will be off once you release 'o')\n"
|
||||
"Hold BACK to exit.",
|
||||
app_specific_help,
|
||||
left_button_help);
|
||||
widget_add_text_scroll_element(hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg));
|
||||
furi_string_free(msg);
|
||||
}
|
||||
|
||||
static void hid_ptt_menu_help_callback(
|
||||
void* context,
|
||||
uint32_t osIndex,
|
||||
FuriString* osLabel,
|
||||
uint32_t appIndex,
|
||||
FuriString* appLabel) {
|
||||
UNUSED(osIndex);
|
||||
UNUSED(osLabel);
|
||||
UNUSED(appLabel);
|
||||
furi_assert(context);
|
||||
HidPushToTalk* hid_ptt = context;
|
||||
hid_ptt_populate_help(hid_ptt, appIndex);
|
||||
view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp);
|
||||
}
|
||||
|
||||
static void hid_ptt_menu_callback(
|
||||
void* context,
|
||||
uint32_t osIndex,
|
||||
@@ -593,68 +786,9 @@ static void hid_ptt_menu_callback(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char* app_specific_help = "";
|
||||
switch(appIndex) {
|
||||
case HidPushToTalkAppIndexGoogleMeet:
|
||||
app_specific_help =
|
||||
"Google Meet:\n"
|
||||
"This feature is off by default in your audio settings "
|
||||
"and may not work for Windows users who use their screen "
|
||||
"reader. In this situation, the spacebar performs a different action.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexGoogleMeetGlobal:
|
||||
app_specific_help = "Google Meet (Global):\n"
|
||||
"1. Install \"Google Meet - Global Shortcuts\" extension.\n"
|
||||
"2. Open chrome://extensions/shortcuts.\n"
|
||||
"3. Set 'Toggle microphone' to Cmd+Ctrl+7 and enable Global.\n"
|
||||
"4. Set 'Toggle camera' to Cmd+Ctrl+8 and enable Global.\n"
|
||||
"5. Set 'Raise hand' to Cmd+Ctrl+9 and enable Global.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexDiscord:
|
||||
app_specific_help =
|
||||
"Discord:\n"
|
||||
"1. Under App Settings, click Voice & Video. Under Input Mode, "
|
||||
"check the box next to Push to Talk.\n"
|
||||
"2. Scroll down to SHORTCUT, click Record Keybinder.\n"
|
||||
"3. Press PTT in the app to bind it."
|
||||
"4. Go to Keybinds and assign mute button.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexTeamSpeak:
|
||||
app_specific_help = "TeamSpeak:\n"
|
||||
"To make keys working bind them in TeamSpeak settings.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexTeams:
|
||||
app_specific_help =
|
||||
"Teams:\n"
|
||||
"Go to Settings > Privacy. Make sure Keyboard shortcut to unmute is toggled on.\n\n";
|
||||
break;
|
||||
case HidPushToTalkAppIndexZoomGlobal:
|
||||
app_specific_help = "Zoom (Global):\n"
|
||||
"1. Go to Settings > Keyboard Shortcuts.\n"
|
||||
"2. Find the 'Mute/Unmute' shortcut and click 'Edit'.\n"
|
||||
"3. Press the Mute button in the app to bind it.\n"
|
||||
"4. Check global checkbox.\n"
|
||||
"5. Repeat for video and hand shortcuts.\n\n";
|
||||
}
|
||||
|
||||
FuriString* msg = furi_string_alloc();
|
||||
furi_string_cat_printf(
|
||||
msg,
|
||||
"%sGeneral:\n"
|
||||
"To operate properly flipper microphone "
|
||||
"status must be in sync with your computer.\n"
|
||||
"Hold > to change mic status.\n"
|
||||
"Hold < to open this help.\n"
|
||||
"Press BACK to switch mic on/off.\n"
|
||||
"Hold 'o' for PTT mode (mic will be off once you release 'o')\n"
|
||||
"Hold BACK to exit.",
|
||||
app_specific_help);
|
||||
widget_add_text_scroll_element(
|
||||
hid_ptt->help, 0, 0, 128, 64, furi_string_get_cstr(msg));
|
||||
furi_string_free(msg);
|
||||
},
|
||||
true);
|
||||
hid_ptt_populate_help(hid_ptt, appIndex);
|
||||
view_dispatcher_switch_to_view(hid_ptt->hid->view_dispatcher, HidViewPushToTalk);
|
||||
}
|
||||
|
||||
@@ -673,43 +807,148 @@ static void hid_ptt_draw_text_centered(Canvas* canvas, uint8_t y, FuriString* st
|
||||
furi_string_free(disp_str);
|
||||
}
|
||||
|
||||
static void hid_ptt_draw_app_label(Canvas* canvas, uint8_t first_line_y, FuriString* app) {
|
||||
FuriString* first_line = furi_string_alloc_set(app);
|
||||
if(canvas_string_width(canvas, furi_string_get_cstr(first_line)) <= canvas_width(canvas)) {
|
||||
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
|
||||
furi_string_free(first_line);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* app_cstr = furi_string_get_cstr(app);
|
||||
const char* split = strrchr(app_cstr, ' ');
|
||||
if(!split) {
|
||||
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
|
||||
furi_string_free(first_line);
|
||||
return;
|
||||
}
|
||||
|
||||
FuriString* second_line = furi_string_alloc();
|
||||
furi_string_set_strn(first_line, app_cstr, split - app_cstr);
|
||||
furi_string_set_str(second_line, split + 1);
|
||||
|
||||
elements_string_fit_width(canvas, first_line, canvas_width(canvas));
|
||||
elements_string_fit_width(canvas, second_line, canvas_width(canvas));
|
||||
hid_ptt_draw_text_centered(canvas, first_line_y, first_line);
|
||||
hid_ptt_draw_text_centered(canvas, first_line_y + 10, second_line);
|
||||
|
||||
furi_string_free(second_line);
|
||||
furi_string_free(first_line);
|
||||
}
|
||||
|
||||
static void hid_ptt_draw_status_bar(Canvas* canvas, bool show_bt, bool connected) {
|
||||
char time_str[16];
|
||||
DateTime dt;
|
||||
furi_hal_rtc_get_datetime(&dt);
|
||||
|
||||
uint8_t hour = dt.hour;
|
||||
if(locale_get_time_format() == LocaleTimeFormat12h) {
|
||||
if(hour > 12) {
|
||||
hour -= 12;
|
||||
}
|
||||
if(hour == 0) {
|
||||
hour = 12;
|
||||
}
|
||||
}
|
||||
snprintf(time_str, sizeof(time_str), "%02u:%02u", hour, dt.minute);
|
||||
|
||||
uint8_t battery = furi_hal_power_get_pct();
|
||||
if(battery > 100) {
|
||||
battery = 100;
|
||||
}
|
||||
|
||||
canvas_set_color(canvas, ColorWhite);
|
||||
canvas_draw_box(canvas, 0, 0, 64, 13);
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
canvas_draw_rframe(canvas, 0, 0, 64, 13, 1);
|
||||
canvas_draw_line(canvas, 1, 11, 62, 11);
|
||||
|
||||
if(show_bt) {
|
||||
if(connected) {
|
||||
canvas_draw_bitmap(canvas, 2, 2, 16, 8, hid_ptt_bluetooth_connected_16x8_bits);
|
||||
} else {
|
||||
canvas_draw_bitmap(canvas, 2, 2, 5, 8, hid_ptt_bluetooth_idle_5x8_bits);
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t battery_x = 48;
|
||||
const uint8_t battery_y = 3;
|
||||
const uint8_t battery_w = 13;
|
||||
const uint8_t battery_h = 6;
|
||||
canvas_draw_frame(canvas, battery_x, battery_y, battery_w, battery_h);
|
||||
canvas_draw_box(canvas, battery_x + battery_w, battery_y + 2, 1, 2);
|
||||
canvas_draw_box(canvas, battery_x + 1, battery_y + 1, ((battery_w - 2) * battery) / 100, 4);
|
||||
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
canvas_draw_str_aligned(canvas, 33, 10, AlignCenter, AlignBottom, time_str);
|
||||
}
|
||||
|
||||
static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalkModel* model = context;
|
||||
|
||||
// Header
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
#ifdef HID_TRANSPORT_BLE
|
||||
if(model->connected) {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_connected_15x15);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, 0, 0, &I_Ble_disconnected_15x15);
|
||||
const bool app_label_needs_two_lines =
|
||||
canvas_string_width(canvas, furi_string_get_cstr(model->app)) > canvas_width(canvas);
|
||||
|
||||
const uint8_t top_offset = 13;
|
||||
const uint8_t status_bar_bottom_y = top_offset;
|
||||
|
||||
// For Zoom/Zoom Global, keep helper banner higher to show Enter key hint space
|
||||
// For other apps, move it down to close the gap
|
||||
uint8_t helper_top_y = 102;
|
||||
if((model->appIndex == HidPushToTalkAppIndexZoom ||
|
||||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) &&
|
||||
!app_label_needs_two_lines) {
|
||||
helper_top_y = 92;
|
||||
}
|
||||
#endif
|
||||
|
||||
// OS and App labels
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
hid_ptt_draw_text_centered(canvas, 73, model->app);
|
||||
hid_ptt_draw_text_centered(canvas, 84, model->os);
|
||||
|
||||
// Help label
|
||||
canvas_draw_icon(canvas, 0, 88, &I_Help_top_64x17);
|
||||
canvas_draw_line(canvas, 4, 105, 4, 114);
|
||||
canvas_draw_line(canvas, 63, 105, 63, 114);
|
||||
canvas_draw_icon(canvas, 7, 107, &I_Hold_15x5);
|
||||
canvas_draw_icon(canvas, 24, 105, &I_BtnLeft_9x9);
|
||||
canvas_draw_icon(canvas, 34, 108, &I_for_help_27x5);
|
||||
canvas_draw_icon(canvas, 0, 115, &I_Help_exit_64x9);
|
||||
canvas_draw_icon(canvas, 24, 115, &I_BtnBackV_9x9);
|
||||
|
||||
const uint8_t x_1 = 0;
|
||||
const uint8_t x_2 = x_1 + 19 + 4;
|
||||
const uint8_t x_3 = x_1 + 19 * 2 + 8;
|
||||
|
||||
const uint8_t y_1 = 3;
|
||||
const uint8_t y_1 = top_offset + 3;
|
||||
const uint8_t y_2 = y_1 + 19;
|
||||
const uint8_t y_3 = y_2 + 19;
|
||||
|
||||
const uint8_t controls_bottom_y = y_3 + 18;
|
||||
const uint8_t labels_center_y = (controls_bottom_y + helper_top_y) / 2;
|
||||
const uint8_t app_label_y = app_label_needs_two_lines ? (controls_bottom_y + 10) :
|
||||
(labels_center_y - 1);
|
||||
const uint8_t os_label_y = app_label_needs_two_lines ? (app_label_y + 20) : (app_label_y + 11);
|
||||
|
||||
// Header
|
||||
canvas_set_font(canvas, FontPrimary);
|
||||
#ifdef HID_TRANSPORT_BLE
|
||||
hid_ptt_draw_status_bar(canvas, true, model->connected);
|
||||
#else
|
||||
hid_ptt_draw_status_bar(canvas, false, false);
|
||||
#endif
|
||||
|
||||
// OS and App labels
|
||||
canvas_set_font(canvas, FontSecondary);
|
||||
if(app_label_needs_two_lines) {
|
||||
hid_ptt_draw_app_label(canvas, app_label_y, model->app);
|
||||
} else {
|
||||
hid_ptt_draw_text_centered(canvas, app_label_y, model->app);
|
||||
}
|
||||
hid_ptt_draw_text_centered(canvas, os_label_y, model->os);
|
||||
|
||||
// Help label
|
||||
canvas_draw_icon(canvas, 0, helper_top_y, &I_Help_top_64x17);
|
||||
canvas_draw_line(canvas, 4, 109, 4, 118);
|
||||
canvas_draw_line(canvas, 63, 109, 63, 118);
|
||||
canvas_draw_icon(canvas, 0, 119, &I_Help_exit_64x9);
|
||||
canvas_draw_icon(canvas, 24, 119, &I_BtnBackV_9x9);
|
||||
|
||||
// For Zoom/Zoom Global: show "Hold < for Enter" hint
|
||||
if(model->appIndex == HidPushToTalkAppIndexZoom ||
|
||||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) {
|
||||
canvas_draw_icon(canvas, 7, 111, &I_Hold_15x5);
|
||||
canvas_draw_icon(canvas, 24, 109, &I_BtnLeft_9x9);
|
||||
canvas_draw_icon(canvas, 35, 112, &I_for_11x5);
|
||||
canvas_draw_icon(canvas, 48, 112, &I_Return_10x7);
|
||||
}
|
||||
|
||||
// Up
|
||||
canvas_draw_icon(canvas, x_2, y_1, &I_Button_18x18);
|
||||
if(model->up_pressed) {
|
||||
@@ -741,6 +980,11 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
|
||||
}
|
||||
canvas_set_color(canvas, ColorBlack);
|
||||
|
||||
if(hid_ptt_is_zoom_app(model->appIndex)) {
|
||||
hid_ptt_draw_zoom_enter_hint(
|
||||
canvas, x_1, y_2 + 18, model->left_hold_progress, model->left_pressed);
|
||||
}
|
||||
|
||||
// Right / Camera
|
||||
canvas_draw_icon(canvas, x_3, y_2, &I_Button_18x18);
|
||||
if(model->right_pressed) {
|
||||
@@ -756,23 +1000,24 @@ static void hid_ptt_draw_callback(Canvas* canvas, void* context) {
|
||||
|
||||
// Back / Mic
|
||||
const uint8_t x_mic = x_3;
|
||||
canvas_draw_icon(canvas, x_mic, 0, &I_RoundButtonUnpressed_16x16);
|
||||
const uint8_t y_mic = status_bar_bottom_y + ((y_2 - status_bar_bottom_y - 16) / 2);
|
||||
canvas_draw_icon(canvas, x_mic, y_mic, &I_RoundButtonUnpressed_16x16);
|
||||
|
||||
if(!(!model->muted || (model->ptt_pressed))) {
|
||||
// show muted
|
||||
if(model->mic_pressed) {
|
||||
// canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressedCrossed_15x15);
|
||||
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedCrossedBtn_16x16);
|
||||
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedCrossedBtn_16x16);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophoneCrossed_16x16);
|
||||
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophoneCrossed_16x16);
|
||||
}
|
||||
} else {
|
||||
// show unmuted
|
||||
if(model->mic_pressed) {
|
||||
// canvas_draw_icon(canvas, x_mic + 1, 0, &I_MicrophonePressed_15x15);
|
||||
canvas_draw_icon(canvas, x_mic, 0, &I_MicrophonePressedBtn_16x16);
|
||||
canvas_draw_icon(canvas, x_mic, y_mic, &I_MicrophonePressedBtn_16x16);
|
||||
} else {
|
||||
canvas_draw_icon(canvas, x_mic + 5, 2, &I_Mic_7x11);
|
||||
canvas_draw_icon(canvas, x_mic + 5, y_mic + 2, &I_Mic_7x11);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,6 +1058,12 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
|
||||
hid_hal_consumer_key_press(hid_ptt->hid, HID_CONSUMER_VOLUME_DECREMENT);
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = true;
|
||||
if(hid_ptt_is_zoom_app(model->appIndex)) {
|
||||
model->left_hold_progress = 0;
|
||||
furi_timer_start(
|
||||
hid_ptt->left_hold_timer,
|
||||
furi_ms_to_ticks(HID_PTT_LEFT_HOLD_ANIM_STEP_MS));
|
||||
}
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = true;
|
||||
} else if(event->key == InputKeyOk) {
|
||||
@@ -836,6 +1087,8 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
|
||||
}
|
||||
} else if(event->key == InputKeyLeft) {
|
||||
model->left_pressed = false;
|
||||
model->left_hold_progress = 0;
|
||||
furi_timer_stop(hid_ptt->left_hold_timer);
|
||||
} else if(event->key == InputKeyRight) {
|
||||
model->right_pressed = false;
|
||||
|
||||
@@ -866,10 +1119,14 @@ static void hid_ptt_process(HidPushToTalk* hid_ptt, InputEvent* event) {
|
||||
model->muted = !model->muted;
|
||||
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
|
||||
} else if(event->type == InputTypeLong && event->key == InputKeyLeft) {
|
||||
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
|
||||
model->left_pressed = false;
|
||||
view_dispatcher_switch_to_view(
|
||||
hid_ptt->hid->view_dispatcher, HidViewPushToTalkHelp);
|
||||
if(model->appIndex == HidPushToTalkAppIndexZoom ||
|
||||
model->appIndex == HidPushToTalkAppIndexZoomGlobal) {
|
||||
model->left_hold_progress = HID_PTT_LEFT_HOLD_ANIM_STEPS;
|
||||
furi_timer_stop(hid_ptt->left_hold_timer);
|
||||
hid_hal_keyboard_press(hid_ptt->hid, HID_KEYBOARD_RETURN);
|
||||
hid_hal_keyboard_release(hid_ptt->hid, HID_KEYBOARD_RETURN);
|
||||
notification_message(hid_ptt->hid->notifications, &sequence_single_vibro);
|
||||
}
|
||||
}
|
||||
//LED
|
||||
if(!model->muted || (model->ptt_pressed)) {
|
||||
@@ -886,6 +1143,7 @@ static bool hid_ptt_input_callback(InputEvent* event, void* context) {
|
||||
HidPushToTalk* hid_ptt = context;
|
||||
bool consumed = false;
|
||||
if(event->type == InputTypeLong && event->key == InputKeyBack) {
|
||||
furi_timer_stop(hid_ptt->left_hold_timer);
|
||||
hid_hal_keyboard_release_all(hid_ptt->hid);
|
||||
notification_message(hid_ptt->hid->notifications, &sequence_double_vibro);
|
||||
widget_reset(hid_ptt->help);
|
||||
@@ -901,14 +1159,16 @@ View* hid_ptt_get_view(HidPushToTalk* hid_ptt) {
|
||||
return hid_ptt->view;
|
||||
}
|
||||
|
||||
static uint32_t hid_ptt_view(void* context) {
|
||||
static uint32_t hid_ptt_menu_view(void* context) {
|
||||
UNUSED(context);
|
||||
return HidViewPushToTalk;
|
||||
return HidViewPushToTalkMenu;
|
||||
}
|
||||
|
||||
HidPushToTalk* hid_ptt_alloc(Hid* hid) {
|
||||
HidPushToTalk* hid_ptt = malloc(sizeof(HidPushToTalk));
|
||||
hid_ptt->hid = hid;
|
||||
hid_ptt->left_hold_timer =
|
||||
furi_timer_alloc(hid_ptt_left_hold_timer_callback, FuriTimerTypePeriodic, hid_ptt);
|
||||
hid_ptt->view = view_alloc();
|
||||
view_set_context(hid_ptt->view, hid_ptt);
|
||||
view_allocate_model(hid_ptt->view, ViewModelTypeLocking, sizeof(HidPushToTalkModel));
|
||||
@@ -921,6 +1181,7 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) {
|
||||
HidPushToTalkModel * model,
|
||||
{
|
||||
model->muted = true; // assume we're muted
|
||||
model->left_hold_progress = 0;
|
||||
model->os = furi_string_alloc();
|
||||
model->app = furi_string_alloc();
|
||||
},
|
||||
@@ -1134,14 +1395,17 @@ HidPushToTalk* hid_ptt_alloc(Hid* hid) {
|
||||
hid_ptt);
|
||||
|
||||
hid_ptt->help = widget_alloc();
|
||||
view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_view);
|
||||
view_set_previous_callback(widget_get_view(hid_ptt->help), hid_ptt_menu_view);
|
||||
view_dispatcher_add_view(
|
||||
hid->view_dispatcher, HidViewPushToTalkHelp, widget_get_view(hid_ptt->help));
|
||||
ptt_menu_set_long_ok_callback(hid->hid_ptt_menu, hid_ptt_menu_help_callback, hid_ptt);
|
||||
return hid_ptt;
|
||||
}
|
||||
|
||||
void hid_ptt_free(HidPushToTalk* hid_ptt) {
|
||||
furi_assert(hid_ptt);
|
||||
furi_timer_stop(hid_ptt->left_hold_timer);
|
||||
furi_timer_free(hid_ptt->left_hold_timer);
|
||||
notification_message(hid_ptt->hid->notifications, &sequence_reset_red);
|
||||
with_view_model(
|
||||
hid_ptt->view,
|
||||
|
||||
@@ -5,11 +5,18 @@
|
||||
#include "../hid.h"
|
||||
#include "../views.h"
|
||||
|
||||
#define TAG "HidPushToTalkMenu"
|
||||
#include "hid_icons.h"
|
||||
|
||||
#define TAG "HidPushToTalkMenu"
|
||||
#define PTT_MENU_HELP_HINT_DELAY_MS 5000U
|
||||
#define PTT_MENU_HINT_TIMER_PERIOD_MS 150U
|
||||
|
||||
struct HidPushToTalkMenu {
|
||||
View* view;
|
||||
Hid* hid;
|
||||
PushToTalkMenuLongOkCallback long_ok_callback;
|
||||
void* long_ok_callback_context;
|
||||
FuriTimer* hint_timer;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
@@ -60,8 +67,69 @@ typedef struct {
|
||||
size_t window_position;
|
||||
PushToTalkMenuList* lists;
|
||||
int lists_count;
|
||||
uint32_t last_interaction_tick;
|
||||
size_t hint_list_position;
|
||||
size_t hint_item_position;
|
||||
bool hint_visible;
|
||||
uint16_t hint_scroll_tick;
|
||||
} HidPushToTalkMenuModel;
|
||||
|
||||
static void hid_ptt_menu_mark_interaction(HidPushToTalkMenu* hid_ptt_menu) {
|
||||
with_view_model(
|
||||
hid_ptt_menu->view,
|
||||
HidPushToTalkMenuModel * model,
|
||||
{
|
||||
model->last_interaction_tick = furi_get_tick();
|
||||
model->hint_list_position = model->list_position;
|
||||
model->hint_item_position = model->position;
|
||||
model->hint_visible = false;
|
||||
model->hint_scroll_tick = 0;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
static void hid_ptt_menu_hint_timer_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalkMenu* hid_ptt_menu = context;
|
||||
with_view_model(
|
||||
hid_ptt_menu->view,
|
||||
HidPushToTalkMenuModel * model,
|
||||
{
|
||||
const uint32_t now = furi_get_tick();
|
||||
const bool selection_changed = (model->list_position != model->hint_list_position) ||
|
||||
(model->position != model->hint_item_position);
|
||||
|
||||
if(selection_changed) {
|
||||
model->hint_list_position = model->list_position;
|
||||
model->hint_item_position = model->position;
|
||||
model->last_interaction_tick = now;
|
||||
model->hint_visible = false;
|
||||
model->hint_scroll_tick = 0;
|
||||
} else if(!model->hint_visible) {
|
||||
if((now - model->last_interaction_tick) >= PTT_MENU_HELP_HINT_DELAY_MS) {
|
||||
model->hint_visible = true;
|
||||
model->hint_scroll_tick = 0;
|
||||
}
|
||||
} else {
|
||||
model->hint_scroll_tick++;
|
||||
}
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
static void hid_ptt_menu_enter_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalkMenu* hid_ptt_menu = context;
|
||||
hid_ptt_menu_mark_interaction(hid_ptt_menu);
|
||||
furi_timer_start(hid_ptt_menu->hint_timer, furi_ms_to_ticks(PTT_MENU_HINT_TIMER_PERIOD_MS));
|
||||
}
|
||||
|
||||
static void hid_ptt_menu_exit_callback(void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalkMenu* hid_ptt_menu = context;
|
||||
furi_timer_stop(hid_ptt_menu->hint_timer);
|
||||
}
|
||||
|
||||
static void
|
||||
hid_ptt_menu_draw_list(Canvas* canvas, void* context, const PushToTalkMenuItemArray_t items) {
|
||||
furi_assert(context);
|
||||
@@ -94,13 +162,44 @@ static void
|
||||
|
||||
FuriString* disp_str;
|
||||
disp_str = furi_string_alloc_set(PushToTalkMenuItemArray_cref(it)->label);
|
||||
elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
|
||||
const int32_t text_y = y_offset + (item_position * item_height) + item_height - 4;
|
||||
const int32_t row_left_x = 6;
|
||||
const int32_t row_right_x = item_width - 2;
|
||||
const size_t row_width = row_right_x - row_left_x;
|
||||
|
||||
canvas_draw_str(
|
||||
canvas,
|
||||
6,
|
||||
y_offset + (item_position * item_height) + item_height - 4,
|
||||
furi_string_get_cstr(disp_str));
|
||||
if((position == model->position) && model->hint_visible) {
|
||||
const char* hint_prefix = " | Long press";
|
||||
const char* hint_suffix = "for help";
|
||||
const int32_t icon_y = text_y - 8;
|
||||
|
||||
const size_t label_w = canvas_string_width(canvas, furi_string_get_cstr(disp_str));
|
||||
const size_t prefix_w = canvas_string_width(canvas, hint_prefix);
|
||||
const size_t icon_w = 9;
|
||||
const size_t suffix_w = canvas_string_width(canvas, hint_suffix);
|
||||
|
||||
const size_t group_w = label_w + 2 + prefix_w + 2 + icon_w + 2 + suffix_w;
|
||||
|
||||
int32_t scroll_offset = 0;
|
||||
if(group_w > row_width) {
|
||||
const size_t overflow = group_w - row_width;
|
||||
const size_t cycle = overflow * 2;
|
||||
const size_t step = cycle ? (model->hint_scroll_tick % cycle) : 0;
|
||||
scroll_offset = (step <= overflow) ? step : (cycle - step);
|
||||
}
|
||||
|
||||
const int32_t draw_x = row_left_x - scroll_offset;
|
||||
canvas_draw_str(canvas, draw_x, text_y, furi_string_get_cstr(disp_str));
|
||||
|
||||
int32_t hint_x = draw_x + label_w + 2;
|
||||
canvas_draw_str(canvas, hint_x, text_y, hint_prefix);
|
||||
hint_x += prefix_w + 2;
|
||||
canvas_draw_icon(canvas, hint_x, icon_y, &I_Ok_btn_9x9);
|
||||
hint_x += icon_w + 2;
|
||||
canvas_draw_str(canvas, hint_x, text_y, hint_suffix);
|
||||
} else {
|
||||
elements_string_fit_width(canvas, disp_str, item_width - (6 * 2));
|
||||
canvas_draw_str(canvas, row_left_x, text_y, furi_string_get_cstr(disp_str));
|
||||
}
|
||||
|
||||
furi_string_free(disp_str);
|
||||
}
|
||||
@@ -338,6 +437,39 @@ void ptt_menu_process_ok(HidPushToTalkMenu* hid_ptt_menu) {
|
||||
}
|
||||
}
|
||||
|
||||
void ptt_menu_process_long_ok(HidPushToTalkMenu* hid_ptt_menu) {
|
||||
PushToTalkMenuList* list = NULL;
|
||||
PushToTalkMenuItem* item = NULL;
|
||||
with_view_model(
|
||||
hid_ptt_menu->view,
|
||||
HidPushToTalkMenuModel * model,
|
||||
{
|
||||
list = &model->lists[model->list_position];
|
||||
const size_t items_size = PushToTalkMenuItemArray_size(list->items);
|
||||
if(model->position < items_size) {
|
||||
item = PushToTalkMenuItemArray_get(list->items, model->position);
|
||||
}
|
||||
},
|
||||
false);
|
||||
if(item && list && hid_ptt_menu->long_ok_callback) {
|
||||
hid_ptt_menu->long_ok_callback(
|
||||
hid_ptt_menu->long_ok_callback_context,
|
||||
list->index,
|
||||
list->label,
|
||||
item->index,
|
||||
item->label);
|
||||
}
|
||||
}
|
||||
|
||||
void ptt_menu_set_long_ok_callback(
|
||||
HidPushToTalkMenu* hid_ptt_menu,
|
||||
PushToTalkMenuLongOkCallback callback,
|
||||
void* callback_context) {
|
||||
furi_assert(hid_ptt_menu);
|
||||
hid_ptt_menu->long_ok_callback = callback;
|
||||
hid_ptt_menu->long_ok_callback_context = callback_context;
|
||||
}
|
||||
|
||||
static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
|
||||
furi_assert(context);
|
||||
HidPushToTalkMenu* hid_ptt_menu = context;
|
||||
@@ -375,7 +507,15 @@ static bool hid_ptt_menu_input_callback(InputEvent* event, void* context) {
|
||||
consumed = true;
|
||||
ptt_menu_process_down(hid_ptt_menu);
|
||||
}
|
||||
} else if(event->type == InputTypeLong && event->key == InputKeyOk) {
|
||||
consumed = true;
|
||||
ptt_menu_process_long_ok(hid_ptt_menu);
|
||||
}
|
||||
|
||||
if(event->type != InputTypeRelease) {
|
||||
hid_ptt_menu_mark_interaction(hid_ptt_menu);
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
@@ -392,6 +532,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
|
||||
view_allocate_model(hid_ptt_menu->view, ViewModelTypeLocking, sizeof(HidPushToTalkMenuModel));
|
||||
view_set_draw_callback(hid_ptt_menu->view, hid_ptt_menu_draw_callback);
|
||||
view_set_input_callback(hid_ptt_menu->view, hid_ptt_menu_input_callback);
|
||||
view_set_enter_callback(hid_ptt_menu->view, hid_ptt_menu_enter_callback);
|
||||
view_set_exit_callback(hid_ptt_menu->view, hid_ptt_menu_exit_callback);
|
||||
|
||||
hid_ptt_menu->hint_timer =
|
||||
furi_timer_alloc(hid_ptt_menu_hint_timer_callback, FuriTimerTypePeriodic, hid_ptt_menu);
|
||||
|
||||
with_view_model(
|
||||
hid_ptt_menu->view,
|
||||
@@ -400,6 +545,11 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
|
||||
model->lists_count = 0;
|
||||
model->position = 0;
|
||||
model->window_position = 0;
|
||||
model->last_interaction_tick = furi_get_tick();
|
||||
model->hint_list_position = 0;
|
||||
model->hint_item_position = 0;
|
||||
model->hint_visible = false;
|
||||
model->hint_scroll_tick = 0;
|
||||
},
|
||||
true);
|
||||
return hid_ptt_menu;
|
||||
@@ -407,6 +557,8 @@ HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* hid) {
|
||||
|
||||
void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu) {
|
||||
furi_assert(hid_ptt_menu);
|
||||
furi_timer_stop(hid_ptt_menu->hint_timer);
|
||||
furi_timer_free(hid_ptt_menu->hint_timer);
|
||||
with_view_model(
|
||||
hid_ptt_menu->view,
|
||||
HidPushToTalkMenuModel * model,
|
||||
|
||||
@@ -12,6 +12,13 @@ typedef void (*PushToTalkMenuItemCallback)(
|
||||
uint32_t itemIndex,
|
||||
FuriString* itemLabel);
|
||||
|
||||
typedef void (*PushToTalkMenuLongOkCallback)(
|
||||
void* context,
|
||||
uint32_t listIndex,
|
||||
FuriString* listLabel,
|
||||
uint32_t itemIndex,
|
||||
FuriString* itemLabel);
|
||||
|
||||
HidPushToTalkMenu* hid_ptt_menu_alloc(Hid* bt_hid);
|
||||
|
||||
void hid_ptt_menu_free(HidPushToTalkMenu* hid_ptt_menu);
|
||||
@@ -27,3 +34,8 @@ void ptt_menu_add_item_to_list(
|
||||
void* callback_context);
|
||||
|
||||
void ptt_menu_add_list(HidPushToTalkMenu* hid_ptt_menu, const char* label, uint32_t index);
|
||||
|
||||
void ptt_menu_set_long_ok_callback(
|
||||
HidPushToTalkMenu* hid_ptt_menu,
|
||||
PushToTalkMenuLongOkCallback callback,
|
||||
void* callback_context);
|
||||
|
||||
@@ -153,10 +153,11 @@ async function upload(config) {
|
||||
port.write(`storage remove ${config.output}\x0d`);
|
||||
port.drain();
|
||||
await waitFor(">: ", 1000);
|
||||
port.write(`storage write_chunk ${config.output} ${appFile.length}\x0d`);
|
||||
const appFileBuffer = Buffer.from(appFile, "utf8");
|
||||
port.write(`storage write_chunk ${config.output} ${appFileBuffer.length}\x0d`);
|
||||
await waitFor("Ready", 1000);
|
||||
port.write(appFile);
|
||||
port.drain();
|
||||
port.write(appFileBuffer);
|
||||
await new Promise(resolve => port.drain(resolve));
|
||||
await waitFor(">: ", 1000);
|
||||
|
||||
console.log("Launching application");
|
||||
|
||||
@@ -26,8 +26,9 @@ That list is only for default SubGHz app, apps like *Weather Station* have their
|
||||
- BETT `433.92MHz` `AM650` (18 bits, Static)
|
||||
- Beninca ARC (TOGO2VA) `433.92MHz` `AM650` (128 bits, Dynamic AES128) (button code `0` emulates `hidden button` option on the remote)
|
||||
- BFT Mitto `433.92MHz` `AM650` (64 bits, Dynamic, KeeLoq based with Seed taken from serial)
|
||||
- CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic)
|
||||
- CAME TWEE `433.92MHz` `AM650` (54 bits, Static)
|
||||
- Erreka - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - secure learning with Seed)
|
||||
- CAME Atomo `433.92MHz, 868MHz` `AM650` (62 bits, Dynamic) (TOPD4REN, TOP44RBN, TOP42R, TOP44R)
|
||||
- CAME TWEE `433.92MHz` `AM650` (54 bits, Pseudo-Dynamic) (+ TOP44FGN) (aka New Fixed Code)
|
||||
- CAME `433.92MHz, 868MHz` `AM650` (12, 24 bits, Static)
|
||||
- Ditec GOL4 `433.92MHz` `AM650` (54 bits, Dynamic) (should be compatible with BIXLG4, BIXLS2, BIXLP2) - (right arrow emulates button `0` (hidden button))
|
||||
- Prastel `433.92MHz, 868MHz` `AM650` (25, 42 bits, Static)
|
||||
@@ -37,7 +38,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their
|
||||
- Dickert MAHS `AM650` (36 bits, Static)
|
||||
- Doitrand `AM650` (37 bits, Dynamic)
|
||||
- Elplast/P-11B/3BK/E.C.A `433MHz` `AM650` (18 bits, Static)
|
||||
- FAAC SLH `433.92MHz, 868.35MHz` `AM650` (64 bits, Dynamic) (+ Genius KILO TX2/4 JLC)
|
||||
- FAAC SLH `433.92MHz, 868.35MHz` `AM650` (64 bits, Dynamic) (+ Genius KILO TX2/4 JLC, other Genius models)
|
||||
- Gate TX `433.92MHz` `AM650` (64 bits, Static)
|
||||
- Hormann `868MHz` `AM650` (44 bits, Static)
|
||||
- HCS101 `AM650` (64 bits, Simple Dynamic, KeeLoq-like)
|
||||
@@ -45,6 +46,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their
|
||||
- KingGates Stylo 4k `433.92MHz` `AM650` (89 bits, Dynamic, KeeLoq based)
|
||||
- Mastercode `AM650` (36 bits, Static)
|
||||
- Megacode `AM650` (24 bits, Static)
|
||||
- Nord ICE `433.92MHz` `AM650` (33 bits, Static)
|
||||
- Nero Sketch `AM650` (40 bits, Static)
|
||||
- Nice Flo `433.92MHz` `AM650` (12, 24 bits, Static)
|
||||
- Nice FloR-S `433.92MHz` `AM650` (52 bits, Dynamic)
|
||||
@@ -61,6 +63,7 @@ That list is only for default SubGHz app, apps like *Weather Station* have their
|
||||
- Nero Radio `434.42MHz` `AM650` (56 bits, Static mode only, Dynamic is unsupported)
|
||||
- Security+1.0 `315MHz, 433.92MHz, 390MHz` `AM650` (42 bits, Dynamic)
|
||||
- Security+2.0 `310MHz, 390MHz, 868MHz` `AM650` (62 bits, Dynamic)
|
||||
- Allstar Firefly 318ALD31K `318MHz` `AM650` (18 bits, Static)
|
||||
|
||||
### Sensors & Smart home
|
||||
- Intertechno V3 `AM650` (32 bits, Static) - Lights, sockets, other.
|
||||
@@ -110,7 +113,7 @@ The following manufacturers have KeeLoq support in Unleashed firmware:
|
||||
- DEA Mio - `433.92MHz` `AM650` (KeeLoq, 64 bits) (modified serial in Hop, uses last 3 digits modifying first one (example - 419 -> C19) - simple learning)
|
||||
- DoorHan - 315MHz, `433.92MHz` `AM650` (KeeLoq, 64 bits)
|
||||
- DTM Neo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - simple learning)
|
||||
- Elmes Poland - `433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning)
|
||||
- Elmes Poland - `303, 433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning)
|
||||
- FAAC RC,XT - `433.92MHz, 868MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Genius Bravo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) (Genius ECHO, Genius Bravo (Button code 0xB for prog. mode))
|
||||
- Gibidi - `433.92MHz` `AM650` (KeeLoq, 64 bits)
|
||||
@@ -123,7 +126,7 @@ The following manufacturers have KeeLoq support in Unleashed firmware:
|
||||
- KEY (KeeLoq, 64 bits)
|
||||
- Monarch - `433.92MHz` `AM650` (KeeLoq, 64 bits) (no serial in Hop, uses fixed value 0x100 - normal learning)
|
||||
- Motorline - `433.92MHz` `AM650` (KeeLoq, 64 bits) (normal learning)
|
||||
- Mutanco/Mutancode (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Clemsa Mutancode (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Mhouse - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- Nice Smilo - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- Normstahl - `433.92MHz` `AM650` (KeeLoq, 64 bits)
|
||||
@@ -131,6 +134,46 @@ The following manufacturers have KeeLoq support in Unleashed firmware:
|
||||
- Sommer `434.42MHz, 868.80MHz` `FSK12K (or FSK476)` (KeeLoq, 64 bits) (normal learning) (TX03-868-4, Pearl, and maybe other models are supported (SOMloq))
|
||||
- Steelmate - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Stilmatic (R-Tech) - `433.92MHz` `AM650` (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning) (receiver checks for 10bit only (unverified))
|
||||
- Pujol - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - special learning) (Pujol Vario 4, other models)
|
||||
- Pujol Vario - `433.92MHz` `AM650` (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning) (older models)
|
||||
- Wisniowski (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Doormatic (KeeLoq, 64 bits) (normal learning)
|
||||
- Elvox (KeeLoq, 64 bits) (normal learning)
|
||||
- ET Blue (KeeLoq, 64 bits) (10bit serial part in Hop - normal learning)
|
||||
- ET Blue Mix (KeeLoq, 64 bits) (10bit serial part in Hop - normal learning)
|
||||
- ATA PTX4 (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- Verex (KeeLoq, 64 bits) (normal learning)
|
||||
- Mc Garcia (KeeLoq, 64 bits) (10bit serial part in Hop - simple learning)
|
||||
- Fadini (KeeLoq, 64 bits) (12bit serial part in Hop - simple learning)
|
||||
- Seav (KeeLoq, 64 bits) (12bit serial part in Hop - normal learning)
|
||||
- AERF Otros (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Collbaix (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Hy dom (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Medva (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF VDS (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Temp (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Sabutom (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- AERF Motorgate (KeeLoq, 64 bits) (10bit serial part in Hop - special learning)
|
||||
- JCM1G EMFA (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G DMIL (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Antonio Meca (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Hy dom (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Baleato (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Cas (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Cubells (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Cyacsa (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Forsa (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Gandara (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Pujol (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Gibidi (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Gibidi2 (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Hybrid (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Tech (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Norton (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Nueva Castilla (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Puertas Lorenzo (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Serviparking (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
- JCM1G Zibor (KeeLoq, 64 bits) (8bit serial part in Hop - simple learning)
|
||||
|
||||
### Alarms, unknown origin, etc.
|
||||
- APS-1100/APS-2550 (KeeLoq, 64 bits)
|
||||
@@ -166,6 +209,11 @@ The following manufacturers have KeeLoq support in Unleashed firmware:
|
||||
- Tomahawk TZ-9030 (KeeLoq, 64 bits)
|
||||
- Tomahawk Z,X 3-5 (KeeLoq, 64 bits)
|
||||
- ZX-730-750-1055 (KeeLoq, 64 bits)
|
||||
- Zero Simple (KeeLoq, 64 bits)
|
||||
- Zero Normal (KeeLoq, 64 bits)
|
||||
- FFFF Simple (KeeLoq, 64 bits)
|
||||
- FFFF Normal (KeeLoq, 64 bits)
|
||||
- Miserere (KeeLoq, 64 bits) (normal learning)
|
||||
|
||||
*Note: Most KeeLoq manufacturers operate in the 433 MHz and 868 MHz frequency bands with AM650 modulation. Some operate at other frequencies or modulations. Not all KeeLoq systems are supported for full decoding or emulation.*
|
||||
|
||||
|
||||
@@ -172,7 +172,10 @@ bool iso14443_4_layer_decode_response(
|
||||
bit_buffer_copy_right(output_data, block_data, 1);
|
||||
} else {
|
||||
if(!bit_buffer_starts_with_byte(block_data, instance->pcb_prev)) break;
|
||||
bit_buffer_copy_right(output_data, block_data, 1);
|
||||
// Fix for some EMV cards with strange response
|
||||
if(bit_buffer_get_size_bytes(block_data) > 1) {
|
||||
bit_buffer_copy_right(output_data, block_data, 1);
|
||||
}
|
||||
ret = true;
|
||||
}
|
||||
} while(false);
|
||||
|
||||
@@ -15,6 +15,11 @@ extern "C" {
|
||||
#define ISO15693_3_FDT_LISTEN_FC (4320U)
|
||||
#define ISO15693_3_POLL_POLL_MIN_US (1500U)
|
||||
|
||||
/* NVM write time varies by tag type (ICODE SLIX: 9.6ms, generic: up to 20ms).
|
||||
* ISO 15693-3 write commands require a longer FWT than reads to account for
|
||||
* EEPROM programming delays before the tag responds. 20ms covers all known tags. */
|
||||
#define ISO15693_3_FDT_WRITE_POLL_FC (271200U)
|
||||
|
||||
#define ISO15693_3_REQ_FLAG_SUBCARRIER_1 (0U << 0)
|
||||
#define ISO15693_3_REQ_FLAG_SUBCARRIER_2 (1U << 0)
|
||||
#define ISO15693_3_REQ_FLAG_DATA_RATE_LO (0U << 1)
|
||||
|
||||
@@ -176,6 +176,25 @@ Iso15693_3Error
|
||||
return ret;
|
||||
}
|
||||
|
||||
Iso15693_3Error iso15693_3_write_block_response_parse(const BitBuffer* buf) {
|
||||
Iso15693_3Error ret = Iso15693_3ErrorNone;
|
||||
|
||||
do {
|
||||
if(iso15693_3_error_response_parse(&ret, buf)) break;
|
||||
|
||||
typedef struct {
|
||||
uint8_t flags;
|
||||
} WriteBlockResponseLayout;
|
||||
|
||||
if(bit_buffer_get_size_bytes(buf) != sizeof(WriteBlockResponseLayout)) {
|
||||
ret = Iso15693_3ErrorUnexpectedResponse;
|
||||
break;
|
||||
}
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Iso15693_3Error iso15693_3_get_block_security_response_parse(
|
||||
uint8_t* data,
|
||||
uint16_t block_count,
|
||||
|
||||
@@ -28,6 +28,8 @@ Iso15693_3Error
|
||||
Iso15693_3Error
|
||||
iso15693_3_read_block_response_parse(uint8_t* data, uint8_t block_size, const BitBuffer* buf);
|
||||
|
||||
Iso15693_3Error iso15693_3_write_block_response_parse(const BitBuffer* buf);
|
||||
|
||||
Iso15693_3Error iso15693_3_get_block_security_response_parse(
|
||||
uint8_t* data,
|
||||
uint16_t block_count,
|
||||
|
||||
@@ -13,6 +13,14 @@ extern "C" {
|
||||
*/
|
||||
typedef struct Iso15693_3Poller Iso15693_3Poller;
|
||||
|
||||
/**
|
||||
* @brief Get the Iso15693_3 data accumulated by the poller.
|
||||
*
|
||||
* @param[in] instance pointer to the Iso15693_3Poller instance.
|
||||
* @return pointer to the Iso15693_3Data structure filled during activation.
|
||||
*/
|
||||
const Iso15693_3Data* iso15693_3_poller_get_data(Iso15693_3Poller* instance);
|
||||
|
||||
/**
|
||||
* @brief Enumeration of possible Iso15693_3 poller event types.
|
||||
*/
|
||||
@@ -144,6 +152,40 @@ Iso15693_3Error iso15693_3_poller_get_blocks_security(
|
||||
uint8_t* data,
|
||||
uint16_t block_count);
|
||||
|
||||
/**
|
||||
* @brief Write a single Iso15693_3 block.
|
||||
*
|
||||
* Must ONLY be used inside the callback function.
|
||||
*
|
||||
* @param[in, out] instance pointer to the instance to be used in the transaction.
|
||||
* @param[in] data pointer to the buffer containing the data to write.
|
||||
* @param[in] block_number block number to write.
|
||||
* @param[in] block_size size of the block in bytes.
|
||||
* @return Iso15693_3ErrorNone on success, an error code on failure.
|
||||
*/
|
||||
Iso15693_3Error iso15693_3_poller_write_block(
|
||||
Iso15693_3Poller* instance,
|
||||
const uint8_t* data,
|
||||
uint8_t block_number,
|
||||
uint8_t block_size);
|
||||
|
||||
/**
|
||||
* @brief Write multiple consecutive Iso15693_3 blocks.
|
||||
*
|
||||
* Must ONLY be used inside the callback function.
|
||||
*
|
||||
* @param[in, out] instance pointer to the instance to be used in the transaction.
|
||||
* @param[in] data pointer to the buffer containing the data to write.
|
||||
* @param[in] block_count number of blocks to write.
|
||||
* @param[in] block_size size of each block in bytes.
|
||||
* @return Iso15693_3ErrorNone on success, an error code on failure.
|
||||
*/
|
||||
Iso15693_3Error iso15693_3_poller_write_blocks(
|
||||
Iso15693_3Poller* instance,
|
||||
const uint8_t* data,
|
||||
uint16_t block_count,
|
||||
uint8_t block_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -234,6 +234,57 @@ Iso15693_3Error iso15693_3_poller_read_blocks(
|
||||
return ret;
|
||||
}
|
||||
|
||||
Iso15693_3Error iso15693_3_poller_write_block(
|
||||
Iso15693_3Poller* instance,
|
||||
const uint8_t* data,
|
||||
uint8_t block_number,
|
||||
uint8_t block_size) {
|
||||
furi_assert(instance);
|
||||
furi_assert(data);
|
||||
|
||||
bit_buffer_reset(instance->tx_buffer);
|
||||
bit_buffer_reset(instance->rx_buffer);
|
||||
|
||||
bit_buffer_append_byte(
|
||||
instance->tx_buffer, ISO15693_3_REQ_FLAG_SUBCARRIER_1 | ISO15693_3_REQ_FLAG_DATA_RATE_HI);
|
||||
bit_buffer_append_byte(instance->tx_buffer, ISO15693_3_CMD_WRITE_BLOCK);
|
||||
bit_buffer_append_byte(instance->tx_buffer, block_number);
|
||||
bit_buffer_append_bytes(instance->tx_buffer, data, block_size);
|
||||
|
||||
Iso15693_3Error ret;
|
||||
|
||||
do {
|
||||
ret = iso15693_3_poller_send_frame(
|
||||
instance, instance->tx_buffer, instance->rx_buffer, ISO15693_3_FDT_WRITE_POLL_FC);
|
||||
if(ret != Iso15693_3ErrorNone) break;
|
||||
|
||||
ret = iso15693_3_write_block_response_parse(instance->rx_buffer);
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Iso15693_3Error iso15693_3_poller_write_blocks(
|
||||
Iso15693_3Poller* instance,
|
||||
const uint8_t* data,
|
||||
uint16_t block_count,
|
||||
uint8_t block_size) {
|
||||
furi_assert(instance);
|
||||
furi_assert(data);
|
||||
furi_assert(block_count);
|
||||
furi_assert(block_size);
|
||||
|
||||
Iso15693_3Error ret = Iso15693_3ErrorNone;
|
||||
|
||||
for(uint32_t i = 0; i < block_count; ++i) {
|
||||
ret =
|
||||
iso15693_3_poller_write_block(instance, &data[block_size * i], (uint8_t)i, block_size);
|
||||
if(ret != Iso15693_3ErrorNone) break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Iso15693_3Error iso15693_3_poller_get_blocks_security(
|
||||
Iso15693_3Poller* instance,
|
||||
uint8_t* data,
|
||||
|
||||
@@ -33,8 +33,6 @@ struct Iso15693_3Poller {
|
||||
void* context;
|
||||
};
|
||||
|
||||
const Iso15693_3Data* iso15693_3_poller_get_data(Iso15693_3Poller* instance);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -65,24 +65,28 @@ void mf_classic_poller_free(MfClassicPoller* instance) {
|
||||
bit_buffer_free(instance->tx_encrypted_buffer);
|
||||
bit_buffer_free(instance->rx_encrypted_buffer);
|
||||
|
||||
// Clean up resources in MfClassicPollerDictAttackContext
|
||||
MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx;
|
||||
// Clean up dict attack resources when the poller was in dict attack mode.
|
||||
if(instance->mode == MfClassicPollerModeDictAttackStandard ||
|
||||
instance->mode == MfClassicPollerModeDictAttackEnhanced ||
|
||||
instance->mode == MfClassicPollerModeDictAttackCUID) {
|
||||
MfClassicPollerDictAttackContext* dict_attack_ctx = &instance->mode_ctx.dict_attack_ctx;
|
||||
|
||||
// Free the dictionaries
|
||||
if(dict_attack_ctx->mf_classic_system_dict) {
|
||||
keys_dict_free(dict_attack_ctx->mf_classic_system_dict);
|
||||
dict_attack_ctx->mf_classic_system_dict = NULL;
|
||||
}
|
||||
if(dict_attack_ctx->mf_classic_user_dict) {
|
||||
keys_dict_free(dict_attack_ctx->mf_classic_user_dict);
|
||||
dict_attack_ctx->mf_classic_user_dict = NULL;
|
||||
}
|
||||
// Free the dictionaries
|
||||
if(dict_attack_ctx->mf_classic_system_dict) {
|
||||
keys_dict_free(dict_attack_ctx->mf_classic_system_dict);
|
||||
dict_attack_ctx->mf_classic_system_dict = NULL;
|
||||
}
|
||||
if(dict_attack_ctx->mf_classic_user_dict) {
|
||||
keys_dict_free(dict_attack_ctx->mf_classic_user_dict);
|
||||
dict_attack_ctx->mf_classic_user_dict = NULL;
|
||||
}
|
||||
|
||||
// Free the nested nonce array if it exists
|
||||
if(dict_attack_ctx->nested_nonce.nonces) {
|
||||
free(dict_attack_ctx->nested_nonce.nonces);
|
||||
dict_attack_ctx->nested_nonce.nonces = NULL;
|
||||
dict_attack_ctx->nested_nonce.count = 0;
|
||||
// Free the nested nonce array if it exists
|
||||
if(dict_attack_ctx->nested_nonce.nonces) {
|
||||
free(dict_attack_ctx->nested_nonce.nonces);
|
||||
dict_attack_ctx->nested_nonce.nonces = NULL;
|
||||
dict_attack_ctx->nested_nonce.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
free(instance);
|
||||
@@ -162,6 +166,7 @@ NfcCommand mf_classic_poller_handler_start(MfClassicPoller* instance) {
|
||||
|
||||
instance->mfc_event.type = MfClassicPollerEventTypeRequestMode;
|
||||
command = instance->callback(instance->general_event, instance->context);
|
||||
instance->mode = instance->mfc_event_data.poller_mode.mode;
|
||||
|
||||
if(instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackStandard ||
|
||||
instance->mfc_event_data.poller_mode.mode == MfClassicPollerModeDictAttackCUID) {
|
||||
|
||||
@@ -183,6 +183,7 @@ struct MfClassicPoller {
|
||||
|
||||
MfClassicType current_type_check;
|
||||
uint8_t sectors_total;
|
||||
MfClassicPollerMode mode;
|
||||
MfClassicPollerModeContext mode_ctx;
|
||||
|
||||
Crypto1* crypto;
|
||||
|
||||
@@ -290,9 +290,7 @@ bool mf_desfire_file_settings_parse(MfDesfireFileSettings* data, const BitBuffer
|
||||
printf("\r\n");
|
||||
break;
|
||||
}
|
||||
if(additional_access_rights_len >
|
||||
MF_DESFIRE_MAX_KEYS * sizeof(MfDesfireFileAccessRights))
|
||||
break;
|
||||
if(additional_access_rights_len > MF_DESFIRE_MAX_KEYS - 1) break;
|
||||
|
||||
memcpy(
|
||||
&file_settings_temp.access_rights[1],
|
||||
|
||||
@@ -169,6 +169,10 @@ static MfUltralightCommand
|
||||
|
||||
MfUltralightPage pages[64] = {};
|
||||
uint8_t page_cnt = (end_page - start_page) + 1;
|
||||
if(page_cnt > COUNT_OF(pages)) {
|
||||
command = MfUltralightCommandNotProcessedNAK;
|
||||
break;
|
||||
}
|
||||
mf_ultralight_listener_perform_read(pages, instance, start_page, page_cnt, do_i2c_check);
|
||||
|
||||
bit_buffer_copy_bytes(instance->tx_buffer, (uint8_t*)pages, page_cnt * 4);
|
||||
|
||||
@@ -118,6 +118,40 @@ SlixError slix_poller_set_password(
|
||||
SlixPassword password,
|
||||
SlixRandomNumber random_number);
|
||||
|
||||
/**
|
||||
* @brief Write a single block to a Slix card.
|
||||
*
|
||||
* Must ONLY be used inside the callback function.
|
||||
*
|
||||
* @param[in, out] instance pointer to the instance to be used in the transaction.
|
||||
* @param[in] data pointer to the buffer containing the data to write.
|
||||
* @param[in] block_number block number to write.
|
||||
* @param[in] block_size size of the block in bytes.
|
||||
* @return SlixErrorNone on success, an error code on failure.
|
||||
*/
|
||||
SlixError slix_poller_write_block(
|
||||
SlixPoller* instance,
|
||||
const uint8_t* data,
|
||||
uint8_t block_number,
|
||||
uint8_t block_size);
|
||||
|
||||
/**
|
||||
* @brief Write multiple consecutive blocks to a Slix card.
|
||||
*
|
||||
* Must ONLY be used inside the callback function.
|
||||
*
|
||||
* @param[in, out] instance pointer to the instance to be used in the transaction.
|
||||
* @param[in] data pointer to the buffer containing the data to write.
|
||||
* @param[in] block_count number of blocks to write.
|
||||
* @param[in] block_size size of each block in bytes.
|
||||
* @return SlixErrorNone on success, an error code on failure.
|
||||
*/
|
||||
SlixError slix_poller_write_blocks(
|
||||
SlixPoller* instance,
|
||||
const uint8_t* data,
|
||||
uint16_t block_count,
|
||||
uint8_t block_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -128,3 +128,31 @@ SlixError slix_poller_set_password(
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
SlixError slix_poller_write_block(
|
||||
SlixPoller* instance,
|
||||
const uint8_t* data,
|
||||
uint8_t block_number,
|
||||
uint8_t block_size) {
|
||||
furi_assert(instance);
|
||||
furi_assert(data);
|
||||
|
||||
const Iso15693_3Error error =
|
||||
iso15693_3_poller_write_block(instance->iso15693_3_poller, data, block_number, block_size);
|
||||
return slix_process_iso15693_3_error(error);
|
||||
}
|
||||
|
||||
SlixError slix_poller_write_blocks(
|
||||
SlixPoller* instance,
|
||||
const uint8_t* data,
|
||||
uint16_t block_count,
|
||||
uint8_t block_size) {
|
||||
furi_assert(instance);
|
||||
furi_assert(data);
|
||||
furi_assert(block_count);
|
||||
furi_assert(block_size);
|
||||
|
||||
const Iso15693_3Error error =
|
||||
iso15693_3_poller_write_blocks(instance->iso15693_3_poller, data, block_count, block_size);
|
||||
return slix_process_iso15693_3_error(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* allstar_firefly.c -- Allstar Firefly 318ALD31K native SubGHz protocol
|
||||
*
|
||||
* Implements the SubGhzProtocol vtable for the Supertex ED-9 based gate remote.
|
||||
* Uses subghz_block_generic_serialize / deserialize for standard-format .sub
|
||||
* files, encoding the 9-position trinary DIP code as a uint64 (base-3, MSB
|
||||
* first: '+' = 2, '0' = 3, '-' = 0).
|
||||
*
|
||||
* Saved file format:
|
||||
* Filetype: Flipper SubGhz Key File
|
||||
* Version: 1
|
||||
* Frequency: 318000000
|
||||
* Preset: FuriHalSubGhzPresetOok650Async
|
||||
* Protocol: Allstar Firefly
|
||||
* Key: 00 00 00 00 00 00 34 B9
|
||||
* Bit: 18
|
||||
*
|
||||
* See allstar_firefly.h for full protocol documentation.
|
||||
*/
|
||||
|
||||
#include "allstar_firefly.h"
|
||||
|
||||
#include "../blocks/const.h"
|
||||
#include "../blocks/decoder.h"
|
||||
#include "../blocks/encoder.h"
|
||||
#include "../blocks/generic.h"
|
||||
#include "../blocks/math.h"
|
||||
|
||||
#define TAG "AllstarFirefly"
|
||||
|
||||
#define DIP_P 0b11 //(+)
|
||||
#define DIP_O 0b10 //(0)
|
||||
#define DIP_N 0b00 //(-)
|
||||
|
||||
#define DIP_PATTERN "%c%c%c%c%c%c%c%c%c"
|
||||
|
||||
#define SHOW_DIP_P(dip, check_dip) \
|
||||
((((dip >> 0x10) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0xE) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0xC) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0xA) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0x8) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0x6) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0x4) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0x2) & 0x3) == check_dip) ? '*' : '_'), \
|
||||
((((dip >> 0x0) & 0x3) == check_dip) ? '*' : '_')
|
||||
|
||||
static const SubGhzBlockConst subghz_protocol_allstar_firefly_const = {
|
||||
.te_short = 600,
|
||||
.te_long = 4000,
|
||||
.te_delta = 300,
|
||||
.min_count_bit_for_found = 18,
|
||||
};
|
||||
|
||||
struct SubGhzProtocolDecoderAllstarFirefly {
|
||||
SubGhzProtocolDecoderBase base;
|
||||
|
||||
SubGhzBlockDecoder decoder;
|
||||
SubGhzBlockGeneric generic;
|
||||
};
|
||||
|
||||
struct SubGhzProtocolEncoderAllstarFirefly {
|
||||
SubGhzProtocolEncoderBase base;
|
||||
|
||||
SubGhzProtocolBlockEncoder encoder;
|
||||
SubGhzBlockGeneric generic;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
AllstarFireflyDecoderStepReset = 0,
|
||||
AllstarFireflyDecoderStepSaveDuration,
|
||||
AllstarFireflyDecoderStepCheckDuration,
|
||||
} AllstarFireflyDecoderStep;
|
||||
|
||||
const SubGhzProtocolDecoder subghz_protocol_allstar_firefly_decoder = {
|
||||
.alloc = subghz_protocol_decoder_allstar_firefly_alloc,
|
||||
.free = subghz_protocol_decoder_allstar_firefly_free,
|
||||
|
||||
.feed = subghz_protocol_decoder_allstar_firefly_feed,
|
||||
.reset = subghz_protocol_decoder_allstar_firefly_reset,
|
||||
|
||||
.get_hash_data = NULL,
|
||||
.get_hash_data_long = subghz_protocol_decoder_allstar_firefly_get_hash_data,
|
||||
.serialize = subghz_protocol_decoder_allstar_firefly_serialize,
|
||||
.deserialize = subghz_protocol_decoder_allstar_firefly_deserialize,
|
||||
.get_string = subghz_protocol_decoder_allstar_firefly_get_string,
|
||||
.get_string_brief = NULL,
|
||||
};
|
||||
|
||||
const SubGhzProtocolEncoder subghz_protocol_allstar_firefly_encoder = {
|
||||
.alloc = subghz_protocol_encoder_allstar_firefly_alloc,
|
||||
.free = subghz_protocol_encoder_allstar_firefly_free,
|
||||
|
||||
.deserialize = subghz_protocol_encoder_allstar_firefly_deserialize,
|
||||
.stop = subghz_protocol_encoder_allstar_firefly_stop,
|
||||
.yield = subghz_protocol_encoder_allstar_firefly_yield,
|
||||
};
|
||||
|
||||
const SubGhzProtocol subghz_protocol_allstar_firefly = {
|
||||
.name = SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME,
|
||||
.type = SubGhzProtocolTypeStatic,
|
||||
.flag = SubGhzProtocolFlag_315 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable |
|
||||
SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send,
|
||||
|
||||
.decoder = &subghz_protocol_allstar_firefly_decoder,
|
||||
.encoder = &subghz_protocol_allstar_firefly_encoder,
|
||||
};
|
||||
|
||||
void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment) {
|
||||
UNUSED(environment);
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance =
|
||||
malloc(sizeof(SubGhzProtocolEncoderAllstarFirefly));
|
||||
|
||||
instance->base.protocol = &subghz_protocol_allstar_firefly;
|
||||
instance->generic.protocol_name = instance->base.protocol->name;
|
||||
|
||||
instance->encoder.repeat = 5;
|
||||
instance->encoder.size_upload = 256;
|
||||
instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration));
|
||||
instance->encoder.is_running = false;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_protocol_encoder_allstar_firefly_free(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance = context;
|
||||
free(instance->encoder.upload);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generating an upload from data.
|
||||
* @param instance Pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
*/
|
||||
static void subghz_protocol_encoder_allstar_firefly_get_upload(
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance) {
|
||||
furi_assert(instance);
|
||||
size_t index = 0;
|
||||
|
||||
// Send key and GAP
|
||||
for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) {
|
||||
if(bit_read(instance->generic.data, i - 1)) {
|
||||
// Send bit 1
|
||||
instance->encoder.upload[index++] =
|
||||
level_duration_make(true, (uint32_t)subghz_protocol_allstar_firefly_const.te_long);
|
||||
if(i == 1) {
|
||||
//Send gap if bit was last
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)(subghz_protocol_allstar_firefly_const.te_short * 50) + 400);
|
||||
} else {
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)subghz_protocol_allstar_firefly_const.te_short);
|
||||
}
|
||||
} else {
|
||||
// Send bit 0
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
true, (uint32_t)subghz_protocol_allstar_firefly_const.te_short);
|
||||
if(i == 1) {
|
||||
//Send gap if bit was last
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)(subghz_protocol_allstar_firefly_const.te_short * 50) + 400);
|
||||
} else {
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)subghz_protocol_allstar_firefly_const.te_long);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance->encoder.size_upload = index;
|
||||
return;
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance = context;
|
||||
SubGhzProtocolStatus ret = SubGhzProtocolStatusError;
|
||||
do {
|
||||
ret = subghz_block_generic_deserialize_check_count_bit(
|
||||
&instance->generic,
|
||||
flipper_format,
|
||||
subghz_protocol_allstar_firefly_const.min_count_bit_for_found);
|
||||
if(ret != SubGhzProtocolStatusOk) {
|
||||
break;
|
||||
}
|
||||
// Optional value
|
||||
flipper_format_read_uint32(
|
||||
flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1);
|
||||
|
||||
subghz_protocol_encoder_allstar_firefly_get_upload(instance);
|
||||
instance->encoder.is_running = true;
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void subghz_protocol_encoder_allstar_firefly_stop(void* context) {
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance = context;
|
||||
instance->encoder.is_running = false;
|
||||
}
|
||||
|
||||
LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context) {
|
||||
SubGhzProtocolEncoderAllstarFirefly* instance = context;
|
||||
|
||||
if(instance->encoder.repeat == 0 || !instance->encoder.is_running) {
|
||||
instance->encoder.is_running = false;
|
||||
return level_duration_reset();
|
||||
}
|
||||
|
||||
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
|
||||
|
||||
if(++instance->encoder.front == instance->encoder.size_upload) {
|
||||
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
|
||||
instance->encoder.front = 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment) {
|
||||
UNUSED(environment);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance =
|
||||
malloc(sizeof(SubGhzProtocolDecoderAllstarFirefly));
|
||||
instance->base.protocol = &subghz_protocol_allstar_firefly;
|
||||
instance->generic.protocol_name = instance->base.protocol->name;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_allstar_firefly_free(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
free(instance);
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_allstar_firefly_reset(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepReset;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_allstar_firefly_feed(
|
||||
void* context,
|
||||
bool level,
|
||||
volatile uint32_t duration) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
|
||||
// Allstar Firefly Decoder
|
||||
// 2026 - by @jlaughter
|
||||
// Remake to match other protocols code base - @xMasterX (MMX)
|
||||
|
||||
switch(instance->decoder.parser_step) {
|
||||
case AllstarFireflyDecoderStepReset:
|
||||
if((!level) &&
|
||||
(DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short * 50) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta * 5)) {
|
||||
// 30k us
|
||||
// Found GAP
|
||||
instance->decoder.decode_data = 0;
|
||||
instance->decoder.decode_count_bit = 0;
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration;
|
||||
}
|
||||
break;
|
||||
case AllstarFireflyDecoderStepSaveDuration:
|
||||
if(level) {
|
||||
instance->decoder.te_last = duration;
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepCheckDuration;
|
||||
} else {
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case AllstarFireflyDecoderStepCheckDuration:
|
||||
if(!level) {
|
||||
// Bit 1 is long and short timing = 4000us HIGH (te_last) and 600us LOW
|
||||
if((DURATION_DIFF(
|
||||
instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_long) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta) &&
|
||||
(DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 1);
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration;
|
||||
// Bit 0 is short and long timing = 600us HIGH (te_last) and 4000us LOW
|
||||
} else if(
|
||||
(DURATION_DIFF(
|
||||
instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_short) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta) &&
|
||||
(DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_long) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 0);
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepSaveDuration;
|
||||
} else if(
|
||||
// End of the key
|
||||
DURATION_DIFF(duration, subghz_protocol_allstar_firefly_const.te_short * 50) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta * 5) {
|
||||
// Found next GAP and add bit 0 or 1
|
||||
if((DURATION_DIFF(
|
||||
instance->decoder.te_last, subghz_protocol_allstar_firefly_const.te_long) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 1);
|
||||
}
|
||||
if((DURATION_DIFF(
|
||||
instance->decoder.te_last,
|
||||
subghz_protocol_allstar_firefly_const.te_short) <
|
||||
subghz_protocol_allstar_firefly_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 0);
|
||||
}
|
||||
// If got 18 bits key reading is finished
|
||||
if(instance->decoder.decode_count_bit ==
|
||||
subghz_protocol_allstar_firefly_const.min_count_bit_for_found) {
|
||||
instance->generic.data = instance->decoder.decode_data;
|
||||
instance->generic.data_count_bit = instance->decoder.decode_count_bit;
|
||||
|
||||
if(instance->base.callback)
|
||||
instance->base.callback(&instance->base, instance->base.context);
|
||||
}
|
||||
instance->decoder.decode_data = 0;
|
||||
instance->decoder.decode_count_bit = 0;
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepReset;
|
||||
} else {
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
instance->decoder.parser_step = AllstarFireflyDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
return subghz_protocol_blocks_get_hash_data_long(
|
||||
&instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
SubGhzRadioPreset* preset) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
return subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
return subghz_block_generic_deserialize_check_count_bit(
|
||||
&instance->generic,
|
||||
flipper_format,
|
||||
subghz_protocol_allstar_firefly_const.min_count_bit_for_found);
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_allstar_firefly_get_string(void* context, FuriString* output) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderAllstarFirefly* instance = context;
|
||||
|
||||
uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key(
|
||||
instance->generic.data, instance->generic.data_count_bit);
|
||||
|
||||
furi_string_cat_printf(
|
||||
output,
|
||||
"%s %db\r\n"
|
||||
"Key:0x%05lX Yek:0x%05lX\r\n"
|
||||
" +: " DIP_PATTERN "\r\n"
|
||||
" o: " DIP_PATTERN "\r\n"
|
||||
" -: " DIP_PATTERN "\r\n",
|
||||
instance->generic.protocol_name,
|
||||
instance->generic.data_count_bit,
|
||||
(uint32_t)(instance->generic.data & 0xFFFFF),
|
||||
(uint32_t)(code_found_reverse & 0xFFFFF),
|
||||
SHOW_DIP_P(instance->generic.data, DIP_P),
|
||||
SHOW_DIP_P(instance->generic.data, DIP_O),
|
||||
SHOW_DIP_P(instance->generic.data, DIP_N));
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* allstar_firefly.h - Allstar Firefly 318ALD31K native SubGHz protocol
|
||||
*
|
||||
* Registers the Allstar Firefly gate remote as a first-class SubGHz protocol,
|
||||
* supporting decode from air, save/load of .sub files, and retransmit.
|
||||
*
|
||||
* Protocol summary (measured from captured remotes):
|
||||
* Carrier : 318 MHz OOK (FuriHalSubGhzPresetOok650Async)
|
||||
* Code type : Static 9-bit trinary (Supertex ED-9 encoder IC)
|
||||
* Frame : 18 symbols (2 per bit), no separate sync pulse
|
||||
* Repeats : 20 frames per keypress
|
||||
* Inter-frame gap: ~30 440 us
|
||||
*
|
||||
* Symbol encoding - each bit = two (pulse, gap) pairs:
|
||||
* '+' ON : H H = [4045us HIGH, 607us LOW] x2
|
||||
* '-' OFF : L L = [530us HIGH, 4139us LOW] x2
|
||||
* '0' FLOAT : H L = [4045us HIGH, 607us LOW, 530us HIGH, 4139us LOW]
|
||||
*
|
||||
* Save file format:
|
||||
* Filetype: Flipper SubGhz Key File
|
||||
* Version: 1
|
||||
* Frequency: 318000000
|
||||
* Preset: FuriHalSubGhzPresetOok650Async
|
||||
* Protocol: Allstar Firefly
|
||||
* Key: +--000++-
|
||||
*
|
||||
* To register with the SubGHz app, in protocol_list.c add:
|
||||
* #include "allstar_firefly.h"
|
||||
* &subghz_protocol_allstar_firefly, (in the protocol array)
|
||||
*/
|
||||
|
||||
#include "base.h"
|
||||
|
||||
/* Protocol name (must match what is written to .sub files) */
|
||||
#define SUBGHZ_PROTOCOL_ALLSTAR_FIREFLY_NAME "Allstar Firefly"
|
||||
|
||||
typedef struct SubGhzProtocolDecoderAllstarFirefly SubGhzProtocolDecoderAllstarFirefly;
|
||||
typedef struct SubGhzProtocolEncoderAllstarFirefly SubGhzProtocolEncoderAllstarFirefly;
|
||||
|
||||
extern const SubGhzProtocolDecoder subghz_protocol_allstar_firefly_decoder;
|
||||
extern const SubGhzProtocolEncoder subghz_protocol_allstar_firefly_encoder;
|
||||
extern const SubGhzProtocol subghz_protocol_allstar_firefly;
|
||||
|
||||
/**
|
||||
* Allocate SubGhzProtocolEncoderAllstarFirefly.
|
||||
* @param environment Pointer to a SubGhzEnvironment instance
|
||||
* @return SubGhzProtocolEncoderAllstarFirefly* pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
*/
|
||||
void* subghz_protocol_encoder_allstar_firefly_alloc(SubGhzEnvironment* environment);
|
||||
|
||||
/**
|
||||
* Free SubGhzProtocolEncoderAllstarFirefly.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
*/
|
||||
void subghz_protocol_encoder_allstar_firefly_free(void* context);
|
||||
|
||||
/**
|
||||
* Deserialize and generating an upload to send.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus subghz_protocol_encoder_allstar_firefly_deserialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format);
|
||||
|
||||
/**
|
||||
* Forced transmission stop.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
*/
|
||||
void subghz_protocol_encoder_allstar_firefly_stop(void* context);
|
||||
|
||||
/**
|
||||
* Getting the level and duration of the upload to be loaded into DMA.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderAllstarFirefly instance
|
||||
* @return LevelDuration
|
||||
*/
|
||||
LevelDuration subghz_protocol_encoder_allstar_firefly_yield(void* context);
|
||||
|
||||
/**
|
||||
* Allocate SubGhzProtocolDecoderAllstarFirefly.
|
||||
* @param environment Pointer to a SubGhzEnvironment instance
|
||||
* @return SubGhzProtocolDecoderAllstarFirefly* pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
*/
|
||||
void* subghz_protocol_decoder_allstar_firefly_alloc(SubGhzEnvironment* environment);
|
||||
|
||||
/**
|
||||
* Free SubGhzProtocolDecoderAllstarFirefly.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
*/
|
||||
void subghz_protocol_decoder_allstar_firefly_free(void* context);
|
||||
|
||||
/**
|
||||
* Reset decoder SubGhzProtocolDecoderAllstarFirefly.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
*/
|
||||
void subghz_protocol_decoder_allstar_firefly_reset(void* context);
|
||||
|
||||
/**
|
||||
* Parse a raw sequence of levels and durations received from the air.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
* @param level Signal level true-high false-low
|
||||
* @param duration Duration of this level in, us
|
||||
*/
|
||||
void subghz_protocol_decoder_allstar_firefly_feed(void* context, bool level, uint32_t duration);
|
||||
|
||||
/**
|
||||
* Getting the hash sum of the last randomly received parcel.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
* @return hash Hash sum
|
||||
*/
|
||||
uint32_t subghz_protocol_decoder_allstar_firefly_get_hash_data(void* context);
|
||||
|
||||
/**
|
||||
* Serialize data SubGhzProtocolDecoderAllstarFirefly.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @param preset The modulation on which the signal was received, SubGhzRadioPreset
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_serialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
SubGhzRadioPreset* preset);
|
||||
|
||||
/**
|
||||
* Deserialize data SubGhzProtocolDecoderAllstarFirefly.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_allstar_firefly_deserialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format);
|
||||
|
||||
/**
|
||||
* Getting a textual representation of the received data.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderAllstarFirefly instance
|
||||
* @param output Resulting text
|
||||
*/
|
||||
void subghz_protocol_decoder_allstar_firefly_get_string(void* context, FuriString* output);
|
||||
@@ -260,6 +260,10 @@ static void subghz_protocol_encoder_came_atomo_get_upload(
|
||||
btn = 0x4;
|
||||
} else if(btn == 0x4) {
|
||||
btn = 0x6;
|
||||
} else if(btn == 0x5) {
|
||||
btn = 0x0C;
|
||||
} else if(btn == 0x6) {
|
||||
btn = 0x0E;
|
||||
}
|
||||
|
||||
// override button if we change it with signal settings button editor
|
||||
@@ -484,9 +488,11 @@ void subghz_protocol_decoder_came_atomo_feed(void* context, bool level, uint32_t
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
switch(instance->decoder.parser_step) {
|
||||
case CameAtomoDecoderStepReset:
|
||||
// There are two known options for the header: 72K us (TOP42R, TOP44R) or 12k us (found on TOP44RBN)
|
||||
// There are two known options for the header: 72K us (TOP42R, TOP44R) or 12k us (found on TOP44RBN) / 19k us (TOPD4REN)
|
||||
if((!level) && ((DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 10) <
|
||||
subghz_protocol_came_atomo_const.te_delta * 20) ||
|
||||
(DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 16) <
|
||||
subghz_protocol_came_atomo_const.te_delta * 10) ||
|
||||
(DURATION_DIFF(duration, subghz_protocol_came_atomo_const.te_long * 60) <
|
||||
subghz_protocol_came_atomo_const.te_delta * 40))) {
|
||||
//Found header CAME
|
||||
@@ -663,6 +669,10 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins
|
||||
instance->btn = 0x3;
|
||||
} else if(btn_decode == 0x6) {
|
||||
instance->btn = 0x4;
|
||||
} else if(btn_decode == 0x0C) {
|
||||
instance->btn = 0x5;
|
||||
} else if(btn_decode == 0x0E) {
|
||||
instance->btn = 0x6;
|
||||
}
|
||||
|
||||
uint32_t hi = pack[0] << 24 | pack[1] << 16 | pack[2] << 8 | pack[3];
|
||||
@@ -673,7 +683,7 @@ static void subghz_protocol_came_atomo_remote_controller(SubGhzBlockGeneric* ins
|
||||
if(subghz_custom_btn_get_original() == 0) {
|
||||
subghz_custom_btn_set_original(instance->btn);
|
||||
}
|
||||
subghz_custom_btn_set_max(3);
|
||||
subghz_custom_btn_set_max(4);
|
||||
}
|
||||
|
||||
void atomo_encrypt(uint8_t* buff) {
|
||||
@@ -740,6 +750,12 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) {
|
||||
case 0x4:
|
||||
btn = 0x1;
|
||||
break;
|
||||
case 0x5:
|
||||
btn = 0x1;
|
||||
break;
|
||||
case 0x6:
|
||||
btn = 0x1;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -758,6 +774,12 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) {
|
||||
case 0x4:
|
||||
btn = 0x2;
|
||||
break;
|
||||
case 0x5:
|
||||
btn = 0x2;
|
||||
break;
|
||||
case 0x6:
|
||||
btn = 0x2;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -776,6 +798,36 @@ static uint8_t subghz_protocol_came_atomo_get_btn_code(void) {
|
||||
case 0x4:
|
||||
btn = 0x3;
|
||||
break;
|
||||
case 0x5:
|
||||
btn = 0x4;
|
||||
break;
|
||||
case 0x6:
|
||||
btn = 0x4;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if(custom_btn_id == SUBGHZ_CUSTOM_BTN_RIGHT) {
|
||||
switch(original_btn_code) {
|
||||
case 0x1:
|
||||
btn = 0x5;
|
||||
break;
|
||||
case 0x2:
|
||||
btn = 0x5;
|
||||
break;
|
||||
case 0x3:
|
||||
btn = 0x5;
|
||||
break;
|
||||
case 0x4:
|
||||
btn = 0x5;
|
||||
break;
|
||||
case 0x5:
|
||||
btn = 0x6;
|
||||
break;
|
||||
case 0x6:
|
||||
btn = 0x5;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -111,7 +111,7 @@ void* subghz_protocol_encoder_came_twee_alloc(SubGhzEnvironment* environment) {
|
||||
instance->base.protocol = &subghz_protocol_came_twee;
|
||||
instance->generic.protocol_name = instance->base.protocol->name;
|
||||
|
||||
instance->encoder.repeat = 3;
|
||||
instance->encoder.repeat = 1;
|
||||
instance->encoder.size_upload = 1536; // 1308
|
||||
instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration));
|
||||
instance->encoder.is_running = false;
|
||||
@@ -324,9 +324,13 @@ void subghz_protocol_decoder_came_twee_feed(void* context, bool level, uint32_t
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
switch(instance->decoder.parser_step) {
|
||||
case CameTweeDecoderStepReset:
|
||||
if((!level) && (DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 51) <
|
||||
subghz_protocol_came_twee_const.te_delta * 20)) {
|
||||
//Found header CAME
|
||||
if((!level) && ((DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 51) <
|
||||
subghz_protocol_came_twee_const.te_delta * 20) ||
|
||||
(DURATION_DIFF(duration, subghz_protocol_came_twee_const.te_long * 12) <
|
||||
subghz_protocol_came_twee_const.te_delta * 10))) {
|
||||
// Found header CAME
|
||||
// Original TWEE uses 51k us delay
|
||||
// TOP44FGN uses 12k us delay
|
||||
instance->decoder.parser_step = CameTweeDecoderStepDecoderData;
|
||||
instance->decoder.decode_data = 0;
|
||||
instance->decoder.decode_count_bit = 0;
|
||||
|
||||
@@ -125,6 +125,41 @@ void subghz_protocol_encoder_faac_slh_free(void* context) {
|
||||
free(instance);
|
||||
}
|
||||
|
||||
static bool subghz_protocol_faac_slh_encrypt(SubGhzProtocolEncoderFaacSLH* instance) {
|
||||
uint32_t fix = instance->generic.serial << 4 | instance->generic.btn;
|
||||
uint32_t hop = 0;
|
||||
uint32_t decrypt = 0;
|
||||
uint64_t man = 0;
|
||||
char fixx[8] = {};
|
||||
int shiftby = 32;
|
||||
|
||||
for(int i = 0; i < 8; i++) {
|
||||
fixx[i] = (fix >> (shiftby -= 4)) & 0xF;
|
||||
}
|
||||
|
||||
if((instance->generic.cnt % 2) == 0) {
|
||||
decrypt = fixx[6] << 28 | fixx[7] << 24 | fixx[5] << 20 |
|
||||
(instance->generic.cnt & 0xFFFFF);
|
||||
} else {
|
||||
decrypt = fixx[2] << 28 | fixx[3] << 24 | fixx[4] << 20 |
|
||||
(instance->generic.cnt & 0xFFFFF);
|
||||
}
|
||||
for
|
||||
M_EACH(manufacture_code, *subghz_keystore_get_data(instance->keystore), SubGhzKeyArray_t) {
|
||||
if(strcmp(furi_string_get_cstr(manufacture_code->name), "FAAC_SLH") == 0) {
|
||||
//FAAC Learning
|
||||
man = subghz_protocol_keeloq_common_faac_learning(
|
||||
instance->generic.seed, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(hop) {
|
||||
instance->generic.data = (uint64_t)fix << 32 | hop;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* instance) {
|
||||
// override button if we change it with signal settings button editor
|
||||
// else work as standart
|
||||
@@ -237,16 +272,6 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t fix = instance->generic.serial << 4 | instance->generic.btn;
|
||||
uint32_t hop = 0;
|
||||
uint32_t decrypt = 0;
|
||||
uint64_t man = 0;
|
||||
int res = 0;
|
||||
char fixx[8] = {};
|
||||
int shiftby = 32;
|
||||
for(int i = 0; i < 8; i++) {
|
||||
fixx[i] = (fix >> (shiftby -= 4)) & 0xF;
|
||||
}
|
||||
|
||||
if(allow_zero_seed || (instance->generic.seed != 0x0)) {
|
||||
// check OFEX mode
|
||||
@@ -277,32 +302,7 @@ static bool subghz_protocol_faac_slh_gen_data(SubGhzProtocolEncoderFaacSLH* inst
|
||||
}
|
||||
}
|
||||
|
||||
if((instance->generic.cnt % 2) == 0) {
|
||||
decrypt = fixx[6] << 28 | fixx[7] << 24 | fixx[5] << 20 |
|
||||
(instance->generic.cnt & 0xFFFFF);
|
||||
} else {
|
||||
decrypt = fixx[2] << 28 | fixx[3] << 24 | fixx[4] << 20 |
|
||||
(instance->generic.cnt & 0xFFFFF);
|
||||
}
|
||||
for
|
||||
M_EACH(manufacture_code, *subghz_keystore_get_data(instance->keystore), SubGhzKeyArray_t) {
|
||||
res = strcmp(furi_string_get_cstr(manufacture_code->name), instance->manufacture_name);
|
||||
if(res == 0) {
|
||||
switch(manufacture_code->type) {
|
||||
case KEELOQ_LEARNING_FAAC:
|
||||
//FAAC Learning
|
||||
man = subghz_protocol_keeloq_common_faac_learning(
|
||||
instance->generic.seed, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(hop) {
|
||||
instance->generic.data = (uint64_t)fix << 32 | hop;
|
||||
}
|
||||
return true;
|
||||
return subghz_protocol_faac_slh_encrypt(instance);
|
||||
}
|
||||
|
||||
bool subghz_protocol_faac_slh_create_data(
|
||||
@@ -324,7 +324,7 @@ bool subghz_protocol_faac_slh_create_data(
|
||||
instance->manufacture_name = manufacture_name;
|
||||
instance->generic.data_count_bit = 64;
|
||||
allow_zero_seed = true;
|
||||
bool res = subghz_protocol_faac_slh_gen_data(instance);
|
||||
bool res = subghz_protocol_faac_slh_encrypt(instance);
|
||||
if(res) {
|
||||
return SubGhzProtocolStatusOk ==
|
||||
subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
|
||||
@@ -620,8 +620,7 @@ static void subghz_protocol_faac_slh_check_remote_controller(
|
||||
|
||||
for
|
||||
M_EACH(manufacture_code, *subghz_keystore_get_data(keystore), SubGhzKeyArray_t) {
|
||||
switch(manufacture_code->type) {
|
||||
case KEELOQ_LEARNING_FAAC:
|
||||
if(strcmp(furi_string_get_cstr(manufacture_code->name), "FAAC_SLH") == 0) {
|
||||
// FAAC Learning
|
||||
man = subghz_protocol_keeloq_common_faac_learning(
|
||||
instance->seed, manufacture_code->key);
|
||||
|
||||
@@ -369,7 +369,7 @@ static bool subghz_protocol_keeloq_gen_data(
|
||||
} else if(
|
||||
(strcmp(instance->manufacture_name, "DTM_Neo") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "FAAC_RC,XT") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Mutanco_Mutancode") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Clemsa_Mutancode") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Came_Space") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Genius_Bravo") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "GSN") == 0) ||
|
||||
@@ -378,20 +378,33 @@ static bool subghz_protocol_keeloq_gen_data(
|
||||
(strcmp(instance->manufacture_name, "Pecinin") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Steelmate") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Cardin_S449") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Stilmatic") == 0)) {
|
||||
(strcmp(instance->manufacture_name, "Stilmatic") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Wisniowski") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "ATA_PTX4") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Fadini") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Seav") == 0)) {
|
||||
// DTM Neo, Came_Space uses 12bit serial -> simple learning
|
||||
// FAAC_RC,XT , Mutanco_Mutancode, Genius_Bravo, GSN 12bit serial -> normal learning
|
||||
// FAAC_RC,XT , Clemsa_Mutancode, Genius_Bravo, GSN 12bit serial -> normal learning
|
||||
// Rosh, Rossi, Pecinin -> 12bit serial - simple learning
|
||||
// Steelmate -> 12bit serial - normal learning
|
||||
// Cardin_S449 -> 12bit serial - normal learning
|
||||
// Stilmatic (r-tech) -> 12bit serial - normal learning
|
||||
// Wisniowski -> 12bit serial - normal learning
|
||||
// ATA_PTX4 -> 12bit serial - normal learning
|
||||
// Fadini -> 12bit serial - simple learning
|
||||
// Seav -> 12bit serial - normal learning
|
||||
decrypt = btn << 28 | (instance->generic.serial & 0xFFF) << 16 |
|
||||
instance->generic.cnt;
|
||||
} else if(
|
||||
(strcmp(instance->manufacture_name, "NICE_Smilo") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "NICE_MHOUSE") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "JCM_Tech") == 0)) {
|
||||
// Nice Smilo, MHouse, JCM -> 8bit serial - simple learning
|
||||
(strcmp(instance->manufacture_name, "JCM_Tech") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Pujol_Vario") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Pujol") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Erreka") == 0)) {
|
||||
// Nice Smilo, MHouse, JCM, Pujol_Vario -> 8bit serial - simple learning
|
||||
// Pujol -> 8bit serial - special learning
|
||||
// Erreka -> 8bit serial - secure learning with seed
|
||||
decrypt = btn << 28 | (instance->generic.serial & 0xFF) << 16 |
|
||||
instance->generic.cnt;
|
||||
} else if(
|
||||
@@ -455,6 +468,28 @@ static bool subghz_protocol_keeloq_gen_data(
|
||||
fix, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
case KEELOQ_LEARNING_AERF:
|
||||
man = subghz_protocol_keeloq_common_learning_aerf(
|
||||
fix, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
case KEELOQ_LEARNING_ERREKA:
|
||||
man = subghz_protocol_keeloq_common_learning_erreka(
|
||||
fix, instance->generic.seed, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
case KEELOQ_LEARNING_PUJOL:
|
||||
man = subghz_protocol_keeloq_common_learning_pujol(
|
||||
fix, manufacture_code->key);
|
||||
hop = subghz_protocol_keeloq_common_encrypt(decrypt, man);
|
||||
break;
|
||||
case KEELOQ_LEARNING_SIMPLE_JCM:
|
||||
//Simple Learning 8 bit serial
|
||||
decrypt = btn << 28 | (instance->generic.serial & 0xFF) << 16 |
|
||||
instance->generic.cnt;
|
||||
hop = subghz_protocol_keeloq_common_encrypt(
|
||||
decrypt, manufacture_code->key);
|
||||
break;
|
||||
case KEELOQ_LEARNING_UNKNOWN:
|
||||
if(kl_type_en == 1) {
|
||||
hop = subghz_protocol_keeloq_common_encrypt(
|
||||
@@ -513,7 +548,7 @@ bool subghz_protocol_keeloq_create_data(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool subghz_protocol_keeloq_bft_create_data(
|
||||
bool subghz_protocol_keeloq_seed_create_data(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
uint32_t serial,
|
||||
@@ -1103,6 +1138,55 @@ static uint32_t subghz_protocol_keeloq_check_remote_controller_selector(
|
||||
return decrypt;
|
||||
}
|
||||
break;
|
||||
case KEELOQ_LEARNING_AERF:
|
||||
man = subghz_protocol_keeloq_common_learning_aerf(fix, manufacture_code->key);
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt_derived(hop, man, 0x240u);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt_derived(hop, man, 0x210u);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt(hop, man);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
break;
|
||||
case KEELOQ_LEARNING_ERREKA:
|
||||
man = subghz_protocol_keeloq_common_learning_erreka(
|
||||
fix, instance->seed, manufacture_code->key);
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt(hop, man);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
break;
|
||||
case KEELOQ_LEARNING_PUJOL:
|
||||
man = subghz_protocol_keeloq_common_learning_pujol(fix, manufacture_code->key);
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt(hop, man);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
break;
|
||||
case KEELOQ_LEARNING_SIMPLE_JCM:
|
||||
// Simple Learning 8 bit serial
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key);
|
||||
if(subghz_protocol_keeloq_check_decrypt(instance, decrypt, btn, end_serial)) {
|
||||
*manufacture_name = furi_string_get_cstr(manufacture_code->name);
|
||||
keystore->mfname = *manufacture_name;
|
||||
return decrypt;
|
||||
}
|
||||
break;
|
||||
case KEELOQ_LEARNING_UNKNOWN:
|
||||
// Simple Learning
|
||||
decrypt = subghz_protocol_keeloq_common_decrypt(hop, manufacture_code->key);
|
||||
@@ -1627,7 +1711,9 @@ void subghz_protocol_decoder_keeloq_get_string(void* context, FuriString* output
|
||||
code_found_reverse_lo,
|
||||
instance->generic.btn,
|
||||
instance->manufacture_name);
|
||||
} else if(strcmp(instance->manufacture_name, "BFT") == 0) {
|
||||
} else if(
|
||||
(strcmp(instance->manufacture_name, "BFT") == 0) ||
|
||||
(strcmp(instance->manufacture_name, "Erreka") == 0)) {
|
||||
// Allow counter edit
|
||||
subghz_block_generic_global.cnt_is_available = true;
|
||||
subghz_block_generic_global.cnt_length_bit = 16;
|
||||
|
||||
@@ -140,3 +140,97 @@ inline uint64_t
|
||||
subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man) {
|
||||
return (man & 0xFFFFFFFFFF000000) | (data & 0xFFFFFF);
|
||||
}
|
||||
|
||||
// Key utils
|
||||
|
||||
static inline uint32_t subghz_protocol_keeloq_common_manufacturer_nl_extend(
|
||||
uint32_t x,
|
||||
uint32_t k_lo,
|
||||
uint32_t k_hi,
|
||||
uint32_t outer_limit) {
|
||||
uint32_t r4 = outer_limit;
|
||||
uint32_t r5 = 0u;
|
||||
const uint32_t r6 = KEELOQ_NLF;
|
||||
|
||||
while(r5 != r4) {
|
||||
if(r5 < 0x210u) {
|
||||
uint32_t r1 = (x >> 15) & 1u;
|
||||
uint32_t r7 = r1 ^ ((x >> 1) | (x << 31));
|
||||
r1 = (15u - r5) & 0x3Fu;
|
||||
uint32_t lr = 32u - r1;
|
||||
uint32_t ip = r1 - 32u;
|
||||
lr = k_hi << lr;
|
||||
r1 = k_lo >> r1;
|
||||
ip = (r1 < 32u) ? (k_hi >> ip) : 0u;
|
||||
r1 = (r1 | lr | ip) & 1u;
|
||||
ip = (x >> 30) & 1u;
|
||||
r1 ^= r7;
|
||||
r7 = (x >> 25) & 1u;
|
||||
r7 += ip << 1;
|
||||
ip = (x >> 19) & 1u;
|
||||
ip += r7 << 1;
|
||||
r7 = (x >> 8) & 1u;
|
||||
r7 += ip << 1;
|
||||
x &= 1u;
|
||||
x += r7 << 1;
|
||||
x = (int32_t)r6 >> (x & 31u);
|
||||
x &= 1u;
|
||||
x ^= r1;
|
||||
}
|
||||
r5 += 1u;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
static inline uint32_t subghz_protocol_keeloq_common_word_rotate16(uint32_t v) {
|
||||
return (v >> 16) | (v << 16);
|
||||
}
|
||||
|
||||
inline uint32_t subghz_protocol_keeloq_common_decrypt_derived(
|
||||
uint32_t hop_encrypted,
|
||||
uint64_t derived_manufacturing_key,
|
||||
uint32_t outer_limit) {
|
||||
return subghz_protocol_keeloq_common_manufacturer_nl_extend(
|
||||
hop_encrypted,
|
||||
(uint32_t)derived_manufacturing_key,
|
||||
(uint32_t)(derived_manufacturing_key >> 32u),
|
||||
outer_limit);
|
||||
}
|
||||
|
||||
// Protocol (Manufacturer) specific learning
|
||||
// TODO: Better documentation for these functions
|
||||
|
||||
inline uint64_t subghz_protocol_keeloq_common_learning_aerf(uint32_t data, const uint64_t key) {
|
||||
uint32_t k_lo = (uint32_t)key;
|
||||
uint32_t k_hi = (uint32_t)(key >> 32);
|
||||
uint32_t d = data & 0x0FFFFFFFu;
|
||||
uint32_t x = d | 0x20000000u;
|
||||
x = subghz_protocol_keeloq_common_manufacturer_nl_extend(x, k_lo, k_hi, 0x40u);
|
||||
uint32_t k1 = x;
|
||||
x = d | 0x60000000u;
|
||||
x = subghz_protocol_keeloq_common_manufacturer_nl_extend(x, k_lo, k_hi, 0x40u);
|
||||
return ((uint64_t)x << 32) | k1;
|
||||
}
|
||||
|
||||
inline uint64_t
|
||||
subghz_protocol_keeloq_common_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key) {
|
||||
uint32_t d = data & 0x0FFFFFFFu;
|
||||
uint32_t k1 = subghz_protocol_keeloq_common_decrypt(d | 0x20000000u, key);
|
||||
uint32_t r4 = mix >> 4;
|
||||
uint32_t r1 = (mix << 4) & 0xF000F000u;
|
||||
r4 = (r4 & 0x0F000F00u) | r1;
|
||||
uint32_t r5 = mix & 0x00FF00FFu;
|
||||
uint32_t x = r4 | r5;
|
||||
x |= 0x60000000u;
|
||||
uint32_t k2 = subghz_protocol_keeloq_common_decrypt(x, key);
|
||||
return ((uint64_t)k2 << 32) | k1;
|
||||
}
|
||||
|
||||
inline uint64_t subghz_protocol_keeloq_common_learning_pujol(uint32_t data, const uint64_t key) {
|
||||
uint32_t d = data & 0x0FFFFFFFu;
|
||||
uint32_t w1 = subghz_protocol_keeloq_common_decrypt(d | 0x20000000u, key);
|
||||
uint32_t w2 = subghz_protocol_keeloq_common_decrypt(d | 0x60000000u, key);
|
||||
uint32_t k1 = subghz_protocol_keeloq_common_word_rotate16(w1);
|
||||
uint32_t k2 = subghz_protocol_keeloq_common_word_rotate16(w2);
|
||||
return ((uint64_t)k2 << 32) | k1;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
// #define BENINCA_ARC_KEY_TYPE 9u -- RESERVED
|
||||
#define KEELOQ_LEARNING_SIMPLE_KINGGATES 10u
|
||||
#define KEELOQ_LEARNING_NORMAL_JAROLIFT 11u
|
||||
#define KEELOQ_LEARNING_ERREKA 12u
|
||||
#define KEELOQ_LEARNING_PUJOL 13u
|
||||
#define KEELOQ_LEARNING_AERF 14u
|
||||
#define KEELOQ_LEARNING_SIMPLE_JCM 15u
|
||||
|
||||
/**
|
||||
* Simple Learning Encrypt
|
||||
@@ -101,3 +105,19 @@ uint64_t subghz_protocol_keeloq_common_magic_serial_type2_learning(uint32_t data
|
||||
*/
|
||||
|
||||
uint64_t subghz_protocol_keeloq_common_magic_serial_type3_learning(uint32_t data, uint64_t man);
|
||||
|
||||
// Protocol (Manufacturer) specific learning
|
||||
// TODO: Better documentation for these functions
|
||||
|
||||
uint64_t subghz_protocol_keeloq_common_learning_aerf(uint32_t data, const uint64_t key);
|
||||
|
||||
uint64_t
|
||||
subghz_protocol_keeloq_common_learning_erreka(uint32_t data, uint32_t mix, const uint64_t key);
|
||||
|
||||
uint64_t subghz_protocol_keeloq_common_learning_pujol(uint32_t data, const uint64_t key);
|
||||
|
||||
// Utils
|
||||
uint32_t subghz_protocol_keeloq_common_decrypt_derived(
|
||||
uint32_t hop_encrypted,
|
||||
uint64_t derived_manufacturing_key,
|
||||
uint32_t outer_limit);
|
||||
|
||||
@@ -570,8 +570,8 @@ static void subghz_protocol_kinggates_stylo_4k_remote_controller(
|
||||
if(((decrypt >> 28) == instance->btn) && (((decrypt >> 24) & 0x0F) == 0x0C) &&
|
||||
(((decrypt >> 16) & 0xFF) == (instance->serial & 0xFF))) {
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(ret) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
static const SubGhzBlockConst subghz_protocol_nice_flo_const = {
|
||||
.te_short = 700,
|
||||
.te_long = 1400,
|
||||
.te_delta = 200,
|
||||
.te_delta = 250,
|
||||
.min_count_bit_for_found = 12,
|
||||
};
|
||||
|
||||
@@ -213,8 +213,8 @@ void subghz_protocol_decoder_nice_flo_feed(void* context, bool level, uint32_t d
|
||||
switch(instance->decoder.parser_step) {
|
||||
case NiceFloDecoderStepReset:
|
||||
if((!level) && (DURATION_DIFF(duration, subghz_protocol_nice_flo_const.te_short * 36) <
|
||||
subghz_protocol_nice_flo_const.te_delta * 36)) {
|
||||
//Found header Nice Flo
|
||||
subghz_protocol_nice_flo_const.te_delta * 29)) {
|
||||
//Found header Nice Flo (25200us +- 7250us)
|
||||
instance->decoder.parser_step = NiceFloDecoderStepFoundStartBit;
|
||||
}
|
||||
break;
|
||||
@@ -328,8 +328,8 @@ void subghz_protocol_decoder_nice_flo_get_string(void* context, FuriString* outp
|
||||
furi_string_cat_printf(
|
||||
output,
|
||||
"%s %dbit\r\n"
|
||||
"Key:0x%08lX\r\n"
|
||||
"Yek:0x%08lX\r\n",
|
||||
"Key:0x%06lX\r\n"
|
||||
"Yek:0x%06lX\r\n",
|
||||
instance->generic.protocol_name,
|
||||
instance->generic.data_count_bit,
|
||||
code_found_lo,
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
#include "nord_ice.h"
|
||||
#include "../blocks/const.h"
|
||||
#include "../blocks/decoder.h"
|
||||
#include "../blocks/encoder.h"
|
||||
#include "../blocks/generic.h"
|
||||
#include "../blocks/math.h"
|
||||
|
||||
#define TAG "SubGhzProtocolNord_Ice"
|
||||
|
||||
static const SubGhzBlockConst subghz_protocol_nord_ice_const = {
|
||||
.te_short = 300,
|
||||
.te_long = 800,
|
||||
.te_delta = 150,
|
||||
.min_count_bit_for_found = 33,
|
||||
};
|
||||
|
||||
struct SubGhzProtocolDecoderNord_Ice {
|
||||
SubGhzProtocolDecoderBase base;
|
||||
|
||||
SubGhzBlockDecoder decoder;
|
||||
SubGhzBlockGeneric generic;
|
||||
};
|
||||
|
||||
struct SubGhzProtocolEncoderNord_Ice {
|
||||
SubGhzProtocolEncoderBase base;
|
||||
|
||||
SubGhzProtocolBlockEncoder encoder;
|
||||
SubGhzBlockGeneric generic;
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
Nord_IceDecoderStepReset = 0,
|
||||
Nord_IceDecoderStepSaveDuration,
|
||||
Nord_IceDecoderStepCheckDuration,
|
||||
} Nord_IceDecoderStep;
|
||||
|
||||
const SubGhzProtocolDecoder subghz_protocol_nord_ice_decoder = {
|
||||
.alloc = subghz_protocol_decoder_nord_ice_alloc,
|
||||
.free = subghz_protocol_decoder_nord_ice_free,
|
||||
|
||||
.feed = subghz_protocol_decoder_nord_ice_feed,
|
||||
.reset = subghz_protocol_decoder_nord_ice_reset,
|
||||
|
||||
.get_hash_data = NULL,
|
||||
.get_hash_data_long = subghz_protocol_decoder_nord_ice_get_hash_data,
|
||||
.serialize = subghz_protocol_decoder_nord_ice_serialize,
|
||||
.deserialize = subghz_protocol_decoder_nord_ice_deserialize,
|
||||
.get_string = subghz_protocol_decoder_nord_ice_get_string,
|
||||
.get_string_brief = NULL,
|
||||
};
|
||||
|
||||
const SubGhzProtocolEncoder subghz_protocol_nord_ice_encoder = {
|
||||
.alloc = subghz_protocol_encoder_nord_ice_alloc,
|
||||
.free = subghz_protocol_encoder_nord_ice_free,
|
||||
|
||||
.deserialize = subghz_protocol_encoder_nord_ice_deserialize,
|
||||
.stop = subghz_protocol_encoder_nord_ice_stop,
|
||||
.yield = subghz_protocol_encoder_nord_ice_yield,
|
||||
};
|
||||
|
||||
const SubGhzProtocol subghz_protocol_nord_ice = {
|
||||
.name = SUBGHZ_PROTOCOL_NORD_ICE_NAME,
|
||||
.type = SubGhzProtocolTypeStatic,
|
||||
.flag = SubGhzProtocolFlag_433 | SubGhzProtocolFlag_AM | SubGhzProtocolFlag_Decodable |
|
||||
SubGhzProtocolFlag_Load | SubGhzProtocolFlag_Save | SubGhzProtocolFlag_Send,
|
||||
|
||||
.decoder = &subghz_protocol_nord_ice_decoder,
|
||||
.encoder = &subghz_protocol_nord_ice_encoder,
|
||||
};
|
||||
|
||||
void* subghz_protocol_encoder_nord_ice_alloc(SubGhzEnvironment* environment) {
|
||||
UNUSED(environment);
|
||||
SubGhzProtocolEncoderNord_Ice* instance = malloc(sizeof(SubGhzProtocolEncoderNord_Ice));
|
||||
|
||||
instance->base.protocol = &subghz_protocol_nord_ice;
|
||||
instance->generic.protocol_name = instance->base.protocol->name;
|
||||
|
||||
instance->encoder.repeat = 3;
|
||||
instance->encoder.size_upload = 128;
|
||||
instance->encoder.upload = malloc(instance->encoder.size_upload * sizeof(LevelDuration));
|
||||
instance->encoder.is_running = false;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_protocol_encoder_nord_ice_free(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolEncoderNord_Ice* instance = context;
|
||||
free(instance->encoder.upload);
|
||||
free(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generating an upload from data.
|
||||
* @param instance Pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
*/
|
||||
static void subghz_protocol_encoder_nord_ice_get_upload(SubGhzProtocolEncoderNord_Ice* instance) {
|
||||
furi_assert(instance);
|
||||
size_t index = 0;
|
||||
|
||||
// Send key and GAP
|
||||
for(uint8_t i = instance->generic.data_count_bit; i > 0; i--) {
|
||||
if(bit_read(instance->generic.data, i - 1)) {
|
||||
// Send bit 1
|
||||
instance->encoder.upload[index++] =
|
||||
level_duration_make(true, (uint32_t)subghz_protocol_nord_ice_const.te_long);
|
||||
if(i == 1) {
|
||||
//Send gap if bit was last
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)subghz_protocol_nord_ice_const.te_short * 25);
|
||||
} else {
|
||||
instance->encoder.upload[index++] =
|
||||
level_duration_make(false, (uint32_t)subghz_protocol_nord_ice_const.te_short);
|
||||
}
|
||||
} else {
|
||||
// Send bit 0
|
||||
instance->encoder.upload[index++] =
|
||||
level_duration_make(true, (uint32_t)subghz_protocol_nord_ice_const.te_short);
|
||||
if(i == 1) {
|
||||
//Send gap if bit was last
|
||||
instance->encoder.upload[index++] = level_duration_make(
|
||||
false, (uint32_t)subghz_protocol_nord_ice_const.te_short * 25);
|
||||
} else {
|
||||
instance->encoder.upload[index++] =
|
||||
level_duration_make(false, (uint32_t)subghz_protocol_nord_ice_const.te_long);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance->encoder.size_upload = index;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysis of received data
|
||||
* @param instance Pointer to a SubGhzBlockGeneric* instance
|
||||
*/
|
||||
static void subghz_protocol_nord_ice_check_remote_controller(SubGhzBlockGeneric* instance) {
|
||||
instance->serial = (instance->data >> 15) << 9 |
|
||||
(instance->data & 0x1FF); // 26 bits for serial
|
||||
instance->btn = (instance->data >> 9) & 0x3F; // 6 bits for button
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus
|
||||
subghz_protocol_encoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolEncoderNord_Ice* instance = context;
|
||||
SubGhzProtocolStatus ret = SubGhzProtocolStatusError;
|
||||
do {
|
||||
ret = subghz_block_generic_deserialize_check_count_bit(
|
||||
&instance->generic,
|
||||
flipper_format,
|
||||
subghz_protocol_nord_ice_const.min_count_bit_for_found);
|
||||
if(ret != SubGhzProtocolStatusOk) {
|
||||
break;
|
||||
}
|
||||
// Optional value
|
||||
flipper_format_read_uint32(
|
||||
flipper_format, "Repeat", (uint32_t*)&instance->encoder.repeat, 1);
|
||||
|
||||
subghz_protocol_nord_ice_check_remote_controller(&instance->generic);
|
||||
subghz_protocol_encoder_nord_ice_get_upload(instance);
|
||||
instance->encoder.is_running = true;
|
||||
} while(false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void subghz_protocol_encoder_nord_ice_stop(void* context) {
|
||||
SubGhzProtocolEncoderNord_Ice* instance = context;
|
||||
instance->encoder.is_running = false;
|
||||
}
|
||||
|
||||
LevelDuration subghz_protocol_encoder_nord_ice_yield(void* context) {
|
||||
SubGhzProtocolEncoderNord_Ice* instance = context;
|
||||
|
||||
if(instance->encoder.repeat == 0 || !instance->encoder.is_running) {
|
||||
instance->encoder.is_running = false;
|
||||
return level_duration_reset();
|
||||
}
|
||||
|
||||
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
|
||||
|
||||
if(++instance->encoder.front == instance->encoder.size_upload) {
|
||||
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
|
||||
instance->encoder.front = 0;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void* subghz_protocol_decoder_nord_ice_alloc(SubGhzEnvironment* environment) {
|
||||
UNUSED(environment);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = malloc(sizeof(SubGhzProtocolDecoderNord_Ice));
|
||||
instance->base.protocol = &subghz_protocol_nord_ice;
|
||||
instance->generic.protocol_name = instance->base.protocol->name;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_nord_ice_free(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
free(instance);
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_nord_ice_reset(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepReset;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_nord_ice_feed(void* context, bool level, volatile uint32_t duration) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
|
||||
// Nord ICE Decoder
|
||||
// 2026.03 - @xMasterX (MMX)
|
||||
|
||||
// Key samples
|
||||
//
|
||||
// Serial Btn Serial
|
||||
// 0x9467688A btn 1 = 10010100011001110 110100 010001010
|
||||
// 0x9467308A btn 2 = 10010100011001110 011000 010001010
|
||||
// 0x9467628A btn 3 = 10010100011001110 110001 010001010
|
||||
// 0x9467648A btn 4 = 10010100011001110 110010 010001010
|
||||
|
||||
switch(instance->decoder.parser_step) {
|
||||
case Nord_IceDecoderStepReset:
|
||||
if((!level) && (DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short * 25) <
|
||||
subghz_protocol_nord_ice_const.te_delta * 11)) {
|
||||
//Found GAP
|
||||
instance->decoder.decode_data = 0;
|
||||
instance->decoder.decode_count_bit = 0;
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration;
|
||||
}
|
||||
break;
|
||||
case Nord_IceDecoderStepSaveDuration:
|
||||
if(level) {
|
||||
instance->decoder.te_last = duration;
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepCheckDuration;
|
||||
} else {
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case Nord_IceDecoderStepCheckDuration:
|
||||
if(!level) {
|
||||
// Bit 0 is short and long timing = 300us HIGH (te_last) and 800us LOW
|
||||
if((DURATION_DIFF(instance->decoder.te_last, subghz_protocol_nord_ice_const.te_short) <
|
||||
subghz_protocol_nord_ice_const.te_delta) &&
|
||||
(DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_long) <
|
||||
subghz_protocol_nord_ice_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 0);
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration;
|
||||
// Bit 1 is long and short timing = 800us HIGH (te_last) and 300us LOW
|
||||
} else if(
|
||||
(DURATION_DIFF(instance->decoder.te_last, subghz_protocol_nord_ice_const.te_long) <
|
||||
subghz_protocol_nord_ice_const.te_delta) &&
|
||||
(DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short) <
|
||||
subghz_protocol_nord_ice_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 1);
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepSaveDuration;
|
||||
} else if(
|
||||
// End of the key
|
||||
DURATION_DIFF(duration, subghz_protocol_nord_ice_const.te_short * 25) <
|
||||
subghz_protocol_nord_ice_const.te_delta * 11) {
|
||||
//Found next GAP and add bit 0 or 1 (only bit 0 was found on the remotes)
|
||||
if((DURATION_DIFF(
|
||||
instance->decoder.te_last, subghz_protocol_nord_ice_const.te_short) <
|
||||
subghz_protocol_nord_ice_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 0);
|
||||
}
|
||||
if((DURATION_DIFF(
|
||||
instance->decoder.te_last, subghz_protocol_nord_ice_const.te_long) <
|
||||
subghz_protocol_nord_ice_const.te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(&instance->decoder, 1);
|
||||
}
|
||||
// If got 33 bits key reading is finished
|
||||
if(instance->decoder.decode_count_bit ==
|
||||
subghz_protocol_nord_ice_const.min_count_bit_for_found) {
|
||||
instance->generic.data = instance->decoder.decode_data;
|
||||
instance->generic.data_count_bit = instance->decoder.decode_count_bit;
|
||||
if(instance->base.callback)
|
||||
instance->base.callback(&instance->base, instance->base.context);
|
||||
}
|
||||
instance->decoder.decode_data = 0;
|
||||
instance->decoder.decode_count_bit = 0;
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepReset;
|
||||
} else {
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
instance->decoder.parser_step = Nord_IceDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t subghz_protocol_decoder_nord_ice_get_hash_data(void* context) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
return subghz_protocol_blocks_get_hash_data_long(
|
||||
&instance->decoder, (instance->decoder.decode_count_bit / 8) + 1);
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_nord_ice_serialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
SubGhzRadioPreset* preset) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
return subghz_block_generic_serialize(&instance->generic, flipper_format, preset);
|
||||
}
|
||||
|
||||
SubGhzProtocolStatus
|
||||
subghz_protocol_decoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
return subghz_block_generic_deserialize_check_count_bit(
|
||||
&instance->generic,
|
||||
flipper_format,
|
||||
subghz_protocol_nord_ice_const.min_count_bit_for_found);
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_nord_ice_get_string(void* context, FuriString* output) {
|
||||
furi_assert(context);
|
||||
SubGhzProtocolDecoderNord_Ice* instance = context;
|
||||
|
||||
subghz_protocol_nord_ice_check_remote_controller(&instance->generic);
|
||||
|
||||
uint64_t code_found_reverse = subghz_protocol_blocks_reverse_key(
|
||||
instance->generic.data, instance->generic.data_count_bit);
|
||||
|
||||
// for future use
|
||||
// // push protocol data to global variable
|
||||
// subghz_block_generic_global.btn_is_available = false;
|
||||
// subghz_block_generic_global.current_btn = instance->generic.btn;
|
||||
// subghz_block_generic_global.btn_length_bit = 4;
|
||||
// //
|
||||
|
||||
furi_string_cat_printf(
|
||||
output,
|
||||
"%s %db\r\n"
|
||||
"Key: 0x%08llX\r\n"
|
||||
"Yek: 0x%08llX\r\n"
|
||||
"Serial: 0x%07lX\r\n"
|
||||
"Btn: %02X",
|
||||
instance->generic.protocol_name,
|
||||
instance->generic.data_count_bit,
|
||||
(uint64_t)(instance->generic.data & 0xFFFFFFFFF),
|
||||
(code_found_reverse & 0xFFFFFFFFF),
|
||||
instance->generic.serial,
|
||||
instance->generic.btn);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.h"
|
||||
|
||||
#define SUBGHZ_PROTOCOL_NORD_ICE_NAME "Nord ICE"
|
||||
|
||||
typedef struct SubGhzProtocolDecoderNord_Ice SubGhzProtocolDecoderNord_Ice;
|
||||
typedef struct SubGhzProtocolEncoderNord_Ice SubGhzProtocolEncoderNord_Ice;
|
||||
|
||||
extern const SubGhzProtocolDecoder subghz_protocol_nord_ice_decoder;
|
||||
extern const SubGhzProtocolEncoder subghz_protocol_nord_ice_encoder;
|
||||
extern const SubGhzProtocol subghz_protocol_nord_ice;
|
||||
|
||||
/**
|
||||
* Allocate SubGhzProtocolEncoderNord_Ice.
|
||||
* @param environment Pointer to a SubGhzEnvironment instance
|
||||
* @return SubGhzProtocolEncoderNord_Ice* pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
*/
|
||||
void* subghz_protocol_encoder_nord_ice_alloc(SubGhzEnvironment* environment);
|
||||
|
||||
/**
|
||||
* Free SubGhzProtocolEncoderNord_Ice.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
*/
|
||||
void subghz_protocol_encoder_nord_ice_free(void* context);
|
||||
|
||||
/**
|
||||
* Deserialize and generating an upload to send.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus
|
||||
subghz_protocol_encoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format);
|
||||
|
||||
/**
|
||||
* Forced transmission stop.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
*/
|
||||
void subghz_protocol_encoder_nord_ice_stop(void* context);
|
||||
|
||||
/**
|
||||
* Getting the level and duration of the upload to be loaded into DMA.
|
||||
* @param context Pointer to a SubGhzProtocolEncoderNord_Ice instance
|
||||
* @return LevelDuration
|
||||
*/
|
||||
LevelDuration subghz_protocol_encoder_nord_ice_yield(void* context);
|
||||
|
||||
/**
|
||||
* Allocate SubGhzProtocolDecoderNord_Ice.
|
||||
* @param environment Pointer to a SubGhzEnvironment instance
|
||||
* @return SubGhzProtocolDecoderNord_Ice* pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
*/
|
||||
void* subghz_protocol_decoder_nord_ice_alloc(SubGhzEnvironment* environment);
|
||||
|
||||
/**
|
||||
* Free SubGhzProtocolDecoderNord_Ice.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
*/
|
||||
void subghz_protocol_decoder_nord_ice_free(void* context);
|
||||
|
||||
/**
|
||||
* Reset decoder SubGhzProtocolDecoderNord_Ice.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
*/
|
||||
void subghz_protocol_decoder_nord_ice_reset(void* context);
|
||||
|
||||
/**
|
||||
* Parse a raw sequence of levels and durations received from the air.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
* @param level Signal level true-high false-low
|
||||
* @param duration Duration of this level in, us
|
||||
*/
|
||||
void subghz_protocol_decoder_nord_ice_feed(void* context, bool level, uint32_t duration);
|
||||
|
||||
/**
|
||||
* Getting the hash sum of the last randomly received parcel.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
* @return hash Hash sum
|
||||
*/
|
||||
uint32_t subghz_protocol_decoder_nord_ice_get_hash_data(void* context);
|
||||
|
||||
/**
|
||||
* Serialize data SubGhzProtocolDecoderNord_Ice.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @param preset The modulation on which the signal was received, SubGhzRadioPreset
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus subghz_protocol_decoder_nord_ice_serialize(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
SubGhzRadioPreset* preset);
|
||||
|
||||
/**
|
||||
* Deserialize data SubGhzProtocolDecoderNord_Ice.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
* @param flipper_format Pointer to a FlipperFormat instance
|
||||
* @return status
|
||||
*/
|
||||
SubGhzProtocolStatus
|
||||
subghz_protocol_decoder_nord_ice_deserialize(void* context, FlipperFormat* flipper_format);
|
||||
|
||||
/**
|
||||
* Getting a textual representation of the received data.
|
||||
* @param context Pointer to a SubGhzProtocolDecoderNord_Ice instance
|
||||
* @param output Resulting text
|
||||
*/
|
||||
void subghz_protocol_decoder_nord_ice_get_string(void* context, FuriString* output);
|
||||
@@ -86,6 +86,8 @@ const SubGhzProtocol* const subghz_protocol_registry_items[] = {
|
||||
&subghz_protocol_jarolift,
|
||||
&subghz_protocol_ditec_gol4,
|
||||
&subghz_protocol_keyfinder,
|
||||
&subghz_protocol_nord_ice,
|
||||
&subghz_protocol_allstar_firefly,
|
||||
};
|
||||
|
||||
const SubGhzProtocolRegistry subghz_protocol_registry = {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
#include "../registry.h"
|
||||
#include "../subghz_protocol_registry.h"
|
||||
|
||||
#include "princeton.h"
|
||||
#include "keeloq.h"
|
||||
#include "nice_flo.h"
|
||||
@@ -87,3 +86,5 @@
|
||||
#include "jarolift.h"
|
||||
#include "ditec_gol4.h"
|
||||
#include "keyfinder.h"
|
||||
#include "nord_ice.h"
|
||||
#include "allstar_firefly.h"
|
||||
|
||||
@@ -57,7 +57,7 @@ bool subghz_protocol_keeloq_create_data(
|
||||
* @param preset Modulation, SubGhzRadioPreset
|
||||
* @return true On success
|
||||
*/
|
||||
bool subghz_protocol_keeloq_bft_create_data(
|
||||
bool subghz_protocol_keeloq_seed_create_data(
|
||||
void* context,
|
||||
FlipperFormat* flipper_format,
|
||||
uint32_t serial,
|
||||
|
||||
@@ -101,10 +101,6 @@ bool subghz_protocol_raw_save_to_file_init(
|
||||
if(!storage_simply_mkdir(instance->storage, SUBGHZ_RAW_FOLDER)) {
|
||||
break;
|
||||
}
|
||||
// Create saved directory if necessary
|
||||
if(!storage_simply_mkdir(instance->storage, SUBGHZ_RAW_FOLDER)) {
|
||||
break;
|
||||
}
|
||||
|
||||
furi_string_set(instance->file_name, dev_name);
|
||||
// First remove subghz device file if it was saved
|
||||
|
||||
@@ -69,7 +69,7 @@ vars.AddVariables(
|
||||
BoolVariable(
|
||||
"DEBUG",
|
||||
help="Enable debug build",
|
||||
default=True,
|
||||
default=False,
|
||||
),
|
||||
BoolVariable(
|
||||
"LIB_DEBUG",
|
||||
@@ -79,7 +79,7 @@ vars.AddVariables(
|
||||
BoolVariable(
|
||||
"COMPACT",
|
||||
help="Optimize for size",
|
||||
default=False,
|
||||
default=True,
|
||||
),
|
||||
EnumVariable(
|
||||
"TARGET_HW",
|
||||
|
||||
@@ -877,6 +877,8 @@ Function,+,canvas_draw_str_aligned,void,"Canvas*, int32_t, int32_t, Align, Align
|
||||
Function,+,canvas_draw_triangle,void,"Canvas*, int32_t, int32_t, size_t, size_t, CanvasDirection"
|
||||
Function,+,canvas_draw_xbm,void,"Canvas*, int32_t, int32_t, size_t, size_t, const uint8_t*"
|
||||
Function,+,canvas_draw_xbm_ex,void,"Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*"
|
||||
Function,+,canvas_get_buffer,uint8_t*,Canvas*
|
||||
Function,+,canvas_get_buffer_size,size_t,const Canvas*
|
||||
Function,+,canvas_get_font_params,const CanvasFontParameters*,"const Canvas*, Font"
|
||||
Function,+,canvas_glyph_width,size_t,"Canvas*, uint16_t"
|
||||
Function,+,canvas_height,size_t,const Canvas*
|
||||
@@ -2350,11 +2352,14 @@ Function,+,iso15693_3_is_equal,_Bool,"const Iso15693_3Data*, const Iso15693_3Dat
|
||||
Function,+,iso15693_3_load,_Bool,"Iso15693_3Data*, FlipperFormat*, uint32_t"
|
||||
Function,+,iso15693_3_poller_activate,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3Data*"
|
||||
Function,+,iso15693_3_poller_get_blocks_security,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t"
|
||||
Function,+,iso15693_3_poller_get_data,const Iso15693_3Data*,Iso15693_3Poller*
|
||||
Function,+,iso15693_3_poller_get_system_info,Iso15693_3Error,"Iso15693_3Poller*, Iso15693_3SystemInfo*"
|
||||
Function,+,iso15693_3_poller_inventory,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*"
|
||||
Function,+,iso15693_3_poller_read_block,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint8_t, uint8_t"
|
||||
Function,+,iso15693_3_poller_read_blocks,Iso15693_3Error,"Iso15693_3Poller*, uint8_t*, uint16_t, uint8_t"
|
||||
Function,+,iso15693_3_poller_send_frame,Iso15693_3Error,"Iso15693_3Poller*, const BitBuffer*, BitBuffer*, uint32_t"
|
||||
Function,+,iso15693_3_poller_write_block,Iso15693_3Error,"Iso15693_3Poller*, const uint8_t*, uint8_t, uint8_t"
|
||||
Function,+,iso15693_3_poller_write_blocks,Iso15693_3Error,"Iso15693_3Poller*, const uint8_t*, uint16_t, uint8_t"
|
||||
Function,+,iso15693_3_reset,void,Iso15693_3Data*
|
||||
Function,+,iso15693_3_save,_Bool,"const Iso15693_3Data*, FlipperFormat*"
|
||||
Function,+,iso15693_3_set_uid,_Bool,"Iso15693_3Data*, const uint8_t*, size_t"
|
||||
@@ -3394,6 +3399,8 @@ Function,+,slix_poller_get_random_number,SlixError,"SlixPoller*, SlixRandomNumbe
|
||||
Function,+,slix_poller_read_signature,SlixError,"SlixPoller*, SlixSignature*"
|
||||
Function,+,slix_poller_send_frame,SlixError,"SlixPoller*, const BitBuffer*, BitBuffer*, uint32_t"
|
||||
Function,+,slix_poller_set_password,SlixError,"SlixPoller*, SlixPasswordType, SlixPassword, SlixRandomNumber"
|
||||
Function,+,slix_poller_write_block,SlixError,"SlixPoller*, const uint8_t*, uint8_t, uint8_t"
|
||||
Function,+,slix_poller_write_blocks,SlixError,"SlixPoller*, const uint8_t*, uint16_t, uint8_t"
|
||||
Function,+,slix_reset,void,SlixData*
|
||||
Function,+,slix_save,_Bool,"const SlixData*, FlipperFormat*"
|
||||
Function,+,slix_set_uid,_Bool,"SlixData*, const uint8_t*, size_t"
|
||||
@@ -3737,8 +3744,8 @@ Function,+,subghz_protocol_encoder_raw_stop,void,void*
|
||||
Function,+,subghz_protocol_encoder_raw_yield,LevelDuration,void*
|
||||
Function,+,subghz_protocol_faac_slh_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint32_t, uint32_t, const char*, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_jarolift_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_keeloq_bft_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_keeloq_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, const char*, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_keeloq_seed_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_kinggates_stylo_4k_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*"
|
||||
Function,+,subghz_protocol_marantec_crc8,uint8_t,"uint8_t*, size_t"
|
||||
Function,+,subghz_protocol_nice_flor_s_create_data,_Bool,"void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*, _Bool"
|
||||
|
||||
|
Reference in New Issue
Block a user