prettyprint

2022年6月23日 星期四

在Raspberry Pi 3B+安裝FreeBSD 13.1 並實作WiFi Client/Access Point/Wireless Router等功能

     本實驗在Raspberry Pi 3B+硬體上安裝FreeeBSD 13.1系統,用來實現並測試當作WiFi Client 、 Access Point(AP)與Wireless Router的功能,三種模式如下圖所示:




因為FreeBSD尚未支援Raspberry Pi內建無線網卡的Driver,所以本次實驗以外加一片usb網卡(TP-Link TL-WN725N)來測試。

👉安裝FreeBSD 13.1系統在Raspberry Pi 3B+上

從FreeBSD官網下載image檔案:FreeBSD-13.1-RELEASE-arm64-aarch64-RPI.img.xz

下載Raspberry Pi Imager(https://downloads.raspberrypi.org/imager/imager_latest.exe)燒錄FreeBSD image到SD(註1)上,過程可參閱文末成果影片連結。

FreeBSD使用Raspberry Pi硬體,螢幕輸出的介面有兩種: primary為TTL Serial, Secondary 為Vodeo(HDMI介面),本實驗使用USB-UART介面接到筆電的USB上,開啟PuTTY Serial Port,如下圖,Raspberry Pi pinout可參閱https://pinout.xyz/。[RPI: Gnd(6), TX(8), RX(10)]

  • 系統root內定的password為root

本實驗使用的TP-Link TL-WN725N在FreeBSD下的driver 為rtwn,使用指令dmesg查閱,如下圖所示:

但使用內定參數實測後,網路存取速度異常慢,調整設定,載入kdlload wlan_amrr模組或設定dev.rtwn.0.ratectl=0,即可獲得良好的網路存取速度,可在/boot/loader.conf設定如下:
dev.rtwn.0.ratectl=0
wlan_amrr_load="YES"


👉設定成為WiFi Client使用:

  • 在/etc/rc.conf設定
wlans_rtwn0="wlan0"
ifconfig_wlan0="WPA SYNCDHCP"
create_args_wlan0="country TW"

  • 增設/etc/wpa_supplicant.conf檔案,內容填入
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=wheel
network={
        ssid="your ssid"
scan_ssid=1
key_mgmt=WPA-PSK
psk="your passwd"
}
更改正確的ssid與psk
重新啟用系統後即可連至WiFi網路


👉設定成為Access Point(Bridge)使用:
Raspberry Pi wifi介面改成hostap mode,再將有線網路介面ue0與無線網路介面wlan0,加入到bridge members中。如上圖,終端無線設備(手機、筆電等)連到 Raspberry Pi AP後,即可將無線網路橋接到ISP Router連到Internet。終端無線設備屬於同一個網段,本實驗的例子為192.168.1.0/24。
  • 在/etc/rc.conf設定如下
#WLAN hostap mode
wlans_rtwn0="wlan0"
create_args_wlan0="wlanmode hostap -apbridge country TW"
hostapd_enable="YES"
# create a bridge
cloned_interfaces="bridge0"
ifconfig_bridge0="inet 192.168.1.61/24 addm ue0 addm wlan0 up"
ifconfig_ue0="up"
ifconfig_wlan0="up"
#enable geteway(net.inet.ip.forwarding)
gateway_enable="YES"

  • 在/etc/hostapd.conf下設定如下
interface=wlan0
debug=1
ctrl_interface=/var/run/hostapd
ctrl_interface_group=wheel
ssid=myAP_SSID                       #==>改成你的SSID
wpa=2
wpa_passphrase=PassW0rd      #==>改成你的passphrase
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP

重新啟用系統Raspberry Pi即成為無線網路的Access Point。
但在系統重新啟動後,並無法正常使用,bridge0並未將ue0與wlan0加入bridge0的members中。
以dmesg查看driver啟用順序,發現bridge0先啟用後,ue0與wlan0再啟用,推測因此bridge0才無法把加入ue0與wlan0正確加入到bridge0的members中。
以netstat -r 查看 route table並未正確產生

因此把bridge0建立時間延後到啟用local daemon時,再來create bridge並加入ue0與wlan0到members中,步驟如下:
  1. 取消/etc/rc.conf中的下面兩行
    cloned_interfaces="bridge0"
    ifconfig_bridge0="inet 192.168.1.61/24 addm ue0 addm wlan0 up"
  2. 在/etc/rc.local加入
    #!/bin/sh
    br=`ifconfig bridge create`
    ifconfig $br addm ue0 addm wlan0
    dhclient $br
修改後重啟系統即可正常使用AP功能了。

👉設定成為Wireless Router使用:
設定為router模式時,WiFi與有線LAN分別屬於不同網段,因此Raspberry Pi須具備路由器功能。如上圖所示,有線的LAN連接ISP Router為192.168.1.0/24,無線網路為10.10.200.0/24,Raspberry Pi Router無線網路介面設定IP為10.10.200.1。所以Raspberry Pi上需增加NAT(Firewall)、DHCP server與DNS server等功能(DNS server為選項)。
設定步驟如下:

➤DHCP Server使用isc-dhcp時:

執行指令
pkg install -y isc-dhcp44-server
加入hdcp server功能,啟用方式

  1. 在/etc/rc.conf中加入
    dhcpd_enable="YES"
    dhcpd_flags="-q"
    dhcpd_conf="/usr/local/etc/dhcpd.conf"
    dhcpd_ifaces="wlan0"
    dhcpd_withumask="022"
  2. 在/usr/local/etc/dhcpd.conf中加入
    option domain-name "mytest.org";
    default-lease-time 60;
    max-lease-time 72;
    # A slightly different configuration for an internal subnet.
     subnet 10.10.200.0 netmask 255.255.255.0 {
            range 10.10.200.10 10.10.200.20;
            option domain-name-servers 192.168.1.1;
            option routers 10.10.200.1;
            default-lease-time 600;
            max-lease-time 7200;
    }

➤DHCP Server使用dnsmasq時:

執行指令
pkg install -y dnsmasq
  1. 在/etc/rc.conf中加入
    dnsmasq_enable="YES"
  2. 在/usr/local/etc/dnsmasq.conf中加入
    interface=wlan0
    dhcp-range=10.10.200.90,10.10.200.100,255.255.255.0,24h

➤啟用Firewall(NAT)與hostap等功能:

本實驗使用IPFW與IN-Kernel NAT
  • 在/etc/rc.conf設定如下(使用dnsmasq)
ifconfig_ue0="DHCP"
#WLAN WiFi hostap
wlans_rtwn0="wlan0"
create_args_wlan0="wlanmode hostap -apbridge country TW"
ifconfig_wlan0="inet 10.10.200.1 netmask 255.255.255.0"
hostapd_enable="YES"

ifconfig_ue0="up"
ifconfig_wlan0="up"

#Filrewall and NAT enable
firewall_enable="YES"
firewall_nat_enable="YES"
firewall_script="/etc/ipfw.rules"
gateway_enable="YES"

dnsmasq_enable="YES"

  • 在/etc/ipfw.rules下設定如下:

ipfw -q -f flush
cmd="ipfw -q add"
skip="skipto 1000"
pif=ue0
wif=wlan0
ks="keep-state"
good_tcpo="22,25,37,53,80,443,110"
ipfw disable one_pass
ipfw -q nat 1 config if $pif same_ports unreg_only reset
$cmd 005 allow all from any to any via $wif
$cmd 010 allow all from any to any via lo0 # exclude loopback traffic
$cmd 099 reass all from any to any in # reassemble inbound packets
$cmd 100 nat 1 ip from any to any in via $pif # NAT any inbound packets
# Allow the packet through if it has an existing entry in the dynamic rules table
$cmd 101 check-state
# Authorized outbound packets
$cmd 200 $skip tcp from any to any $good_tcpo out via $pif setup $ks
$cmd 210 $skip udp from any to any 53 out via $pif $ks
$cmd 220 $skip icmp from any to any out via $pif $ks
# allow all outbound
$cmd 230 $skip tcp from any to any out via $pif setup $ks
#Authorized inbound packets
$cmd 500 allow icmp from any to me via $pif
$cmd 501 allow tcp from any to me 22 via $pif setup $ks
# otherwise deny
$cmd 999 deny log all from any to any
$cmd 1000 nat 1 ip from any to any out via $pif # skipto location for outbound stateful rules
$cmd 1001 allow ip from any to any

詳細過程可觀看下列影片:




註1:

本次實驗使用不同SD卡安裝系統,若讀者發現系統DISK IO異常,也許可換不同廠牌SD卡試試看。


2022年6月9日 星期四

FreeBSD 13.1 GNOME、KDE5和Xfce中文桌面環境安裝

 FreeBSD桌面環境常用的有GNOME、KDE和Xfce。本文著重介紹在中文環境的安裝。

壹、FreeBSD 13.1基本安裝:

安裝過程主要使用內定值,其他特別事項如下說明:

  • 若不選擇安裝ports,則以後可使用portsnap fetch / portsnap extract指令安裝

  • 可安裝在ZFS partition或是一般的UFS partition
  • 使用ZFS可選用不同等級的 redundancy 增加系統可靠度
  • 開啟自動網路校時
  • 新增一位使用者 group 為wheel,可執行su指令成為root身分。

貳、安裝桌面環境

一、安裝共同部分:

  1. 安裝xorg,以pkg指令安裝
    pkg install -y xorg
  2. 安裝中文字型,使用ports安裝
    cd /usr/ports/chinese/CNS11643-font
    make install clean

二、安裝桌面環境:選擇一種安裝

  1. GNOME:
    執行:pkg install gnome
    在/etc/rc.conf加入以下各行,讓系統啟動後直接進入桌面環境
    dbus_enable="YES"
    hald_enable="YES"
    gdm_enable="YES"
    gnome_enable="YES"


  2. KDE5:
    執行:pkg install kde5,安裝完整KDE 或 執行: pkg install plasma5-desktop安裝精簡版的KDE
    執行: pkd install sddm, 安裝桌面登入管理
    在/etc/rc.conf加入以下各行,讓系統啟動後直接進入桌面環境
    dbus_enable="YES"
    hald_enable="YES"
    sddm_enable="YES
    "

  3. Xfce:
    執行:pkg install xfce
    執行: pkd install slim, 安裝桌面登入管理
    執行: echo ". /usr/local/etc/xdg/xfce4/xinitrc" > ~/.xinitrc
    在/etc/rc.conf加入以下各行讓系統啟動後直接進入桌面環境
    dbus_enable="YES"
    hald_enable="YES"
    slim_enable="YES"

三、桌面環境中文介面:

重新啟動系統後即可自動進入桌面環境登入介面,登入後啟用終端機程式,安裝ibus中文輸入法
執行:pkg install -y zh-ibus-chewing(注音)或 pkg install -y zh-ibus-canglie(倉頡)
接著設定中文環境
  • GNOME:
進入settings的region & language設定中文並無法更改中文介面,直接更改
/usr/local/etc/gdm/locale.conf檔案內容
LANG="zh_TW.UTF-8"
LC_CTYPE="zh_TW.UTF-8"
LC_MESSAGES="zh_TW.UTF-8"
LC_ALL="zh_TW.UTF-8"

詳細過程可參閱以下影片:



  • KDE5:
在settings region/language 下選擇中文,並且在
.profile檔案下加入以下設定,才能正常啟用輸入法:
~/.profile
XIM=ibus; export XIM
GTK_IM_MODULE=ibus; export GTK_IM_MODULE
QT_IM_MODULE=ibus; export QT_IM_MODULE
XMODIFIERS=@im=ibus; export XMODIFIERS
XIM_PROGRAM="ibus-daemon"; export XIM_PROGRAM
XIM_ARGS="--daemonize --xim"; export XIM_ARGS

詳細過程可參閱以下影片:


  • Xfce:
在~/.xinitrc檔案下加入下列設定以啟用中文與輸入法:
XIM=ibus; export XIM
GTK_IM_MODULE=ibus; export GTK_IM_MODULE
QT_IM_MODULE=ibus; export QT_IM_MODULE
XMODIFIERS="@im=ibus"; export XMODIFIERS
XIM_PROGRAM="ibus-daemon"; export XIM_PROGRAM
XIM_ARGS="--daemonize --xim"; export XIM_ARGS

LANG="zh_TW.UTF-8"; export LANG
LC_CTYPE="zh_TW.UTF-8"; export LC_CTYPE
LC_MESSAGES="zh_TW.UTF-8"; export LC_MESSAGES
LC_ALL="zh_TW.UTF-8"; export LC_ALL

exec startxfce4

===============
xfce音效輸出設定
執行:
pkg install -y xfce4-pulseaudio-plugin
pkg install -y xfce4-volumed-pulse
== /etc/rc.conf加入
sound_load="YES"
snd_hda_load="YES"
=============
詳細過程可參閱以下影片:






2022年5月31日 星期二

ZFS介紹--在FreeBSD與ubuntu環境下實作(ZFS file system introduction and experiment in FreeBSD and ubuntu)

本篇文章著重在FreeBSD與ubuntu下實作ZFS 檔案系統,ZFS理論與優缺點部分不再贅述。

ZFS (Zettabyte file system):

實作上以兩個指令(zpool與zfs)來運作 

  1. zpool: 設定ZFS storage pools
  2. zfs --設定 ZFS datasets

zpool指令: 管理實體儲存設備,形成storage pool

  1. Device: 一個實體儲存設備,例如硬碟等.
  2. Virtual Device(vdev): 一個虛擬設備(vdev)可以是單一個device組成,也可以是多的devices組成mirror, raidz, raidz2, raidz3等主要儲存虛擬設備,也可以當成是hot spare,log等虛擬設備。 
  3. zfs pool: 一個storage pool是由多個vdevs組成,若主要儲存是由多個vdevs組成,則這幾個vdevs在storage pool中是以stripe方式組成。因此若vdev是由單一個device構成,則損換一個device則整個storage pool就會損壞。因此構成storage pool的vdevs一般會採用mirror 或raidz[1-3]較為適合。

zfs指令:管理在storage pool內的dataset

dataset分為:

  1. File system: 能直接掛載在檔案系統中,就像一般檔案系統一樣。
  2. Volume: 是一種block device,可視為是一顆邏輯硬碟,若要掛載在檔案系統中使用,需partition與format後才可mount在檔案系統下。一般直接用來當作iSCSI target的device。
  3. Snapshot: file system或volume特定時間點的快照,一般以 filesystem@name 或 volume@name形式表示。
  4. Bookmark: 像snapshot,但沒有實際保留資料空間。

實機測試:

實際在FreeBSD與ubuntu下,使用12個 20GB虛擬硬碟測試。
包含:
  • ZFS pool硬碟設備故障,如何手動與自動更換故障的設備。
  • ZFS pool在不同作業系統中實體移轉。
  • zfs dataset 的snapshot, rollback, clone
  • zfs dataset在同機器或跨機器間備份與回存(zfs send/zfs receive)
實作過程請參閱以下影片




2022年5月9日 星期一

Raspberry Pi 3 + FreeBSD + ZFS + iSCSI (Part 1)

實驗在Raspberry pi 3 B+上安裝FreeBSD作業系統,啟用ZFS檔案系統,比較ZFS dataset有使用壓縮(Compression)與副本(copies)屬性使用磁碟空間比較。另外建立一個ZFS Volume(Block device)作為iSCSI Target使用。以Windows系統連線iSCSI Target並格式化為NTFS。測試ZFS snapshot, rollback 等功能。 

一、下載Raspberry Pi的FreeBSD OS Image:

從FreeBSD官網下載作業系統映像檔,如下圖所示:

下載檔案:FreeBSD-13.0-RELEASE-arm64-aarch64-RPI.img.xz


二、使用Raspberry Pi Imager安裝作業系統到SD Card上:
  1. 從https://www.raspberrypi.com/software/下載Raspberry Pi Imager:
  2. 安裝FreeBSD Image:
    選用use custom選項,安裝剛剛下載的OS映像檔。

三、設定FreeBSD Static IP, nameserver, 與sshd允許遠端連線:

  1. static IP: 查看網路面卡名稱(ue0),編輯/etc/rc.conf加入
    ipconfig_ue0="inet x.x.x.x netmask x.x.x.x"
    defaultrouter="x.x.x.x"

  2. nameserver:編輯/etc/resolv.conf加入nameserver ip:

  3. sshd:編輯/etc/ssh/sshd_config更改permitRootLogin成yes

四、把外接USB硬碟加入ZFS pool:

啟用ZFS服務:
  1. 編輯/etc/rc.conf加入zfs_enable="YES"

  2. 執行service zfs start啟用zfs service
  3. 查看外接USB硬碟設備代號(da0),
    執行zpool create storage da0建立名稱為storage的zfs pool。

五、比較zfs dataset具有compression與copies屬性儲存空間的差異

  1. 分別建立storage/normal, storage/compress與storage/data三個filesystem,

  2. 設定storage/compress具有compression屬性
  3. 設定storage/data具有copies=2屬性,可把資料儲存兩份,增加可靠性
  4. 複製相同的資料到這三個檔案系統,查看使用硬碟空間的差異。



    六、建立iSCSI Target,使用Windows iSCSI Initiator 連線成為自己的硬碟

  1. 建立zfs volume(block device)作為iSCSI Target設備 zfs create -V 25G storage/iscsi01
  2. 編輯/etc/ctl.conf
    更改etc.conf權限,chmod 644 /etc/ctl.conf
    編輯/etc/rc.conf加入ctld_enable="YES"
    啟用service ctld start

  3. 啟用Windows iSCSI Initiator 連線iSCSI Target

  4. 使用Windoes disk managment格式化iSCSI 硬碟

七、zfs snapshot與rollback展示:

2022年4月15日 星期五

ESP-IDF: Using ESP-IDF Classic BT SPP API to implement ESP32 Bluetooth SPP Server and Client devices

 本實驗使用ESP-IDF Classic BT API來製作含有5個藍芽設備形成的piconet,一個ESP32-WROOM-32作為Master,兩個ESP32(ESP32-S, ESP32-CAM)為Slave,一台Android phone 為Slave執行Bluetooth Serial Terminal當作控制台,即時接受piconet中元件連線狀態,另外一台筆電為Slave執行Bluetooth Serial Terminal。Slave可向Master發送控制命令,或藉由Master向其他Slave發送控制命令或文字訊息。

本實驗共訂出5個指令類型:

  1. AT+ST<device_name>:向Master註冊自已的名稱<device_name>,例如:AT+STP0
  2. AT+LS: 列出已連線的Slaves,,例如:AT+LS。
  3. AT+CMD<target_device_name:command>:向其他device送出命令,例如:AT+CMDC0:ON
  4. AT+TXT<target_device_name:TEXT>:向其他device送出Text,例如:AT+TXTT0:TEST
  5. AT+DIS: 向Master送出離線訊息,,例如:AT+DIS。
本實驗Master名稱固定為M0, 擔任控制台的Slave指定為P0。如下圖所示:


一、成果展示影片:



二、使用軟體環境:

  1. VSCode
  2. ESP-IDF extension
  3. Bluetooth serial terminal for Android and for Windows

三、ESP-IDF Classic BT API - GAP & SPP:

SPP Bluetooth Stack:
BT GAP API提供設備間配對連線等,BT SPP API提供Serial Port Service(Server or client)等。
ESP32 Master與Slave端程式碼大同小異,一端負責提供SPP Service Server,另一端為SPP Client。
有關ESP藍芽架構主要參考文件為下列官方網址:

GAP與SPP以register callback function 來處理相對應的Event,API相關文件說明如下連結。

詳細程式碼如下所示,完整程式說明請觀看成果展示影片。

四、程式碼:

1. Server端(Run at Master side)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_bt_defs.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#include "esp_err.h"
#include "esp_log.h"
#include "string.h"
#include "driver/gpio.h"


#define TAG "MY_SPP_MASTER"
#define BT_DEVICE_NAME  "MY_SPP_MASTER"
#define SPP_SERVER_1 "MY_SPP_SERVER"
#define CLIENT_NAME_MAX 10

QueueHandle_t spp_queue_handle;
char rmt_bd_name[ESP_BT_GAP_MAX_BDNAME_LEN+1];
// slave device link list
typedef struct _tbd{
    uint32_t handle;
    uint8_t bd_client_name[CLIENT_NAME_MAX+1];
    uint8_t len;
    struct _tbd *next;
} bd_client_t;
bd_client_t *bd_link_header=NULL;

//input data item
typedef struct {
    uint32_t handle;
    uint16_t len;
    uint8_t in_data[ESP_SPP_MAX_MTU];
} spp_queue_item_t;
spp_queue_item_t spp_queue_item;

bool pannel_connected=false;
uint32_t pannel_handle = 0;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch(event) {
        case ESP_BT_GAP_DISC_RES_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_DISC_RES_EVT");
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_DISC_STATE_CHANGED_EVT");
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVCS_EVT");
        break; 
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVC_REC_EVT");
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_AUTH_CMPL_EVT");
            if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
                ESP_LOGI(TAG, "bda:%x%x%x%x%x%x, device_name:%s--", 
                param->auth_cmpl.bda[0],param->auth_cmpl.bda[1],param->auth_cmpl.bda[2],param->auth_cmpl.bda[3],param->auth_cmpl.bda[4],param->auth_cmpl.bda[5],
                (char*)param->auth_cmpl.device_name);
            }
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT");
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CFM_REQ_EVT");
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT");
        break;
        case ESP_BT_GAP_KEY_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT");
        break;
        case ESP_BT_GAP_READ_RSSI_DELTA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_RSSI_DELTA_EVT");
        break;
        case ESP_BT_GAP_CONFIG_EIR_DATA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CONFIG_EIR_DATA_EVT");
        break;
        case ESP_BT_GAP_SET_AFH_CHANNELS_EVT:
            ESP_LOGI(TAG, "");
        break;
        case ESP_BT_GAP_READ_REMOTE_NAME_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_REMOTE_NAME_EVT");
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_MODE_CHG_EVT");
        break;
        case ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT");
        break;
        case ESP_BT_GAP_QOS_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_QOS_CMPL_EVT");
        break;
        default:
        break;
    }
}
char msg[100];
char temp_name[30];
void my_bt_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
    switch(event) {
        case ESP_SPP_INIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_INIT_EVT");
        break;
        case ESP_SPP_UNINIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_UNINIT_EVT");
        break;
        case ESP_SPP_DISCOVERY_COMP_EVT:
            ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT");
        break;
        case ESP_SPP_OPEN_EVT:
            ESP_LOGI(TAG,"ESP_SPP_OPEN_EVT");
        break;
        case ESP_SPP_CLOSE_EVT:
            ESP_LOGI(TAG, "close handle:%d, status:%d", param->close.handle, param->close.status);
            spp_queue_item.handle = param->close.handle;
            spp_queue_item.len = 6;
            memcpy(spp_queue_item.in_data, (uint8_t*)"AT+DIS", 6);
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_START_EVT:
            ESP_LOGI(TAG, "SPP_START_EVENT:status:%d, scn:%d", param->start.status, param->start.scn);
        break;
        case ESP_SPP_CL_INIT_EVT:
            ESP_LOGI(TAG, "ESP_SPP_CL_INIT_EVT");
        break;
        case ESP_SPP_DATA_IND_EVT:
            spp_queue_item.handle = param->data_ind.handle;
            spp_queue_item.len = param->data_ind.len;
            memcpy(spp_queue_item.in_data, param->data_ind.data, param->data_ind.len);
            if (spp_queue_item.in_data[spp_queue_item.len-2] == (uint8_t)'\r' && spp_queue_item.in_data[spp_queue_item.len-1] == (uint8_t)'\n') {
                spp_queue_item.len -= 2;
            }
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_CONG_EVT:
            ESP_LOGI(TAG, "cong handle:%d, cong:%d, status:%d", param->cong.handle, param->cong.cong, param->cong.status);
        break;
        case ESP_SPP_WRITE_EVT:
            ESP_LOGI(TAG, "ESP_SPP_WRITE_EVT");
        break;
        case ESP_SPP_SRV_OPEN_EVT:
            ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT:rem_bda:%x:%x:%x:%x:%x:%x",
            param->srv_open.rem_bda[0],param->srv_open.rem_bda[1],param->srv_open.rem_bda[2],param->srv_open.rem_bda[3],param->srv_open.rem_bda[4],param->srv_open.rem_bda[5]);
            ESP_LOGI(TAG, "handle:%d, new handle:%d", param->srv_open.handle, param->srv_open.new_listen_handle);
        break;
        case ESP_SPP_SRV_STOP_EVT:
            ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
        break;
        default:
        break;
    }
}

