3.4.9. APP - Google Fhn

3.4.9.1. Google Fhn原理简介和概述

image1

  • 支持的板级: bd57

  • 支持的芯片: AW33N

  1. Google FHN(Find Hub Network) 是谷歌公司提供的一种寻找丢失设备的服务和协议。“查找中心”网络 (FHN) 定义了一种端到端加密方法,用于跟踪信标低功耗蓝牙 (BLE) 设备。FHN 作为快速配对规范的扩展功能。如果提供商的设备与 FHN 兼容,并且愿意为这些设备启用位置信息跟踪功能,则应启用此扩展功能。

  2. 在GFPS协议中主要可以分为以下角色:

    • 快速配对搜索器(Fast Pair Seeker):搜索器通常是手机,用于寻找要配对的设备。

    • 快速配对提供方(Fast Pair Provider):提供方是广播其存在和配对就绪状态的设备(例如,可被发现的耳机)

Note

由于地区网络限制,该Google FHN配对所需谷歌手机需要实现科学上网。

3.4.9.2. 快速上手

本章节主要介绍拿到sdk后如何快速运行google fhn应用功能

3.4.9.2.1. 开发环境搭建

  • 首先请确保正确的搭建了 Windows 下的第三方集成开发环境 CodeBlocks和杰理的编译工具链 jl_toolchain。

开发环境安装说明: https://doc.zh-jieli.com/Tools/zh-cn/dev_tools/dev_env/index.html#

3.4.9.2.2. app应用选择和板级配置

配置app选择:apps\demo\transfer\include\app_config.h

//apps example 选择,只能选1个,要配置对应的board_config.h,配置应用切换时对应选择的应用都要置1
#define CONFIG_APP_GOOGLE_FHN             0 //Google fhn, 使用需要配置板级CONFIG_BOARD_AW33N_FHN

配置板级apps\demo\transfer\board\bd57\board_config.h

/*
 *  板级配置选择
 */
#define CONFIG_BOARD_AW33N_FHN

#include "board_aw33n_fhn_cfg.h"

3.4.9.2.3. 添加谷歌注册模型ID

Note

所有提供方模型都必须先向 Google 注册,然后才能与快速配对搭配使用。注册后,Google 会分发模型 ID 和反欺骗公钥/私钥对。注册期间提供的信息会用于向用户显示的配对建议,也可能会用于其他用户体验。在SDK中JL提供测试用的模型ID,用于Google FHN测试体验。

模型ID信息的修改文件位于:apps\demo\transfer\examples\google_fhn\ble_google_fhn.c

static const uint8_t google_model_id_used[3] = {0x48, 0xE5, 0x1E};
static const char google_public_key_used[64] = {
    0x3F, 0xD4, 0x33, 0xE2, 0x60, 0x15, 0xDD, 0xFE, 0x46, 0x29, 0x88, 0xD6, 0x46, 0xF6, 0x45, 0x1E,
    0x59, 0xAF, 0xEA, 0xDF, 0xAD, 0x10, 0x55, 0x33, 0x14, 0x80, 0xBC, 0x7B, 0x61, 0xE9, 0x2F, 0x93,
    0x11, 0x3F, 0xC5, 0x12, 0x2A, 0x5B, 0x99, 0xFC, 0xDF, 0x49, 0xDF, 0x4D, 0x7A, 0x50, 0x9F, 0xE8,
    0xB5, 0x5B, 0xBB, 0x33, 0x0D, 0xB9, 0x42, 0xC7, 0x7C, 0xB6, 0xCF, 0x35, 0xC3, 0x33, 0x09, 0x81
};
static const char google_private_key_used[32] = {
    0x1A, 0xFD, 0x9A, 0x90, 0xE7, 0x41, 0xA1, 0x9D, 0xE0, 0x51, 0x7E, 0x26, 0x1F, 0xC2, 0xC7, 0xFE,
    0x15, 0x1A, 0xDE, 0xEC, 0xA1, 0xFA, 0x44, 0x23, 0x95, 0x21, 0x45, 0xFB, 0x1C, 0x99, 0x41, 0xD6
};
  • google_model_id_used:从Google申请的模型ID

  • google_public_key_used:从Google申请的公钥

  • google_private_key_used:从Google申请的私钥

