【IoT迷你赛】在中移动标准板上利用tos实现GPS追踪器

因为收到的TencentOS tiny内测开发板只有ESP8266,利用WiFi来实现GPS跟踪有点不太现实。而最近正好从中移动手里薅了一个标准开发板(如下图),上面自带GSM模组M6312,就想着把tos搞到这个开发板上来利用,M6312接入网络来实现地理位置上报。

移植的过程中除了搞定tos在MAC系统的STM32CubeIDE上的编译问题外,最大的一个麻烦就是当前开发库还不支持M6312,所以只能自己动手现撸一个。期间遇到的一个坑是在接收数据的过程中,除了你要获取完所有的数据外,额外的数据也必需清理干净,这个问题我搞了很久。现说明如下:

M6312在收到数据后返回的格式如下:

<IPDATA: 4>\r\nDATA\r\nOK\r\n

其中4是数据长度,也就是说按上例,在跳过"\r\n"后收完4字节数据"DATA"后还余下"\r\nOK\r\n"这些数据,这些数据必需要清理掉,不能放任不管。

__STATIC__ void m6312_incoming_data_process(void)
{
    // <IPDATA: 4>\r\nDATA\r\nOK\r\n
    //
    // "<IPDATA: "    prefix
    // "4"            data length
    // "\r\nOK\r\n"   suffix

    uint8_t data=0;
    int data_len = 0;
    int discard_suffix = 1;

    // 获取数据实际长度,并路过"\r\n"
    while (data != '\n') {
        if (tos_at_uart_read(&data, 1) != 1) {
            return;
        }

        if(data >= '0' && data <= '9') {
            data_len = data_len * 10 + (data - '0');
        }
    }
    
    if (data_len > sizeof(incoming_data_buffer)) {
        discard_suffix = 0;
        data_len = sizeof(incoming_data_buffer);
    }

    // 获取数据
    if (tos_at_uart_read(incoming_data_buffer, data_len) != data_len) {
        return;
    }

    // 在把数据传进内核之前,必需先把垃圾数据清理干净
    // discard suffix "\r\nOK\r\n"
    while(discard_suffix && (tos_at_uart_read_timed(&data, 1, 1000) == 1)) { }

    tos_at_channel_write(0, incoming_data_buffer, data_len);

}

M6312的使用方法略去不表,类似ESP8266等。这部分代码暂时还没有合进主仓库,需要的同学可能要稍等一下,不会太久。

接下来就是接收GPS数据。GPS模块用的是ATGM336H,因M6312占用的是USART2,所以GPS接到USART3。而这个GPS模块有个问题就是一直往外吐数据,没法禁止,数据量还大,如果每来个字符都中断处理一下,MCU就全忙这事了,因此对GPS数据采用DMA收取分析。

先定义一下用到的相关数据结构

#define USART_RX_BUFSZ	1024
typedef struct {
	uint32_t len : 24;
	uint32_t end : 8;
	DMA_HandleTypeDef *dmarx;
	uint8_t  buf[USART_RX_BUFSZ];
} UsartDmaData_t;

#define DMA_HUART huart3
#define HDMA_USART_RX hdma_usart3_rx

初始化DMA

void Init_Dma_Recv() {
	__HAL_UART_ENABLE_IT(&DMA_HUART, UART_IT_IDLE);
	dma_data.dmarx = &HDMA_USART_RX;
	HAL_UART_Receive_DMA(&DMA_HUART, dma_data.buf, USART_RX_BUFSZ);
}

收到DMA IDLE中断表示数据接收完成了

void DmaReceiveUsartData(UART_HandleTypeDef *huart) {
	UsartDmaData_t *data = &dma_data;
	uint32_t flag = __HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE);
	if(flag == RESET) {
		return;
	}

	//软件清空空闲中断标志位
	volatile uint32_t tmp;
	tmp = huart->Instance->SR;
	tmp = huart->Instance->DR;

	__HAL_UART_CLEAR_IDLEFLAG(huart);
	HAL_UART_DMAStop(huart);

	if(0 == data->dmarx) {
		return ;
	}

	tmp = data->dmarx->Instance->CNDTR;
	data->len = USART_RX_BUFSZ - tmp;
	data->end = 1;
}

