树莓派网站容灾:利用DNSPod,Google App Engine和Github

背景介绍

把网站托管在树莓派上后如果家里停电或是宽带故障,会造成网站中断。本文提供一个免费的解决方案(前提是你需要有自己的一个域名,并由DNSPod解析)

DNSPod

首先需要在DNSPod里设置好需要failover的域名CNAME:比如hugozhu.myalert.info

其中默认指向pi.myalert.info, 这是一个域名的A Record,会由运行在树莓派上的脚本来更新动态IP,国外则指向github。当停电时我们需要自动把`默认`这条纪录修改成github。

使用下面命令获得相应CNAME的domain_id:

curl -k https://dnsapi.cn/Domain.List -d "login_email=xxx&login_password=xxx" 

使用下面命令获得相应CNAME的record_id:

curl -k https://dnsapi.cn/Record.List -d "login_email=xxx&login_password=xxx&domain_id=xxx"

Google App Engine

切换DNS脚本

package dnspod

import (
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

const (
	login_email    = "<your_login_email>"
	login_password = "<your_login_password>"
	format         = "json"
	domain_id      = "<domain_id>"
	record_id      = "<record_id>"
	sub_domain     = "<your_subdomain>"
	record_type    = "CNAME"
	record_line    = "默认"
	ttl            = "600"
)

func Update(client *http.Client, cname string) string {
	body := url.Values{
		"login_email":    {login_email},
		"login_password": {login_password},
		"format":         {format},
		"domain_id":      {domain_id},
		"record_id":      {record_id},
		"sub_domain":     {sub_domain},
		"record_type":    {record_type},
		"record_line":    {record_line},
		"value":          {cname},
		"ttl":            {ttl},
	}
	req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(body.Encode()))
	req.Header.Set("Accept", "text/json")
	req.Header.Set("Content-type", "application/x-www-form-urlencoded")
	resp, err := client.Do(req)
	if err != nil {
		return err.Error()
	}
	defer resp.Body.Close()
	bytes, _ := ioutil.ReadAll(resp.Body)
	return string(bytes)
}

检测接口

部署一个web应用到Google App Engine上,该应用接受树莓派上的一个URL(注意这里不应该用需failver的域名),并请求该域名以检查网站是否正常。这里也可以使用监控宝来监控,但只有付费专业版才支持出错后回调URL。

[Read More]

使用Goroutine和Channel实现按键超时交互

背景介绍

前面的文章(见参考链接)已经介绍了如何使用按键作为树莓派的输入。在实际应用中可以通过按下按键循环显示预先设定的脚本输出到显示屏幕,需求如下:

  1. 如果按键不被触动,则定时5秒执行脚本获取最新内容显示;
  2. 因为不同的脚本获取内容速度会不一样,我们要求如果超过500ms脚本还未返回,需要在屏幕上显示“loading…”这样的过渡内容,如果脚本在500ms内返回,则不显示。

使用Goroutine和Channel可以很方便的实现这个需求。

代码

var screen_chan chan int
var switch_chan = make(chan bool)

func main() {
	//a goroutine 检查按键是否被按
	go func() {
		last_time := time.Now().UnixNano() / 1000000
		btn_pushed := 0
		total_mode := 3
		for msg := range WiringPiISR(PIN_GPIO_6, INT_EDGE_FALLING) {
			if msg > -1 {
				n := time.Now().UnixNano() / 1000000
				delta := n - last_time
				if delta > 300 { //如果两次按键变化的间隔时间<300ms,是因为接触信号不稳定可以忽略掉
					last_time = n
					btn_pushed++
					screen_chan <- btn_pushed % total_mode
				}
			}
		}
	}()

	//a goroutine 根据管道消息刷新屏幕
	go loop_update_display()

	//选择确实的屏幕内容脚本编号
	screen_chan <- 0

	//a goroutine: 定时5s向管道发送更新屏幕内容的信号
	ticker := time.NewTicker(5 * time.Second)
	go func() {
		for {
			<-ticker.C
			screen_chan <- -1
		}
	}()
	
	...	
}