3.4.9.2.4. 烧录固件

Note

正确修改了sdk的Google授权信息,那么现在把Google FHN固件写入,打开codeblocks工程,apps\demo\transfer\board\bd57\AW33N_transfer.cbp,点击编译烧录,编译烧录教程参考:

链接:https://doc.zh-jieli.com/Tools/zh-cn/dev_tools/build_download/build_with_codeblocks.html

3.4.9.2.5. 测试使用

Note

烧录完上电后,Google FHN配件会进入软关机状态,用户需要长按ADkey K1开机,开机后默认打开广播,此时用户可打开谷歌手机,手机在搜索到设备后会自动弹窗,用户便可以点击弹窗开始配网。如遇网络不好或手机不弹窗,可以重新刷新网络或重启手机。

  1. 谷歌手机弹出配对窗口

image2

  1. 使用说明弹窗

image3

  1. 进行配对流程

image4

  1. 配对完成

image5

  1. 设备界面

image6

  1. 操作响铃

image7

Important

如果成功连接绑定连接并且可以控制播声,那么恭喜你!已经完成了JL Google FHN的上手使用,可以依靠后续章节去开发属于你的Google FHN应用

3.4.9.3. 代码架构介绍

3.4.9.3.1. Google FHN SDK框图介绍

image8

Note

上图为JL Google FHN SDK的框图架构,依托杰理的平台和gfps协议库,用户工程师不需要关注gfps中google fhn状态机的转换运作原理,也不需要关注gfps协议是如何和谷歌手机进行加密交握手完成配对,只需要简单的在我们上层留出的接口和杰理SDK原有的功能去适配自己产品上层的交互逻辑和外设即可。

3.4.9.3.2. 应用层代码文件介绍

  • app_google_fhn.c — google fhn初始化

  • ble_google_fhn.c/h — ble gatt初始化和数据收发

  • ble_google_fhn_profile.h — ble gatt profile初始化,暂无定义

3.4.9.4. Google FHN状态介绍

3.4.9.4.1. 状态介绍

google fhn通过状态机的控制会在不同行为场景中转化为不同的状态,常见的状态如下:

  1. 等待配对状态(pair mode):产品还未与谷歌设备配对时,或者产品恢复出厂设置时,此状态会发送等待配对的蓝牙广播。为了尽可能快的被谷歌设备发现,此状态会高速广播,功耗比较大。

  2. 配对连接状态(pairing):产品与谷歌设备连接完成配对过程,此状态产品与谷歌设备交换大量的数据,功耗也比较大,因为此状态出现非常低频, 所以此状态对产品续航影响小。

  3. 高速连接并寻找状态(产品响铃)(high connected):完成配对的产品,可以在查找中心应用中点击 “响铃”按钮,可以使产品响铃,来找到产品,因为此状态出现低频, 所以此状态对产品续航影响小。

  4. 慢速广播状态(待机状态)(separated):如果已配对产品与谷歌设备远离,产品与苹果设备蓝牙断开了,此时产品就会发送慢速蓝牙广播,等待下一次重连被寻找,如果用户不尝试“响铃”功能,产品会一直处于此状态。

3.4.9.4.2. 状态回调函数

  • JL Google FHN SDK中在外部设置定义了回调函数供用户捕捉对应的状态以便用户自定义外设行为(例如配对完成后或者解绑有特殊的铃声或者断连处理)。

ble_google_fhn.c

//重写弱函数, 获取谷歌fhn的配网状态, 0: 开始进入配网  1: 配网完成
void gfps_platform_state_callback(uint8_t state)
{
    log_info("gfps_platform_state_callback state %d", state);

    if (state == 0x01) {
#if (CONFIG_APP_FMY_FHN_DUAL)
        //记录谷歌fhn配网完成
        u8 protocol_init_state = 0x02; //GOOGLE_FHN_PAIRED_DONE
        syscfg_write(CFG_LAST_SW_APP_ID, (uint8_t *)&protocol_init_state, sizeof(protocol_init_state));
#endif
    }
}

