歡迎您光臨本站 註冊首頁

Linux 的魅力: Nokia N810 開發

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  
Nokia N810 警報介面允許開發人員通過編程的方式有效且輕鬆地設置警報。Peter Seebach 演示了如何將一個小型命令行程序連接到這個 API 並充分利用它。

作為 PDA 的潛在替代者,Nokia 770 最嚴重的一個缺陷是無法設置警報來喚醒設備。N800 引入了一個顯著改進的警報介面,而且延續到了最新的 N8100 中。在本文中,我將介紹使用 C 語言編寫的警報介面 API,並提出一個介面,使這個 API 可以用於其他語言中的 shell 腳本或程序。

首先,簡要介紹一下 N810。N810 是一個嵌入式手持系統,屏幕解析度為 800x480。它擁有藍牙、無線網路和 USB 連接功能。底層內核是 2.6.21 Linux® 內核,適用於硬體。

N810 與之前的 N800 非常類似。新增功能包括 GPS 和一個內置鍵盤。N81 惟一遜色於 N800 的地方是它只提供了一個可用的 MMC/SD 卡插槽,而 N800 提供了兩個 MMC/SD 卡插槽,都是最大尺寸。N810 在 “內部” 插槽中帶有一個硬連接的 2GB 卡,並為移動介質提供了一個 miniSD 插槽(如果您像我一樣,在使用嵌入式系統的幾年時間中累積了大量 SD 卡,一定會覺得很鬱悶)。

N810 的開發環境實際上與 N800 的開發環境相同(請參閱我撰寫的 其他有關 770 和 N800 的文章)。Scratchbox 和 maemo 環境進行了更新,但基本過程仍然是一樣的,Scratchbox 和 SDK 安裝依然快速而便捷。有兩個主要更改可能會影響一般開發人員。第一個變化是 xterm 安裝成開箱即用的;這是一個大的改進。第二個變化是在安裝 openssh-server 包時,會提示設置新的根密碼。這是對之前行為的重大改進:默認根密碼是 “rootme”。顯然,應該選擇一些其他密碼。

警報 API

警報 API 在去年發行的 maemo 3.0 中引入。它給出一組調用來與警報守護程序交互,警報守護程序提供警報服務。您應該喜歡使用這種介面嘗試編寫自己的警報,而且您肯定不會在 N810 這類環境中編寫自己的計時器代碼。嵌入式硬體中的電源管理是一個高級主題,而且容易出錯;所以要將這項工作交給專門的代碼處理。

即使您的警報代碼編寫得非常恰當,集中化服務仍然具有絕對優勢。假設您編寫了一個完美的警報介面,它很少喚醒系統,可能大約每 5 分鐘一次。這對電池壽命幾乎沒有任何影響。現在假設另一個和您一樣棒的人編寫了一些類似的介面。安裝了其中幾個介面,每 5 分鐘系統會被喚醒若干次。更糟的是,用戶往往挑選不同的時間間隔。因此,如果一件事件每 3 分鐘喚醒一次,另一件事件每 5 分鐘喚醒一次,還有一個每 7 分鐘喚醒一次的事件,這種情況是最糟糕的:即使沒有一個警報的時間間隔是 3 分鐘,但實際上每 2 分鐘就會收到一次警報 — 更糟的是,警報一起響起時可能會引起單獨的喚醒和休眠周期。所以請使用標準的 API。

警報 API 可以做許多事情。實際上,它可以配置非常靈活的計劃內通知行為。警報事件可能會顯示給用戶,也可能不會。警報任務可以執行三件主要事情:顯示消息、運行程序和通過 D-Bus 將消息發送給其他應用程序。您可能認為所有這些事情不全是警報,但它們與日曆應用程序提供的簡單提醒郵件的功能是相同的。簡而言之,如果實現足夠的警報功能來支持日曆等類似應用程序,最好進一步標準化並處理妥當。

警報以 /var/lib/alarmd/alarm_queue.xml 格式存儲為 XML。此文件要具有一定的易讀性,但不要大量使用奇怪數字來編碼標記。使用其他應用程序來創建事件,然後閱讀存儲在此文件中的 XML,可以深入了解警報事件結構的組成。

為什麼使用 getopt?

出於某些原因,人們喜歡編寫他們自己的參數解析器(我也如此,曾經編寫過功能非常全面的代碼來替代 getopt())。但是,在類似 UNIX® 的環境中,十有八九應該使用 getopt()。它提供了易於理解和熟悉的語義,可以滿足用戶的需求。對大多數程序而言,它足以表達各種選項。此外,它簡化了代碼並消除了 bug。我多次發現使用自己的參數解析器的程序有 bug。1997 年我編寫了自己的解析器,但我發現直到現在它仍存在 bug。

不管您是使用 shell、C 還是一些其他語言進行編程,請找到類似 getopt 的功能並使用它(使用 shell,相應的 POSIX 就是 shell 內置 getopts,它與 shell 的集成比與 getopt 命令的集成更好)。

一個簡單的提示器

