蓝牙Beacon室内定位全栈

本文详细介绍了使用Beacon技术进行室内定位的全过程,包括Beacon设备的优势、数据生产(涉及三维建模、坐标系统处理)、管理后台及移动端模型展示、Beacon数据获取以及定位算法。在数据生产环节,利用QGIS和AutoCAD结合地理坐标系与高斯-克吕格投影坐标系进行建模。在移动端,通过Cordova和WebApp获取Beacon数据,并利用Android Beacon Library进行信号处理。最后,通过信号衰减量比例进行三角定位,实现室内定位功能。

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

GPS是成熟很久的技术,智能手机发展起来之后GPS成了手机手机的必备模块之一,但室内没有GPS信号,使得室内定位到目前为止都是难点之一。但理论上技术倒真没难到什么程度,只要有可以在室内用的定位信号基站,手机端可以接收并解析就行了。但也有如下难点:

  • 室内距离短,电磁波传播速度太快,模仿GPS通过数据发送时间和接收时间的时间差来计算接收端与发送端之间的距离就不靠谱。
  • 室内遮挡多会导致信号衰减,因此通过信号衰减量来计算接收端与发送端之间的距离也不靠谱。
  • 便宜和兼容性,得方便移动设备用,高端的扫地机器人可以通过激光雷达给房间建模然后规划路线,这定位是精准了,但是让每个手机自带激光雷达明显不现实。超声波也可以用来定位,但这东西放手机上就没啥用。

Beacon的出现,这一切迎刃而解。

2012年,蓝牙4.0标准发布,低功耗蓝牙(Bluetooth Low Energy,BLE)技术成熟,一刻纽扣电池就可以让一个BLE设备工作很久。2013年苹果WWDC发布的设备支持iBeacon,标志着Beacon协议开始广泛用于个人移动设备。Beacon设备会每隔一定的时间广播一个数据包(BeaconName+UUID+Major+Minor+TX power)到周围,当手机接近时扫描到该设备,就能接收到其广播的数据包。Beacon设备用于室内定位有以下优势:

  • 几乎所有的手机都有蓝牙模块,不用担心兼容性问题。
  • 价格便宜,一百个Beacon设备也比不上一个激光雷达,可以大批量部署。
  • 传播距离近,遮挡等干扰少,信号衰减可以作为计算距离的参考。

接下来是我研究Beacon室内定位的整个流程,可能带有非常明显的小作坊的色彩,也还有不少部分需要完善,但也算是五脏齐全了。

数据生产

室内定位首先要有室内地图,或者室内的三维模型,理论上室内定位不需要理真实坐标,可以完全以建模或者室内地图的坐标系来,只要保证Beacon信标的坐标系与室内地图的坐标系一致即可。但这么做明显不利于后期可能存在的室内定位与室外定位相结合,也不利于标准化的生产和部署。所以我这里与真是坐标系相结合,建筑、楼层、信标的位置都直接保存成经纬度,根据我在博文《三维GIS建模不要用墨卡托投影》中的说明,我在三维建模时采用了CGS2000高斯-克吕格三度带投影坐标系。

在这里我首先说一下我的数据库设计,首先我假设了有多个需要室内定位的项目,每个项目可能有好几栋楼,每栋楼有若干层,每层部署若干Beacon信标(理论上8到10米,离地2米高左右的楼顶、墙壁或柱子上部署),楼栋和楼层都有自己的模型,数据库每张表的关系也这么设计。

然后就是我生产的目标数据是什么了,这跟我的技术路线有关,至少生产出来的三维模型能在我的管理后台和手机上用。技术选型有以下的考量:

  • 如果有多个项目,我的后台管理系统得统一管理,最好把多个项目的模型同时显示,方便查看。
  • 移动端最好跨平台,支持安卓和苹果,方便更广泛的使用。

管理后台其实很好确定了,要显示那么大范围的三维,Cesium基本上是唯一的选择。移动客户端就比较难受,想到跨平台三维,首先就能想到Unity这类游戏开发的,肯定是可以跨平台的。但是很遗憾,Unity不支持在线的三维模型资源,都得先下载到本地才能加载。最后移动端选择了直接做成WebApp然后通过Cordova打包,所以实际上移动端也能直接用Cesium的,但移动端通常只显示一栋楼或者一个项目的模型就行,还得显示个地球,有点多余,因此采用了微软的基于WebGL的三维引擎babylonjs