/*************************************************************************************************/
/*!
 *  \brief      蓝牙状态回调
 *
 *  \param      [in]
 *
 *  \return
 *
 *  \note
 */
/*************************************************************************************************/
void gfps_ble_state_cb(ble_state_e state)
{
    log_info(">>> ble state 0x%x", state);

    switch (state) {
    case BLE_ST_CONNECT:
        log_info("BLE_ST_CONNECT");
#if (CONFIG_APP_FMY_FHN_DUAL)
        if ((!get_gfps_pair_state()) && apple_fmy_protocol_exit_flag) {
            //谷歌fhn连接后退出苹果fmy
            apple_fmy_protocol_exit_flag = 0;
            apple_fmy_protocol_exit(); //退出苹果fmy协议
            //TODO:双findmy应用共用同个蜂鸣器, 苹果fmy退出后会关闭该蜂鸣器, 谷歌需要重新打开配置
            FHN_DEV_SOUND_INIT();
            FHN_DEV_SOUND_STATE(PORT_VALUE_LOW);
        }
#endif
        break;
    default:
        break;
    }
}

3.4.9.5. play sound功能

3.4.9.5.1. play sound功能介绍

Note

play sound功能是官方定义的近距离查找功能,SDK中默认打开,当谷歌终端靠近fhn设备并且点击查找中心的响铃的按键则可以通过ble连接gatt控制触发这种行为,SDK中默认的方案使用GPIO驱动蜂鸣器(有源或无源)来实现响声。

3.4.9.5.2. play sound配置

board_aw33n_fhn_cfg.h

//*********************************************************************************//
//                                 fhn蜂鸣器配置                                        //
//*********************************************************************************//
#define GPIO_MODE                           0                             //蜂鸣器使用IO工作
#define GPTIMER_PWM_MODE                    1                             //蜂鸣器使用gptier pwm工作
#define LED_PWM_MODE                        2                             //蜂鸣器使用led_pwm模块工作,低功耗下可用

#if TCFG_FMY_PERIPHERAL_SELECT
#define SOUND_GPIO_PORT                     IO_PORTB_01                   // 定义IO使能蜂鸣器,关闭请注释定义
#else
#define SOUND_GPIO_PORT                     IO_PORTB_00                   // 定义IO使能蜂鸣器,关闭请注释定义
#endif

#define SOUND_PASSIVE_BUZZER                GPIO_MODE                     // 无源蜂鸣器推声模式选择

#if (SOUND_PASSIVE_BUZZER == GPTIMER_PWM_MODE)
#define DEV_SOUND_GPTIM_PWM_FREQ            2000                          //选择PWM输出频率, 默认2K
#define DEV_SOUND_GPTIM_PWM_DUTY            7500                          //选择PWM输出占空比, 默认75%
#endif

#if (SOUND_PASSIVE_BUZZER == LED_PWM_MODE)
#define DEV_SOUND_PWM_CYCLE                 50                            //选择PWM输出周期, 单位10us,默认500us
#define DEV_SOUND_PWM_DUTY                  75                            //选择PWM输出占空比, 默认75%
#endif

其中:

  • SOUND_GPIO_PORT为蜂鸣器驱动GPIO口

  • SOUND_PASSIVE_BUZZER为是否采用无源蜂鸣器发声(用pwm波推声),DISABLE则采用有源蜂鸣器驱动,只是简单的GPIO翻转来驱动发声

3.4.9.5.3. play sound 驱动函数

ble_google_fhn.c

static void port_io_init(uint32_t port_io, uint32_t dir)
{
    gpio_hw_set_die(IO_PORT_SPILT(port_io), 1);
    gpio_hw_set_pull_down(IO_PORT_SPILT(port_io), 0);
    gpio_hw_set_pull_up(IO_PORT_SPILT(port_io), 0);
    gpio_hw_set_direction(IO_PORT_SPILT(port_io), dir);
}

