来自Wi-Fi专家的声音

 

SX-580为台灯的无线化赋能 (3)

撰写于:2014年9月26日
作者:silex Wi-Fi专家

在上一篇文章中,我们做到了“使用hostapd和udhcpd创建一个iPhone可以连接的接入点”。这次我们将实现WEB服务器和CGI。
运行网络服务器
SX-580 SDK的busybox默认实现了WEB服务器功能。通过 httpd --help 可以显示简单的使用方法。
# httpd --help
BusyBox v1.19.4 (2014-08-05 19:14:14 PDT) multi-call binary.

Usage: httpd [-ifv[v]] [-c CONFFILE] [-p [IP:]PORT] [-r REALM] [-h HOME]
or httpd -d/-e STRING

Listen for incoming HTTP requests


        -i              Inetd mode
        -f              Don't daemonize
        -v[v]           Verbose
        -p [IP:]PORT    Bind to IP:PORT (default *:80)
        -r REALM        Authentication Realm for Basic Authentication
        -h HOME         Home directory (default .)
        -c FILE         Configuration file (default {/etc,HOME}/httpd.conf)
        -e STRING       HTML encode STRING
        -d STRING       URL decode STRING
  先在根目录(/root)中创建一个 index.html。
<html>
<body>
This is SX-580 SDK HTTP test<br>
</body>
</html>
从Shell启动httpd,然后使用'ps'命令来确认httpd是否正在运行。


# httpd
# ps
PID USER       VSZ STAT COMMAND
    1 root      1204 S    init
    2 root         0 SW   [kthreadd]
    3 root         0 SW   [ksoftirqd/0]
    .
    .
    .
  316 root      2224 S    hostapd -B /etc/hostapd.conf
  322 root      1192 S    udhcpd /etc/udhcpd.conf
  325 root      1196 S    inetd /tmp/inetd.conf
  326 root      1200 S    -sh
  335 root      1188 S    httpd
  341 root      1192 R    ps
 
在这种状态下,将iPhone连接到SSID IMAPP-SDK,然后在浏览器中打开'192.168.99.1',就可以看到之前创建的index.html页面(默认情况下字体可能非常小,可能需要放大才能容易阅读)。


WEB服务器测试 

如上所述,创建一个“只读 ”的WEB服务器非常简单。但是,我们这次的主题是“通过iPhone使用Wi-Fi控制的台灯”,所以它必须能够接受来自WEB浏览器的操作。来自WEB浏览器的操作由“表单”和“CGI”这两个元素组成。

关于表单
表单是通过HTML语法,以
开始,并以
结束的区块,其中包含标签可以通过type=属性指定为text、password、radio、checkbox、file、submit、image、button等。关于表单标签的解释在互联网上有很多,所以这里不详细讨论。

form标签的method=属性可以指定为get或者post。虽然名字可能会引起混淆,但并不是get是接收而post是发送,它们的基本动作是相同的。form标签的action=属性是用来指定URL的。通常这里会放入CGI的URL。

HTTP的动作始终遵循“客户端(浏览器)向服务器发送请求”→“服务器向客户端返回信息”的顺序。从协议层面上讲,“请求”被称为HTTP请求。
GET /index.html HTTP/1.1
Host: 192.168.99.100
User-Agent: Mozilla/5.0
Accept: text/html, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive


这样的一系列字符串(行结束是CR+LF)被发送。第一行的第一个词是“请求”,在HTTP上定义了“OPTIONS”、“GET”、“HEAD”、“POST”、“PUT”、“DELETE”、“TRACE”、“CONNECT”等,但通常只用“GET”和“POST”两种GET和POST在协议上的形式几乎相同,不同的是GET没有“Content-Length:”头,空行(CR+LF)标志着请求的结束,而POST在空行之后会跟着“Content-Length:”指定长度的数据。

在使用表单时,无论是GET还是POST,表单内的输入标签的信息都会被附加到请求中并发送到服务器。例如:

 



 

当在类似的表单中按下Submit按钮时


arg1=abcdefg&arg2=this+is+test%2b
这样的字符串会被传递到服务器。以name=value&name=value&...的形式重复,其中value部分的空格字符(' ')会被替换为+,而其他“URL中不能包含的字符(※注)”会被转换为%xx的形式,用16进制表示。这个规则被称为“URLencode”。
(※注)官方规定,“、<、>、%、# 和控制字符(0x00-0x1F、0x7F)是”URL中不能包含的字符“。使用0x80以上的汉字编码通常也会被替换为%xx形式的编码,并且URL参数的分隔符”&“、”+“和”="也会被替换为%26, %2b, %3d。有些浏览器甚至将波浪号“~”替换为%7E,具体细节取决于实施情况。