void set_bd_client(spp_queue_item_t item) {
    bd_client_t *client;
    bd_client_t *end = bd_link_header;

    client = malloc(sizeof(bd_client_t));
    client->handle = item.handle;
    memcpy(client->bd_client_name, item.in_data+5, item.len-5);
    if (memcmp(client->bd_client_name, (uint8_t*)"P0", item.len-5)==0) {
        pannel_connected=true;
        pannel_handle = item.handle;
    }
    client->len = item.len-5;
    client->next=NULL;
    if(end == NULL) {
        bd_link_header = client;
    } else {
        while(end) {
            if (memcmp(end->bd_client_name, item.in_data+5, item.len-5) == 0) {
                end->handle=client->handle;
                free(client);
                break;
            } else if (end->next == NULL) {
                end->next = client;
                if (pannel_handle) {
                    memcpy((uint8_t*)msg, client->bd_client_name, client->len);
                    sprintf(msg+client->len, ":Connected");
                    esp_spp_write(pannel_handle, client->len+10, (uint8_t*)msg);
                }
                break;
            }
            end = end->next;
        }
    }
}

void show_bd_connected_client(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    while (end) {
        esp_spp_write(item.handle, end->len, end->bd_client_name);
        vTaskDelay(20/portTICK_PERIOD_MS);
        esp_spp_write(item.handle, 2, (uint8_t*)"\r\n");
        vTaskDelay(20/portTICK_PERIOD_MS);
        end = end->next;
    }
}