func loop_update_display() {
	current_screen := 0
	for msg := range screen_chan {
		switch_screen := false
		if msg >= 0 {
		   //说明是按钮触发的消息,而不是定时器触发的(-1)
			if msg != current_screen {
				//btn pushed
				current_screen = msg
				switch_screen = true
				go func() {
					select {
					case <-time.After(500 * time.Millisecond):
						display_loading()
						<-switch_chan
					case <-switch_chan:
					}
				}()
			}
		}
		switch current_screen {
		case 0:
			display_screen0()
		case 1:
			display_screen1()
		case 2:
			display_screen2()
		}
		if switch_screen {
			switch_chan <- true
		}
	}
}
go func() {
	select {
	case <-time.After(500 * time.Millisecond):
		display_loading(current_screen)
		<-switch_chan
	case <-switch_chan:
	}
}()

超时控制的代码就是上面几行了。首先如果是按键触发,主goroutine会创建一个检查超时的goroutine,该goroutine执行select语句时会试图从两个管道里获取消息,先获取到消息的管道会继续执行相应分支的代码。

[Read More]

Go语言内存模型

名词定义

执行体 - Go里的Goroutine或Java中的Thread

背景介绍

内存模型的目的是为了定义清楚变量的读写在不同执行体里的可见性。理解内存模型在并发编程中非常重要,因为代码的执行顺序和书写的逻辑顺序并不会完全一致,甚至在编译期间编译器也有可能重排代码以最优化CPU执行, 另外还因为有CPU缓存的存在,内存的数据不一定会及时更新,这样对内存中的同一个变量读和写也不一定和期望一样。

Java的内存模型规范类似,Go语言也有一个内存模型,相对JMM来说,Go的内存模型比较简单,Go的并发模型是基于CSP(Communicating Sequential Process)的,不同的Goroutine通过一种叫Channel的数据结构来通信;Java的并发模型则基于多线程和共享内存,有较多的概念(violatie, lock, final, construct, thread, atomic等)和场景,当然java.util.concurrent并发工具包大大简化了Java并发编程。

Go内存模型规范了在什么条件下一个Goroutine对某个变量的修改一定对其它Goroutine可见。

Happens Before

在一个单独的Goroutine里,对变量的读写和代码的书写顺序一致。比如以下的代码:

package main

import (
	"log"
)

var a, b, c int

func main() {
	a = 1
	b = 2
	c = a + 2
	log.Println(a, b, c)
}

尽管在编译期和执行期,编译器和CPU都有可能重排代码,比如,先执行b=2,再执行a=1,但c=a+2是保证在a=1后执行的。这样最后的执行结果一定是1 2 3,不会是1 2 2。但下面的代码则可能会输出0 0 01 2 2, 0 2 3 (b=2比a=1先执行), 1 2 3等各种可能。

package main

import (
	"log"
)

var a, b, c int

func main() {
	go func() {
		a = 1
		b = 2
	}()
	go func() {
		c = a + 2
	}()
	log.Println(a, b, c)
}

Happens-before 定义

Happens-before用来指明Go程序里的内存操作的局部顺序。如果一个内存操作事件e1 happens-before e2,则e2 happens-after e1也成立;如果e1不是happens-before e2,也不是happens-after e2,则e1和e2是并发的。

[Read More]

使用Go语言在树莓派上编程

WiringPi是树莓派上比较好的一个开发库,是用C语言写的。使用cgo,我们可以在Go语言里方便的调用WiringPI的函数,于是我包装了一个WiringPi-Go,目前支持wiringPi的基本功能,硬件SPI协议驱动Nokia 5110屏幕,以及中断,未来还会增加PWM和I2C协议的支持。

下面是一个完整的使用例子,结合了之前的两个电路:链接1链接2

通过push button可以切换液晶屏显示不同脚本的输出内容。

lcd_switch.go

package main

import (
	. "github.com/hugozhu/rpi"
	"github.com/hugozhu/rpi/pcd8544"
	"log"
	"os/exec"
	"time"
)