技术路线一确定,数据生产的目标也就确定了,生产成Cesium和babylonjs都可以很方便使用的gltf模型。

楼栋白模

这不是一个有真实需求的项目,因此我一丁点数据都没有,所有的数据都得自力更生,首先是楼栋白模。楼栋白模还是很方便制作的。

  1. 我从OpenStreetMap上下载了我需要做的楼栋的数据,下载下来的是WGS84地理坐标系OSM格式的数据。
  2. QGIS软件加载OSM数据,并把里面的房屋面提取出来成shp,假设名字叫fwm.shp,然后将数据重投影成CGS2000高斯克-吕格3度带投影坐标系,根据数据所在的经纬度来确定带号fwm_projection.shp。同时地理坐标系的数据也要保留着(因为WGS84地理坐标系和CGS2000地理坐标系非常相似,因此保留WGS84地理坐标系的数据也能当CGS2000地理坐标系的数据用)。
  3. 将fwm_projection.shp用QGIS打开,要素另存为AutoCAD DXF文件fwm_projection.dxf。
  4. 用AutoCAD打开fwm_projection.dxf文件,使用三维工具根据楼层的高度拉升成三维数据,保存成fwm_projection.dwg,这时候已经是一个三维模型了,接下来是将他转换成gltf(这里用glb,免得一个模型是多个文件)。
  5. 如果有FME2020以上的版本,可以直接将fwm_projection.dwg转换成fwm_projection.glb,如果没有也可以用Sketchup Pro导入dwg,然后导出成fbx或者obj模型,导出的时候注意勾选导出全部平面为三角形和切换YZ坐标,导入和导出时都要将单位设置成米,最后用Windows 10自带的3D查看器打开并保存成glb即可。我在博文《三维GIS建模不要用墨卡托投影》中有提到Cesium的三维笛卡尔坐标系跟建模软件的坐标系不一致的问题,因此这里的模型旋转问题需要处理,要就在建模的时候处理,要就在客户端去处理,反正就是要旋转一下。
  6. 接下来就是坐标问题了,这么创建的模型的坐标原点,对应的是平面图形的左下角,如果是比较规则的房屋面倒还好,特别是左下角恰好是房屋定点就比较方便,下面说通用的情况。在QGIS里面打开之前地理坐标系的房屋面,也就是fwm.shp,使用选择工具选中建模的那个房屋面,在工具箱中打开矢量地理对象>最小边界矢量图像,输入图层选择fwm.shp,几何图像类型选择边界框,勾选上仅选中的要素,就可以生成房屋面的边界举行,那这个矩形的左下角就对应了模型的坐标原点了,将地图放大到最大,鼠标放在矩形的左下角,鼠标所在位置的经纬度就是我们要记录下来的位置了。保存起来,以后房屋的模型就以这个坐标加载到场景。

楼层模型

楼层模型的生产与白模的生产大体上类似,不过因为我没有原数据,数据获取有点麻烦。

  • 首先我在网上搜了一下我要做的这栋楼的户型图,运气好,搜到了。
  • 然后我将户型图导入上面的fwm_projection.dxf,调整大小,方向和位置,让他能跟房屋面的外廓线匹配起来,之后再照着户型图用CAD画了一遍,就能保证位置和户型大体上差不多了。
  • 再之后就跟做白模差不多了,不过做楼层模型可以做个材质,这在Sketchup里很方便。还有一点要注意,楼层的插入经纬度跟楼栋不一定是一样的,比如有的楼下面10层是商场,上面40层是办公楼,或者有AB栋连在一起我只做B栋的楼层模型但是房屋白模是AB栋一体的。

管理后台展示模型

其实这部分倒是简单了,如上面所说,后台管理用的Cesium,总的来说就是选中项目之后定位到项目的几栋房屋所在的位置,点选中楼栋之后楼栋半透明,显示楼层列表,并默认选择第一楼层,选择哪个楼层就显示哪个楼层的模型。核心代码如下:

/************************
 * 初始化事件
 ************************/