void get_bd_client_name(uint32_t handle, char *name) {
    bd_client_t *end = bd_link_header;
    
    while (end) {
        if ((end->handle) == handle) {
            memcpy((uint8_t*)name, end->bd_client_name, end->len);
            name[end->len] = '\0';
            break;
        }
        end=end->next;
    }
    name=NULL;
}
void bd_send_cmd(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    int colon=0;
    for (int i=0; i < item.len; i++) {
        if (item.in_data[i] == (uint8_t)':') {
            colon = i;
            break;
        }
    }
    if (colon == 0) return;
    if (memcmp(item.in_data+6, (uint8_t*)"M0",colon-6)==0) { // M0 is default master device name
        if (memcmp(item.in_data+colon+1, (uint8_t*)"ON", item.len-(colon+1)) == 0) {
            gpio_set_level(GPIO_NUM_4, 1);
        }
        if (memcmp(item.in_data+colon+1, (uint8_t*)"OFF", item.len-(colon+1)) == 0) {
            gpio_set_level(GPIO_NUM_4, 0);
        }
    } else {
        while(end) {
            if (memcmp(end->bd_client_name, item.in_data+6, end->len)==0) {
                esp_spp_write(end->handle, item.len-(colon+1), item.in_data+colon+1);
                break;
            }
            end=end->next;
        }
    }
}