#if (SOUND_PASSIVE_BUZZER == GPTIMER_PWM_MODE)
// 无源蜂鸣器需要交流信号去驱动,用gptimer-pwm来推信号
#include "gptimer.h"
static u32 gptimer_pwm_id = 0;
static void fhn_sound_passive_buzzer_init(void)
{
    if (gptimer_pwm_id == 0) {
        const struct gptimer_config pwm_config = {
            .pwm.freq = DEV_SOUND_GPTIM_PWM_FREQ, //设置输出频率
            .pwm.duty = DEV_SOUND_GPTIM_PWM_DUTY, //设置占空比
            .pwm.port = ((u32)(SOUND_GPIO_PORT)) / 16, //设置pwm输出IO_ PORTA_02
            .pwm.pin = BIT(((u32)(SOUND_GPIO_PORT)) % 16), //设置pwm输出IO_PORTA_02
            .mode = GPTIMER_MODE_PWM, //设置工作模式
        };
        gptimer_pwm_id = gptimer_init(TIMER1, &pwm_config);
        log_info("gptimer pwm init id: 0x%x", gptimer_pwm_id);
    }
}

static void fhn_sound_passive_buzzer_deinit(void)
{
    if (gptimer_pwm_id) {
        gptimer_deinit(gptimer_pwm_id);
        log_info("gptimer pwm deinit id: 0x%x", gptimer_pwm_id);
        gptimer_pwm_id = 0;
    }
}
/*************************************************************************************************/
/*!
 *  \brief     提供开关sound
 *
 *  \param      [in] pwm_ch, on_off: 1 or 0
 *
 *  \return
 *
 *  \note
 */
/*************************************************************************************************/
static void fhn_sound_passive_onoff(bool on_off)
{
    // 开关低功耗,pwm模块会在低功耗时被关闭
    fhn_state_idle_set_active(on_off);
    // 开、关pwm输出
    if (on_off) {
        gpio_set_mode(IO_PORT_SPILT(SOUND_GPIO_PORT), PORT_OUTPUT_LOW); //IO口设为输出
        gptimer_set_pwm_freq(gptimer_pwm_id, DEV_SOUND_GPTIM_PWM_FREQ);
        gptimer_set_pwm_duty(gptimer_pwm_id, DEV_SOUND_GPTIM_PWM_DUTY); //设置pwm占空比
        gptimer_start(gptimer_pwm_id); //启动timer
        gptimer_pwm_flip(gptimer_pwm_id); //pwm翻转
        log_info("pwm on");
    } else {
        gpio_set_mode(IO_PORT_SPILT(SOUND_GPIO_PORT), PORT_HIGHZ); //IO口设为高阻
        gptimer_pause(gptimer_pwm_id);
        log_info("pwm off");
    }
}
#endif

#if (SOUND_PASSIVE_BUZZER == LED_PWM_MODE)
// 无源蜂鸣器需要交流信号去驱动,用pwm_led来推信号,低功耗模式下可用
#include "led_api.h"
#include "pwm_led.h"
static void fhn_sound_passive_buzzer_init(void)
{
    const led_board_cfg_t buzzer_cfg = {
        .layout = ONE_IO_ONE_LED,
        .led0.port = SOUND_GPIO_PORT,  //设置输出io
        .led0.logic = BRIGHT_BY_HIGH,
        .led0.brightness = DEV_SOUND_PWM_DUTY,  //设置占空比
    };
    led_effect_board_init(&buzzer_cfg);
    log_info("buzzer init ");

}

static void fhn_sound_passive_buzzer_deinit(void)
{
    pwm_led_hw_close();
    log_info("buzzer deinit ");

}
/*************************************************************************************************/
/*!
 *  \brief     提供开关sound
 *
 *  \param      [in] pwm_ch, on_off: 1 or 0
 *
 *  \return
 *
 *  \note
 */
