Merge remote-tracking branch 'ul/dev' into mntm-dev

This commit is contained in:
WillyJL
2026-06-01 19:12:46 +02:00
64 changed files with 4882 additions and 625 deletions
+27
View File
@@ -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
View File
@@ -1,4 +1,5 @@
#include "canvas_i.h"
#include "canvas.h"
#include "icon_animation_i.h"
#include <furi.h>
+16
View File
@@ -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
-16
View File
@@ -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

+363 -99
View File
@@ -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");
+53 -5
View File
@@ -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.*
+4 -1
View File
@@ -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;
+1 -3
View File
@@ -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);
+34
View File
@@ -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
+28
View File
@@ -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);
}
+378
View File
@@ -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));
}
+143
View File
@@ -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);
+54 -2
View File
@@ -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;
+8 -4
View File
@@ -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;
+38 -39
View File
@@ -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);
+93 -7
View File
@@ -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;
+94
View File
@@ -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;
}
+20
View File
@@ -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);
+1 -1
View File
@@ -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) {
+5 -5
View File
@@ -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,
+352
View File
@@ -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);
}
+109
View File
@@ -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);
+2
View File
@@ -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 = {
+2 -1
View File
@@ -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"
+1 -1
View File
@@ -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,
-4
View File
@@ -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
+2 -2
View File
@@ -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",
+8 -1
View File
@@ -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"
1 entry status name type params
877 Function + canvas_draw_triangle void Canvas*, int32_t, int32_t, size_t, size_t, CanvasDirection
878 Function + canvas_draw_xbm void Canvas*, int32_t, int32_t, size_t, size_t, const uint8_t*
879 Function + canvas_draw_xbm_ex void Canvas*, int32_t, int32_t, size_t, size_t, IconRotation, const uint8_t*
880 Function + canvas_get_buffer uint8_t* Canvas*
881 Function + canvas_get_buffer_size size_t const Canvas*
882 Function + canvas_get_font_params const CanvasFontParameters* const Canvas*, Font
883 Function + canvas_glyph_width size_t Canvas*, uint16_t
884 Function + canvas_height size_t const Canvas*
2352 Function + iso15693_3_load _Bool Iso15693_3Data*, FlipperFormat*, uint32_t
2353 Function + iso15693_3_poller_activate Iso15693_3Error Iso15693_3Poller*, Iso15693_3Data*
2354 Function + iso15693_3_poller_get_blocks_security Iso15693_3Error Iso15693_3Poller*, uint8_t*, uint16_t
2355 Function + iso15693_3_poller_get_data const Iso15693_3Data* Iso15693_3Poller*
2356 Function + iso15693_3_poller_get_system_info Iso15693_3Error Iso15693_3Poller*, Iso15693_3SystemInfo*
2357 Function + iso15693_3_poller_inventory Iso15693_3Error Iso15693_3Poller*, uint8_t*
2358 Function + iso15693_3_poller_read_block Iso15693_3Error Iso15693_3Poller*, uint8_t*, uint8_t, uint8_t
2359 Function + iso15693_3_poller_read_blocks Iso15693_3Error Iso15693_3Poller*, uint8_t*, uint16_t, uint8_t
2360 Function + iso15693_3_poller_send_frame Iso15693_3Error Iso15693_3Poller*, const BitBuffer*, BitBuffer*, uint32_t
2361 Function + iso15693_3_poller_write_block Iso15693_3Error Iso15693_3Poller*, const uint8_t*, uint8_t, uint8_t
2362 Function + iso15693_3_poller_write_blocks Iso15693_3Error Iso15693_3Poller*, const uint8_t*, uint16_t, uint8_t
2363 Function + iso15693_3_reset void Iso15693_3Data*
2364 Function + iso15693_3_save _Bool const Iso15693_3Data*, FlipperFormat*
2365 Function + iso15693_3_set_uid _Bool Iso15693_3Data*, const uint8_t*, size_t
3399 Function + slix_poller_read_signature SlixError SlixPoller*, SlixSignature*
3400 Function + slix_poller_send_frame SlixError SlixPoller*, const BitBuffer*, BitBuffer*, uint32_t
3401 Function + slix_poller_set_password SlixError SlixPoller*, SlixPasswordType, SlixPassword, SlixRandomNumber
3402 Function + slix_poller_write_block SlixError SlixPoller*, const uint8_t*, uint8_t, uint8_t
3403 Function + slix_poller_write_blocks SlixError SlixPoller*, const uint8_t*, uint16_t, uint8_t
3404 Function + slix_reset void SlixData*
3405 Function + slix_save _Bool const SlixData*, FlipperFormat*
3406 Function + slix_set_uid _Bool SlixData*, const uint8_t*, size_t
3744 Function + subghz_protocol_encoder_raw_yield LevelDuration void*
3745 Function + subghz_protocol_faac_slh_create_data _Bool void*, FlipperFormat*, uint32_t, uint8_t, uint32_t, uint32_t, const char*, SubGhzRadioPreset*
3746 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*
3747 Function + subghz_protocol_keeloq_create_data _Bool void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, const char*, SubGhzRadioPreset*
3748 Function + subghz_protocol_keeloq_seed_create_data _Bool void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, uint32_t, const char*, SubGhzRadioPreset*
3749 Function + subghz_protocol_kinggates_stylo_4k_create_data _Bool void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*
3750 Function + subghz_protocol_marantec_crc8 uint8_t uint8_t*, size_t
3751 Function + subghz_protocol_nice_flor_s_create_data _Bool void*, FlipperFormat*, uint32_t, uint8_t, uint16_t, SubGhzRadioPreset*, _Bool