接下来就是解析GPS数据的时候了

int parse_gps(char *data, int len, char *lat, char *lng) {
	*lat = *lng = 0;
	// 找到$GNRMC开头
	char *gpsline = strnstr(data, "$GNRMC", len);
	if(gpsline == 0) {
		return -1;
	}
	len = gpsline - data;

	// 找到$GNRMC行尾
	char *end = strnstr(gpsline, "\r\n", len);
	if(end == 0) {
		return -2;
	}

	// 断尾
	*end = 0;
	len = end - gpsline;

	printf("%s\n", gpsline);

	// 判断是否是有效数据
	gpsline = strnstr(gpsline, ",A,", len);
	if(gpsline == 0) {
		return -3;
	}

	// skip ",A,"
	gpsline += 3;
	len = end - gpsline;

	// 合理值在50左右
	if(len <= 40) {
		return -4;
	}


	int commaInx = 0;
	for(int i=0; i<len; i++) {
		char c = gpsline[i];
		if(c == ',') {
			commaInx++;
			continue;
		}

		if(commaInx == 0 || commaInx == 1) {
			*(lat++) = c;
			*lat = 0;
		} else if(commaInx == 2 || commaInx == 3) {
			*(lng++) = c;
			*lng = 0;
		} else {
			return 0;
		}
	}

	return -5;
}

把收到的数据送到MQTT任务,发送到服务器。

在管理平台创建一个GPS产品,创建两个设备,一个名叫ChinaMobileStandardBoard对应该中移动开发板,一个叫Server,它的作用见后文。

再创建两条规则:

  1. 把xx/ChinaMobileStandardBoard/gps topic的lat,lng两个字段转发到xx/Server/gps的topic
  1. 把xx/Server/cmd 的topic里的cmd字段转发到xx/ChinaMobileStandardBoard/cmd的topic

接下来就是作为主控端,接收需要的数据的时候了,这里我没有用腾讯云的SDK,而是用的go语言的github.com/eclipse/paho.mqtt.golang,这就是前面说的Server的设备。

代码我全都贴出来吧:

package main

import (
	"encoding/json"
	"fmt"
	MQTT "github.com/eclipse/paho.mqtt.golang"
	"html/template"
	"log"
	"math"
	"net/http"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

var qos = 0
var chcmd chan string
var gps string

func Pub(client MQTT.Client) {
	for {
		pub_topic := "jiubugaoshuni/Server/cmd"
		cmd := <-chcmd
		payload := fmt.Sprintf("{\"cmd\":%s}", cmd)
		fmt.Printf("publish payload: %s\n", payload)
		token := client.Publish(pub_topic, byte(qos), false, payload)
		token.Wait()
	}
}


func Sub(client MQTT.Client, opts *MQTT.ClientOptions) {
	sub_topic := "jiubugaoshuni/Server/gps"
	chgps := make(chan [2]string)

	for {
		if token := client.Subscribe(sub_topic, byte(qos),
			func(client MQTT.Client, msg MQTT.Message) {
				fmt.Println(msg.Topic(), string(msg.Payload()))
				chgps <- [2]string{msg.Topic(), string(msg.Payload())}
			}); token.Wait() && token.Error() != nil {

			fmt.Println(token.Error())
			os.Exit(1)
		}

		incoming := <-chgps
		fmt.Printf("RECEIVED TOPIC: %s MESSAGE: %s\n", incoming[0], incoming[1])
		gps = incoming[1]
	}

}

type GPS struct {
	Lat  string `json:"lat"`
	Lng  string `json:"lng"`
	Latf string
	Lngf string
}

func calc_gps_degree(s string) string {
	v, _ := strconv.ParseFloat(s, 64)
	v /= 100.0

	df := math.Floor(v)
	mf := v - df
	mf *= 100

	df = df + mf/60.0

	degree := fmt.Sprintf("%12.8f", df)

	return degree
}

func SyncGpsHandler(w http.ResponseWriter, r *http.Request) {
	var g GPS
	json.Unmarshal([]byte(gps), &g)
	if len(g.Lat) > 0 {
		g.Lat = g.Lat[:len(g.Lat)-1]
		g.Latf = calc_gps_degree(g.Lat)
	}
	if len(g.Lng) > 0 {
		g.Lng = g.Lng[:len(g.Lng)-1]
		g.Lngf = calc_gps_degree(g.Lng)
	}

	fmt.Printf("> %s  %s %s %s %s\n", gps, g.Lat, g.Lng, g.Latf, g.Lngf)
	cmd := exec.Command("python", "./gps.py", g.Lngf, g.Latf)
	buf, _ := cmd.Output()

	fmt.Printf("# %s\n", buf)
	//fmt.Fprintf(w, "{\"lat\":%s, \"lng\":%s}", g.Latf, g.Lngf)
	fmt.Fprintf(w, string(buf))
}

func GpsHandler(w http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("./views/gps.html")
	if err != nil {
		fmt.Fprintf(w, "parse template error: %s", err.Error())
		return
	}
	t.Execute(w, nil)
}

func CmdHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()

	values := make(map[string]string)
	for k, v := range r.Form {
		val := strings.Join(v, "")
		values[k] = val
	}

	cmd := values["cmd"]

	select {
	case chcmd <- cmd:
	default:
	}
	fmt.Fprintf(w, "OK")
}

