數位時鐘 - Part 2

簡介

本教學文件使用 LinkIt 7687 開發板來建立一個數位時鐘。文件分為三個部分,每個部分各自描述如何使用開發板的不同功能來完成該專案。第二部分展示了如何使用 Android 應用程式並採用 SDK 的智慧連接 (Smart connection)功能來與 LinkIt 7687 開發板通訊。第三部分展示了如何使用休眠模式來實現省電功能。

 

介紹

這是 LinkIt 7687 數位時鐘教學文件的第二部。本文件將教您:

  • 使用 NVDM (Non-volatile Data Management) 模組來檢查 Wi-Fi 配置資料是否已經存在於開發板中。
  • 使用 "Smart Connection" 來接收由 Android 手機發送的 SSID 和密碼資訊。
  • 使用 Android Studio 以及 Elian 程式庫建立一個 Android 應用程式來廣播 smart connection 訊息。
  • 使用 NVDM 模組以保存由 smart connection 收到的 SSID 和密碼,在下一次開機時使用。
  • 使用 HAL GPIO API 檢查 EINT 按鈕狀態, 按下按鈕可以強制忽略已保存的 SSID 和密碼,並且再進入 smart connection 狀態來等待新的 Wi-Fi 設置。

前置作業

您必須先準備好以下作業:

  • LinkIt SDK v3.3.1.
  • 一個連接互聯網的無線網路環境
  • 一個連接上 AP 的 Android 手機,並開啟開發者選項 (Developer option)

開始之前

如果您從未使用過 LinkIt 7687 開發板建立專案,請必須先安裝 LinkIt SDK v3 並且設定您的開發環境。請先閱讀 LinkIt 開發平臺 for RTOS 入門指南 以瞭解完整的資訊。

如果這是您首次建立 Android 應用,您必須先安裝 Android Studio。請點擊這裡以瞭解安裝細節。同時也需要安裝 Android SDK 以及 NDK。 Android SDK 可以經由 Android Studio 裡的 SDK manager 功能裡取得。Android NDK 則可以從這裡取得。

硬體設置

如果您已經完成本教學文件的 第一部,那麼您已經擁有硬體來實現本文。如果您還未完成第一部,請參考 第一部 的硬體設置章節來準備硬體。

軟體實作

本章節描述需要用到的軟體,包括聯發科技的 Elian 庫,Android 應用的程式碼,以及 LinkIt 7687 開發板的程式碼。此專案會使用 Elian 程式庫讓手機廣播帶有 Wi-Fi 配置資訊的 smart connection 訊息,讓 LinkIt 7687 開發板取得 Wi-Fi 配置資訊後連上 AP。

工作流程

在 LinkIt 7687 開發板端的軟體流程如下:

  • 檢查 Wi-Fi 配置資訊是否已存在於開發板
  • 如果有,使用此 Wi-Fi 配置直接初始化 Wi-Fi
  • 如果沒有,以不帶 Wi-Fi 配置的方式啟動 Wi-Fi 初始化,接著進入 smart connection 模式來接收 Wi-Fi 配置,收到資訊之後,更新並連接 AP

使用 NVDM 模組檢查 Wi-Fi 配置資訊

LinkIt 開發平臺 for RTOS 提供 NVDM 中介軟體模組, 它可以來存取開發板上的使用者資料。

首先是呼叫 nvdm_init() 來初始化 NVDM。再使用 nvdm_read_data_item() 來檢查保存在參數 group_namedata_item_name 裡的資料來確定 Wi-Fi 配置資訊是否存在。此範例使用的 group_nameuserdata_item_name 分別為 ssidpwd

uint8_t ssid[WIFI_MAX_LENGTH_OF_SSID + 1];
uint8_t pwd[WIFI_LENGTH_PASSPHRASE + 1]; 
uint32_t ssid_len = WIFI_MAX_LENGTH_OF_SSID;
uint32_t pwd_len = WIFI_LENGTH_PASSPHRASE;
 