為簡潔起見,我跳過警報介面的 D-Bus 部分,討論消息和代碼執行功能,因為它們不需要開發 D-Bus 代碼來處理傳入通知。能從命令行創建簡單的事件提醒程序固然很好。考慮到這一點,從命令行程序創建任意警報事件會更好,這些事件將執行程序或顯示消息。

警報系統相當靈活。警報有一個初始時間、一個警報重發頻率的設置(只能以分鐘計算)和重發次數。它無法處理每件事;例如,它不能處理在每個星期同一時間發生的每周一次的會議(因為 Daylight Savings Time 會將它拋出)。可以指定發出警報時的聲音、描述警報的圖片和消息。

有大量標記被提供用於控制警報的行為。這些標記都是布爾型標記;如果沒有指定標記,就隱式指定了與標記相反的狀態。一個由各個標記組成的集合加上一個由要設置的值組成的小集合非常適合 getopt(),但是首先要考慮使用哪些標記,以及將它們保持在哪裡。因為標記精確對應警報結構,所以我從警報結構開始討論。

首先,我們需要一個 alarm_event_t 結構,對它清零。其中一個 maemo 樣常式序使用 memset() 對它清零,但這可能會不正確;使用 memset 來用 0 進行填充不能保證會生成 null 指針(不過在此系統上碰巧如此)。標準省去了一些麻煩,但標準也提供了一定的靈活性:可以使用單個零初始化程序初始化第一個欄位,每個後續欄位通過一個顯式的 0 初始化,從而保證得到 null 指針:


清單 1. 對事件清零
          alarm_event_t event = { 0 };  

現在,只需相應地初始化成員。至今為止,最複雜的成員是時間。人們希望計時器使用秒作為時間單位。因此,需要稍微思考一下。下面的代碼塊解釋了三種可能的時間格式:


清單 2. 何時調用喚醒?
          time_t  parse_time(char *s) {          int Y, M, D, h, m;          time_t secs = time(NULL);          struct tm t = *(localtime(&secs));            if (sscanf(s, "%d-%d-%d %d:%d", &Y, &M, &D, &h, &m) == 5) {                  if (Y < 100) {                          t.tm_year = (t.tm_year - (t.tm_year % 100)) + Y;                  } else {                          t.tm_year = Y - 1900;                  }                  t.tm_mon = M - 1;                  t.tm_mday = D;                  t.tm_hour = h;                  t.tm_min = m;          } else if (sscanf(s, "%d:%d", &h, &m) == 2) {                  t.tm_hour = h;                  t.tm_min = m;          } else {                  m = strtol(s, &s, 10);                  if (*s || !m) {                          usage("enter '[YYYY-MM-DD] hh:mm' or delay in minutes.");                  }                  t.tm_min += m;          }          return mktime(&t);  }  

這裡採用了最常用的標準日期格式:ISO 日期(包括年),僅僅是一天的某個時間或以分鐘計算的時間偏移。不支持 12 小時時鐘,也不支持輸入月和日。後者是由於很難猜到用戶希望輸入它們的順序導致的;前者是因為我很懶。

使用標準 getopt() 函數處理大多數命令行參數都很簡單(請參見 側欄 了解更多詳細信息)。getopt() 常式根據接受的選項字元的字元串來處理參數(必須傳遞給它 argc 和 argv)。只能接受單個字元的選項;字元串中的每個字元代表程序已知的一個選項。如果一個選項的字元後跟有冒號,它就可以接受其他參數。例如,選項字元串 ab: 表示調用程序已知兩個選項:-a 和 -b,-b 后的下一個單詞是補充參數。代碼類似如下:


清單 3. 選項的部分列表
          while ((o = getopt(argc, argv, "ABDIZc:C:i:nr:R:s:t:z:")) != -1) {          switch (o) {          case 'A':                  event.flags |= ALARM_EVENT_ACTDEAD;                  break;          [...]          case 'c':                  event.exec_name = strdup(optarg);                  break;          [...]          case '?':          default:                  usage("unknown argument -%c.", optopt);                  break;          }  }     

如果您不熟悉 getopt(),則需要解釋一下。當沒有更多的選項時,getopt() 返回 -1。否則,返回下一個匹配標記的字元。如果此標記帶有參數,則參數存儲在全局變數 optarg 中。問號表示無效選項,在這種情況下,觸發它的字元存儲在變數 optopt 中。事實上,上面的錯誤消息是不正確的;如果需要某個參數的選項沒有參數,也會生成此消息。但是,對此程序的有效調用不會發生此錯誤,有效調用需要其他參數。

getopt() 返回 -1 后,全局變數 optind 保存第一個不屬於命令行選項的參數的索引。還剩下兩項內容需要解析:時間說明符,它是選項后的第一個參數,以及消息,消息是剩餘的參數。消息是可選的;如果已經指定了 -c 標記,則不需要任何消息。因為此程序只能顯示消息或運行命令,所以它拒絕創建既不是消息也不是命令的警報。

填充了事件結構后,將它提交給警報 API 就很簡單了:


清單 4. 結束時通知我
          cookie = alarm_event_add(&event);  if (cookie == 0) {          die("got an error: %d", alarmd_get_error());          exit(1);  }  