func http_server() {
	http.HandleFunc("/Gps", GpsHandler)
	http.HandleFunc("/Cmd", CmdHandler)
	err := http.ListenAndServe(":7777", nil)
	for {
		log.Print("http coroutine exited")
		if err != nil {
			log.Fatal("ListenAndServe: ", err)
		}
	}
}

func https_server() {
	http.HandleFunc("/SyncGps", SyncGpsHandler)
	http.HandleFunc("/Gps", GpsHandler)
	http.HandleFunc("/Cmd", CmdHandler)
	http.HandleFunc("/static/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, r.URL.Path[1:])
	})
	for {
		err := http.ListenAndServeTLS(":7777", "./xxxx.xyz.crt", "./xxxx.xyz.key", nil)
		log.Print("https coroutine exited")
		if err != nil {
			log.Fatal("ListenAndServe: ", err)
		}
	}
}

func main() {
	broker := "tcp://111.230.189.156:1883"
	password := "meiyoumima;hmacsha1"
	user := "jiubugaoshuniServer;21010406;12365;4294967295"
	clientid := "jiubugaoshuniServer"

	chcmd = make(chan string, 1)

	opts := MQTT.NewClientOptions()
	opts.AddBroker(broker)
	opts.SetClientID(clientid)
	opts.SetUsername(user)
	opts.SetPassword(password)
	opts.SetCleanSession(false)

	client := MQTT.NewClient(opts)
	if token := client.Connect(); token.Wait() && token.Error() != nil {
		panic(token.Error())
	}

	go Pub(client)
	go Sub(client, opts)
	go https_server()

	for {
		time.Sleep(1 * time.Second)
	}

	client.Disconnect(250)
}

GPS数据不能直接用,先要转国测局标准,如果是用的百度地图还要再转相应的百度标准,实在懒得写GO语言的转换代码了,加之之前有写好的python代码,就直接拿来调用了。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math
import sys
from math import pi

a = 6378245.0;
ee = 0.00669342162296594323;


class Storage(dict) :
    def __getattr__(self, key) :
        try :
            return self[key]
        except KeyError, k:
            raise AttributeError, k

    def __setattr__(self, key, value) :
        self[key] = value

    def __delattr__(self, key) :
        try :
            del self[key]
        except KeyError, k:
            raise AttributeError, k

    def __repr__(self) :
        return '<Storage ' + dict.__repr__(self) + '>'

class GPS(Storage) :
    def __init__(self, lng = 0.0, lat = 0.0) :
        self.lng = lng
        self.lat = lat

gps_data = []