nvdm_init();
if (nvdm_read_data_item("user", "ssid", ssid, &amssid_len) == NVDM_STATUS_ITEM_NOT_FOUND ||
    nvdm_read_data_item("user", "pwd", pwd, &pwd_len) == NVDM_STATUS_ITEM_NOT_FOUND)
{
  // not found
}
else
{
  // found
}

如果有找到 Wi-Fi 配置資訊,可參考第一部裡的初始化 Wi-Fi 章節來進行 Wi-Fi 初始化。Wi-Fi 配置資訊已存在的這種情況將簡稱為 Case 2。而 Wi-Fi 配置資訊不存在的情況將簡稱為 Case 1。

不帶 Wi-Fi 配置的 Wi-Fi 初始化

如果 Wi-Fi 配置資訊不存在於開發板,就需要利用手機來發送 Wi-Fi 配置。而在進入 smart connection 模式來接收 Wi-Fi 配置之前則需要先初始化 Wi-Fi。

首先呼叫 wifi_init(),接下來等待 WIFI_EVENT_IOT_INIT_COMPLETE 發生。此範例使用一個全域信號 (global semaphore) 來實作同步的等待,流程如下:

  • 主程式 main() 在呼叫 wifi_init() 後,將建立並等待全域信號
  • _wifi_event_handler 收到 WIFI_EVENT_IOT_INIT_COMPLETE 事件 返回後,它將觸發全域信號
  • 此時主程式 main() 等待成功,將繼續運行

以下代碼說明其流程:

static int32_t _wifi_event_handler(...)
{
    switch(event)
    {
    case WIFI_EVENT_IOT_INIT_COMPLETE:
        xSemaphoreGive(wifi_inited);
        break;
 
        ...
    }
}
 
void network_init_wo_AP(void)
{
    ...
 
    wifi_connection_register_event_handler(WIFI_EVENT_IOT_INIT_COMPLETE , _wifi_event_handler);
 
    wifi_config.opmode = WIFI_MODE_STA_ONLY;
    wifi_inited = xSemaphoreCreateBinary();
 
    wifi_init(&wifi_config, NULL);
 
    ...
 
    // wait for result
    xSemaphoreTake(wifi_inited, portMAX_DELAY);
}

啟動 Smart Connection

WIFI_EVENT_IOT_INIT_COMPLETE 事件發生,就代表 Wi-Fi 初始化完成, 也就可以使用 smart connection 功能了。

首先,呼叫 wifi_smart_connection_init() 以提供金鑰 (key) 以及回呼函數。此金鑰會被使用來加密廣播訊息,讓只有使用同樣金鑰的手機和 7687 裝置設備能正確的辨認廣播訊息。回呼函數的功能則用來接收 smart connection 狀態的改變。

此範例同樣使用全域信號來實作同步等待 smart connection 的狀態改變,流程如下:

  • 主程式 main() 呼叫 wifi_smart_connection_init() 後,建立並等待全域信號
  • _smtcn_handler 收到 WIFI_SMART_CONNECTION_EVENT_INFO_COLLECTED 事件後,它將觸發全域信號
  • main() 程式將繼續運行並且使用 wifi_smart_connection_get_result() 取得 SSID 和密碼

以下代碼說明其流程:

static void _smtcn_handler(...)
{
    switch (event)
    {
        ...
        case WIFI_SMART_CONNECTION_EVENT_INFO_COLLECTED:
            xSemaphoreGive(smtcn_ready);
            wifi_smart_connection_deinit();
            break;
    }
}
 