const (
	DIN        = PIN_MOSI
	SCLK       = PIN_SCLK
	DC         = PIN_GPIO_2
	RST        = PIN_GPIO_0
	CS         = PIN_CE0
	PUSHBUTTON = PIN_GPIO_6
	CONTRAST   = 40 //may need tweak for each Nokia 5110 screen
)

var screen_chan chan int
var TOTAL_MODES = 3

func init() {
	WiringPiSetup()
	pcd8544.LCDInit(SCLK, DIN, DC, CS, RST, CONTRAST)
	screen_chan = make(chan int, 1)
}

func main() {
	//a goroutine to check button push event
	go func() {
		last_time := time.Now().UnixNano() / 1000000
		btn_pushed := 0
		for pin := range WiringPiISR(PUSHBUTTON, INT_EDGE_FALLING) {
			if pin > -1 {
				n := time.Now().UnixNano() / 1000000
				delta := n - last_time
				if delta > 300 { //software debouncing
					log.Println("btn pushed")
					last_time = n
					btn_pushed++
					screen_chan <- btn_pushed % TOTAL_MODES //switch the screen display
				}
			}
		}
	}()

	//a groutine to update display every 5 seconds
	go loop_update_display()

	//set screen 0 to be default display
	screen_chan <- 0

	ticker := time.NewTicker(5 * time.Second)

	for {
		<-ticker.C
		screen_chan <- -1 //refresh current screen every 5 seconds
	}
}

func loop_update_display() {
	current_screen := 0
	for screen := range screen_chan {
		if screen >= 0 {
			if screen != current_screen {
				//btn pushed
				current_screen = screen
				display_loading()
			}
		}
		switch current_screen {
		case 0:
			display_screen0()
		case 1:
			display_screen1()
		case 2:
			display_screen2()
		}
	}
}

func display_loading() {
	pcd8544.LCDclear()
	pcd8544.LCDdrawstring(0, 20, "Loading ...")
	pcd8544.LCDdisplay()
}

func display_screen0() {
	out, err := exec.Command("/bin/screen_0.sh").CombinedOutput()
	if err != nil {
		out = []byte(err.Error())
	}

	pcd8544.LCDclear()
	pcd8544.LCDdrawstring(0, 0, string(out))
	pcd8544.LCDdisplay()
}

func display_screen1() {
	out, err := exec.Command("/bin/screen_1.sh").CombinedOutput()
	if err != nil {
		out = []byte(err.Error())
	}

	pcd8544.LCDclear()
	pcd8544.LCDdrawstring(0, 0, string(out))
	pcd8544.LCDdisplay()
}

func display_screen2() {
	out, err := exec.Command("/bin/screen_2.sh").CombinedOutput()
	if err != nil {
		out = []byte(err.Error())
	}

	pcd8544.LCDclear()
	pcd8544.LCDdrawstring(0, 0, string(out))
	pcd8544.LCDdisplay()
}

/bin/screen_2.sh

[Read More]

使用tsar记录和监控树莓派CPU温度

夏天到了,树莓派的CPU温度也开始节节攀升,虽然我们也可以用云服务cosm来监控,但每5分钟采样一次精度不够高,每分钟采样一次则上传次数又太多了点。最好的方法还是使用tsar这样的工具本地高频(如每1分钟)采样,然后再定时将5分钟的均值上传到cosm绘图。

Tsar是淘宝的一个用来收集服务器系统和应用信息的采集报告工具,如收集服务器的系统信息(cpu,mem等),以及应用数据(nginx、swift等),收集到的数据存储在服务器磁盘上,可以随时查询历史信息,也可以将数据发送到nagios报警。Tsar能够比较方便的增加模块,只需要按照tsar的要求编写数据的采集函数和展现函数,就可以把自定义的模块加入到tsar中。

更新

[2013-04-14] mod_rpi已经被合并到了主干代码:https://github.com/alibaba/tsar/blob/master/modules/mod_rpi.c 只需要增加文件:/etc/tsar/conf.d/rpi.conf,内容为以下即可开始使用mod_rpi模块:

