通篇分为三大块:服务器、蓝牙锁、APP
先说服务器:
使用的是TP5、workman框架使用composer安装的
安装wm可直接参考TP5的官方手册,讲解的很细致https://www.kancloud.cn/manual/thinkphp5/235128
Server.php文件
这里我对Server类进行了一些改动
为了加入定时器的功能
新增加了一个$inner_text_worker = new Worker('Text://0.0.0.0:5678');服务协议,用作APP端发送开锁指令&告知蓝牙锁进行开关锁;
构造函数里面重写了$this->worker->onWorkerStart函数,这样的话Worker控制器里面的onWorkerStart函数将失去作用,如果不在这里重写,去Worker控制器里面的onWorkerStart函数加定时器将不起作用,因为Server构造函数里面已经运行了(Worker::runAll();)所有协议。
由于这里进行了AES的加、解密,参考的时候可以忽略加解密容易浏览。
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace think\worker;
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\MySQL\Connection;
/**
* Worker控制器扩展类
*/
abstract class Server
{
protected $worker;
protected $worker2;
protected $socket = '';
protected $protocol = 'http';
protected $host = '0.0.0.0';
protected $port = '2346';
protected $processes = 1;
/**
* 架构函数
* @access public
*/
public function __construct()
{
// 实例化 Websocket 服务
$this->worker = new Worker($this->socket ?: $this->protocol . '://' . $this->host . ':' . $this->port);
// 设置进程数
$this->worker->count = $this->processes;
// 设置进程名称
$this->worker->name = "bluetooth";
// 初始化
$this->init();
// 自定义开始
// worker进程中开启一个Text协议进程
$this->worker->onWorkerStart = function ($worker) {
require_once "/data/var/www/html/zmartec_bluetooth/vendor/workerman/workerman/Lib/Connection.php";
// 将db实例存储在全局变量中(也可以存储在某类的静态成员中)
global $db;
$db = new Connection("mysql主机IP地址", "mysql端口", "mysql用户", "密码", "数据库名称");
// 心跳 start
// 进程启动后设置一个每秒运行一次的定时器
Timer::add(1, function ()use($worker){
$time_now = time();
foreach ($worker->connections as $connection) {
// 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通讯时间间隔大于心跳间隔(300秒),则认为客户端已经下线,关闭连接
if ($time_now - $connection->lastMessageTime > 300) {
if ($connection->uid) {
$connection->close();
echo "\r\n" . "客户端:" . $connection->uid . "超过心跳时间,被断开" . "\r\n"; // $connection->uid
} else {
$connection->close();
echo "\r\n" . "客户端:" . xxx . "超过心跳时间,被断开" . "\r\n"; // $connection->uid
}
}
}
});
// 心跳end
// Text协议,处理APP的开锁、关锁指令
$inner_text_worker = new Worker('Text://0.0.0.0:5678');
$inner_text_worker->onMessage = function ($connection, $buffer) {
global $worker;
// $data数组格式,里面有uid,表示向哪个uid的用户推送数据
$data = json_decode($buffer, true);
var_dump($data);
$uid = $data['serial'];
$send_data = $data['data'];
var_dump("开锁明文串:" . $data['plaintext']);
// 获取校验值
$xor = get_xor_value(preg_replace('/(0x)/', '', substr_replace($send_data, '', -1)));
echo "获取校验值:" . dechex($xor) . "\r\n";
// echo "转换之后的获取校验值:" . hexToStr(dechex($xor)) . "\r\n";
// 通过workerman,向uid的页面推送数据
$ret = $this->sendMessageByUid($uid, "aacc22" . $send_data . (strlen(dechex($xor)) == 1 ? "0" . dechex($xor) : dechex($xor)));
// var_dump("Text协议发送结果:" . $ret);
// 返回推送结果
$connection->send($ret ? 'ok' : 'fail');
};
$inner_text_worker->listen();
};
// 自定义结束
// 设置回调'onWorkerStart',
foreach (['onConnect', 'onMessage', 'onClose', 'onError', 'onBufferFull', 'onBufferDrain', 'onWorkerStop', 'onWorkerReload'] as $event) {
if (method_exists($this, $event)) {
$this->worker->$event = [$this, $event];
}
}
// Run worker
Worker::runAll();
}
protected function init()
{
}
// 针对uid推送数据
public function sendMessageByUid($uid, $message)
{
global $worker;
// echo "uid-only:";
// var_dump($worker);// $worker->uidConnections[$connection->uid]
if(isset($worker->uidConnections[$uid]))
{
$connection = $worker->uidConnections[$uid];
// var_dump("开锁完整串:" . toHexString(hexToStr(preg_replace('/(,)/', '', preg_replace('/(0x)/', '', $message)))) . "(" . strlen(toHexString(hexToStr(preg_replace('/(,)/', '', preg_replace('/(0x)/', '', $message))))) . ")". "开锁完整串");
$connection->send(hexToStr(preg_replace('/(,)/', '', preg_replace('/(0x)/', '', $message))));
return true;
} else {
echo "no-2\r\n";
}
return false;
}
}
Worker.php控制器类:
<?php
namespace app\index\controller;
use think\worker\Server;
use Workerman\Lib\Timer;
use think\Model;
use Workerman\MySQL\Connection;
use app\index\controller\User;
use think\Controller;
/**
* 类
* 处理服务器与蓝牙锁设备
* 之间通讯
*/
class Worker extends Server
{
// protected $socket = 'http://0.0.0.0:2348';
protected $socket = 'tcp://0.0.0.0:2349';
// 加解密串
protected $key_init = "0x23,0x4f,0xe6,0x27,0x45,0x69,0x73,0x5b,0x0,0x18,0xc3,0xd1,0xa5,0xc5,0x28,0xc1";
protected $host = "mysql主机IP地址";
protected $port = "mysql端口";
private $user = "mysql用户";
private $password = "密码";
private $db_name = "数据库名称";
/**
* 收到信息
*
* @param $connection
* @param $data
*/
public function onMessage($connection, $data)
{
/**
* start-注释区这块用作给苏工测试,后续删除
*/
file_put_contents('/tmp/zmartec_bluetooth.log', date("Y-m-d H:i:s")."\r\n" . toHexString($data) . "\r\n\r\n", FILE_APPEND|LOCK_EX);
/**
* end-注释区这块用作给苏工测试,后续删除
*/
$data = toHexString($data);
// 这里是为了处理粘包问题,只做了粘包3次的处理
// 如果你有更好的处理方式,可换成你自己的
if ($this->loop($connection, $data)) {
$data_new = original_data_process($data);
$length_byte = hexdec($data_new[2]);
$total_length = count($data_new);
if ($total_length - $length_byte - 4 > 0) {
$data = substr($data, ($length_byte + 4) * 2);
var_dump("处理过后的子串1:" . $data);
if ($this->loop($connection, $data)) {
$length_byte_new = hexdec($data_new[($length_byte + 4 + 2)]);
var_dump("xxx" . $length_byte_new);
if ($total_length - $length_byte - 4 - $length_byte_new -4 > 0) {
$data = substr($data, (($length_byte_new + 4) * 2));
// var_dump("字符串总长度:" . strlen($data));
var_dump("处理过后的子串2:" . $data);
// die;
$this->loop($connection, $data);
}
}
}
}
}
/**
* 当连接建立时触发的回调函数
*
* @param $connection
*/
public function onConnect($connection)
{
// 通过全局变量获得db实例
global $db;
$time = time();
echo "已连接Client的IP:" . $connection->getRemoteIp() . "\r\n";
}
/**
* 当连接断开时触发的回调函数
*
* @param $connection
*/
public function onClose($connection)
{
global $worker;
echo "\r\n断开连接Client的IP:" . $connection->getRemoteIp() . "\r\n";
/**
* 这里如果设备异常断开,
* 会导致同一台设备再此连接时候也以序列号
* 为uid标识的设备也被unset掉
*/
// if (isset($connection->uid)) {
// // 连接断开时删除映射
// unset($worker->uidConnections[$connection->uid]);
// }
echo "\r\n连接id:" . $connection->id . "disconnect \r\n";
}
/**
* 当客户端的连接上发生错误时触发
*
* @param $connection
* @param $code
* @param $msg
*/
public function onError($connection, $code, $msg)
{
echo "error $code $msg\n";
}
/**
* 每个进程启动
*
* @param $worker
*/
public function onWorkerStart($worker)
{
require_once __DIR__ . '/../../../vendor/workerman/workerman/Lib/Connection.php';
// 将db实例存储在全局变量中(也可以存储在某类的静态成员中)
global $db;
$db = new Connection($this->host, $this->port, $this->user, $this->password, $this->db_name);
// 进程启动后设置一个每秒运行一次的定时器
Timer::add(1, function ()use($worker){
$time_now = time();
foreach ($worker->connections as $connection) {
// 有可能该connection还没收到过消息,则lastMessageTime设置为当前时间
if (empty($connection->lastMessageTime)) {
$connection->lastMessageTime = $time_now;
continue;
}
// 上次通讯时间间隔大于心跳间隔,则认为客户端已经下线,关闭连接
if ($time_now - $connection->lastMessageTime > 1000) {
$connection->close();
}
}
});
echo $worker->id . "\r\n";
}
public function loop($connection, $data)
{
// 给connection临时设置一个lastMessageTime属性,用来记录上次收到消息的时间
$connection->lastMessageTime = time();
// 通过全局变量获得db实例
global $db,$worker;
// 转为数组
$data = original_data_process($data);
// 获取长度位
$length_byte = hexdec($data[2]);
// 获取开头标识位
$head_byte = array_slice($data, 0, 2);
$head_byte = implode('', $head_byte);
if ($head_byte != 'aacc') {
echo "数据异常\r\n";
return true;
}
if ($length_byte == 34) {
// 校验数据
if (true !== verify_xor_value($data)) {
//$connection->send("xor is not match.");
echo "xor is not match.\r\n";
// return ;
}
// 解密
$data_slice = array_slice($data, 3, 35 - 1);
print_r("发送的被解密串:" . json_encode($data_slice) . '\r\n');
$random = array_slice($data_slice, 0, 18);
print_r("发送的随机串:" . json_encode($random) . '\r\n');
$ciphertext = array_slice($data_slice, 18, 33);
print_r("发送的密文串:" . json_encode($ciphertext) . '\r\n');
$decrypt = zm_decrypt($ciphertext, $this->key_init, $random);
var_dump("解密的完整串:" . $decrypt);
// 序列号
$serial = substr($decrypt, -10);
// 设备号
$device_num = substr($decrypt, 6, 16);
$start = substr($decrypt, 0, 6);
$start_4_byte = substr($decrypt, 0, 4);
// $connection->send("result." . json_encode($decrypt));//return '';
// 设备连接
if ("010203" == $start) {
// 判断当前客户端是否已经验证,即是否设置了uid
if (!isset($connection->uid)) {
$time = time();
// 拿到序列号作为uid
$connection->uid = $serial;
echo "\r\n" . date('Y-m-d H:i:s') . "\r\n";
echo "设备连接开始:" . "\r\n";
var_dump("连接设备的序列号:" . $serial);
var_dump("连接设备的设备号:" . $device_num);
/* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
* 实现针对特定uid推送数据
*/
$worker->uidConnections[$connection->uid] = $connection;
// Array
// (
// [device_num] => 330b41c003300310
// )
$device_id = $db->select('device_num')
->from('zm_device')
->where('serial= :serial AND device_num = :device_num')
->bindValues(array('serial'=>"$serial", 'device_num' => "$device_num"))
->row();
if (!$device_id) {
$result_device = $db->insert('zm_device')->cols(array(
'serial' => "$serial",
'device_num' => "$device_num"
))->query();
}
// 记录设备在线状态
$online_device_id = $db->select('id')
->from('zm_online_device')
->where('device_id= :device_id')
->bindValues(array('device_id'=>"$device_id[device_num]"))
->row();
$result = $db->insert('zm_online_device')->cols(array(
'device_id' => '1',
'host' => $connection->getRemoteIP(),
'created_time' => $time
))->query();
echo "设备连接处理结束:" . "\r\n";
// 设备连接确认
$connection->send(hexToStr("AACC060000000000FFFF"));
return true;
} else {
$connection->send(hexToStr("AACC060000000000FFFF"));
}
// 上锁
} else if ("9999" == $start_4_byte) {
$serial_4_byte = substr($decrypt, -8);
echo "\r\n" . date('Y-m-d H:i:s') . "\r\n";
var_dump("上锁序列号:" . $serial_4_byte);
// 故障、状态字节
$fault_st_2_byte = substr($decrypt, -10, 2);
$device_num = $db->select('device_num')
->from('zm_device')
->where('serial= :serial')
->bindValues(array('serial' => "00$serial_4_byte"))
->row();
// var_dump("上锁序列号查询结果" . $device_num);
if ($device_num) {
$lock = $db->update('zm_device')
->cols(array('is_lock_status' => '00'))
->where("serial = '00$serial_4_byte'")
->query();
echo "上锁故障、状态字节值:" . $fault_st_2_byte . "\r\n";
echo "上锁状态字节值:" . substr($fault_st_2_byte, -2) . "\r\n";
if ($lock || substr($fault_st_2_byte, -2) == '00') {
echo "\r\n上锁成功:" . $serial_4_byte . "\r\n";
$connection->send(hexToStr("AACC060000000000FFFF"));
} else if (substr($fault_st_2_byte, -2) == '01') {
echo "\r\n上锁失败:" . $serial_4_byte . "\r\n";
$connection->send(hexToStr("AACC060000000000FFFF"));
return true;
} else {
echo "\r\n上锁失败,序列号:" . $serial_4_byte . "\r\n";
$connection->send(hexToStr("AACC060000000000FFFF"));
return true;
}
} else {
echo "\r\n设备不存在,没有设备记录\r\n";
$connection->send(hexToStr("AACC060000000000FFFF"));
return true;
}
}
} else if ($length_byte == 16){ // 定位数据
echo "\r\n" . date('Y-m-d H:i:s') . "\r\n";
echo "这是定位数据信息\r\n";
echo implode(',', $data) . "\r\n";
echo "发送定位设备的IP:" . $connection->getRemoteIp() . "\r\n";
echo "发送定位设备的端口:" . $connection->getRemotePort() . "\r\n";
echo "当前设备经度:" . hexdec(implode('', array_slice($data, 8, 1))) . "." . ((strlen(hexdec(implode('', array_slice($data, 9, 3)))) != 4) ? hexdec(implode('', array_slice($data, 9, 3))) : '0'.hexdec(implode('', array_slice($data, 9, 3)))) . "\r\n";
echo "当前设备纬度:" . hexdec(implode('', array_slice($data, 4, 1))) . "." . ((strlen(hexdec(implode('', array_slice($data, 5, 3)))) != 4) ? hexdec(implode('', array_slice($data, 5, 3))) : '0'.hexdec(implode('', array_slice($data, 5, 3))));
echo "\r\n";
$location_data = array_slice($data, 3, $length_byte);
$verify_value = $data[$length_byte+3];
// 校验数据
if (true !== common_verify_xor_value($location_data, $verify_value)) {
//$connection->send("xor is not match.");
echo "The location xor is not match.\r\n";
}
return true;
} else if ($length_byte == 4) {
echo "\r\n" . date('Y-m-d H:i:s') . "\r\n";
echo "这是心跳包数据信息\r\n";
echo implode(',', $data) . "\r\n";
echo "发送心跳包设备的IP:" . $connection->getRemoteIp() . "\r\n";
echo "发送心跳包设备的端口:" . $connection->getRemotePort() . "\r\n";
echo "\r\n";
return true;
} else if ($length_byte == 10) {
echo "这是所有应答包数据信息\r\n";
echo implode(',', $data);
echo "\r\n";
// 校验数据
if (true !== response_verify_xor_value($data)) {
//$connection->send("xor is not match.");
echo "The response xor is not match.\r\n";
}
// 获取序列号
$serial = array_slice($data, -5, 4);
$serial = implode('', $serial);
// 获取电压
$electric = array_slice($data, 3, 2);
$electric = implode('', $electric);
// 获取标识符
$flag = array_slice($data, -6, 1);
$flag = implode('', $flag);
if ('ff' == $flag) {
echo "默认应答包:ff\r\n";
echo "设备电压值:" . substr(hexdec(implode('', array_slice($data, 3, 2))), 0, 1) . "." . substr(hexdec(implode('', array_slice($data, 3, 2))), 1, 2) . "V" . "\r\n";
$db->update('zm_device')
->cols(array('electric' => "$electric"))
->where("serial = '00$serial'")
->query();
} else if ('01' == $flag) {
echo "开锁成功\r\n";
$db->update('zm_device')
->cols(array('electric' => "$electric", 'is_lock_status' => '01'))
->where("serial = '00$serial'")
->query();
$connection->send(hexToStr("AACC060000000000FFFF"));
} else if ('00' == $flag) {
echo "开锁失败\r\n";
$db->update('zm_device')
->cols(array('electric' => "$electric", 'is_lock_status' => '00'))
->where("serial = '00$serial'")
->query();
$connection->send(hexToStr("AACC060000000000FFFF"));
} else {
echo "不知情况data:" . implode(',', $data) . "\r\n";
}
return true;
} else {
echo 'xxx';
return false;
}
}
}
未完待续。。。
转载于:https://blog.51cto.com/laok8/1964571