Golang笔记–03–面向对象与GPIO

Overview

前言

这段时间对Go语言的使用依旧是对接物联网设备,解析协议与管理连接,同时review了一下之前同事写的代码改bug,踩到了MySQL的坑,具体放到下一篇来讲。

另外本周在树莓派上通过串口调试模块,一开始选择了C语言,接着又切到Python,依旧卡在配置开发环境,一些树莓派上的C Native库在macOS无法编译通过,最后还是使用Go,两天时间写代码和调试,用Go来控制GPIO和串口真的非常方便。

Golang面向对象

在iOS及macOS中面向对象是天生的,操作的都是对象,对象都是指针类型,很多人也从运行时源码详细解析了Objective-C面向对象原理,而Golang在语法上就没有继承的概念,不过可以嵌入和接口来实现。

假定现在有一种数据包Packet,Packet可能有响应BytesResp,类型为[]byte,不通类型的数据包具有各自的解析方式、推送后台接口URL、JSON数据格式、插入数据库的SQL,但他们具有相同的头部(包含设备信息、数据域长度、校验值等等)、起始符和终止符,设备通过TCP长连接与程序保持通信,引用上一篇Golang笔记中的例子,我们在拿到二进制数据后,解析数据,并返回响应。

 1p, err := model.NewPacketWithByte(data)
 2if err != nil {
 3	log.WithField("err", err).Info("数据解析错误")
 4	continue
 5}
 6pushData(p.JSONData(), p.PushURL())
 7go func() {
 8	err = dataBase.InsertPacket(p)
 9	if err != nil {
10		log.WithField("err", err).Info("插入数据失败")
11	}
12}()
13resp := p.BytesResp()
14if resp != nil {
15	conn.Write(resp)
16}

第一行代码从二进制数据解析到一个实现了Packet接口的对象,定义如下

1type Packet interface {
2	InsertSQL() string
3	BytesData() []byte
4	BytesResp() []byte
5	JSONData() []byte
6	PushURL() string
7}

最初的做法是在第一行解析数据时,返回不同的struct,每个struct包含一个Header字段,各自实现Packet定义的方法,而设备通信协议中定义了大概16种类型,按照一个请求对应一个响应,总共定义了32种struct,编写和改动都很麻烦,十分冗杂。而且对于来自设备的主动请求,总需要在最后返回响应,对于服务器主动下发的请求,获得设备响应后并不需要再返回响应,一些数据只需要记录头部信息与原始数据,另一部分则需要逐字节解析并入库。

原先的Header以及心跳包HeartBeat定义如下,HeartBeat实现了Packet定义的全部方法,多了很多冗余的代码。

 1type Header struct {
 2	DevEUI          string `json:"DevEUI"`
 3	ControlCode     string `json:"ControlCode"`
 4	DataFieldLength int    `json:"DataFieldLength"`
 5	CS              int    `json:"CS"`
 6}
 7type HeartBeat struct {
 8	Header                Header  `json:"Header"`
 9	UTCTime               string  `json:"UTCTime"`
10	Latitude              float64 `json:"Latitude"`
11	Longitude             float64 `json:"Longitude"`
12	Preserve              string  `json:"Preserve"`
13}
14func (h *HeartBeat) InsertSQL() string {
15        ......
16}
17......
18func (h *HeartBeat) PushURL() string {
19        ......
20}

HeartBeat实现了Packet定义的全部方法,可以当作Packet使用,但如果Header实现了Packet的全部方法并作为匿名字段嵌入HeartBeat,那么HeartBeat也会获得Header的全部方法,如果在HeartBeat上重新实现Packet定义的BytesResp()方法,直接调用时就会覆盖内部Header的实现,但可以通过显式指定Header来调用,修改后如下。

 1type Header struct {
 2	DevEUI          string `json:"DevEUI"`
 3	ControlCode     string `json:"ControlCode"`
 4	DataFieldLength int    `json:"DataFieldLength"`
 5	CS              int    `json:"CS"`
 6}
 7type HeartBeat struct {
 8	Header               
 9	UTCTime               string  `json:"UTCTime"`
10	Latitude              float64 `json:"Latitude"`
11	Longitude             float64 `json:"Longitude"`
12	Preserve              string  `json:"Preserve"`
13}
14func (h *Header) InsertSQL() string {
15        ......
16}
17......
18func (h *Header) PushURL() string {
19        ......
20}

通过在Header中提供了默认的接口实现,减少冗余代码,需要特殊处理的数据包可以自行实现接口,覆盖方法。

Golang串口及GPIO编程

由于需要采集地磁场变化数据,如果让硬件工程师重新设计PCB,打样加编程,估计这个月都搞不完,所以想到用树莓派来对接,树莓派3自带40Pin引脚,关于开启串口编程的功能也是整了很久,都可以写一篇文章来讲了,这里指记录下如何通过串口和GPIO调用模块。

手上的模块带有12个引脚,其中:

一个VCC接3.3V电源输入;

两个GND接地;

一个RST复位,低电平5ms以上有效;

一个IO1输入读取上位机状态,低电平时才发送数据;

一个INTin输入用于触发接受指令,下降沿触发后,等待80ms接受指令;

一个INTout输出用于触发上位机发送数据,下降沿触发后发送数据;