在method=get的情况下,字符串会通过在URL末尾添加一个?来附加,然后传递给服务器。在协议层面上,它看起来像下面这样。


GET /cgi-bin/test.cgi?arg1=abcdefg&arg2=this+is+test%2b HTTP/1.1
Host: 192.168.99.100
User-Agent: Mozilla/5.0
Accept: text/html, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive


在method=post的情况下,字符串作为HTTP数据传递给服务器。在协议层面上,它看起来像下面这样。


POST /cgi-bin/test.cgi HTTP/1.1
Host: 192.168.99.100
User-Agent: Mozilla/5.0
Accept: text/html, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-length: 33

arg1=abcdefg&arg2=this+is+test%2b


一般来说,GET的实现(稍微)容易一些,但由于URL总长度有限制(旧浏览器为256个字符,新浏览器为1024个字符),因此不能传递太多的参数。此外,由于参数会在URL中“可见”出来,所以浏览器的URL栏会显得杂乱无章,看起来不太好看。
POST可以传递比GET更多的参数,并且URL的显示也更加清晰。但是,解析Content-Length并解析接收到的数据的CGI实现稍微有些麻烦。


关于 CGI
CGI是Common Gateway Interface 的缩写,是WEB服务器调用外部程序的一种机制。为了接收分析通过表单发送的数据并触发某些动作,通常使用CGI。
CGI的原理很简单,对于满足“特定条件的请求”,WEB服务器会调用外部程序,并将执行结果作为HTTP响应返回。但是,如何设置这些“特定条件”,以及可以设置得多灵活,这取决于WEB服务器的实现。在busybox httpd的情况下,以/cgi-bin开头的URL路径被解释为CGI。

CGI在Unix系统实现上有很强的依赖性(※注),CGI作为WEB服务器的 “子进程 ”通过fork被创建,其标准输入会接收到HTTP接收数据(请求头结束后的空行之后),标准输出被视为HTTP发送数据(包括响应头)。WEB服务器向CGI进程传递的参数通过“环境变量 ”进行,这些详细规范也因WEB服务器的实现而异,但与接收表单数据有关。 
REQUEST_METHOD HTTP请求的类型,例如”GET“或”POST“。
QUERY_STRING         附加在URL的第一个&之后的数据。
CONTENT_LENGTH   附加在POST请求中的数据的长度。
CONTENT_TYPE        附加在POST请求中的数据的格式。
这四个环境变量在大多数WEB服务器实现中是通用的。
(※注)在没有子进程或环境变量等概念的实时操作系统(RTOS)中,CGI的机制完全不同。可以说每个RTOS或每个HTTP服务器都有自己的一套,一点也不“通用”。

在业界,CGI通常多用Perl或 PHP等解释型语言来实现,但如果可以作为子进程执行,任何形式都是可以的。可以用C语言编译成二进制文件,也可以实现为Shell脚本。

让我们尝试用 Shell脚本编写一个简单的CGI。用mkdir 在 /root 下创建一个cgi-bin 目录,然后创建以下 test.cgi 文件。请务必使用 chmod a+x 赋予其执行属性。


#!/bin/sh
echo "HTTP/1.0 200 OK"
echo "Content-Type:text/plain"
echo
echo "This is SX-580 CGI test"
echo script_name = $SCRIPT_NAME
echo request_method = $REQUEST_METHOD
echo query_string = $QUERY_STRING
echo content_length = $CONTENT_LENGTH
echo content_type = $CONTENT_TYPE

 

在这种状态下,如果您访问http://192.168.99.1/cgi-bin/test.cgi,应该会看到一个显示如下内容的页面:



CGI 测试 

如果在浏览器中编辑 URL 并以 http://192.168.99.1/cgi-bin/test.cgi?arg1=abcdefg 的形式访问,现在应该可以看到 “arg1=abcdefg ”被传递到 “QUERY_STRING”。

向CGI传递参数的测试

虽然 CGI 的原理很简单,但实现起来需要很多专业知识。例如,对于 POST 请求,需要从标准输入中接收通过环境变量 CONTENT_LENGTH 指定的字节数的参数,或者将 URL 编码的参数解码并分解成“名称=值”的列表形式存储起来,这些对于 Shell 脚本来说稍微有些困难。“通常情况下,CGI 用 Perl 或 PHP 实现较多”,这是因为这些解释型语言为 CGI 实现提供了充分的功能,并且它们在 Linux(Perl)和 Windows(PHP)上都是标准实现的。

SX-580 SDK 并没有包含 Perl。虽然实现 Perl也不是不可能,但 Perl 的交叉编译本身就足够写成一篇文章的难度,而且 Perl 作为一个庞大且处理能力较重的脚本语言,对于 ARM9 454MHz 的 SX-580 来说可能负担过重。这次我们不使用 Perl,而是决定用 Shell 脚本努力实现,并在必要时使用 C 语言。

