起因是早上起床后要用热水,不想下床按烧水壶开关,需要能遥控打开。
大概用到:插板、esp-01s继电器模块、usb转串口ttl模块、https服务器、小度音箱。
大概原理:
https服务器上提供开关状态查询接口,esp-01s烧录编写的固件,使得可以连接wifi后轮询这个接口,并根据接口返回的状态打开或关闭继电器。在小度技能平台创建开关技能,在https服务器上提供oauth2.0接口和遵循dueros智能家具协议的开关控制接口。这样,通过小度的真机测试模式,让小度“打开开关”,小度会调到https服务器打开开关接口,修改开关状态,间接控制开关开合。
大概过程:
(1)插板改造,把esp-01s继电器模块的常开端串到插板的一根线上。
(2)esp-01s可以使用nodemcu固件,直接lua开发,简化过程。
关于nodemcu固件,可以去https://nodemcu-build.com/在线编译,通过填邮箱、选择需要的模块、点击开始构建后,过会会收到包含固件下载地址的邮件。这里之前编译过,当时选了模块有(adc,enduser_setup,file,gpio,http,mqtt,net,node,tmr,uart,wifi,tls),选了的模块,在lua脚本中,可以使用相关api,api文档地址https://nodemcu.readthedocs.io/en/master/。
关于固件烧录,esp-01s供电为3.3v,usb转ttl供电为5v,这里烧录时做了个转接板来连接,用asm1117 3.3v稳压器得到的3.3v(usb转ttl带了3.3v供电,但没用,部分资料显示可能会烧坏,没试),RST与GND上接了微动开关做重启,GPIO0与GND接了拨动开关用来切换下载模式。连接好后,打开NodeMCU-PyFlasher-4.0-x64.exe,选择串口和固件,115200,Quad I/O,点击刷写,等待完成。不正常的话多试几次,连线建议用短线。
(3)写入lua程序。
换用ESPlorer,选择串口,115200,打开串口,按前面转接板上的微动开关重启,刷的固件会初始化文件系统。软件右边显示命令行,左面写命令或脚本。左边写print("hello")并选择发送,会发送到命令行,执行后命令行显示结果hello。
这时候可以编写需要的init.lua脚本,通过upload上传,写入esp-01s,再按微动开关重启,nodemcu自动加载init.lua脚本执行,连接wifi,请求接口。
到这里,可以通过修改接口返回值,测试能看控制开关。
-- //init.lua
print('hello')
-- wifi连接
wifi.setmode(wifi.STATION)
station_cfg={}
station_cfg.ssid="changeme"
station_cfg.pwd="changeme"
station_cfg.auto=true
station_cfg.save=true
wifi.sta.config(station_cfg)
print(wifi.sta.getip())
-- //wifi.sta.disconnect()
-- ap模式,手机连接此wifi,到192.168.4.1配置wifi
enduser_setup.start(
function()
print("enduser conn wifi as:" .. wifi.sta.getip())
end,
function(err, str)
print("enduser conn wifi err #" .. err .. ": " .. str)
end
)
-- //print(uart.setup(0, 9600, 8, 0, 1, 1 ))
-- gpio0控制继电器,对应nodemc的pin 3,gpio2控制板载led,对应4
pin1 = 3
pin2 = 4
gpio.mode(pin1,gpio.OUTPUT)
gpio.mode(pin2,gpio.OUTPUT)
gpio.write(pin1,gpio.HIGH)
gpio.write(pin2,gpio.HIGH)
myurl = "https://test.xxxxxxx.com/test/led.php"
mytimer = tmr.create()
mytimer:alarm(5000, tmr.ALARM_AUTO, function()
ip = wifi.sta.getip()
if (ip == nil) then
print('wifi not connect...')
return
else
print(wifi.sta.getip())
end
print('http req led.php....')
http.get(myurl, nil, function(code, data)
if (code < 0) then
print("HTTP request failed...")
else
print("HTTP request succ...", code, data)
if (data == "00") then
gpio.write(pin1,gpio.LOW)
gpio.write(pin2,gpio.LOW)
elseif (data =="01") then
gpio.write(pin1,gpio.LOW)
gpio.write(pin2,gpio.HIGH)
elseif (data =="10") then
gpio.write(pin1,gpio.HIGH)
gpio.write(pin2,gpio.LOW)
elseif (data =="11") then
gpio.write(pin1,gpio.HIGH)
gpio.write(pin2,gpio.HIGH)
else
print('led nop...')
end
end
end)
collectgarbage()
end)
-- //mytimer.stop()
connect($host, $port);
$redis->select($db);
return $redis;
}
function getLed() {
$redis = getRedis('127.0.0.1', 6379, 0);
$led = $redis->get("led");
$redis->close();
return $led;
}
function setLed($led) {
$redis = getRedis('127.0.0.1', 6379, 0);
$redis->set("led", $led);
$redis->close();
}
$m = $_SERVER['REQUEST_METHOD'];
$t = isset($_REQUEST['t']) ? $_REQUEST['t'] : '';
if ($m == 'GET') {
if (empty($t)) {
$led = getLed();
if (!empty($led)) {
echo $led;
} else {
echo 'none';
}
exit;
}
}
if ($m == 'GET' && $t == 'ui') {
} else {
echo 'nop';
exit;
}
$led = isset($_REQUEST['led']) ? $_REQUEST['led'] : '';
$sw = isset($_REQUEST['sw']) ? $_REQUEST['sw'] : '';
$msg = '';
if (!empty($led)) {
setLed($led);
$msg = 'set led = ' . $led . "\n";
} else if (!empty($sw)) {
if ($sw === 'on') {
setLed('01');
$msg = "开关打开\n";
} else {
setLed('11');
$msg = "开关关闭\n";
}
} else {
$led = getLed();
$msg = 'now led = ' . $led . "\n";
}
?>
LED
[set 00]
[set 01]
[set 10]
[set 11]
[get now]
开关:[开] [关]
(4)在小度技能平台,创建我的开关,配置服务里配置各url地址和值。
授权信息配置部分配置oauth2.0权限,设备云信息配置开关接口,如:
授权地址:https://test.xxxxxx.com/oauth2.0/authorize.php
Client_Id:191223d5e1bcb5f9e51bca66113ce3a1
Token地址:https://test.xxxxxx.com/oauth2.0/token.php
ClientSecret:191223d5e1bcb5f9e51bca66113ce3a2
WebService:https://test.xxxxxx.com/iot/dueros.php
这里oauth2.0只简单实现功能,dueros.php按协议实现简单功能。
-- authorize.php -----------------------------------
0) {
$url .= "&";
} else {
$url .= "?";
}
$url .= "code=" . $code . "&state=" . $state;
header('Location: ' . $url);
return;
} else if ($response_type === 'token') {
$access_token = '191223d5e1bcb5f9e51bca66113ce3a4';
$expires_in = 60 * 60 * 2;
$url = $redirect_uri;
$url .= "#access_token=" . $access_token . "&state=" . $state . "&token_type=access_token&expires_in=" . $expires_in;
header('Location: ' . $url);
return;
} else {
Header("HTTP/1.1 400 Bad Request");
Header("content-type: text/plain;charset=UTF-8");
$message = "不支持的response_type";
echo $message;
return;
}
-- token.php -----------------------------------
'191223d5e1bcb5f9e51bca66113ce3a4',
"token_type" => 'access_token',
"expires_in" => 60 * 60 *2,
"refresh_token" => '191223d5e1bcb5f9e51bca66113ce3a5',
"scpoe" => ''
)
);
return;
} else if ("refresh_token" === $grant_type) {
Header("HTTP/1.1 200 OK");
Header("content-type: application/json;charset=UTF-8");
echo json_encode(
array(
"access_token" => '191223d5e1bcb5f9e51bca66113ce3a4',
"token_type" => 'access_token',
"expires_in" => 60 * 60 *2,
"refresh_token" => '191223d5e1bcb5f9e51bca66113ce3a5',
"scpoe" => ''
)
);
return;
} else {
Header("HTTP/1.1 400 Bad Request");
Header("content-type: text/plain;charset=UTF-8");
echo '不支持的grant_type';
return;
}
-- dueros.php ----------------------------------
connect($host, $port);
$redis->select($db);
return $redis;
}
function getLed() {
$redis = getRedis('127.0.0.1', 6379, 0);
$led = $redis->get("led");
$redis->close();
return $led;
}
function setLed($led) {
$redis = getRedis('127.0.0.1', 6379, 0);
$redis->set("led", $led);
$redis->close();
}
// https://www.xxxxxxxxxxxx.com/iot/dueros.php
// --------------------
$path = $namespace . '/' . $name;
if ('DuerOS.ConnectedHome.Discovery/DiscoverAppliancesRequest' === $path) {
//echo "discovery";
$ret = array(
'header' => array(
"namespace" => "DuerOS.ConnectedHome.Discovery",
"name" => "DiscoverAppliancesResponse",
"messageId" => $messageId,
"payloadVersion" => "1"
),
'payload' => array(
"discoveredAppliances" => array(
array(
"applianceTypes" => "SWITCH",
"applianceId" => "20191222220215",
"friendlyDescription" => "我的开关开关。。",
"friendlyName" => "我的开关",
"isReachable" => true,
"manufacturerName" => "zhanglc",
"modelName" => "my sw",
"version" => "0.0。1",
"actions" => array(
"turnOn", "turnOff"
),
"attributes" => array(
"name" => "powerState",
"value" => "ON",
"scale" => "",
"timestampOfSample" => time(),
"uncertaintyInMilliseconds" => 0
)
)
)
)
);
echo json_encode($ret);
return;
}
if ('DuerOS.ConnectedHome.Control/TurnOnRequest' === $path) {
setLed('00');
//echo "turn on";
$ret = array(
'header' => array(
"namespace" => "DuerOS.ConnectedHome.Control",
"name" => "TurnOnConfirmation",
"messageId" => $messageId,
"payloadVersion" => "1"
),
'payload' => array(
"attributes" => array(
"name" => "powerState",
"value" => "ON",
"scale" => "",
"timestampOfSample" => time(),
"uncertaintyInMilliseconds" => 0
)
)
);
echo json_encode($ret);
return;
}
if ('DuerOS.ConnectedHome.Control/TurnOffRequest' === $path) {
setLed('10');
//echo "turn off";
$ret = array(
'header' => array(
"namespace" => "DuerOS.ConnectedHome.Control",
"name" => "TurnOffConfirmation",
"messageId" => $messageId,
"payloadVersion" => "1"
),
'payload' => array(
"attributes" => array(
"name" => "powerState",
"value" => "ON",
"scale" => "",
"timestampOfSample" => time(),
"uncertaintyInMilliseconds" => 0
)
)
);
echo json_encode($ret);
return;
}
//Header("content-type: application/json;charset=UTF-8");
Header("content-type: text/plain;charset=UTF-8");
echo "不支持的请求: ".$path;
return;
(5)真机测试,开启技能调试模式,手机小度app上看设备,应该是会显示出此设备(原先直接向小度发语言提示没找到设备),向小度发语言“打开开关”,开关被打开。