void bd_send_txt(spp_queue_item_t item) {
    bd_client_t *end = bd_link_header;
    int colon=0;
    for (int i=0; i < item.len; i++) {
        if (item.in_data[i] == (uint8_t)':') {
            colon = i;
            break;
        }
    }
    if (colon == 0) return;
    char name[10];
    while(end) {
        memcpy((uint8_t*)name, end->bd_client_name, end->len);
        name[end->len]='\0';
        printf("%s:%d\n", name, end->handle);
        if (memcmp(end->bd_client_name, item.in_data+6, end->len)==0) {
            esp_spp_write(end->handle, item.len-(colon+1), item.in_data+colon+1);
            break;
        }
        end=end->next;
    }
    
}

void bd_disconnect(spp_queue_item_t item) {
    bd_client_t *pre;
    bd_client_t *end = bd_link_header;
    
    get_bd_client_name(item.handle, temp_name);

    if (strcmp(temp_name, "P0")==0) {
        pannel_connected = false;
        pannel_handle=0;
    }
    pre = bd_link_header;
    while (end) {
        if (item.handle == end->handle) {
            pre->next = end->next;
            if (pannel_connected && pannel_handle) {
                memcpy((uint8_t*)msg, end->bd_client_name, end->len);
                sprintf(msg+end->len, ":Disconnected");
                esp_spp_write(pannel_handle, end->len+13, (uint8_t*)msg);
            }
            if (end == bd_link_header) {
                bd_link_header = end->next;
            } else {
                pre->next = end->next;
            }
            free(end);
            break;
        }
        pre = end;
        end = end->next;
    }
}
void QueueReceiveTask(void* param) {
    spp_queue_item_t item;
    while(1) {
        if (xQueueReceive(spp_queue_handle, &item, portMAX_DELAY) == pdTRUE) {
            item.in_data[item.len]='\0';
            if (memcmp(item.in_data, (uint8_t*)"AT+ST", 5) == 0) {
                set_bd_client(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+LS", 5) == 0) {
                show_bd_connected_client(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+CMD", 6) == 0) {
                bd_send_cmd(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+TXT", 6) == 0) {
                bd_send_txt(item);
            } else if (memcmp(item.in_data, (uint8_t*)"AT+DIS", 6) == 0) {
                bd_disconnect(item);
            }
        }   
        vTaskDelay(10);
    } 
}

void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs flash init error");
        return;
    }
 
    gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT);
    gpio_set_level(GPIO_NUM_4, 0);
    spp_queue_handle =  xQueueCreate(10, sizeof(spp_queue_item_t));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_cfg);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err = esp_bluedroid_init();
    err = esp_bluedroid_enable();
    
    err = esp_spp_register_callback(my_bt_spp_cb);
    err = esp_spp_init(ESP_SPP_MODE_CB);

    err = esp_spp_start_srv(ESP_SPP_SEC_AUTHENTICATE, ESP_SPP_ROLE_MASTER, 1, SPP_SERVER_1);

    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    
    esp_bt_cod_t cod;
    cod.major = 0x1;
    cod.minor=0x0;
    err = esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR);

    esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(esp_bt_io_cap_t));
    err = esp_bt_dev_set_device_name(BT_DEVICE_NAME);
    err = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);

    esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_VARIABLE;
    esp_bt_pin_code_t pin_code;
    esp_bt_gap_set_pin(pin_type, 0, pin_code);

    xTaskCreatePinnedToCore(QueueReceiveTask, "TASK", 5120, 0, 10,0,1);
 }