void smart_connect(const char* key, char* ssid, char* pwd)
{
    uint8_t ssid_len = 0;
    uint8_t pwd_len = 0;
 
    smtcn_ready = xSemaphoreCreateBinary();
 
    wifi_smart_connection_init(key, key ? strlen(key) : 0, _smtcn_handler);
    wifi_smart_connection_start(0);
 
    // wait for result
    xSemaphoreTake(smtcn_ready, portMAX_DELAY);
 
    wifi_smart_connection_get_result(ssid, &ssid_len, pwd, &pwd_len, NULL, NULL);
    ssid[ssid_len] = 0;
    pwd[pwd_len] = 0;
}

更新 Wi-Fi 配置

從 smart connection 取得 SSID 和密碼資訊之後,呼叫 wifi_config_set_ssid()wifi_config_set_wpa_psk_key()wifi_config_reload_setting() 更新 Wi-Fi 配置。裝置將連接到指定 AP 並且通過 DHCP 服務來取得 IP address。

此範例再次使用全域信號來實作同步等待,流程如下:

  • 主程式 main() 呼叫 wifi_config_reload_setting() 後,等待全域信號
  • ip_ready_callback 被系統呼叫時,將觸發全域信號,讓 main() 繼續運行

以下代碼說明其流程:

static void _ip_ready_callback(struct netif *netif)
{
    ...
    LOG_I(common, "ip ready");
    xSemaphoreGive(ip_ready);
}
 
void network_init_wo_AP(void)
{
    ...
 
    ip_ready = xSemaphoreCreateBinary();
 
    sta_if = netif_find_by_type(NETIF_TYPE_STA);
    netif_set_status_callback(sta_if, _ip_ready_callback);
    dhcp_start(sta_if);
    ...
}

寫入 Wi-Fi 配置資訊

這時,裝置已成功連接上 Wi-Fi 網路。您現在可以儲存 SSID 和密碼,在下一次開機時重複使用。請使用 nvdm_write_data_item() 來保存資料。此範例將 group_name 參數設為 "user",data_item_name 參數設為 "ssid" 和 "pwd":

nvdm_write_data_item("user", "ssid", NVDM_DATA_ITEM_TYPE_STRING, ssid, strlen(ssid));
nvdm_write_data_item("user", "pwd", NVDM_DATA_ITEM_TYPE_STRING, pwd, strlen(pwd));

透過 GPIO 狀態檢查來強制進入 Smart Connection 狀態

如果 Wi-Fi 配置資訊已存在,7687 裝置會在開機時直接使用它並跳過 smart connection 流程。但如果 Wi-Fi 配置資訊已經過期而不再正確了,比如說,AP 設定有修改過,您還有使用按鈕來強制 7687 裝置進入 smart sonnection 狀態的選擇。

LinkIt 7687 HDK 上 EINT 按鈕對應到 HAL_GPIO_0,當按鈕被按下時讀取的傳回值為 HIGH,而當按鈕沒有被按下時傳回值是 LOW。以下範例代碼說明如何檢查 GPIO 狀態:

/* check GPIO 0 state */
hal_gpio_init(HAL_GPIO_0);
hal_pinmux_set_function(HAL_GPIO_0, HAL_GPIO_0_GPIO0);
hal_gpio_set_direction(HAL_GPIO_0, HAL_GPIO_DIRECTION_INPUT);
hal_gpio_pull_down(HAL_GPIO_0);
hal_gpio_get_input(HAL_GPIO_0, &gpio0_state);
hal_gpio_deinit(HAL_GPIO_0);
LOG_I(common, "gpio 0 state: %d", gpio0_state);
 
if (gpio0_state == HAL_GPIO_DATA_HIGH ||
    nvdm_read_data_item("user", "ssid", ssid, &ssid_len) == NVDM_STATUS_ITEM_NOT_FOUND ||
    nvdm_read_data_item("user", "pwd", pwd, &pwd_len) == NVDM_STATUS_ITEM_NOT_FOUND)
{
    // not found or button is pressed
}

納入 Smart Connection 模組

