# 前言
最近在做物联网的项目,有一个需求是要每隔一段时间要拍摄一张现场的图片并上传至云服务器保存。在查阅了很多资料后,发现这方面的资料是真的匮乏。同时,tb 上的摄像头产品也太高度集成了,很难进行二次开发。一次机缘巧合下,在逛 tb 的时候偶然发现一款产品,就是如下图所示 Air724UG 模块,自带 4G 通信模块和摄像头接口,而且成本也比较便宜,带通信卡和摄像头总价格不超过 80,简直就是完美符合我需求的天选产品。同时该系列产品的官方网站上也给出了很多代码 demo,非常有利于初学者的学习。最终,在经历了几天的学习后,终于成功把这个需求实现了,开心ヾ (・ω・`) o,于是在此记录下来,希望能帮助到其他有需要的小伙伴们~
# 基础知识
如果买了这个模块,首先把卖家写的五篇开发文档看一下,如下
- 1-Air724UG (4G 全网通 GPRS) 开发 - 硬件使用说明
- 2-Air724UG (4G 全网通 GPRS) 开发 - 下载 AT 指令固件
- 200-Air724UG (4G 全网通 GPRS) 开发 - 下载和运行第一个 lua 程序
- 201-Air724UG 模块 (4G 全网通 GPRS 开发)- 模块测试 - 测试 SD 卡和扬声器 (喇叭) 播放功能
- 202-Air724UG 模块 (4G 全网通 GPRS 开发)- 模块测试 - 摄像头扫码,LCD 显示摄像头图像
最重要的是最后一个文档,因为我们后面要修改这里面的代码。
看完文档后,应该就会对 Air724UG 模块有了大致的了解,同时我们也会发觉二次开发是基于 Lua 语言的。有小伙伴可能就会问,没学过 Lua 语言肿么办
莫慌,因为合宙官方给出了很多案例的示例代码,包括上面第五个文档里面用的摄像头案例的代码。我们可以打开代码看一下,官方的代码规范写的很好,每一句代码几乎都有注释,这就非常有利于我们初学者的学习~
博主也是在大致浏览了下代码后,尝试性的将 socket 的示例代码和 camera 的示例代码融合起来,成功的解决了这个需求,下面就具体看看修改后的代码吧~
# 编写 TCP 客户端代码
目前拍摄的需求已经通过默认的 camera 示例代码实现了,下面要实现的就是如何把照片上传到云端。搞过开发的同学应该都大致了解 socket 这个东西,我们各种网络通信的基础就是 socket,我们用 socket 也可以很快搭建一个 tcp 服务器,这样就相当于我们的摄像头模块是 tcp 客户端,部署了代码的云服务器是 tcp 服务器端,两端连通以后,就可以实现将照片传输到云端啦~
下面先看一下 TCP 客户端具体代码
主要用到的就是官方 demo\socket\async\asyncSocket
的案例和 demo\camera
这两个案例,我们按照 socket 案例的 main 文件修改 camera 案例的 main 文件,修改后如下
-- 必须在这个位置定义 PROJECT 和 VERSION 变量 | |
--PROJECT:ascii string 类型,可以随便定义,只要不使用,就行 | |
--VERSION:ascii string 类型,如果使用 Luat 物联云平台固件升级的功能,必须按照 "X.X.X" 定义,X 表示 1 位数字;否则可随便定义 | |
PROJECT = "CAMERA" | |
VERSION = "2.0.0" | |
-- 加载日志功能模块,并且设置日志输出等级 | |
-- 如果关闭调用 log 模块接口输出的日志,等级设置为 log.LOG_SILENT 即可 | |
require "log" | |
LOG_LEVEL = log.LOGLEVEL_TRACE | |
--[[ | |
如果使用 UART 输出日志,打开这行注释的代码 "--log.openTrace (true,1,115200)" 即可,根据自己的需求修改此接口的参数 | |
如果要彻底关闭脚本中的输出日志(包括调用 log 模块接口和 Lua 标准 print 接口输出的日志),执行 log.openTrace (false, 第二个参数跟调用 openTrace 接口打开日志的第二个参数相同),例如: | |
1、没有调用过 sys.opntrace 配置日志输出端口或者最后一次是调用 log.openTrace (true,nil,921600) 配置日志输出端口,此时要关闭输出日志,直接调用 log.openTrace (false) 即可 | |
2、最后一次是调用 log.openTrace (true,1,115200) 配置日志输出端口,此时要关闭输出日志,直接调用 log.openTrace (false,1) 即可 | |
]] | |
--log.openTrace(true,1,115200) | |
require "sys" | |
require "utils" | |
require "patch" | |
require "pins" | |
require "net" | |
-- 每 1 分钟查询一次 GSM 信号强度 | |
-- 每 1 分钟查询一次基站信息 | |
-- net.startQueryAll(60000, 60000) | |
--8 秒后查询第一次 csq | |
net.startQueryAll(8 * 1000, 60 * 1000) | |
-- 此处关闭 RNDIS 网卡功能 | |
-- 否则,模块通过 USB 连接电脑后,会在电脑的网络适配器中枚举一个 RNDIS 网卡,电脑默认使用此网卡上网,导致模块使用的 sim 卡流量流失 | |
-- 如果项目中需要打开此功能,把 ril.request ("AT+RNDISCALL=0,1") 修改为 ril.request ("AT+RNDISCALL=1,1") 即可 | |
-- 注意:core 固件:V0030 以及之后的版本、V3028 以及之后的版本,才以稳定地支持此功能 | |
ril.request("AT+RNDISCALL=0,1") | |
-- 加载控制台调试功能模块(此处代码配置的是 uart1,波特率 115200) | |
-- 此功能模块不是必须的,根据项目需求决定是否加载 | |
-- 使用时注意:控制台使用的 uart 不要和其他功能使用的 uart 冲突 | |
-- 使用说明参考 demo/console 下的《console 功能使用说明.docx》 | |
--require "console" | |
--console.setup(1, 115200) | |
-- 加载硬件看门狗功能模块 | |
-- 根据自己的硬件配置决定:1、是否加载此功能模块;2、配置 Luat 模块复位单片机引脚和互相喂狗引脚 | |
-- 合宙官方出售的 Air201 开发板上有硬件看门狗,所以使用官方 Air201 开发板时,必须加载此功能模块 | |
--[[ | |
require "wdt" | |
wdt.setup(pio.P0_30, pio.P0_31) | |
]] | |
-- 加载网络指示灯和 LTE 指示灯功能模块 | |
-- 根据自己的项目需求和硬件配置决定:1、是否加载此功能模块;2、配置指示灯引脚 | |
-- 合宙官方出售的 Air720U 开发板上的网络指示灯引脚为 pio.P0_1,LTE 指示灯引脚为 pio.P0_4 | |
require "netLed" | |
pmd.ldoset(2,pmd.LDO_VLCD) | |
netLed.setup(true,pio.P0_1,pio.P0_4) | |
-- 网络指示灯功能模块中,默认配置了各种工作状态下指示灯的闪烁规律,参考 netLed.lua 中 ledBlinkTime 配置的默认值 | |
-- 如果默认值满足不了需求,此处调用 netLed.updateBlinkTime 去配置闪烁时长 | |
-- 加载错误日志管理功能模块【强烈建议打开此功能】 | |
-- 如下 2 行代码,只是简单的演示如何使用 errDump 功能,详情参考 errDump 的 api | |
require "errDump" | |
errDump.request("udp://ota.airm2m.com:9072") | |
-- 加载远程升级功能模块【强烈建议打开此功能】 | |
-- 如下 3 行代码,只是简单的演示如何使用 update 功能,详情参考 update 的 api 以及 demo/update | |
--PRODUCT_KEY = "v32xEAKsGTIEQxtqgwCldp5aPlcnPs3K" | |
--require "update" | |
--update.request() | |
require"color_lcd_spi_st7735" | |
--require"color_lcd_spi_gc9106l" | |
require"testCamera" | |
-- 系统工具 | |
require "misc" | |
require "testSocket" | |
require "ntp" | |
ntp.timeSync(1,function()log.info("----------------> AutoTimeSync is Done ! <----------------")end) | |
-- 启动系统框架 | |
sys.init(0, 0) | |
sys.run() |
然后在 camera 案例下添加一个 testSocket.lua
文件,编写代码如下
--- testSocket | |
-- @module asyncSocket | |
-- @author AIRM2M | |
-- @license MIT | |
-- @copyright openLuat.com | |
-- @release 2018.10.27 | |
require "socket" | |
module(..., package.seeall) | |
-- 此处的 IP 和端口请填上你自己的 socket 服务器和端口 | |
-- local ip, port, c = "180.97.80.55", "12415" | |
local ip, port, c = "xxx.xxx.xxx.xxx", "9999" | |
-- 异步接口演示代码 | |
local asyncClient | |
sys.taskInit(function() | |
while true do | |
while not socket.isReady() do sys.wait(1000) end | |
asyncClient = socket.tcp() | |
while not asyncClient:connect(ip, port) do sys.wait(2000) end | |
while asyncClient:asyncSelect() do end | |
asyncClient:close() | |
end | |
end) | |
-- 测试代码,用于发送消息给 socket | |
function sendFile(size) | |
sys.taskInit( | |
function() | |
while not socket.isReady() do sys.wait(2000) end | |
local fileHandle = io.open("/testCamera.jpg","rb") | |
if not fileHandle then | |
log.error("testALiYun.otaCb1 open file error") | |
return | |
end | |
log.info("-----------------------------------------------start send photo") | |
asyncClient:asyncSend(size) | |
while true do | |
local data = fileHandle:read(size) | |
if not data then break end | |
asyncClient:asyncSend(data) | |
end | |
log.info("-----------------------------------------------end send photo") | |
fileHandle:close() | |
end | |
) | |
end |
注意把上面的 ip 地址换成你后面部署 tcp 服务器端的云服务器的 ip 地址
这个文件编写完成以后,就可以在 testCamera.lua
文件里面调用这个函数了, testCamera.lua
文件具体编辑如下,其中中间省略了一些没有修改的代码
--- 模块功能:camera 功能测试. | |
-- @author openLuat | |
-- @module fs.testFs | |
-- @license MIT | |
-- @copyright openLuat | |
-- @release 2018.03.27 | |
module(...,package.seeall) | |
require"pm" | |
require"scanCode" | |
require"utils" | |
require"common" | |
require"testUartSentFile" | |
require"testSocket" | |
local WIDTH,HEIGHT = disp.getlcdinfo() | |
local DEFAULT_WIDTH,DEFAULT_HEIGHT = 320,240 | |
-- 扫码结果回调函数 | |
-- @bool result,true 或者 false,true 表示扫码成功,false 表示超时失败 | |
-- @string [opt=nil] codeType,result 为 true 时,表示扫码类型;result 为 false 时,为 nil;支持 QR-Code 和 CODE-128 两种类型 | |
-- @string [opt=nil] codeStr,result 为 true 时,表示扫码结果的字符串;result 为 false 时,为 nil | |
local function scanCodeCb(result,codeType,codeStr) | |
... | |
end | |
local bf302A_sdr = | |
{ | |
... | |
} | |
local gc6153 = | |
{ | |
... | |
} | |
local gc0310_ddr = | |
{ | |
... | |
} | |
local gc0310_ddr_big = | |
{ | |
.... | |
} | |
local gc0310_sdr = | |
{ | |
... | |
} | |
function scan() | |
... | |
end | |
-- 拍照并显示 | |
function takePhotoAndDisplay() | |
... | |
end | |
-- 拍照并通过 uart1 发送出去 | |
function takePhotoAndSendToUart() | |
... | |
end | |
-- 拍照并通过 socket 发送出去 | |
function takePhotoAndSendToSocket() | |
-- 唤醒系统 | |
pm.wake("testTakePhoto") | |
-- 打开摄像头 | |
disp.cameraopen(1,0,0,1) | |
--disp.cameraopen (1,0,0,0) -- 因目前 core 中还有问题没解决,所以不能关闭隔行隔列 | |
-- 打开摄像头预览 | |
-- 如果有 LCD,使用 LCD 的宽和高 | |
-- 如果无 LCD,宽度设置为 240 像素,高度设置为 320 像素,240*320 是 Air268F 支持的最大分辨率 | |
disp.camerapreview(0,0,0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT) | |
-- 设置照片的宽和高像素并且开始拍照 | |
-- 此处设置的宽和高和预览时的保持一致 | |
disp.cameracapture(DEFAULT_WIDTH,DEFAULT_HEIGHT) | |
-- 设置照片保存路径 | |
disp.camerasavephoto("/testCamera.jpg") | |
log.info("-----------------------------------------------testCamera.takePhotoAndSendToSocket fileSize",io.fileSize("/testCamera.jpg")) | |
-- 关闭摄像头预览 | |
disp.camerapreviewclose() | |
-- 关闭摄像头 | |
disp.cameraclose() | |
-- 允许系统休眠 | |
pm.sleep("testTakePhoto") | |
testSocket.sendFile(io.fileSize("/testCamera.jpg")) | |
sys.timerStart(takePhotoAndSendToSocket,1*60*1000) | |
end | |
-- sys.timerStart(takePhotoAndDisplay,1000) | |
-- sys.timerStart(takePhotoAndSendToUart,1000) | |
sys.timerStart(takePhotoAndSendToSocket,10000) | |
-- sys.timerStart(scan,1000) |
修改完这三个文件之后,我们就可以把这个案例烧录到 Air724UG 模块上了,这样我们 TCP 客户端就弄好了,下面来看下 TCP 服务器端的代码吧~
# 编写 TCP 服务端代码
这里 socket 的编程可以用 python 写,也可以用 go 或其他语言写,因为我最近在学 go,所以我就用 go 来编写 tcp 服务器端的代码啦,具体 main.go
文件编写如下
package main | |
import ( | |
"bytes" | |
"encoding/binary" | |
"encoding/hex" | |
"fmt" | |
"log" | |
"net" | |
"os" | |
"strconv" | |
"time" | |
) | |
func GetDetailTime() string { | |
now := time.Now() | |
return fmt.Sprintf("%02d%02d%02d%02d%02d%02d", now.Year(), int(now.Month()), | |
now.Day(), now.Hour(), now.Minute(), now.Second()) | |
} | |
func handleClient(conn *net.TCPConn) { | |
var buf [256]byte | |
var imageData [10 * 1024]byte | |
for { | |
n, _ := conn.Read(buf[0:]) | |
fmt.Println("------------------------receive bytes--------------------", n, string(buf[0:n])) | |
size, _ := strconv.Atoi(string(buf[0:n])) | |
fmt.Println("------------------------file size--------------------", size) | |
i := 0 | |
for i < size { | |
n, _ := conn.Read(imageData[i:]) | |
i += n | |
} | |
encodedStr := hex.EncodeToString(imageData[0:i]) | |
fmt.Println("------------------------received bytes--------------------", i) | |
fmt.Println(encodedStr) | |
imgName := fmt.Sprintf("%s.jpg", GetDetailTime()) | |
byte2image(imageData, i, imgName) | |
} | |
} | |
func byte2image(b [10 * 1024]byte, n int, path string) { | |
fp, err := os.Create(path) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
defer fp.Close() | |
buf := new(bytes.Buffer) | |
binary.Write(buf, binary.LittleEndian, b[0:n]) | |
fp.Write(buf.Bytes()) | |
} | |
func main() { | |
address := net.TCPAddr{ | |
IP: net.ParseIP("0.0.0.0"), // 把字符串 IP 地址转换为 net.IP 类型 | |
Port: 9999, | |
} | |
fmt.Println("v2.0 server listen at ", address.IP, address.Port) | |
listener, err := net.ListenTCP("tcp4", &address) // 创建 TCP4 服务器端监听器 | |
if err != nil { | |
log.Fatal(err) // Println + os.Exit(1) | |
} | |
for { | |
conn, err := listener.AcceptTCP() | |
if err != nil { | |
log.Fatal(err) // 错误直接退出 | |
} | |
fmt.Println("remote address:", conn.RemoteAddr()) | |
// go echo(conn) | |
go handleClient(conn) | |
} | |
} |
编写完成之后,可以通过在当前目录下运行如下命令将代码打包成 Linux 可执行文件
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go
这样就可以在当前目录下生成 main
可执行文件了,这也是我喜欢 go 的原因,同样的代码,可以方便的打包成 windows 可执行文件和 linux 可执行文件,十分的方便
将打包生成的 main
文件传输到云服务器上,然后执行,TCP 服务器端代码就运行起来了,记得开放云服务器防火墙的对应端口
接下来只要保证 TCP 客户端代码里的 ip 地址和端口正确,照片就可以顺利的上传到云服务器上,然后保存到可执行文件的目录下了~
如果这篇文件对你有帮助,记得给博主点个赞支持一下呀 (✿◡‿◡)