Linux 的魅力: Nokia N810 开发

来源:developerWorks 中国 作者:Peter Seebach
  
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)


时间:2008-09-23 16:45 来源:developerWorks 中国 作者:Peter Seebach 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量