2. Client端(Run at Slave side)
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_bt_api.h"
#include "esp_spp_api.h"
#include "esp_err.h"
#include "esp_log.h"
#include "string.h"
#include "driver/gpio.h"


#define TAG "MY_SPP_C1"
#define BT_DEVICE_NAME  "MY_SPP_C1"
uint8_t client_name[7]="AT+STC1";
#define SPP_SERVER_NAME "MY_SPP_MASTER"
uint8_t *spp_peer_name;

esp_bd_addr_t spp_server_addr = {0};
uint32_t hServer=0;


QueueHandle_t spp_queue_handle;

typedef struct {
    uint32_t handle;
    uint16_t len;
    uint8_t in_data[ESP_SPP_MAX_MTU];
} spp_queue_item_t;

spp_queue_item_t spp_queue_item;

uint8_t bd_name_len;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch(event) {
        case ESP_BT_GAP_DISC_RES_EVT:
            for (int i = 0 ; i < param->disc_res.num_prop; i++) {
                if (param->disc_res.prop[i].type == ESP_BT_GAP_DEV_PROP_EIR) {
                    spp_peer_name = esp_bt_gap_resolve_eir_data(param->disc_res.prop[i].val, ESP_BT_EIR_TYPE_CMPL_LOCAL_NAME, &bd_name_len);
                    if (!spp_peer_name) {
                        spp_peer_name = esp_bt_gap_resolve_eir_data(param->disc_res.prop[i].val, ESP_BT_EIR_TYPE_SHORT_LOCAL_NAME, &bd_name_len);
                    }
                    if (spp_peer_name) {
                        if (bd_name_len > ESP_BT_GAP_MAX_BDNAME_LEN) bd_name_len = ESP_BT_GAP_MAX_BDNAME_LEN;
                        spp_peer_name[bd_name_len]='\0';
                        // connect to default MASTER: MY_SPP_MASTER
                        if (strncmp(SPP_SERVER_NAME, (char*)spp_peer_name, bd_name_len) == 0) {
                            esp_bt_gap_cancel_discovery();
                            memcpy(spp_server_addr, param->disc_res.bda, ESP_BD_ADDR_LEN);
                            esp_spp_start_discovery(spp_server_addr);
                        }
                    } else {
                        ESP_LOGI(TAG, "GAP_DISC_RES, no peer_name, len:%d", bd_name_len);
                    }
                }
            }
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
            ESP_LOGI(TAG, "GAP_DISC_STATE_CHANGE");
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVCS_EVT");
        break; 
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_RMT_SRVC_REC_EVT");
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_AUTH_CMPL_EVT");
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_PIN_REQ_EVT");
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_NOTIF_EVT");
        break;
        case ESP_BT_GAP_KEY_REQ_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_KEY_REQ_EVT");
        break;
        case ESP_BT_GAP_READ_RSSI_DELTA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_READ_RSSI_DELTA_EVT");
        break;
        case ESP_BT_GAP_CONFIG_EIR_DATA_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_CONFIG_EIR_DATA_EVT");
        break;
        case ESP_BT_GAP_SET_AFH_CHANNELS_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_SET_AFH_CHANNELS_EVT");
        break;
        case ESP_BT_GAP_READ_REMOTE_NAME_EVT:
            ESP_LOGI(TAG, "remote Name:%s",(char*)param->read_rmt_name.rmt_name);
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_MODE_CHG_EVT");
        break;
        case ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_REMOVE_BOND_DEV_COMPLETE_EVT");
        break;
        case ESP_BT_GAP_QOS_CMPL_EVT:
            ESP_LOGI(TAG, "ESP_BT_GAP_QOS_CMPL_EVT");
        break;
        default:
        break;
    }
}