function initEvent() {
   
   
    $('#project_select').on('change', function () {
   
   
        const project = _projects[$('#project_select').val()]
        const rectangle = Cesium.Rectangle.fromDegrees(project.minLongitude, project.minLatitude, project.maxLongitude, project.maxLatitude);
        Cesium.Camera.DEFAULT_VIEW_RECTANGLE = rectangle;
        _viewer.camera.flyTo({
   
   
            destination: rectangle
        });
        showBuilding(project);
    })

    var handler = new Cesium.ScreenSpaceEventHandler(_viewer.scene.canvas);
    handler.setInputAction(function (movement) {
   
   
        var pick = _viewer.scene.pick(movement.position);
        if (Cesium.defined(pick) && Cesium.defined(pick.id)) {
   
   
            const entity = pick.id;
            if (entity.id.startsWith('building_')) {
   
   
                entity.model.color = Cesium.Color.WHITE.withAlpha(0.3);
                const buildingId = entity.id.replace('building_', '');
                showFloors(buildingId);
            }
        }
        else {
   
   
            clearFloors();
        }

    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}

/************************
 * 加载所有项目数据
 ************************/
function loadProjects() {
   
   
    $('#project_select').empty();
    $.post(`${
     
     Config.BASE_URL}/api/project/list-all`, JSON.stringify({
   
   }), function (response) {
   
   
        if (response.succeeded) {
   
   
            _projects = response.data;
            for (let index = 0; index < _projects.length; index++) {
   
   
                const project = _projects[index];
                $('#project_select').append(`<option value=${
     
     index}>${
     
     project.name}</option>`)
            }
            if (_projects.length > 0) {
   
   
                const project = _projects[0];
                const rectangle = Cesium.Rectangle.fromDegrees(project.minLongitude, project.minLatitude, project.maxLongitude, project.maxLatitude);
                Cesium.Camera.DEFAULT_VIEW_RECTANGLE = rectangle;
                _viewer.camera.flyTo({
   
   
                    destination: rectangle
                });
                showBuildings(project);
            }
        }
    })
}

/**
 * 展示一个项目所有的建筑物
 * @param {*} project 
 */
function showBuildings(project) {
   
   
    const buildings = project.buildings;
    const midLat = (project.minLatitude + project.maxLatitude) / 2;
    buildings.forEach(building => {
   
   
        var entity = _viewer.entities.add({
   
   
            name: building.name,
            id: 'building_' + building.id,
            position: new Cesium.Cartesian3.fromDegrees(building.x, building.y, building.height),
            model: {
   
   
                uri: `${
     
     Config.BASE_URL}/api/building/download-model/${
     
     building.model}`,
            },
        });
    });
}

/**
 * 展示一个建筑物的楼层
 * @param {*} buildingId 
 */
function showFloors(buildingId) {
   
   
    $('#lg_floor').empty();
    $.get(`${
     
     Config.BASE_URL}/api/building/${
     
     buildingId}`, function (response) {
   
   
        if (response.succeeded) {
   
   
            _selectedBuilding = response.data;
            for (let index = 0; index < _selectedBuilding.floors.length; index++) {
   
   
                const floor = _selectedBuilding.floors[index];
                $('#lg_floor').append(`<a href="/service/javascript: void(0);" class="list-group-item list-group-item-action">${
     
     floor.name}</a>`);
            }
            $('#lg_floor').fadeIn();
            $('#lg_floor .list-group-item').on('click', function (e) {
   
   
                const index = $(e.target).index();
                showFloor(index);
            })
            if (_selectedBuilding.floors.length > 0) {
   
   
                showFloor(0)
            }
        }
    })
}

/**
 * 展示单个楼层
 * @param {*} index 
 */
function showFloor(index) {
   
   
    _viewer.entities.values.forEach(entity => {
   
   
        if (entity.id.startsWith('floor_')) {
   
   
            _viewer.entities.remove(entity);
        }
    });
    $('#lg_floor .list-group-item').removeClass('active');
    $(`#lg_floor .list-group-item:nth-child(${
     
     index + 1})`).addClass('active')
    const floor = _selectedBuilding.floors[index];
    var entity = _viewer.entities.add({
   
   
        name: floor.name,
        id: 'floor_' + floor.id,
        position: new Cesium.Cartesian3.fromDegrees(floor.x, floor.y, floor.height),
        model

低功耗蓝牙项目,需要一块懂省电的板

思澈 SF32LB52 芯片,BLE 协议栈深度优化,上手即开发

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值