简介:这份资源面向需要用PHP与臻识摄像机做数据交互的开发者,聚焦设备对接中的通信实现与安全校验问题。包内共2个PHP文件,压缩包约4KB,属于轻量级代码示例,主要包含对接测试入口与Base64相关处理逻辑,便于快速理解请求构造与数据编码环节。资源围绕建立连接、身份验证、数据交换与CRC16校验等关键点展开,其中CRC16用于生成16位校验码,可帮助检测传输过程中的数据错误,保障通信完整性。已有676人学习,适合具备PHP基础、熟悉HTTP协议并希望了解设备控制与加密通信的开发者参考。通过阅读代码,读者可掌握与臻识摄像机交互的基本流程,理解CRC16在PHP中的实现思路,并据此扩展实时视频获取、镜头控制或参数调整等功能,为后续查阅官方SDK与API文档打下实践基础。
1. 拆开“PHP对接臻识摄像机.7z”:一套车牌识别系统的落地骨架
拿到“PHP对接臻识摄像机.7z”这个标题,很多人第一反应是去翻压缩包里有什么源码。但真正在一线做过停车场、道闸、称重、门禁这类项目的人都知道,臻识摄像机的对接核心不在 PHP 代码本身,而在于摄像机对外暴露的协议接口和PHP 侧如何稳定地收、存、回传。臻识(VZEN)的相机在车牌识别、车辆检测这个圈子里用量很大,常见于出入口道闸场景,它支持 HTTP、SDK、ONVIF、GB28181 等多种对接方式,而 PHP 作为后端,最常走的是 HTTP 主动上报加轮询查询这条路线。
这套方案能解决什么问题?简单说:让摄像机识别到车牌后,把车牌号、颜色、抓拍图、时间这些数据,可靠地送到你的 PHP 业务系统里,再驱动道闸、计费、放行。适合谁?适合做智慧停车、园区出入口、无人值守称重的 PHP 后端开发者,尤其是那些手里已经有一台臻识相机、想用 PHP 快速跑通链路的同学。下面我按“先搞懂相机怎么说话,再写 PHP 怎么接,最后讲坑”的顺序,把这条路走一遍。
2. 臻识摄像机的数据出口:先搞清楚它怎么把车牌吐出来
对接任何硬件,第一步永远不是写代码,而是搞清楚它的数据从哪个口子出来、长什么样。臻识相机的数据出口主要有三种形态,选错了后面全是返工。
2.1 HTTP 主动上报(Notify)与轮询查询(Get)的区别
臻识相机最常见的两种 HTTP 交互模式:
- 主动上报:相机识别到车牌后,自己作为客户端,向你配置的服务器地址 POST 一条 JSON 或 XML 数据。你在相机 Web 后台里填一个“上报地址”,比如
http://your-server.com/vzen/notify。 - 轮询查询:你的 PHP 定时去请求相机的查询接口,比如
/queryIO、/GetRecord,把最近一条识别记录拉回来。
选哪个?出入口这种实时性要求高的场景,主动上报是主力,轮询作为补偿。因为上报是事件驱动,车牌一识别就推,延迟低;轮询有间隔,容易漏掉连续过车。但上报有个前提:你的服务器必须有一个公网可达或相机能访问到的地址,内网部署时通常是同网段。
我一般会两个都开:上报负责实时触发道闸,轮询负责每 30 秒对一次账,防止上报丢包导致漏车。
2.2 上报数据的字段结构:车牌、时间、图片地址怎么读
臻识上报的 JSON 结构,不同固件版本字段名会有差异,但核心字段基本稳定。下面是一个典型的车牌识别上报体(字段名以实际固件为准,这里给的是常见形态):
{ "AlarmInfoPlate": { "channel": 0, "deviceName": "VZEN-01", "ipaddr": "192.168.1.100", "result": { "PlateResult": { "license": "京A12345", "colorType": 1, "confidence": 95, "timeStamp": "20240115103000", "imagePath": "/192.168.1.100/2024/01/15/xxx.jpg", "carColor": 2 } } } }几个关键点:
license是车牌字符串,注意可能是 UTF-8,也可能是 GBK,取决于固件,中文车牌乱码十有八九是编码问题。timeStamp是相机本地时间,格式yyyyMMddHHmmss,不要直接当服务器时间用,相机时间可能没校准。imagePath是相对路径,需要拼上相机的 HTTP 前缀才能下载,比如http://192.168.1.100/192.168.1.100/2024/...。confidence是置信度,低于 80 的识别结果建议做二次确认,别直接放行。
2.3 相机端要配的三件事:上报地址、心跳、白名单
在写 PHP 之前,先把相机配好,否则你代码写得再对也收不到数据:
- 上报地址:在相机 Web 后台的“网络配置 → 平台接入”里,填你的 PHP 接口 URL,协议选 HTTP,方法选 POST。
- 心跳:部分固件有“心跳保活”,填一个
/heartbeat接口,相机定时 GET,用来确认服务器活着。这个接口返回 200 就行,不用返回内容。 - 白名单:如果相机有 IP 白名单或鉴权开关,先把你的服务器 IP 加进去,或者关掉鉴权,否则上报会被拒。
提示:相机后台的配置项名称各固件版本不完全一致,找不到就翻“平台接入”“主动注册”“HTTP 推送”这几个菜单,本质是同一个东西。
3. 用 PHP 接住上报:从入口路由到数据落库
相机把数据推过来了,PHP 这边要做的就是“接住、解析、存下来、给响应”。这一章把最小可运行链路写出来。
3.1 最小可运行的接收接口:一个 index.php 搞定
先不搞框架,用一个裸 PHP 文件跑通,确认链路通了再谈工程化。
<?php // vzen_notify.php —— 臻识相机上报接收入口 // 相机 POST 过来的原始数据先落盘,方便排查 $raw = file_get_contents('php://input'); file_put_contents('/tmp/vzen_raw_' . date('Ymd') . '.log', $raw . "\n", FILE_APPEND); // 解析 JSON $data = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { // 有些固件推的是 XML,这里做一次兜底 $xml = simplexml_load_string($raw); if ($xml === false) { http_response_code(400); echo json_encode(['code' => 1, 'msg' => 'parse error']); exit; } $data = json_decode(json_encode($xml), true); } // 提取车牌信息 $plate = $data['AlarmInfoPlate']['result']['PlateResult'] ?? null; if (!$plate) { echo json_encode(['code' => 0, 'msg' => 'no plate']); exit; } $license = trim($plate['license']); $confidence = intval($plate['confidence'] ?? 0); $timestamp = $plate['timeStamp'] ?? date('YmdHis'); $imagePath = $plate['imagePath'] ?? ''; // 落库(示例用 PDO) $pdo = new PDO('mysql:host=127.0.0.1;dbname=parking;charset=utf8mb4', 'root', 'pass'); $stmt = $pdo->prepare( "INSERT INTO vzen_record (license, confidence, shoot_time, image_path, raw_json, created_at) VALUES (?, ?, ?, ?, ?, NOW())" ); $stmt->execute([$license, $confidence, $timestamp, $imagePath, $raw]); // 必须快速返回,相机有超时 echo json_encode(['code' => 0, 'msg' => 'ok']);逻辑说明:
php://input拿原始 body,先落盘再解析,这是排查“相机到底推没推、推了什么”的唯一后悔药。- JSON 解析失败时兜底 XML,因为臻识不同固件推送格式不统一。
- 提取
PlateResult后落库,raw_json字段存原始报文,方便事后对账。 - 最后必须返回一个 JSON,且要快。相机上报超时时间通常只有 1~3 秒,你这边做重活会丢数据。
参数说明:
confidence阈值建议在业务层判断,不要在这里过滤,先存全量。shoot_time存相机时间,同时created_at存服务器时间,两个都留,对账时用得上。
3.2 图片抓拍怎么存:路径拼接与异步下载
上报体里的imagePath只是路径,图片还在相机里。要存图,得自己下载。
<?php // download_image.php —— 根据上报的 imagePath 下载抓拍图 function downloadVzenImage($cameraIp, $imagePath, $saveDir) { // 拼接完整 URL,注意 imagePath 可能已含 IP 段 $url = 'http://' . $cameraIp . '/' . ltrim($imagePath, '/'); $ctx = stream_context_create(['http' => ['timeout' => 3]]); $img = @file_get_contents($url, false, $ctx); if ($img === false) { return false; } // 按日期分目录,避免单目录文件过多 $subDir = $saveDir . '/' . date('Ymd'); if (!is_dir($subDir)) { mkdir($subDir, 0755, true); } $file = $subDir . '/' . md5($imagePath) . '.jpg'; file_put_contents($file, $img); return $file; }逻辑说明:
- 图片下载不要放在上报接口里同步做,相机超时等不起。正确做法是上报接口只落库,然后丢一个队列或定时任务去下载。
imagePath拼接时注意有的固件返回的路径已经带了 IP,有的没有,先ltrim再拼,避免双斜杠。- 按日期分目录是血泪经验,单目录几万个文件后
ls都能卡死。
参数说明:
- 超时设 3 秒,相机在内网,超过 3 秒基本是网络问题,重试即可。
- 文件名用
md5(imagePath)去重,防止重复下载。
3.3 用 MySQL 存识别记录:表结构与索引怎么设计
表结构直接决定后面查询快不快。下面是我常用的建表语句:
CREATE TABLE `vzen_record` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `license` VARCHAR(16) NOT NULL COMMENT '车牌号', `confidence` TINYINT UNSIGNED DEFAULT 0 COMMENT '置信度', `shoot_time` VARCHAR(14) DEFAULT '' COMMENT '相机时间 yyyyMMddHHmmss', `image_path` VARCHAR(255) DEFAULT '' COMMENT '相机图片路径', `local_image` VARCHAR(255) DEFAULT '' COMMENT '本地存储路径', `raw_json` TEXT COMMENT '原始报文', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_license_time` (`license`, `shoot_time`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计要点:
license用VARCHAR(16),新能源车牌 8 位,留余量。- 联合索引
idx_license_time用于“查某车牌某时间段的记录”,这是最高频查询。 raw_json用 TEXT,别用 JSON 类型,因为有些老 MySQL 版本不支持,而且你只是存,不查。shoot_time用字符串存,因为相机给的就是字符串,转 DATETIME 反而容易在时区上翻车。
注意:如果日均过车量大,
vzen_record要按月分表,否则半年后单表千万级,查询会明显变慢。
4. 业务闭环:从识别记录到道闸放行与计费
数据存下来只是第一步,真正产生价值的是“识别到车牌之后干什么”。这一章讲业务侧怎么接。
4.1 车牌匹配与放行逻辑:白名单、月租、临时车怎么分流
一条识别记录进来,业务上通常分三类处理:
| 车辆类型 | 判断依据 | 动作 |
|---|---|---|
| 白名单/固定车 | 车牌在vzen_whitelist表且未过期 | 直接开闸 |
| 月租车 | 在vzen_monthly表且当前日期在租期内 | 开闸并记录 |
| 临时车 | 以上都不匹配 | 计费后开闸或等待缴费 |
<?php // handle_pass.php —— 根据车牌决定是否放行 function handlePass($pdo, $license, $cameraIp) { // 1. 查白名单 $stmt = $pdo->prepare("SELECT id FROM vzen_whitelist WHERE license = ? AND expire_at > NOW() LIMIT 1"); $stmt->execute([$license]); if ($stmt->fetch()) { return openGate($cameraIp); // 直接开闸 } // 2. 查月租 $stmt = $pdo->prepare("SELECT id FROM vzen_monthly WHERE license = ? AND start_at <= NOW() AND end_at >= NOW() LIMIT 1"); $stmt->execute([$license]); if ($stmt->fetch()) { return openGate($cameraIp); } // 3. 临时车,走计费流程 return ['action' => 'charge', 'license' => $license]; }逻辑说明:
- 判断顺序很重要:先白名单再月租最后临时,因为白名单优先级最高。
- 每次判断都带时间条件,避免过期卡还能用。
- 临时车不直接开闸,返回计费指令,由上层决定。
参数说明:
expire_at、start_at、end_at都用 DATETIME,别用字符串比较。- 查询走
license索引,单条查询毫秒级。
4.2 调用相机开闸接口:HTTP 命令与返回值判断
放行的本质是给相机发一个开闸命令。臻识相机一般有 IO 控制接口:
<?php // open_gate.php —— 调用相机 IO 输出开闸 function openGate($cameraIp, $channel = 0) { $url = "http://{$cameraIp}/io/control"; $payload = json_encode([ 'channel' => $channel, 'action' => 'open', 'duration' => 500 // 毫秒,继电器吸合时间 ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, ]); $resp = curl_exec($ch); $err = curl_error($ch); curl_close($ch); if ($err) { error_log("openGate fail: {$err}"); return false; } $result = json_decode($resp, true); return isset($result['code']) && $result['code'] === 0; }逻辑说明:
- 开闸接口必须设超时,2 秒足够,超时说明相机离线或网络不通。
duration是继电器吸合时间,太短道闸不动作,太长可能烧继电器,500ms 是常见值。- 返回值要判断,不能发了就当成功,否则道闸没开你也不知道。
参数说明:
channel对应相机的 IO 口,多路道闸时用不同 channel。- 失败要记日志,并触发告警,比如“相机离线”。
4.3 计费与出场对账:临时车怎么算钱
临时车计费的核心是“入场时间”和“出场时间”。入场时间来自第一次识别记录,出场时间来自当前识别。
<?php // calc_fee.php —— 临时车计费 function calcFee($pdo, $license, $exitTime) { // 查最近一次入场记录 $stmt = $pdo->prepare( "SELECT shoot_time FROM vzen_record WHERE license = ? ORDER BY id ASC LIMIT 1" ); $stmt->execute([$license]); $row = $stmt->fetch(); if (!$row) { return 0; // 无入场记录,按免费或异常处理 } $enter = strtotime($row['shoot_time']); $exit = strtotime($exitTime); $hours = ceil(($exit - $enter) / 3600); // 简单计费:首小时 5 元,之后每小时 3 元 $fee = 5 + max(0, $hours - 1) * 3; return $fee; }逻辑说明:
- 入场时间取该车牌最早的一条记录,出场取当前。
ceil向上取整,不足一小时按一小时,这是停车场通行做法。- 计费规则要可配置,别写死在代码里,后面改价很麻烦。
参数说明:
shoot_time是字符串,strtotime能解析yyyyMMddHHmmss格式。- 跨天停车要额外处理,这里只给最小示例。
5. 对接臻识摄像机最容易翻车的 5 个坑
这一章是我踩过的坑,按“现象 → 原因 → 解决”写,能帮你省下不少调试时间。
5.1 相机显示上报成功,PHP 却收不到数据
现象:相机后台日志显示“上报成功”,但 PHP 日志里一条记录都没有。
原因:最常见的是相机和服务器不在同一网段,或者服务器防火墙没放行 80/443 端口。其次是相机填的上报地址带了路径但 PHP 路由没匹配上。
解决:先在服务器上用tcpdump port 80抓包,确认相机到底有没有发过来。如果有包但 PHP 没记录,检查 Web 服务器(Nginx/Apache)的 access log,看请求有没有到 PHP。如果没包,检查网络和防火墙。
5.2 中文车牌乱码,京A 变成问号
现象:数据库里车牌显示为??A12345或乱码。
原因:相机固件推送的编码可能是 GBK,而 PHP 按 UTF-8 解析。
解决:在解析后加一次编码转换:
if (!mb_check_encoding($license, 'UTF-8')) { $license = mb_convert_encoding($license, 'UTF-8', 'GBK'); }同时确认数据库连接字符集是utf8mb4。
5.3 上报接口返回慢,导致相机重复推送
现象:同一条记录在数据库里出现两三次。
原因:PHP 接口处理时间超过相机超时,相机认为失败,重试推送。
解决:上报接口只做“落库 + 返回”,图片下载、业务处理全部异步。接口响应时间控制在 200ms 以内。另外在入库时用license + shoot_time做唯一索引去重。
5.4 图片下载 404,路径拼接错误
现象:imagePath拿到了,但下载图片返回 404。
原因:imagePath的格式因固件而异,有的带 IP 段,有的不带,直接拼接会多一层或漏一层。
解决:先打印几条imagePath看规律,再决定拼接方式。稳妥做法是判断是否以http开头,不是才拼。
5.5 相机时间不准,计费算错
现象:临时车计费金额离谱,比如停了 1 小时收了几十块。
原因:相机本地时间没校准,shoot_time和服务器时间差了几个小时。
解决:相机后台开启 NTP 对时,指向内网 NTP 服务器。业务上,入场时间用服务器created_at兜底,不要完全信相机时间。
6. 让这套对接更稳的两个进阶技巧
跑通最小链路之后,真正决定这套系统能不能上生产的,是幂等去重和断线补偿。这两个点做不好,轻则重复开闸,重则漏车投诉。
先说幂等。相机重推是常态,尤其是网络抖动时。我的做法是在vzen_record表上建一个唯一索引:
ALTER TABLE vzen_record ADD UNIQUE KEY `uk_license_time` (`license`, `shoot_time`);然后在插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE。这样同一条识别记录无论推多少次,库里只有一条。注意shoot_time精度到秒,同一秒同一车牌重复推的概率极低,可以接受。
再说断线补偿。相机离线、服务器重启、网络中断,都会导致上报丢失。补偿靠轮询:写一个定时任务,每 30 秒调一次相机的查询接口,把最近 N 条记录拉回来,和库里比对,缺的补上。
<?php // poll_records.php —— 定时轮询补偿 function pollVzenRecords($pdo, $cameraIp) { $url = "http://{$cameraIp}/queryRecord?count=20"; $resp = @file_get_contents($url); if ($resp === false) return; $list = json_decode($resp, true); foreach ($list['records'] ?? [] as $rec) { $license = $rec['license']; $time = $rec['timeStamp']; // 已存在则跳过 $stmt = $pdo->prepare("SELECT id FROM vzen_record WHERE license=? AND shoot_time=? LIMIT 1"); $stmt->execute([$license, $time]); if ($stmt->fetch()) continue; // 补录 $ins = $pdo->prepare("INSERT INTO vzen_record (license, shoot_time, raw_json) VALUES (?,?,?)"); $ins->execute([$license, $time, json_encode($rec)]); } }轮询接口的路径和参数各固件不同,常见的是/queryRecord或/GetRecord,具体看相机文档。轮询频率别太高,30 秒一次足够,太高会给相机压力。
最后说一个验证方法:拿一张测试车牌,在相机前晃一下,然后查三个地方——相机后台的上报日志、PHP 的原始报文日志、数据库记录。三处对得上,链路就是通的。对不上,就按“相机有没有发 → 服务器有没有收 → PHP 有没有存”逐段排查。
我自己做这类项目,习惯是先把原始报文落盘跑一周,确认没有异常格式再关掉。这个习惯帮我省过好几次大麻烦,因为臻识不同批次的固件,字段真的会变。希望帮到你。
本文还有配套的精品资源,点击获取