void my_bt_spp_cb(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) {
    switch(event) {
        case ESP_SPP_INIT_EVT:
            if (param->init.status == ESP_SPP_SUCCESS) {
                esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
                esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 0x10, 0);
            } else {
                ESP_LOGI(TAG, "ESP_SPP_INIT_ERROR");
            }
        break;
        case ESP_SPP_UNINIT_EVT:
        break;
        case ESP_SPP_DISCOVERY_COMP_EVT:
            // connect to spp server if found
            if (param->disc_comp.status == ESP_SPP_SUCCESS) {
                if (esp_spp_connect(ESP_SPP_SEC_AUTHORIZE, ESP_SPP_ROLE_SLAVE, 1, spp_server_addr)!=ESP_OK) {
                    ESP_LOGI(TAG, "esp_spp_connect error");
                }
            } else {
                ESP_LOGI(TAG, "ESP_SPP_DISCOVERY_COMP_EVT: not found retry...");
                esp_spp_start_discovery(spp_server_addr);
            }
        break;
        case ESP_SPP_OPEN_EVT:
             if (param->open.status == ESP_SPP_SUCCESS) {
                hServer=param->open.handle;
                esp_spp_write(param->open.handle, 7, (uint8_t*)client_name);
             } else {
                 ESP_LOGI(TAG, "CLINET OPEN- failure");
             }
        break;
        case ESP_SPP_CLOSE_EVT:
            ESP_LOGI(TAG, "close handle:%d, status:%d", param->close.handle, param->close.status);
        break;
        case ESP_SPP_START_EVT:
            ESP_LOGI(TAG, "SPP_START_EVENT:status:%d, scn:%d", param->start.status, param->start.scn);
        break;
        case ESP_SPP_CL_INIT_EVT:
            ESP_LOGI(TAG, "Clinet init");
        break;
        case ESP_SPP_DATA_IND_EVT:
            spp_queue_item.handle = param->data_ind.handle;
            spp_queue_item.len = param->data_ind.len;
            memcpy(spp_queue_item.in_data, param->data_ind.data, param->data_ind.len);
            xQueueSend(spp_queue_handle, &spp_queue_item, portMAX_DELAY);
        break;
        case ESP_SPP_CONG_EVT:
            ESP_LOGI(TAG, "cong handle:%d, cong:%d, status:%d", param->cong.handle, param->cong.cong, param->cong.status);
        break;
        case ESP_SPP_WRITE_EVT:
            ESP_LOGI(TAG, "ESP_SPP_WRITE_EVT");
        break;
        case ESP_SPP_SRV_OPEN_EVT:
        ESP_LOGI(TAG, "ESP_SPP_SRV_OPEN_EVT");
        break;
        case ESP_SPP_SRV_STOP_EVT:
        ESP_LOGI(TAG, "ESP_SPP_SRV_STOP_EVT");
        break;
        default:
        break;
    }
}

void QueueReceiveTask(void* param) {
    spp_queue_item_t item;
    while(1) {
        if (xQueueReceive(spp_queue_handle, &item, portMAX_DELAY) == pdTRUE) {
            if (memcmp(item.in_data, (uint8_t*)"ON", 2) == 0) {
                gpio_set_level(GPIO_NUM_4, 1);
            }
            if (memcmp(item.in_data, (uint8_t*)"OFF", 3) == 0) {
                gpio_set_level(GPIO_NUM_4, 0);
            }
        }   
        vTaskDelay(10);
    } 
}