/*************************************************************************************************/
static void fhn_sound_passive_onoff(bool on_off)
{

    LED_PLATFORM_DATA_BEGIN(sound_data)
    .ctl_option = CTL_LED0_ONLY,
     .ctl_mode = CYCLE_ONCE_BRIGHT,
      .ctl_cycle = 20,//*50 =1000
       .ctl_cycle_num = 0,
        .once_bright.bright_time = 10,//*50=500
                     LED_PLATFORM_DATA_END()
                     // 开、关pwm输出
    if (on_off) {
        led_effect_output(&sound_data);
        log_info("pwm on");
    } else {
        pwm_led_hw_close();
        log_info("pwm off");
    }
}
#endif

Note

用户可以自行修改这部分声音外设逻辑来达到自己的发声效果

3.4.9.6. 按键调试

  • 用户可以添加button行为来调试产品的一些按键特性

配置io_key或者ad_key:board_aw33n_fhn_cfg.h

//*********************************************************************************//
//                                 adkey 配置                                      //
//*********************************************************************************//
#define AD_KEY_IO		                    IO_PORTA_01      // 可用的IO见adc_ch_io_table
#define KEY_AD_EN				            DISABLE 		//ENABLE           //<AD按键使能

#define EXTERN_R_UP     100//220 -> 22k,外挂上拉电阻,0使用内部上拉,内部上拉为10k

#if EXTERN_R_UP
#define R_UP       EXTERN_R_UP
#else
#define R_UP       100    //内部上拉为10K,有20%误差
#endif

#define ADC10_33   (0x3ffL)

// 根据具体电路配置
#define ADC10_30   (ADC10_33 * 50   / (50 + R_UP))     //5K
#define ADC10_27   (ADC10_33 * 33.3 / (33.3  + R_UP))  //3.33K
#define ADC10_23   (ADC10_33 * 20   / (20  + R_UP))    //2K
#define ADC10_20   (ADC10_33 * 9.1  / (9.1  + R_UP))   //0.91K
#define ADC10_17   (ADC10_33 *   0  / (  0  + R_UP))   //0Ω
#define ADC10_13   (0)  //NULL
#define ADC10_10   (0)  //NULL
#define ADC10_07   (0)  //NULL
#define ADC10_04   (0)  //NULL
#define ADC10_00   (0)  //NULL

#define AD_NOKEY        ((ADC10_33 + ADC10_30) / 2)
#define ADKEY1_0		((ADC10_30 + ADC10_27) / 2)
#define ADKEY1_1		((ADC10_27 + ADC10_23) / 2)
#define ADKEY1_2		((ADC10_23 + ADC10_20) / 2)
#define ADKEY1_3		((ADC10_20 + ADC10_17) / 2)
#define ADKEY1_4		((ADC10_33 + ADC10_30) / 2)
#define ADKEY1_5		((ADC10_30 + ADC10_27) / 2)
#define ADKEY1_6		((ADC10_27 + ADC10_23) / 2)
#define ADKEY1_7		((ADC10_23 + ADC10_20) / 2)
#define ADKEY1_8		((ADC10_20 + ADC10_17) / 2)

#define TCFG_ADKEY_VALUE0                   0
#define TCFG_ADKEY_VALUE1                   1
#define TCFG_ADKEY_VALUE2                   2
#define TCFG_ADKEY_VALUE3                   3
#define TCFG_ADKEY_VALUE4                   4
#define TCFG_ADKEY_VALUE5                   5
#define TCFG_ADKEY_VALUE6                   6
#define TCFG_ADKEY_VALUE7                   7
#define TCFG_ADKEY_VALUE8                   8
#define TCFG_ADKEY_VALUE9                   9

#define TCFG_ADC_VTEMP_CH_EN                DISABLE //温度采集功能

//*********************************************************************************//
//                                 iokey 配置                                      //
//*********************************************************************************//
#define KEY_IO_EN         	                ENABLE                  //<IO按键使能
#define MOUSE_KEY_SCAN_MODE                 DISABLE_THIS_MOUDLE
#define TCFG_IOKEY_POWER_CONNECT_WAY		ONE_PORT_TO_LOW    //按键一端接低电平一端接IO

