wKioL1m3YkniCtxTAAC8pLc-3p4304.jpg


通篇分为三大块:服务器、蓝牙锁、APP

先说服务器:

使用的是TP5、workman框架使用composer安装的

安装wm可直接参考TP5的官方手册,讲解的很细致https://www.kancloud.cn/manual/thinkphp5/235128

wKioL1nIabGzY4xcAADwFs3xy54876.jpg

Server.php文件

这里我对Server类进行了一些改动

  1. 为了加入定时器的功能

  2. 新增加了一个$inner_text_worker = new Worker('Text://0.0.0.0:5678');服务协议,用作APP端发送开锁指令&告知蓝牙锁进行开关锁;

  3. 构造函数里面重写了$this->worker->onWorkerStart函数,这样的话Worker控制器里面的onWorkerStart函数将失去作用,如果不在这里重写,去Worker控制器里面的onWorkerStart函数加定时器将不起作用,因为Server构造函数里面已经运行了(Worker::runAll();)所有协议。

  4. 由于这里进行了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;
        }
    }
    
    
}



未完待续。。。