通篇分爲三大塊:服務器、藍牙鎖、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 <[email protected]> // +---------------------------------------------------------------------- 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; } } }
未完待續。。。