mod_rpi on

####add it to tsar default output
output_stdio_mod mod_rpi

mod_rpi模块开发方法

首先按照安装说明,见https://github.com/alibaba/tsar将tsar和tsardevel安装好。

首先运行下面的命令生成mod_rpi模块:

hugo@raspberrypi2 ~/projects/tsardevel $ tsardevel rpi 
build:make
install:make install
uninstall:make uninstall
hugo@raspberrypi2 ~/projects/tsardevel $ ls rpi
Makefile  mod_rpi.c  mod_rpi.conf

然后修改mod_rpi.c,增加读取CPU温度的逻辑:

/*
 * (C) 2010-2011 Alibaba Group Holding Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include "tsar.h"

/*
 * Structure for rpi infomation.
 */
struct stats_rpi {
	unsigned int cpu_temp;
};

#define STATS_TEST_SIZE (sizeof(struct stats_rpi))

static char *rpi_usage = "    --rpi               Rapsberry Pi information (CPU temprature ...)";


static void read_rpi_stats(struct module *mod, char *parameter)
{
	FILE *fp;
	char buf[64];
	memset(buf, 0, sizeof(buf));
	struct stats_rpi st_rpi;
	memset(&st_rpi, 0, sizeof(struct stats_rpi));

	if ((fp = fopen("/sys/class/thermal/thermal_zone0/temp", "r")) == NULL) {
		return;
	}

	int cpu_temp;

	fscanf(fp, "%d", &cpu_temp);

	st_rpi.cpu_temp = cpu_temp;

	int pos = sprintf(buf, "%u",
			/* the store order is not same as read procedure */
			st_rpi.cpu_temp);
	buf[pos] = '\0';
	set_mod_record(mod, buf);
	fclose(fp);
	return;
}

static struct mod_info rpi_info[] = {
	{"  temp", SUMMARY_BIT,  0,  STATS_NULL}
};

static void set_rpi_record(struct module *mod, double st_array[],
		U_64 pre_array[], U_64 cur_array[], int inter)
{
	st_array[0] = cur_array[0]/1000.0;
}

void mod_register(struct module *mod)
{	
	register_mod_fileds(mod, "--rpi", rpi_usage, rpi_info, 1, read_rpi_stats, set_rpi_record);
}

最后make && sudo make install将mod_rpi自定义tsar模块安装好。

[Read More]

Raspberry Pi的GPIO中断编程

背景介绍

树莓派的GPIO引脚不仅可以输出高低电平,也可以当做输入端口(可以想象成键盘输入),当GPIO接入的是高电平,GPIO的值可以认为是1,如果是低电平则是0。如下图所示,可以使用一个Push Button开关按键来控制GPIO 25(BCM Numbering)的高低电平以达到控制的目的。

GPIO 25和VCC(3.3V)之间通过R1(10K欧姆)和R2(1K欧姆)上拉电阻相连,当按键未被按下时,GPIO 25上拉到VCC,程序可以读到1,当按键按下时,GPIO 25被下拉电阻R2拉到GND(0V),程序可以读到0。如果不加R1,而GPIO 25不小心被设置成输出低电平时,将直接和VCC相连而造成短路,这样可能会烧掉这个引脚,所以加上限流电阻R1后,即使发生这样的情况,也不会出现短路情况。

应用

如果我们需要根据GPIO 25的值来控制树莓派,比如按下按钮时希望点亮某个LED或在液晶上显示当前时间,就需要通过程序来获取状态的变化。

一种常见的做法是在循环里不断读取该引脚的状态,当发生对应的变化的时执行控制逻辑,但显而易见,这种做法很消耗CPU,如果在循环增加sleep(1000)这样的调用,又很容易错过按键变化。较好的做法则是通过中断来实现。

最新的树莓派Raspbian和Arch Linux内核都已经包含了GPIO的中断处理支持。但使用前需要将指定GPIO引脚输出,方法如下:

首先可以通过命令echo 25 > /sys/class/gpio/export导出GPIO 25端口,执行成功后在相应的目录下看到以下文件,得益于Linux下一切都是文件的设计理念,GPIO的状态可以通过value文件来获取,这样就可以利用Linux的poll/epoll来获取value文件的变化(这点和Linux高性能网络编程是类似的)。

root@raspberrypi2 ~/projects/interrupt_test # ls -l /sys/class/gpio/gpio25/
total 0
-rw-r--r-- 1 root root 4096 Apr  8 23:56 active_low
-rw-r--r-- 1 root root 4096 Apr  8 22:29 direction
-rw-r--r-- 1 root root 4096 Apr  8 22:29 edge
drwxr-xr-x 2 root root    0 Apr  8 23:56 power
lrwxrwxrwx 1 root root    0 Apr  8 23:56 subsystem -> ../../../../class/gpio
-rw-r--r-- 1 root root 4096 Apr  8 22:08 uevent
-rw-r--r-- 1 root root 4096 Apr  8 22:29 value
root@raspberrypi2 ~/projects/interrupt_test # 

wiringPi

wiringPi库封装了一个简单的接口,传入一个回调函数,当事件发生时传入的函数将被调用。

[Read More]

备份Raspberry Pi

树莓派的操作系统安装在SD卡,使用一段时间后还是很有必要备份一下,以防哪天SD卡就坏了。

备份的目的地最方便的还是使用网络存储,我使用的是西部数据的MyBooklive3T网络硬盘。挺不错的一个产品,功能基本满足我的需求。

准备好备份目标盘,将Nas的备份目录mount到树莓派:

mkdir /mnt/backup
mount -t cifs //mybooklive/Public/Backup /mnt/backup -o guest

完整备份

确定相应的SD卡设备ID

root@raspberrypi2 ~/bin # fdisk -l

Disk /dev/mmcblk0: 1973 MB, 1973420032 bytes, 3854336 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x0004f23a

        Device Boot      Start         End      Blocks   Id  System
/dev/mmcblk0p1   *        2048      186367       92160    c  W95 FAT32 (LBA)
/dev/mmcblk0p2          186368     3667967     1740800   83  Linux

Disk /dev/sda: 2107 MB, 2107637760 bytes, 4116480 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes

执行备份

[Read More]

在Raspberry Pi上使用硬件SPI

什么是SPI

SPI (Serial Peripheral Interface),是一种高速,全双工,同步的通信总线协议,基于SPI的设备需要4根线:

  1. SDO / MOSI - 主设备数据输出,从设备数据输入
  2. SDI / MISO - 主设备数据输入,从设备数据输出
  3. SCLK / CLK - 时钟信号,由主设备产生
  4. CS / SS - 从设备使能信号,由主设备控制

通过CS,主设备可以控制和哪个从设备通信。

Bit Banging

Bit-banging是一种用软件替代专职硬件的串行通信的技术。软件直接对微处理器的管脚的状态进行设置和采样,其功能涵盖诸如:时钟,电平,同步等所有参数。与此不同的是(传统的串行通信技术中),专职硬件诸如 modem、UART 或者 位移寄存器等一般是用来处理这些参数并且提供一个(缓存)的数据接口,软件在这种情况下同信号处理无关。

bit-banging 具有明显优点诸如:让相同的设备运行不同的协议而只需很小的(甚至不需)硬件的改动。借助很少的额外设备,我们也许可以从数字管脚(数字终端)可以得到视频信号。

bit-banging 也有一些明显的缺点。在软件仿真的过程中消耗的能量比同样功能的专职硬件大。微处理器过忙地从管脚采样和发送采样信号到管脚。在同等微处理器处理能力下,系统常常会有些噪音。

在Rasperry Pi上使用Bit Banging在实际情况下有可能因为操作系统调度造成时钟信号不稳定而使设备收到错误的消息,具体的表现就是Nokia 5110屏在长时间运行过程中出现白屏或花屏现象,如下图:

