目录
1. 说明
本文介绍一套低成本实现的IOT定位追踪系统方案,实现基于:本地内网服务器-云服务器-IOT终端-手机终端 互联互通基础上的定位追踪应用。
2 完成后的效果
2.1 实时定位
定位实时数据
2.2 轨迹重现
定位轨迹数据
2.3 设备美照
3. 项目设计
- 准备一个云服务器,固定带宽2~5M的一般云主机,装frp服务端,负责系统通讯接驳。
- 本地私有云集群搭建,多核高内存高性能服务器集群,负责提供各种服务,其中之一装Frp客户端,映射各服务端口到云服务器。
- IOT定位器,在相关后台设置上报消息格式,MQTT服务器路径及订阅主题
- IOT设备消息路径:定位器-》云服务器-》私有云 MQTT Server-》Nifi-》Kafka-》应用消费服务
- 使用终端API路径:终端-》云服务器-》私有云 API Server
3.1 系统拓扑图
3.2 技术选型
3.3 消息订阅处理架构图
3.4 frp服务在线监控
4. 实施
4.1 数据模型 - DeviceLocation
参考: FastApi地理坐标数据存取实践_fastapi geoalchemy-CSDN博客
from typing import List, Optional
from datetime import datetime
from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, ForeignKeyConstraint, Index, Integer, String, Table, Text, text
from sqlalchemy.dialects.mysql import TINYINT
from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship
from sqlalchemy.orm.base import Mapped
from geoalchemy2 import Geometry, WKBElement
from sqlalchemy.orm import relationship, Mapped, mapped_column
from db.db_base import BaseModel
from .data_types import DeviceType
import uuid
import secrets
metadata = BaseModel.metadata
class DeviceLocation(BaseModel):
__tablename__ = 'ia_iot_device_location'
__table_args__ = (
Index('Index_1', 'iot_device_id'),
Index('Index_2', 'coordinates')
)
id = mapped_column(BigInteger, primary_key=True)
coordinates: Mapped[WKBElement] = mapped_column(Geometry(geometry_type='POINT', spatial_index=True), nullable=False, comment='地理坐标')
iot_device_id = mapped_column(BigInteger, server_default=text("'0'"))
label = mapped_column(String(255, 'utf8mb4_general_ci'))
4.2 数据报规格定义
订阅主题
数据报格式
银尔达后台配置
4.3 脚本:
高德地图GPS定位纠偏脚本
Javascript 版本: 查看绑定资源
Typescript 版本:
/* eslint-disable @typescript-eslint/no-loss-of-precision */
const x_PI: number = (3.14159265358979324 * 3000.0) / 180.0
const PI: number = 3.1415926535897932384626
const a: number = 6378245.0
const ee: number = 0.00669342162296594323
const bd09togcj02 = (bd_lon: number, bd_lat: number): number[] => {
bd_lon = +bd_lon
bd_lat = +bd_lat
const x: number = bd_lon - 0.0065
const y: number = bd_lat - 0.006
const z: number = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI)
const theta: number = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI)
const gg_lng: number = z * Math.cos(theta)
const gg_lat: number = z * Math.sin(theta)
return [gg_lng, gg_lat]
}
const gcj02tobd09 = (lng: number, lat: number): number[] => {
lat = +lat
lng = +lng
const z: number = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI)
const theta: number = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI)
const bd_lng: number = z * Math.cos(theta) + 0.0065
const bd_lat: number = z * Math.sin(theta) + 0.006
return [bd_lng, bd_lat]
}
const wgs84togcj02 = (lng: number, lat: number): number[] => {
lat = +lat
lng = +lng
if (out_of_china(lng, lat)) {
return [lng, lat]
} else {
const dlat: number = transformlat(lng - 105.0, lat - 35.0)
const dlng: number = transformlng(lng - 105.0, lat - 35.0)
const radlat: number = (lat / 180.0) * PI
let magic: number = Math.sin(radlat)
magic = 1 - ee * magic * magic
const sqrtmagic: number = Math.sqrt(magic)
const dlatAdjusted: number = (dlat * 180.0) / (((a * (1 - ee)) / (magic * sqrtmagic)) * PI)
const dlngAdjusted: number = (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI)
const mglat: number = lat + dlatAdjusted
const mglng: number = lng + dlngAdjusted
return [mglng, mglat]
}
}
const gcj02towgs84 = (lng: number, lat: number): number[] => {
lat = +lat
lng = +lng
if (out_of_china(lng, lat)) {
return [lng, lat]
} else {
const dlat: number = transformlat(lng - 105.0, lat - 35.0)
const dlng: number = transformlng(lng - 105.0, lat - 35.0)
const radlat: number = (lat / 180.0) * PI
let magic: number = Math.sin(radlat)
magic = 1 - ee * magic * magic
const sqrtmagic: number = Math.sqrt(magic)
const dlatAdjusted: number = (dlat * 180.0) / (((a * (1 - ee)) / (magic * sqrtmagic)) * PI)
const dlngAdjusted: number = (dlng * 180.0) / ((a / sqrtmagic) * Math.cos(radlat) * PI)
const mglat: number = lat + dlatAdjusted
const mglng: number = lng + dlngAdjusted
return [lng * 2 - mglng, lat * 2 - mglat]
}
}
const transformlat = (lng: number, lat: number): number => {
lat = +lat
lng = +lng
let ret: number =
-100.0 +
2.0 * lng +
3.0 * lat +
0.2 * lat * lat +
0.1 * lng * lat +
0.2 * Math.sqrt(Math.abs(lng))
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
ret += ((20.0 * Math.sin(lat * PI) + 40.0 * Math.sin((lat / 3.0) * PI)) * 2.0) / 3.0
ret += ((160.0 * Math.sin((lat / 12.0) * PI) + 320 * Math.sin((lat * PI) / 30.0)) * 2.0) / 3.0
return ret
}
const transformlng = (lng: number, lat: number): number => {
lat = +lat
lng = +lng
let ret: number =
300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng))
ret += ((20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0) / 3.0
ret += ((20.0 * Math.sin(lng * PI) + 40.0 * Math.sin((lng / 3.0) * PI)) * 2.0) / 3.0
ret += ((150.0 * Math.sin((lng / 12.0) * PI) + 300.0 * Math.sin((lng / 30.0) * PI)) * 2.0) / 3.0
return ret
}
const out_of_china = (lng: number, lat: number): boolean => {
lat = +lat
lng = +lng
return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55)
}
export default {
bd09togcj02,
gcj02tobd09,
wgs84togcj02,
gcj02towgs84
}
5. 要点:
- API输出坐标数据时,需在Pydantic 模型做数据转换
@field_validator("coordinates", mode="before")
def parse_coordinates(cls, value: WKBElement):
return dump_coords(to_shape(value))[0] if value else None
6. 参考:
- 工具
- Javascript 在线转 Typescript:
Javascript to Typescript converter with ChatGPT | Js2TS.com