void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs flash init error");
        return;
    }
    gpio_set_direction(GPIO_NUM_4, GPIO_MODE_OUTPUT);
    spp_queue_handle =  xQueueCreate(10, sizeof(spp_queue_item_t));
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_cfg);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err = esp_bluedroid_init();
    err = esp_bluedroid_enable();
    
    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &iocap, sizeof(esp_bt_io_cap_t));
    err = esp_bt_dev_set_device_name(BT_DEVICE_NAME);
   
    err = esp_spp_register_callback(my_bt_spp_cb);
    err = esp_spp_init(ESP_SPP_MODE_CB);

    xTaskCreatePinnedToCore(QueueReceiveTask, "TASK", 4096, 0, 1,0,1);
 }

2022年4月6日 星期三

ESP-IDF: ESP32 Bluetooth stereo Speaker with Backward/Play(Pause)/Forward button

本實驗製作一個藍芽喇叭,並附有前一首/下一首,播放/暫停控制鍵。

一、使用元件:

  1. ESP32-S(38 pins)開發版
  2. MAX98357A
  3. 3W喇叭
  4. 按鈕

二、成果展示:




三、軟體開發環境:

  1. VS Code
  2. ESP-IDF extension

四、Bluetooth classic Audio:

ESP32 Bluetooth 架構主要參考文件為下列官方網址:
https://www.espressif.com/sites/default/files/documentation/esp32_bluetooth_architecture_en.pdf
bluetooth audio 使用bluetooth classic A2DP and AVRCP因此只能使用Bluedroid,架構如下
 
memuconfig的bluetooth選項如下:

bluetooth audio 使用A2DP與AVRCP remote control,其profile相關如下圖所示:
依此結構,軟體初始化順序為
controller: init/enable --> bluedroid init/enable --> AVRCP register callback/init --> A2DP register cb --> A2DP sink register cb and data cb/init --> GAP register cb, security param, set device name and scan mode:詳細程式碼如文末所附程式碼(或成果展示影片的詳細步驟)。相關API參考網址:
https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/classic_bt.html

五、ESP32 使用的GPIO Pins:

  1. Buttons and LEDs
  2. I2S pins:

六、程式碼:
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
#include "esp_gap_bt_api.h"
#include "string.h"
#include "nvs_flash.h"
#include "driver/i2s.h"
#include "driver/gpio.h"
#include "esp_log.h"

#define BUTTON_FORWARD_PIN           5
#define BUTTON_PLAY_PAUSE_PIN        23
#define BUTTON_BACKWARD_PIN          19
#define LED_RED_PIN                  17
#define LED_GREEN_PIN                16
#define TAG "MY_BT_SPEAKER"

uint8_t my_bt_speaker_state=ESP_AVRC_PLAYBACK_STOPPED;
const char bt_device_name[]="MY_BT_SPEAKER";
bool avrc_conn=false;

static bool ct_button_play_pause_press=false;
static bool ct_button_backward_press=false;
static bool ct_button_forward_press=false;

void my_bt_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) {
    switch (event) {
        case ESP_BT_GAP_DISC_RES_EVT:
        break;
        case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
        break;
        case ESP_BT_GAP_RMT_SRVCS_EVT:
        break;
        case ESP_BT_GAP_RMT_SRVC_REC_EVT:
        break;
        case ESP_BT_GAP_AUTH_CMPL_EVT:
            ESP_LOGI(TAG,"auth: state:%d, remote_name:%s", param->auth_cmpl.stat, param->auth_cmpl.device_name);
        break;
        case ESP_BT_GAP_PIN_REQ_EVT:
        break;
        case ESP_BT_GAP_CFM_REQ_EVT:
            esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
        break;
        case ESP_BT_GAP_KEY_NOTIF_EVT:
        break;
        case ESP_BT_GAP_MODE_CHG_EVT:
        break;
        default: 
            ESP_LOGI(TAG, "GAP EVENT ID:%d",event);
        break;
    }
}


void my_bt_a2d_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param) {
    switch(event) {
        case ESP_A2D_CONNECTION_STATE_EVT:
        if (param->conn_stat.state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
            esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
            gpio_set_level(LED_GREEN_PIN, 1);
            gpio_set_level(LED_RED_PIN, 0);
        }
        if (param->conn_stat.state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
            esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
            gpio_set_level(LED_GREEN_PIN, 0);
            gpio_set_level(LED_RED_PIN, 1);
        }
        break;
        case ESP_A2D_AUDIO_STATE_EVT:
        break;
        case ESP_A2D_AUDIO_CFG_EVT:
        if (param->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) {
            int sample_rate = 16000;
            char oct0 = param->audio_cfg.mcc.cie.sbc[0];
            if (oct0 & (0x01 << 6)) {
                sample_rate = 32000;
            } else if (oct0 & (0x01 << 5)) {
                sample_rate = 44100;
            } else if (oct0 & (0x01 << 4)) {
                sample_rate = 48000;
            }
            i2s_set_clk(0, sample_rate, 16, 2);
        }
            
        break;
        case ESP_A2D_MEDIA_CTRL_ACK_EVT:
        break;
        case ESP_A2D_PROF_STATE_EVT:
        break;
        default:
        break;
    }
}

void my_bt_a2d_sink_data_cb(const uint8_t *buf, uint32_t len) {
    size_t wb;
    i2s_write(0, buf, len, &wb, portMAX_DELAY);
}