采用硬件SPI,由Pi的管脚14号Pin(左边倒数第二个)SCLK发出一定频率的时钟信号。经过测试,这种方法产生的时钟信号比Bit Banging软件模拟产生的信号要稳定很多。

软件模拟时钟信号波形 硬件SPI时钟信号波形

测试Pi的硬件SPI

确认内核支持

root@raspberrypi2 ~/projects/spi_test # ls -la /dev/spi*
crw------- 1 root root 153, 0 Jan  1  1970 /dev/spidev0.0
crw------- 1 root root 153, 1 Jan  1  1970 /dev/spidev0.1

测试代码

下载 spidev_test.c 或拷贝下面的代码:

[Read More]

在Raspberry Pi上使用Google Channel服务搭建实时应用

前面提到了有关个人网站的实时在线人数问题,本文要讨论的是如何自己来实现一个这样的统计服务。因为网站也同时部署在Github上,海外用户访问Github镜像网站的访问日志Pi是拿不到的,这怎么办?

Google Channel Service

Google Channel Service允许应用和GAE (Google App Engine) 保持一个长连接,允许应用实时发送消息给JavaScript客户端,而不用让客户端用效率很低的定时轮询获取新消息。这个服务是允许有多个发布者和多个订阅者,也能创建多个主题来关联发布者和订阅者。

使用这个服务分两步:

  1. 客户端请求服务器端(部署在GAE上)获取一个Channel的Token:

  2. 客户端根据Channel Token和服务器建立长连接,并开始接收消息,这时其它的客户端(或服务器端)可以想这个通道发送消息

在线人数统计实现

在页面上部署beacon

通常的网站流量统计是依赖部署在页面上的beacon(Javascript或图片标签)来实现的,这样做的好处是可以直接过滤掉一些机器流量,并且可以将日志集中存储在日志收集服务器上,和网站分离开。

于是可以利用GAE实现一个简单的Beacon服务,这里采用Go语言来实现,用Java或Python也是可以的。

package counter

import (
	"encoding/base64"
	"fmt"
	"time"
	"net/http"

	"appengine"
	"appengine/channel"
)

var GIF []byte

const (
	TOPIC = "counter"
)

func init() {
	GIF, _ = base64.StdEncoding.DecodeString("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
	
	
	http.HandleFunc("/beacon.gif", handler)
	http.HandleFunc("/new_token", handler_new_token)	
}

func handler(w http.ResponseWriter, r *http.Request) {
	context := appengine.NewContext(r)

	now := time.Now()
	expire := now.AddDate(30, 0, 0)
	zcookie, _ := r.Cookie("z")
	if zcookie == nil {
		zcookie = &http.Cookie{}
		zcookie.Name = "z"
		zcookie.Value = make_hash("<your_salt>", r.RemoteAddr, now.UnixNano())
		zcookie.Expires = expire
		zcookie.Path = "/"
		http.SetCookie(w, zcookie)
	}

	w.Header().Set("Content-type", "image/gif")
	w.Header().Set("Cache-control", "no-cache, must-revalidate")
	w.Header().Set("Expires", "Sat, 26 Jul 1997 05:00:00 GMT")

	fmt.Fprintf(w, "%s", GIF)

	channel.Send(context, TOPIC, zcookie.Value+"\n"+r.RemoteAddr+"\n"+r.Referer()+"\n"+r.UserAgent())
}

func handler_new_token(w http.ResponseWriter, r *http.Request) {
	c := appengine.NewContext(r)
	tok, err := channel.Create(c, TOPIC)	
	callback := r.FormValue("callback")	
	if err != nil {
		http.Error(w, "Couldn't create Channel", http.StatusInternalServerError)
		c.Errorf("channel.Create: %v", err)
		return
	}
	if callback == "" {
		w.Header().Set("Content-type", "text/javascript")
		fmt.Fprintf(w, "%s", tok)
	} else {
		fmt.Fprintf(w, callback+"('%s')", tok)
	}
}

代码最后一行是将访问日志实时通过Channel发送出去,该通道有一个指定的主题,这样订阅该主题的客户端都可以收到相应的消息。

[Read More]