我們可以結束一個警報並期待它運行。遺憾的是,需要處理幾種特殊狀況。





特殊狀況

我發現這些狀況經過一些測試。可能會出現更多狀況,但這 4 條給出了一些您在將 C API 用於警報守護程序或使用警報守護程序本身時應該注意的事情。

狀況 #1:%s Alarm

如果未指定任何內容,默認標題就是 “%s alarm” — 可能是一個 bug。如果讓 event.title 欄位為空,或者將它設置為空字元串就會出現這種情況。

以下是我的解決方案:在解析用戶參數前,將 event.title 設置為 “Alarm!”。如果該用戶沒有指定標題,就提供一個安全標題。這可防止出現意外事件。

狀況 #2:是否應該顯示對話?

有一個標記可以設置不顯示事件的對話。因為對話只在它有消息時才有意義,所以程序在消息為空時會設置 “無對話” 標記。但是,除非有命令要運行,否則空消息被視為一種錯誤,因為既不運行命令也不顯示消息的警報似乎是多餘的。事實上,這限制了一定的功能;惡意用戶可能希望使用 boot-on-alarm 功能去喚醒系統,而不用執行其他操作。


清單 5. 用戶界面邏輯
          if (!event.exec_name && (argc - optind) < 2) {          usage("if you do not specify a command, you must specify a message.");  }  event.alarm_time = parse_time(argv[optind]);  event.message = parse_message(argv, optind + 1);  if (event.message[0] == '\0') {          event.flags |= ALARM_EVENT_NO_DIALOG;  }  

狀況 #3:聲音文件

聲音參數應該是聲音文件的名稱,用戶可以方便地獲得大量聲音文件。/usr/share/sounds 中有大量有用文件的集合。不過指定完整路徑會困擾用戶。

以下是我的解決方案:


清單 6. 尋找聲音
          case 's':          if (*optarg == '/') {                  event.sound = strdup(optarg);          } else {                  s = malloc(strlen(optarg) + 23);                  sprintf(s, "/usr/share/sounds/%s", optarg);                  if (access(s, R_OK) == 0) {                          event.sound = s;                          break;                  }                  sprintf(s, "/usr/share/sounds/%s.mp3", optarg);                  if (access(s, R_OK) == 0) {                          event.sound = s;                          break;                  }                  sprintf(s, "/usr/share/sounds/%s.wav", optarg);                  if (access(s, R_OK) == 0) {                          event.sound = s;                          break;                  }                  usage("can't find '%s' in /usr/share/sounds, try absolute path.",                  optarg);          }          break;  

這將在系統默認位置搜索聲音文件,並檢查常用(和受支持的)MP3 和 WAV 格式後綴。不費任何力氣即可找到當前目錄中的文件,因為這太簡單了;用戶不需要費很大勁。在邊注中,只有在對話顯示時才播放聲音。

如果您曾使用過 Clock 應用程序來設置警報,可能想知道如何控制警報音量的提高,我也是。據我所知,這是自動化的;警報在開始時無聲播放,然後聲音逐漸變大。

狀況 #4:命令執行

有一些狀況是與命令執行關聯的。第一種情況是如果您有一個對話,執行只在用戶關閉對話時發生,打開期間不會發生。如果沒有對話,則會立即執行命令。這表明,如果確實需要某事發生,則應該將它從任何想要顯示給用戶的警報對話中分離出來;生成第二個事件。

第二種狀況是您不能傳入任意 shell 命令。C API 中的 exec_name 參數傳給 g_shell_parse_argv() 函數,而不是一個完整 shell。顯然,這意味著不存在任何參數擴展或匹配。

如果需要參數擴展或匹配,必須顯式調用一個 shell。為此,我添加了一個選項來完成這個任務:


清單 7. 跳轉到 shell
          case 'C':          s = malloc(strlen(optarg) + 9);          sprintf(s, "sh -c '%s'", optarg);          optarg = s;  case 'c':          event.exec_name = strdup(optarg);          break;  

細心的讀者會發現,如果傳遞的參數包含單引號,將會完全失敗。但是,解決此問題僅僅是一個 shell 編程練習,不是警報 API 的實際部分。





結束語

警報 API 極其靈活,而且相當容易維護。警報守護程序的 C API 部分對用戶而言非常簡單,並且提供了許多功能。雖然這種簡化的介面無法完成您在較大應用程序中所能做的一切事情,但它顯示了極大的靈活性;您可以非常輕鬆地向此 API 添加一天的行程和約會。還可以提供某種方式管理現有警報事件。

N810(和 N800)的一些應用程序實行它們自己的警報管理;這似乎是一個欠妥的選擇。如果您正在處理需要提供警報的應用程序,那麼請花幾分鐘的時間來學習警報 API 並使用它。這將會為您節省時間,並能更好地與其他需要設置警報的程序集成。(責任編輯:A6)



[火星人 ] Linux 的魅力: Nokia N810 開發已經有561次圍觀

http://coctec.com/docs/linux/show-post-69118.html