def transformLng(x, y) :
    ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * math.sqrt(abs(x));
    ret += (20.0 * math.sin(6.0 * x * pi) + 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0;
    ret += (20.0 * math.sin(x * pi) + 40.0 * math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
    ret += (150.0 * math.sin(x / 12.0 * pi) + 300.0 * math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
    return ret;

def transformLat(x, y) :
    ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * math.sqrt(abs(x));
    ret += (20.0 * math.sin(6.0 * x * pi) + 20.0 * math.sin(2.0 * x * pi)) * 2.0 / 3.0;
    ret += (20.0 * math.sin(y * pi) + 40.0 * math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
    ret += (160.0 * math.sin(y / 12.0 * pi) + 320 * math.sin(y * pi / 30.0)) * 2.0 / 3.0;
    return ret;

def wgs82_to_gcj02(lng, lat) :
    dLat = transformLat(lng - 105.0, lat - 35.0)
    dLng = transformLng(lng - 105.0, lat - 35.0)
    radLat = lat / 180.0 * pi
    magic = math.sin(radLat)
    magic = 1 - ee * magic * magic
    sqrtMagic = math.sqrt(magic)

    dLng = (dLng * 180.0) / (a / sqrtMagic * math.cos(radLat) * pi)
    dLat = (dLat * 180.0) / ((a * (1 -ee)) / (magic * sqrtMagic) * pi)

    gcjLng = lng + dLng
    gcjLat = lat + dLat

    x = GPS(gcjLng, gcjLat)
    #print(lng, lat)
    #print(x.lng, x.lat)
    return x

def gcj02_to_bd09(lng, lat) :
    x = lng
    y = lat
    z = math.sqrt(x * x + y * y) + 0.00002 * math.sin(y * pi);
    theta = math.atan2(y, x) + 0.000003 * math.cos(x * pi);
    bdLng = z * math.cos(theta) + 0.0065;
    bdLat = z * math.sin(theta) + 0.006;
    x = GPS(bdLng, bdLat);
    return x

def bd09_to_gcj02(lng, lat) :
    x = lng - 0.0065
    y = lat - 0.006;
    z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * pi);
    theta = math.atan2(y, x) - 0.000003 * math.cos(x * pi);
    gcjLng = z * math.cos(theta);
    gcjLat = z * math.sin(theta);

    x = GPS(gcjLng, gcjLat)
    return x

def out_of_china(lng, lat) :
    if lng < 72.004 or lng > 137.8347 :
        return True
    if lat < 0.8293 or lat > 55.8271 :
        return True

    return False

def transform(lng, lat) :
    if (out_of_china(lng, lat)) :
        return GPS(lng, lat)

    dLng = transformLng(lng - 105.0, lat - 35.0);
    dLat = transformLat(lng - 105.0, lat - 35.0);
    radLat = lat / 180.0 * pi;
    magic = math.sin(radLat);
    magic = 1 - ee * magic * magic;
    sqrtMagic = math.sqrt(magic);
    dLng = (dLng * 180.0) / (a / sqrtMagic * math.cos(radLat) * pi);
    dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
    mgLng = lng + dLng;
    mgLat = lat + dLat;
    return GPS(mgLng, mgLat);

def gcj02_to_wgs84(lng, lat) :
    gps = transform(lng, lat);
    wgsLng = lng * 2 - gps.lng;
    wgsLat = lat * 2 - gps.lat;
    x = GPS(wgsLng, wgsLat);
    return x


if __name__ == '__main__':
    lng = float(sys.argv[1])
    lat = float(sys.argv[2])
    g = wgs82_to_gcj02(lng, lat)
    b = gcj02_to_bd09(g.lng, g.lat)
    print("""{{"lng":"{0}","lat":"{1}"}}""".format(b.lng, b.lat))
    sys.exit(0)

GPS跟踪效果,其实很容易做到根据历史数据画出路径线,但是我不想做这事了。

最后就是最简单的远程控制LED灯了,通过https://xxxx.xxx:7777/Cmd?cmd=x来控制,就不细说了,本来想上传一个视频的,但是还要实名认证,太麻烦,就算了。

附:

申请移动各种开发板的链接: https://open.iot.10086.cn/productservice/onenetdevboard/

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券