最後納入 Smart Connection 模組至專案。

  • 編輯 GCC/Makefile 並插入 LIBS += $(OUTPATH)/libsmtcn.a 和 MODULE_PATH += $(MID_SMTCN_PATH), 如下:

    ...
    LIBS += $(OUTPATH)/libhal.a
    LIBS += $(OUTPATH)/libdhcpd.a
    # add below
    LIBS += $(OUTPATH)/libsmtcn.a
     
    ...
    MODULE_PATH += $(KRL_SRV_PATH)
    MODULE_PATH += $(DRV_BSP_PATH)
    # add below
    MODULE_PATH += $(MID_SMTCN_PATH)
  • 編輯 GCC/feature.mk 並在檔案最後插入 MTK_SMTCN_ENABLE = y 如下:

    MTK_MBEDTLS_CONFIG_FILE             = config-mtk-basic.h
    # add below
    MTK_SMTCN_ENABLE                    = y

建立 Android 應用

您需要使用一台移動裝置來廣播 smart sonnection 配置訊息。在此範例中使用 Android 手機。本章節的目的著重於介紹如何使用 Smart Connection 程式庫,而如果您需要更多有關 Android 應用程式的專案設定資訊,可以參考位於本教程最後附錄中的建立一般 Android 專案和 JNI(Java native 介面)。

增加 Elian (Smart Connection) 函式庫到專案裡

Elian 是聯發科技的 Smart Connection 庫,它包含在 SDK 的路徑 tools\elian\elian_release\elian 並且支援多種手機平臺,包含 Android ,iOS,Windows Phone 和 win32。本教程使用的是 Android 版。

  • 複製 tools\elian\elian_release\elian\elian.h DigitalClock\app\src\main\jni 資料夾
  • 複製 tools\elian\elian_release\elian\android DigitalClock\app\src\main\jniLibs 資料夾
  • build.gradle (Module: app) (您可以在 Project view 找到它),增加 repositories 和sources 段落內容如下:

model {
    repositories {
        libs(PrebuiltLibraries) {
 
            elian {
                headers.srcDir "src/main/jni"
                binaries.withType(SharedLibraryBinary) {
                    sharedLibraryFile = file("src/main/jniLibs/${targetPlatform.getName()}/libelian.a")
                }
            }
        }
    }
    android {
        ...
        sources {
            main {
                jni {
                    dependencies {
                        library "elian" linkage "shared"
                    }
                }
            }
        }
    }
}

初始化 Elian (Smart Connection) 函式庫

呼叫 elianNew() 來初始化 Elian 函式庫,如下:

g_context = elianNew(NULL, 0, target, ELIAN_SEND_V4);

  • 該函數的前兩個參數是金鑰和金鑰長度。請確保設備和手機使用相同的金鑰,因為 Elian 函式庫會使用此金鑰來加密通信資訊,以便只有您的設備能從您的手機收到廣播資訊。本教程使用 NULL 作為默認金鑰。
  • 函數的第三個參數是裝置 MAC 位址。請將它設置為 0xFF,使所有設備都可以接收廣播資訊。
  • 第四個參數是 Elian 協定版本,請使用 ELIAN_SEND_V4。

呼叫 Elian API 來啟動和停止廣播功能

使用 elianPut 以配置 AP 的 SSID 和密碼,讓它嵌入至廣播資訊內。使用 elianStart 以啟動廣播。使用 elianStop 以停止廣播。以下範例代碼說明如何使用 Elian API:

Java_com_mediatek_labs_digitalclock_MainActivity_elianStartSmartConnection(...) {
    ...
 
    elianPut(g_context, TYPE_ID_SSID, (char *)_ssid, strlen(_ssid));
    elianPut(g_context, TYPE_ID_PWD, (char *)_password, strlen(_password));
    elianPut(g_context, TYPE_ID_CUST, (char *)_custom, strlen(_custom));
 
    ...
 
    result = elianStart(g_context);
    LOGI("start smart connection: %d", result);
}
 