#define TCFG_IOKEY_POWER_ONE_PORT			IO_PORTA_01        //IO按键端口
#define TCFG_IOKEY_POWER_ONE_PORT_VALUE		0x0                //power port键值
//#define TCFG_IOKEY_PREV_CONNECT_WAY			ONE_PORT_TO_LOW  //按键一端接低电平一端接IO
//#define TCFG_IOKEY_PREV_ONE_PORT			IO_PORTA_03
//#define TCFG_IOKEY_PREV_ONE_PORT_VALUE		0x1              //prev port键值

#define TCFG_POWER_ON_NEED_KEY		        1                //是否需要长按按键开机配置

按键行为控制:ble_google_fhn.c

void google_fhn_key_deal_test(uint8_t key_type, uint8_t key_value)
{
    log_info(">>>>>>>>>> google_fhn key test %d %d", key_type, key_value);
    if (key_value ==  TCFG_ADKEY_VALUE0) {
        switch (key_type) {
        case KEY_EVENT_CLICK:
            if (fhn_sound_ctrl_timer_id) {
                log_info("####################### fhn_ring_stop");
                sys_timeout_add(NULL, fhn_ring_test, 1);
            }
#if 0
            else {
                log_info("####################### gfps_set_pair_mode_by_user");
                fhn_sound_set_effect(1, 10);
                sys_timeout_add(NULL, gfps_set_pair_mode_by_user_test, 1);
            }
#endif
            break;
        case KEY_EVENT_DOUBLE_CLICK:
            log_info("####################### dult_set_identifier_enable");
            fhn_sound_set_effect(1, 10);
            sys_timeout_add(NULL, dult_set_identifier_enable_test, 1);
            break;
        case KEY_EVENT_TRIPLE_CLICK:
#if 0
            log_info("#######################  gfps_test_bat_low_level");
            sys_timeout_add(NULL, gfps_test_bat_low_level, 1);
#endif
            break;
        case KEY_EVENT_FOURTH_CLICK:
            log_info("####################### gfps_factory_reset");
            fhn_sound_set_effect(1, 100);
            sys_timeout_add(NULL, fhn_factory_reset_test, 1);
            break;
        case KEY_EVENT_LONG:
            log_info("####################### gfps power off");
            app_power_event_to_user(POWER_EVENT_POWER_SOFTOFF);
            break;
        }
    }
}

3.4.9.7. 电池相关功能

在配对时,谷歌主机将会读取配件的产品信息,其中包括此时产品的电量,用户根据产品电池特性来设置电池类型。

使用杰理平台的电量检测来获取产品的电量数据

ble_google_fhn.c

u8 test_bat_low_level = 0;
//重写弱函数,获取电量
extern u8 app_power_get_vbat_percent(void);
void generate_rfcomm_battery_data(u8 *data)
{
    u8 self_val = app_power_get_vbat_percent();

    if (gfps_fhn_enable) {
        u8 bat_level = 0;   // 用户可以自行修改电量等级阈值
        if (self_val > 80) {        // 满电
            bat_level = 0;
        } else if (self_val > 40) { // 中等
            bat_level = 1;
        } else if (self_val > 20) { // 低电
            bat_level = 2;
        } else {                    // 严重低电
            bat_level = 3;
        }
        if (test_bat_low_level) {
            bat_level = test_bat_low_level;
        }
        dult_set_battery_level(bat_level);
        fhn_set_bat_status((bat_level < 2) ? 1 : bat_level);
    }

    if (data != NULL) {
        data[0] = self_val;
        data[1] = 0;
        data[2] = 0;
        log_info("%s, l:%x, r:%x, case:%x", __func__, data[0], data[1], data[2]);
    }
}

3.4.9.8. 常见问题汇总

  • 谷歌手机无法弹窗

Note

1、查看模型ID等数据是否填正确;2、如遇网络不好或手机不弹窗,可以重新刷新网络或重启手机。