通过 CGI 控制GPIO
那么,本系列连载的目的是“通过iPhone使用Wi-Fi控制的台灯”。要点亮或熄灭灯泡,需要打开/关闭100V的AC开关,无论是使用电磁式继电器还是使用二极管的SSR (Solid State Relay),都需要通过某种I/O端口来操作信号。SX-580配备了11个GPIO(通用输入输出)接口,我们将使用其中的一个。

在制作继电器单元之前,我们先来确认一下Linux中GPIO的使用方法。虽然SX-580 SDK的开发者指南中刊登了使用sxgpio_drv.ko进行GPIO操作的示例,但实际上SX-580的Linux内核已经实现了“内核模式GPIO驱动”,即使不使用IOCTL也可以操作GPIO
正如SX-580 SDK开发者指南表6-1所示,SX-580配备了11个GPIO,其中GPIO 6、7、8、9、10这5个GPIO连接到了主板上的LED。例如,GPIO0_6和GPIO1_24表示GPIO的端口号(0~3)和位号(0~31),在Linux上,它们被处理为“端口号 * 32 + 位号”的连续编号。例如,GPIO10是GPIO1_24,即1*32+24等于56号。

Kernel mode GPIO driver 是通过/sys/class/gpio下的虚拟文件集实现的。向/sys/class/gpio/export发送GPIO编号,就会创建一个名为/sys/class/gpio/gpioXX的虚拟目录,通过操作该虚拟目录内的文件,就可以实现对GPIO端口的操作。

将跳线 JP21-3 插入到"GPIO10" 一侧。


# echo 56 > /sys/class/gpio/export
# echo out > /sys/class/gpio/gpio56/direction


如果这样操作,LED5(橙色)应该会点亮。LED是按照负逻辑连接的,当端口输出状态为 "0" 时会点亮。通过向虚拟文件 "value" 发送 0/1,可以控制灯的熄灭/点亮。


# echo 1 > /sys/class/gpio/gpio56/value
# echo 0 > /sys/class/gpio/gpio56/value


此外,通过读取 value 值,也可以知道当前的输出(如果方向设置为 in,则为输入)状态。


# cat /sys/class/gpio/gpio56/value
0


发送GPIO编号到 /sys/class/gpio/unexport 后,虚拟目录就会消失。但是,GPIO的输入输出设置和输出状态等将保持不变,不会重新初始化。


# echo 56 > /sys/class/gpio/unexport


现在,让我们利用这一机制编写一个 CGI,它可以显示 GPIO 的当前状态并打开或关闭它。文件名为/root/cgi-bin/gpiotest.cgi。


#!/bin/sh
gpio_path=/sys/class/gpio/gpio56
if [ $QUERY_STRING = "SW=ON" ]; then
        echo 0 > $gpio_path/value
fi
if [ $QUERY_STRING = "SW=OFF" ]; then
        echo 1 > $gpio_path/value
fi
led_state=`cat $gpio_path/value`
echo "Content-type:text/html"
echo
echo "<html><head><title>SX-580-2700DM</title></head><body>"
echo "<div align="center">"
echo -n "Current state="
if [ $led_state = "1" ]; then
        echo "OFF<br>"
else
        echo "ON<br>"
fi
echo '<br>'
echo '<form method="get" action="/cgi-bin/gpiotest.cgi">'
echo '<input type="hidden" name="SW" value="ON">'
echo '<input type="submit" value="ON">'
echo '</form><br>'
echo '<form method="get" action="/cgi-bin/gpiotest.cgi">'
echo '<input type="hidden" name="SW" value="OFF">'
echo '<input type="submit" value="OFF">'
echo '</form>'
echo '</div></body></html>'

 

Shell 处理起来比较困难”的参数解释,是基于“参数("SW")只有一个”和“SW 只接受 "ON" 或 "OFF" 中的一个值”这样的前提,通过直接比较环境变量(例如 if [ $QUERY_STRING = "SW=ON" ]; then)来实现的。根据HTTP的规范,无论是 "?SW=ON" 还是 "?SW=%4F%4E" 都应该有相同的行为,但它决定不完全支持URL编码。


像往常一样,使用 chmod a+x gpiotest.cgi 命令给予执行权限后,通过iPhone浏览器访问http://192.168.99.1/cgi-bin/gpiotest.cgi 应该能够看到一个页面(如果不放大看会非常小),根据“ON”和“OFF”按钮的操作,LED5应该会相应地点亮和熄灭。

CGI来操作GPIO

下次我们将进行焊接作业来制作继电器主板。同时,CGI的操作界面也将使用图形设计,以实现“类似iPhone”的设计风格。
 

 



嵌入式无线LAN模块产品介绍页面