Java_com_mediatek_labs_digitalclock_MainActivity_elianStopSmartConnection(...) {
    if (!g_context)
        return;
    elianStop(g_context);
    LOGI("stop smart connection");
}

經由 MainActivity Class 觸發 Elian 函式庫

本教程使用預先定義的 SSID 和密碼,並使用 BasicActivity 提供的浮動操作按鈕來啟動和停止 Smart Connection。請記得根據您的 Wi-Fi 環境配置來修改 SSID 和密碼。如下:

public class MainActivity extends AppCompatActivity {
 
    private boolean m_state;
    private String m_ssid = "your_SSID";
    private String m_pwd = "your_pwd";
    private String m_customData = "";
 
    ...
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        elianInitSmartConnection();
        m_state = false;
 
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(!m_state) {
                    elianStartSmartConnection(m_ssid, m_pwd, m_customData);
                    m_state = true;
                    Snackbar.make(view, "Start Smart connection", Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                }
                else
                {
                    elianStopSmartConnection();
                    m_state = false;
                    Snackbar.make(view, "Stop Smart connection", Snackbar.LENGTH_LONG)
                            .setAction("Action", null).show();
                }
            }
        });
    }
}

應用許可權配置

您的應用程式必須擁有 android.permission.INTERNET 許可權以確保 Elian 函式庫能正確的廣播資訊。請修改 AndroidManifest.xml 如下:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mediatek.labs.digitalclock">;
 
    <uses-permission android:name="android.permission.INTERNET" />
 
    ...

執行您的應用

現在,您可以從這裡下載完整的原始程式碼,並按照 LinkIt7687 HDK 入門指示來上傳並且在開發環境中運行代碼。

Case 1 - Wi-Fi 配置資訊不存在

  • 啟動 LinkIt 7687 HDK,您將在 LCD 顯示器裡看見 Waiting for smart connect...
  • 在您的手機上執行 DigitalClock 應用程式並按下右下角按鈕,您將在手機上看見 Start Smart connection 提示
  • 如果 LinkIt 7687 HDK 收到廣播資訊,裝置端的 LCD 顯示器應該顯示 Connecting AP: "your_SSID"
  • 如果一切都沒問題的話,LCD 顯示器接下來就會顯示當前的時間資訊

Case 2 - Wi-Fi 配置資訊存在

  • 啟動 LinkIt 7687 HDK,當它連接上Wi-Fi 網路後,您將在 LCD 顯示器裡看見 Connecting AP: "your_SSID" 然後緊接著顯示當前的時間資訊。
  • 若按住 EINT 按鈕再按下 reset 按鈕,這時就算配置資訊已存在,HDK 仍應該切換到 Case 1 的情況

結論

在本教程中,你學習了如何在手機上以及 LinkIt 7687 開發板上使用 Smart Connection 功能來連接 AP 並且獲得時間資訊。在第三部中,您將學習如何使用休眠模式來實現節能功能。關於聯發科技 LinkIt 7687 開發板的更多資訊,請參閱 LinkIt 開發平臺 for RTOS 開發者指南

故障排除

本章節為您在完成此教程前可能遇見的疑難雜症所提供解答。

  • 我該如何解決這個錯誤資訊?在 Android studio 裡執行 build 選項時得到 "Failed to find Build Tools revision 24.0.0" 提示

    請到 SDK manager -> SDK Tools -> Android SDK Platform-Tools 並點擊 Show Package Details 來安裝 24.0.0 版本或者修改 build.gradle 裡的 buildToolsVersion 把它修改成您使用的版本。

附錄

建立 Android 項目