esp_avrc_rn_evt_cap_mask_t rn_cap_mask={0};
void my_bt_avrc_cb(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t *param) {
    char *text;
    switch(event) {
        case ESP_AVRC_CT_CONNECTION_STATE_EVT:
        if (param->conn_stat.connected) {
            esp_avrc_ct_send_get_rn_capabilities_cmd(1);
            avrc_conn=1;
            esp_avrc_ct_send_register_notification_cmd(1, ESP_AVRC_RN_PLAY_STATUS_CHANGE, 0);
        } else {
            rn_cap_mask.bits=0;
             avrc_conn=0;
        }
        break;
        case ESP_AVRC_CT_PASSTHROUGH_RSP_EVT:
        break;
        case ESP_AVRC_CT_CHANGE_NOTIFY_EVT:
            switch(param->change_ntf.event_id) {
                case ESP_AVRC_RN_VOLUME_CHANGE:
                break;
                case ESP_AVRC_RN_PLAY_STATUS_CHANGE:
                    my_bt_speaker_state = param->change_ntf.event_parameter.playback;
                break;
                default:
                break;
            }
        break;
        case ESP_AVRC_CT_GET_RN_CAPABILITIES_RSP_EVT:
            rn_cap_mask.bits = param->get_rn_caps_rsp.evt_set.bits;
            break;
        case ESP_AVRC_CT_SET_ABSOLUTE_VOLUME_RSP_EVT:
        break;
        case ESP_AVRC_CT_METADATA_RSP_EVT:
            text = (char*)param->meta_rsp.attr_text;
            text[param->meta_rsp.attr_length]='\0';
            ESP_LOGI(TAG,"metadata_rsp:%d, %s, %d", param->meta_rsp.attr_id, text, param->meta_rsp.attr_length);
        break;
        default:
        break;
    }
}
static void ct_play_forward(void *param) {
    if (!ct_button_forward_press) {
        ct_button_forward_press=true;
    }
}
static void ct_play_play_pause(void *param) {
    if (!ct_button_play_pause_press) {
        ct_button_play_pause_press=true;
    }
}
static void ct_play_backward(void *param) {
    if (!ct_button_backward_press) {
        ct_button_backward_press=true;
    }
}
void my_bt_set_gpio_pins() {
    gpio_set_direction(BUTTON_BACKWARD_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(BUTTON_FORWARD_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(BUTTON_PLAY_PAUSE_PIN, GPIO_MODE_INPUT);
    gpio_set_direction(LED_GREEN_PIN, GPIO_MODE_OUTPUT);
    gpio_set_direction(LED_RED_PIN, GPIO_MODE_OUTPUT);

    gpio_set_intr_type(BUTTON_BACKWARD_PIN, GPIO_INTR_POSEDGE);
    gpio_set_intr_type(BUTTON_FORWARD_PIN, GPIO_INTR_POSEDGE);
    gpio_set_intr_type(BUTTON_PLAY_PAUSE_PIN, GPIO_INTR_POSEDGE);
    

    gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
    gpio_isr_handler_add(BUTTON_FORWARD_PIN, ct_play_forward, 0);
    gpio_isr_handler_add(BUTTON_PLAY_PAUSE_PIN, ct_play_play_pause, 0);
    gpio_isr_handler_add(BUTTON_BACKWARD_PIN, ct_play_backward, 0);

    gpio_set_level(LED_GREEN_PIN,0);
    gpio_set_level(LED_RED_PIN,1);
}
void app_main(void)
{
    esp_err_t err;
    err = nvs_flash_init();
    if (err != ESP_OK) {
        ESP_LOGI(TAG, "nvs init error");
        return;
    }

    my_bt_set_gpio_pins();

    i2s_config_t i2s_config = {
        .mode = I2S_MODE_MASTER | I2S_MODE_TX,                          // Only TX
        .sample_rate = 44100,
        .bits_per_sample = 16,
        .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,                   //2-channels
        .communication_format = I2S_COMM_FORMAT_STAND_I2S,              // MAX98357A I2S Philips
        .dma_buf_count = 6,
        .dma_buf_len = 60,
        .intr_alloc_flags = 0,                                          //Default interrupt priority
        .tx_desc_auto_clear = true                                      //Auto clear tx descriptor on underflow
    };
    i2s_driver_install(0, &i2s_config, 0, NULL);

    i2s_pin_config_t pin_config = {
        .bck_io_num = 26,
        .ws_io_num = 22,
        .data_out_num = 25,
        .data_in_num = -1                                                       //Not used
    };

    i2s_set_pin(0, &pin_config);

    esp_bt_controller_config_t bt_controller  = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    err = esp_bt_controller_init(&bt_controller);
    err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);

    err= esp_bluedroid_init();
    err = esp_bluedroid_enable();

    err = esp_avrc_ct_register_callback(my_bt_avrc_cb);
    err = esp_avrc_ct_init();
   
    err = esp_a2d_register_callback(my_bt_a2d_cb);
    err = esp_a2d_sink_register_data_callback(my_bt_a2d_sink_data_cb);
    err = esp_a2d_sink_init();

    err = esp_bt_gap_register_callback(my_bt_gap_cb);
    err = esp_bt_dev_set_device_name(bt_device_name);
    esp_bt_io_cap_t io_cap=ESP_BT_IO_CAP_IO;
    err = esp_bt_gap_set_security_param(ESP_BT_SP_IOCAP_MODE, &io_cap, sizeof(io_cap));
    err = esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
   
   while(1) {
        if (avrc_conn) {
            vTaskDelay(5);
            if (ct_button_backward_press) {
                esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_BACKWARD, ESP_AVRC_PT_CMD_STATE_PRESSED);
                vTaskDelay(300/portTICK_PERIOD_MS);
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                ct_button_backward_press=false;
            } 
            if (ct_button_forward_press) {
                esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_FORWARD, ESP_AVRC_PT_CMD_STATE_PRESSED);
                vTaskDelay(300/portTICK_PERIOD_MS);
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                ct_button_forward_press=false;
            } 
            if (ct_button_play_pause_press) {
                if (my_bt_speaker_state == ESP_AVRC_PLAYBACK_STOPPED || my_bt_speaker_state == ESP_AVRC_PLAYBACK_PAUSED) {
                    esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_PLAY, ESP_AVRC_PT_CMD_STATE_PRESSED);
                    my_bt_speaker_state = ESP_AVRC_PLAYBACK_PLAYING;
                } else  if (my_bt_speaker_state == ESP_AVRC_PLAYBACK_PLAYING) {
                    esp_avrc_ct_send_passthrough_cmd(1, ESP_AVRC_PT_CMD_PAUSE, ESP_AVRC_PT_CMD_STATE_PRESSED);
                    my_bt_speaker_state = ESP_AVRC_PLAYBACK_PAUSED;
                }
                //esp_avrc_ct_send_metadata_cmd(1, ESP_AVRC_MD_ATTR_TITLE);
                vTaskDelay(300/portTICK_PERIOD_MS);
                ct_button_play_pause_press=false;
            } 
        } else {
            vTaskDelay(5);
        }
    }
}