另外两个是TX和RX,供串口读写,保留三个引脚。

用到两个第三方库:

分别操作GPIO以及串口。

按照上述流程,程序启动后

  1. 初始化GPIO;
  2. 初始化串口;
  3. 异步轮询INTout,检测下降沿则开始读取数据;
  4. 通过发送命令发送,发送指令初始化模块。

将上述的步骤用方法实现

下面是一些全局变量

1const (
2	io1    = uint8(5)
3	rst    = uint8(6)
4	intIn  = uint(13)
5	intOut = uint(19)
6)
7var s *serial.Port
8var outPin rpio.Pin
9var inPin rpio.Pin

初始化GPIO

go-rpio默认使用bcm编码,将模块的四个引脚分别接到了对应的GPIO口上

 1 func startGPIO() {
 2 	err := rpio.Open()
 3 	if err != nil {
 4 		log.Fatal(err)
 5 	}
 6 	log.Info("open rpio successfully")
 7	// io1供模块读取,维持低电平
 8 	io1Pin := rpio.Pin(io1)
 9 	io1Pin.Output()
10 	io1Pin.Low()
11	// rst一般情况下不触发,维持高电平
12 	rstPin := rpio.Pin(rst)
13 	rstPin.Output()
14 	rstPin.High()
15	// intOut由模块输出,在树莓派3上设置为输入,并检测下降沿
16 	outPin = rpio.Pin(intOut)
17 	outPin.Input()
18 	outPin.Detect(rpio.FallEdge)
19	// intOut由树莓派3向模块输出,下降沿触发,初始状态设置为高电平
20 	inPin = rpio.Pin(intIn)
21 	inPin.Output()
22 	inPin.High()
23}

初始化串口

成功开启树莓派3的串口编程后,默认设备为**/dev/ttyAMA0**,大多数设备默认的数据位为8,停止字为1,校验为None,波特率自行设置,读超时时间为可以用来设置非阻塞的串口读取。

1 func startSeril() {
2 	c := &serial.Config{Name: "/dev/ttyAMA0", Baud: 115200, ReadTimeout: time.Second * 5}
3 	p, err := serial.OpenPort(c)
4 	s = p
5 	if err != nil {
6 		log.Fatal(err)
7 	}
8 	log.Info("open serial port successfully")
9 }

异步轮询INTout

若检测到下降沿,开始读取串口,否则休眠10ms。

读取串口,检测到结束符或读超时(上面设置为5秒)才停止读取,数据解析自行实现,最后清除串口缓冲区。

 1 func startRead() {
 2 	go func() {
 3 		for {
 4                         // 检测下降沿
 5 			if outPin.EdgeDetected() {
 6 				read()
 7 			} else {
 8                         // 主动休眠
 9 				time.Sleep(time.Millisecond * 10)
10 			}
11 		}
12 	}()
13 }
14 func read() {
15         // 开辟data用于存储数据
16 	data := []byte{}
17         // 循环读写
18 	for {
19                 // 自行设定读缓冲区长度
20 		buf := make([]byte, 50)
21 		n, err := s.Read(buf)
22 		if err != nil {
23 			log.WithField("err", err).Info("read error")
24 			break
25 		}
26 		dataRead := buf[:n]
27 		data = append(data, dataRead...)
28                 // 检测到停止字
29 		if dataRead[n-1] == 0xfe || dataRead[n-1] == 0x21 {
30 			break
31 		}
32 	}
33         if len(data) > 0 {
34                 // 解析数据
35 		decodeBytes(data)
36 	}
37         // 清除串口缓存,包括未读取与未发送的数据
38 	s.Flush()
39 }

初始化模块

这里需要编写一个发送命令方法,控制GPIO与串口。

 1 func sendCMD(cmd string) bool {
 2 	result := false
 3         // 拉低电平,触发下降沿
 4 	inPin.Low()
 5         // 等待2ms后拉高电平
 6 	time.Sleep(time.Millisecond * 2)
 7 	inPin.High()
 8         // 休眠80ms
 9 	time.Sleep(time.Millisecond * 80)
10         // 通过串口下发指令,最多尝试3次,若失败则休眠2ms再继续
11 	data := []byte(cmd)
12 	for i := 0; i < 3; i++ {
13 		_, err := s.Write(data)
14 		if err != nil {
15 			log.WithField("err", err).Info("failed to send cmd: " + cmd)
16 			time.Sleep(time.Millisecond * 2)
17 		} else {
18 			log.Info("REQ: " + cmd)
19 			result = true
20 			break
21 		}
22 	}
23 	return result
24 }

在main方法中依次调用这些方法,完成对模块的初始化和调用,其他模块的对接应该也可以使用类似的流程。

总结

学习和使用了这么久的Golang,花了漫长才理解如何面向对象,之前对接的设备,数据包之间都没有太大关联性,也就没有继承复用的情况,一门语言还是需要通过不断在各种场景下实战才能累积经验。

Golang很适合树莓派,从底层地址操作到Kubernetes都能胜任,灵活而强大,主要还是配置和开发方便,在PC上写好代码,拷贝到树莓派上直接编译运行,另外可以接入4G模块,在户外采集处理后直接上报服务器,需要微调源码时也能直接在树莓派上操作,总之有很大的想象空间的。

comments powered by Disqus