本章節說明如何使用 Android Studio 建立一個對應此教程的 Android 應用程式。

  • 點擊 Start a new Android Studio project (或者從 File 功能表點擊 New 然後 New Project) 來打開 New Project 頁面
  • 在 New Project 頁面裡,修改 Application name 為 "DigitalClock",Company Domain 為 "labs.mediatek.com",在 Project location 輸入您指定的路徑,然後點擊 Next
  • 在 Target Android Devices 頁面裡,點擊 Phone and Tablet,設定 Minimum SDK 為 API 19: Android 4.4 (KitKat) 然後點擊 Next
  • 在 Add an Activity to Mobile 頁面裡,點擊 Basic Activity 然後點擊 Next
  • 在 Customize the Activity 頁面裡,使用預設值,然後點擊 Finish.

注: 您能修改 Application name,Company Domain and Minimum SDK 為任何值。在此範例中使用的值為 DigitalClock, labs.mediatek.com 和 API 19: Android 4.4 (KitKat)

設定 JNI (Java Native Interface)

本章節說明設定 Android 平臺的 JNI 步驟。

  • 設定專案使用 gradle wrapper。在 File 功能表,點擊 Settings,選擇 Build,Execution,Deployment,Build Tools,Gradle 然後點選 Use Default Gradle wrapper (建議) 和點擊 OK
  • 設定 Android NDK 位置。 在 File 功能表點擊 Project Structure... 然後設定 Android NDK location 為其安裝路徑
  • 打開 build.gradle (Project: DigitalClock) (您能在 Project view 的 Gradle Scripts找到它) 修改 classpath com.android.tools.build:gradle:2.1.2com.android.tools.build:gradle-experimental:0.7.2 如下:

    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle-experimental:0.7.2'
     
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
  • 打開 build.gradle (Module: app) (您能在 Project view 的 Gradle Scripts 找到它) 修改 model > android 段如下 (請勿修改 dependencies 那一段):

    model {
        android {
            compileSdkVersion 23
            buildToolsVersion "24.0.0"
     
            defaultConfig {
                applicationId "com.mediatek.labs.digitalclock"
                minSdkVersion.apiLevel 19
                targetSdkVersion.apiLevel 23
                versionCode 1
                versionName "1.0"
            }
            buildTypes {
                release {
                    minifyEnabled false
                    proguardFiles.add(file('proguard-android.txt'))
                }
            }
        }
    }
  • 將 NDK 段加在 model > android ,如下:

    model {
        android {
            // add below ndk section
     
            ndk {
                moduleName "elian-wrapper-jni"
                ldLibs.add("log")
                stl "stlport_static"
            }
        }
    }
  • 打開 MainActivity.java (您能在 Project view 的 app,java,com.mediatek.labs.digitalclock 找到它) 然後加入以下 java 代碼:

    public class MainActivity extends AppCompatActivity {
       ...
     
       static {
           System.loadLibrary("elian-wrapper-jni");
       }
       public native void elianInitSmartConnection();
       public native void elianStartSmartConnection(String SSID, String password, String customData);
       public native void elianStopSmartConnection();
     
       ...
    }
  • 在 Project view,右鍵點擊 app 然後打開 New,再打開 Folder ,再打開 JNI Folder,接受預設值設定,然後點擊 Finish
  • 右鍵點擊 jni 然後打開 New 再打開 C/C++ Source File,然後修改 Name 為 elian-wrapper-jni,Type 為 .cpp ,然後點擊 OK
  • 加以下 API 標頭檔 (header) 至 elian-wrapper-jni.cpp:

    extern "C" {
    JNIEXPORT void JNICALL
            Java_com_mediatek_labs_digitalclock_MainActivity_elianInitSmartConnection(
            JNIEnv *env, jobject instance);
    JNIEXPORT void JNICALL
            Java_com_mediatek_labs_digitalclock_MainActivity_elianStartSmartConnection(
            JNIEnv *env, jobject instance,
            jstring SSID,
            jstring password,
            jstring custom);
    JNIEXPORT void JNICALL
            Java_com_mediatek_labs_digitalclock_MainActivity_elianStopSmartConnection(
            JNIEnv *env, jobject instance);
    }