-
近期文章
近期评论
- admin 发表在《PDF文件分割图片》
- admin 发表在《混乱的五月即将告以段路》
- admin 发表在《系规看来不是300页那么简单》
- admin 发表在《系规看来不是300页那么简单》
- admin 发表在《扩展计划UserScript》
归档
- 2026 年 8 月
- 2026 年 7 月
- 2026 年 6 月
- 2026 年 5 月
- 2026 年 4 月
- 2026 年 3 月
- 2026 年 2 月
- 2026 年 1 月
- 2025 年 12 月
- 2025 年 11 月
- 2025 年 10 月
- 2025 年 9 月
- 2025 年 8 月
- 2025 年 7 月
- 2025 年 6 月
- 2025 年 5 月
- 2025 年 4 月
- 2025 年 3 月
- 2025 年 2 月
- 2025 年 1 月
- 2024 年 12 月
- 2024 年 11 月
- 2024 年 10 月
- 2024 年 9 月
- 2024 年 8 月
- 2024 年 7 月
- 2024 年 6 月
- 2024 年 5 月
- 2024 年 4 月
- 2024 年 2 月
- 2024 年 1 月
- 2023 年 12 月
- 2023 年 11 月
- 2023 年 10 月
- 2022 年 10 月
- 2022 年 9 月
- 2022 年 8 月
- 2022 年 7 月
- 2022 年 6 月
- 2022 年 5 月
- 2022 年 4 月
- 2022 年 3 月
- 2022 年 1 月
- 2021 年 11 月
- 2021 年 10 月
- 2021 年 9 月
- 2021 年 4 月
- 2020 年 11 月
- 2020 年 10 月
- 2020 年 8 月
- 2020 年 5 月
- 2020 年 4 月
- 2020 年 1 月
- 2019 年 11 月
- 2019 年 7 月
- 2019 年 2 月
- 2019 年 1 月
- 2018 年 12 月
- 2018 年 11 月
- 2018 年 8 月
分类
友情链接
其他操作
SGP41
sgp41.py 驱动文件
import time
from machine import I2C
class SGP41:
I2C_ADDR = 0x59 # Fixed SGP41 I2C Address
def __init__(self, i2c: I2C):
self.i2c = i2c
# Verify connection
if self.I2C_ADDR not in self.i2c.scan():
raise RuntimeError("SGP41 sensor not found on I2C bus.")
self.reset()
def _crc8(self, data: bytes) -> int:
"""Calculate Sensirion 8-bit CRC checksum."""
crc = 0xFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x80:
crc = (crc << 1) ^ 0x31
else:
crc <<= 1
crc &= 0xFF
return crc
def _send_command(self, cmd: int, arguments: list = None) -> bytes:
"""Send command with optional data arguments and return raw response bytes."""
buf = bytearray([(cmd >> 8) & 0xFF, cmd & 0xFF])
if arguments:
for arg in arguments:
arg_bytes = bytearray([(arg >> 8) & 0xFF, arg & 0xFF])
buf.extend(arg_bytes)
buf.append(self._crc8(arg_bytes))
self.i2c.writeto(self.I2C_ADDR, buf)
# Give sensor precise processing window time based on command
if cmd == 0x2619: # Execute conditioning
time.sleep_ms(50)
return self.i2c.readfrom(self.I2C_ADDR, 3)
elif cmd == 0x2612: # Measure raw signals
time.sleep_ms(50)
return self.i2c.readfrom(self.I2C_ADDR, 6)
elif cmd == 0x0006: # Reset
time.sleep_ms(10)
return b''
def reset(self):
"""Soft reset the sensor."""
self._send_command(0x0006)
def execute_conditioning(self) -> int:
"""
Executes pixel conditioning. Must be run for 10 seconds on startup
to ensure proper NOx readings. Returns raw VOC ticks.
"""
# Default environment data: 50% RH and 25C mapped into sensor values
default_rh = 0x8000
default_t = 0x6666
reply = self._send_command(0x2619, [default_rh, default_t])
if self._crc8(reply[0:2]) != reply[2]:
raise RuntimeError("CRC Check Match Failure")
return (reply[0] << 8) | reply[1]
def measure_raw(self, humidity: float = None, temperature: float = None) -> tuple:
"""
Measures raw VOC and NOx ticks.
Accepts optional float context parameters to feed compensation loops.
"""
if humidity is not None and temperature is not None:
# Convert raw inputs to compensated ticks per Sensirion spec
rh_ticks = int(round(humidity * 65535 / 100)) & 0xFFFF
t_ticks = int(round((temperature + 45) * 65535 / 175)) & 0xFFFF
else:
rh_ticks = 0x8000 # 50%
t_ticks = 0x6666 # 25C
reply = self._send_command(0x2612, [rh_ticks, t_ticks])
# Parse data blocks & validate checksums
if (self._crc8(reply[0:2]) != reply[2]) or (self._crc8(reply[3:5]) != reply[5]):
raise RuntimeError("CRC Check Match Failure")
sraw_voc = (reply[0] << 8) | reply[1]
sraw_nox = (reply[3] << 8) | reply[4]
return sraw_voc, sraw_nox
使用代码 【必须要10秒启动否则会使用 期间只能VOC】
添加温湿度值可以提升精度
from machine import I2C, Pin
import time
from sgp41 import SGP41
i2c=I2C(-1, scl=Pin(14), sda=Pin(13))
sensor = SGP41(i2c)
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
#读取VOC NOX
voc_raw, nox_raw = sensor.measure_raw()
#请按照1HZ 的速度读取数据
#sensor.measure_raw(humidity=45.2, temperature=23.5)
#使用温湿度提升精度
Gas index 模拟算法
刚刚到只能读取raw值
sensirion_gas_index.py 算法
import math
class SensirionGasIndexAlgorithm:
def __init__(self, is_nox=False):
self.is_nox = is_nox
# 官方标准预设参数
if not is_nox:
self.algorithm_type = 1.0 # VOC
self.index_gain = 250.0
self.index_offset = 100.0 # VOC 基准分
self.gamma = 0.015 # VOC 学习率
else:
self.algorithm_type = 2.0 # NOx
self.index_gain = 1.0 # NOx 增益系数
self.index_offset = 1.0 # NOx 基准分
self.gamma = 0.003 # NOx 学习率
self.sraw_minimum = 0.0
self.sraw_maximum = 65535.0
# 算法内部状态机变量
self.sraw_mean = None
self.mean_variance = 0.0
self.sraw_std_dev = 0.0
def process(self, sraw: int) -> int:
"""输入 SGP41 的原始 Ticks,输出官方标准的 1-500 指数"""
# 1. 边界裁剪
sraw_f = float(sraw)
if sraw_f < self.sraw_minimum: sraw_f = self.sraw_minimum
if sraw_f > self.sraw_maximum: sraw_f = self.sraw_maximum
# 2. 官方核心:自适应均值与方差迭代 (学习空气环境基线)
if self.sraw_mean is None:
self.sraw_mean = sraw_f
self.mean_variance = 0.0
else:
delta = sraw_f - self.sraw_mean
# 滚动更新均值
self.sraw_mean += self.gamma * delta
# 滚动更新方差
self.mean_variance += self.gamma * ((delta * delta) - self.mean_variance)
# 计算标准差,防止分母为 0 加入 0.001
self.sraw_std_dev = math.sqrt(abs(self.mean_variance)) + 0.001
# 3. 计算相对偏差归一化值 (Z-score)
x = (sraw_f - self.sraw_mean) / self.sraw_std_dev
# 4. 官方核心:Sigmoid 非线性感知映射
try:
if self.algorithm_type == 1.0:
# VOC 公式:Ticks 变小代表浓度变高,Index 上升
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(x)))
else:
# NOx 公式:Ticks 变大代表浓度变高,Index 上升
# 官方 NOx 指数较为特殊,常态通常极低(靠近1),有污染时暴增
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(-x)))
except OverflowError:
# 防止 math.exp(x) 产生溢出崩溃
index_raw = 500.0 if x < 0 else 1.0
# 5. 边界剪裁限制在 1 - 500 之间
final_index = int(round(index_raw))
if final_index < 1: final_index = 1
if final_index > 500: final_index = 500
return final_index
使用
import time
from machine import Pin, I2C
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
# 初始化硬件
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
sensor = SGP41(i2c)
# 2. 实例化官方算法引擎
voc_engine = SensirionGasIndexAlgorithm(is_nox=False)
nox_engine = SensirionGasIndexAlgorithm(is_nox=True)
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
print("正在以 1Hz 频率读取标准官方空气指数...")
while True:
start_time = time.ticks_ms()
try:
# 读取原始数据
voc_raw, nox_raw = sensor.measure_raw()
# 使用官方翻译过来的算法处理
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"标准 VOC 指数: {voc_index} | 标准 NOx 指数: {nox_index}")
except Exception as e:
print("错误:", e)
# 严格保持 1Hz 频率
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))
FLASH 保存 sensirion_gas_index.py
import math
import json
import time
class SensirionGasIndexAlgorithm:
def __init__(self, is_nox=False, storage_filename=None):
self.is_nox = is_nox
self.storage_filename = storage_filename
if not is_nox:
self.algorithm_type = 1.0
self.index_gain = 250.0
self.index_offset = 100.0
self.gamma = 0.015
else:
self.algorithm_type = 2.0
self.index_gain = 1.0
self.index_offset = 1.0
self.gamma = 0.003
self.sraw_minimum = 0.0
self.sraw_maximum = 65535.0
self.sraw_mean = None
self.mean_variance = 0.0
self.last_saved_mean = 0.0
self.last_save_time = time.ticks_ms()
self.total_samples = 0
self.has_restored = False
self._load_from_flash()
def _load_from_flash(self):
"""开机时尝试从本地 Flash 读取基线"""
if not self.storage_filename:
return
try:
with open(self.storage_filename, "r") as f:
data = json.load(f)
self.sraw_mean = float(data["mean"])
self.mean_variance = float(data["variance"])
self.last_saved_mean = self.sraw_mean
self.total_samples = int(data.get("samples", 1000))
self.has_restored = True
print(f"[{self.storage_filename}] 成功从 Flash 恢复历史基线: Mean={self.sraw_mean:.1f}")
except Exception:
print(f"[{self.storage_filename}] 未找到历史基线或文件损坏,将重新开始自学习。")
def _save_to_flash(self, force=False):
"""控制写入次数:第一秒强行创建,之后每5分钟无条件记录一次"""
if not self.storage_filename or self.sraw_mean is None:
return
current_time = time.ticks_ms()
# 计算距离上一次保存过去了多少秒
elapsed_seconds = time.ticks_diff(current_time, self.last_save_time) / 1000.0
# 【修改后的逻辑】:
if not force:
# 1. 如果是出厂第一次运行且在开机第 1 秒(第 1 个样本),立刻无条件放行创建文件
if not self.has_restored and self.total_samples <= 1:
pass
else:
# 2. 之后的正常运行期,不管空气有没有变,只要距离上次保存没到 300 秒(5分钟),就拦截
if elapsed_seconds < 300.0:
return
# 满足 5 分钟时间到了,执行物理写入
try:
data = {
"mean": self.sraw_mean,
"variance": self.mean_variance,
"samples": self.total_samples
}
with open(self.storage_filename, "w") as f:
json.dump(data, f)
self.last_saved_mean = self.sraw_mean
self.last_save_time = current_time # 重置时间锚点
self.has_restored = True
print(f"--> [{self.storage_filename}] 已满5分钟,数据成功定时同步至 Flash (总样本数: {self.total_samples})")
except Exception as e:
print("Flash 写入失败:", e)
def process(self, sraw: int) -> int:
sraw_f = float(sraw)
if sraw_f < self.sraw_minimum: sraw_f = self.sraw_minimum
if sraw_f > self.sraw_maximum: sraw_f = self.sraw_maximum
self.total_samples += 1
if self.sraw_mean is None:
self.sraw_mean = sraw_f
self.mean_variance = 0.0
self._save_to_flash() # 触发第1秒瞬间创建文件
return int(self.index_offset)
else:
delta = sraw_f - self.sraw_mean
self.sraw_mean += self.gamma * delta
self.mean_variance += self.gamma * ((delta * delta) - self.mean_variance)
self._save_to_flash() # 每次循环都评估是否到了 5 分钟
sraw_std_dev = math.sqrt(abs(self.mean_variance))
if sraw_std_dev < 0.01:
sraw_std_dev = 0.01
x = (sraw_f - self.sraw_mean) / sraw_std_dev
if x > 70.0: x = 70.0
elif x < -70.0: x = -70.0
if self.algorithm_type == 1.0:
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(x)))
else:
index_raw = self.index_offset + (self.index_gain / (1.0 + math.exp(-x)))
final_index = int(round(index_raw))
if final_index < 1: final_index = 1
if final_index > 500: final_index = 500
return final_index
使用
import time
from machine import Pin, I2C
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
sensor = SGP41(i2c)
# 【核心改变】:传入不同的文件名,分别存储 VOC 和 NOx 的基线
voc_engine = SensirionGasIndexAlgorithm(is_nox=False, storage_filename="voc_base.json")
nox_engine = SensirionGasIndexAlgorithm(is_nox=True, storage_filename="nox_base.json")
print("\n开始 1Hz 空气质量监测...")
while True:
start_time = time.ticks_ms()
try:
voc_raw, nox_raw = sensor.measure_raw()
# 运行算法(内部会自动处理掉电读取与动态保存)
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"VOC 指数: {voc_index} | NOx 指数: {nox_index}")
except Exception as e:
print("循环报错:", e)
# 严格保持 1Hz pacing 步长
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))
最终成品
from machine import I2C, Pin
import time
from sgp41 import SGP41
from sensirion_gas_index import SensirionGasIndexAlgorithm
i2c=I2C(-1, scl=Pin(14), sda=Pin(13))
sensor = SGP41(i2c)
# 2. 实例化官方算法引擎
#voc_engine = SensirionGasIndexAlgorithm(is_nox=False)
#nox_engine = SensirionGasIndexAlgorithm(is_nox=True)
voc_engine = SensirionGasIndexAlgorithm(is_nox=False, storage_filename="voc_base.json")
nox_engine = SensirionGasIndexAlgorithm(is_nox=True, storage_filename="nox_base.json")
#初始化10秒 制度VOC 否则NOX无法使用
for i in range(10):
raw_voc = sensor.execute_conditioning()
print(f"Warm-up Second {i+1}/10 - Raw VOC: {raw_voc}")
time.sleep(1.0)
print("正在以 1Hz 频率读取标准官方空气指数...")
while True:
start_time = time.ticks_ms()
try:
# 读取原始数据
voc_raw, nox_raw = sensor.measure_raw()
# 使用官方翻译过来的算法处理
voc_index = voc_engine.process(voc_raw)
nox_index = nox_engine.process(nox_raw)
print(f"标准 VOC 指数: {voc_index} | 标准 NOx 指数: {nox_index}")
except Exception as e:
print("错误:", e)
# 严格保持 1Hz 频率
elapsed = time.ticks_diff(time.ticks_ms(), start_time)
time.sleep_ms(max(0, 1000 - elapsed))
创建JSON地图
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
*{box-sizing: border-box;}
body{margin:0;padding:10px;font-size:14px;}
.top-bar{
display:flex;
gap:8px;
margin-bottom:10px;
align-items:center;
flex-wrap:wrap;
}
.top-bar button,.top-bar input{
padding:6px 10px;
cursor:pointer;
}
.map-wrap{
position:relative;
width:1900px;
height:900px;
border:2px solid #666;
overflow:hidden;
}
#bgImg{
position:absolute;
left:0;top:0;
width:1900px;height:900px;
object-fit:cover;
display:block;
}
.heat-layer{
position:absolute;left:0;top:0;width:100%;height:100%;
z-index:2;pointer-events:none;display:none;
}
.heat-block{
position:absolute;
filter: blur(12px);
border-radius:6px;
}
.zone-wrap{
position:absolute;
border:2px solid #999;
background:rgba(150,150,150,0.05);
z-index:3;
}
.zone-title{
position:absolute;top:-22px;left:0;
font-weight:bold;white-space:nowrap;
}
.factory-hot{
position:absolute;
border:2px dashed #0066ff;
background:rgba(0,102,255,0.08);
cursor:pointer;
transition:all 0.2s;
}
.factory-hot:hover{background:rgba(0,102,255,0.2);}
.summary-tag{
position:absolute;
transform:translate(-50%,-50%);
padding:4px 10px;border-radius:8px;
color:#fff;font-weight:bold;white-space:nowrap;
box-shadow:0 2px 8px rgba(0,0,0,0.15);
cursor:pointer;
}
.sensor-hot{
position:absolute;
width:32px;height:32px;
transform:translate(-50%,-50%);
border-radius:50%;
background:rgba(0,0,0,0.1);
cursor:pointer;
transition:all 0.2s;
}
.sensor-hot:hover{transform:translate(-50%,-50%) scale(1.2);}
.sensor-dot{
width:12px;height:12px;border-radius:50%;
margin:10px;
}
.drag-rect{
position:absolute;
border:1px dashed #f00;
background:rgba(255,0,0,0.1);
pointer-events:none;
z-index:999;
}
.edit-modal{
position:fixed;
top:50%;left:50%;
transform:translate(-50%,-50%);
background:#fff;
border:1px solid #ccc;
padding:15px;
box-shadow:0 0 20px #0003;
z-index:9999;
display:none;
}
.edit-modal input{
width:280px;
margin:6px 0;
padding:5px;
display:block;
}
.detail-popup{
position:absolute;
background:#fff;border:1px solid #ddd;
padding:12px;box-shadow:0 3px 15px rgba(0,0,0,0.2);
z-index:99;display:none;min-width:220px;
}
.del-btn{
margin-top:8px;
color:#fff;
background:#f44336;
border:none;
padding:4px 8px;
cursor:pointer;
}
</style>
</head>
<body>
<div class="top-bar">
<input type="file" id="uploadImg" accept="image/*">
<button id="drawRectFactory">矩形厂房(拖拽)</button>
<button id="drawSensor">绘制传感器点位</button>
<button id="stopDraw">停止绘制</button>
<button id="toggleHeat">开关热力图层</button>
<button id="toggleSensor">展开/收起点位</button>
<button id="exportJson">导出配置JSON</button>
<button id="importJson">导入配置JSON</button>
<input type="file" id="jsonFile" accept=".json" style="display:none">
</div>
<div class="map-wrap" id="mapWrap">
<img id="bgImg">
<div class="heat-layer" id="heatLayer"></div>
<div class="detail-popup" id="detailPopup"></div>
<div class="drag-rect" id="dragRect"></div>
</div>
<div class="edit-modal" id="editModal">
<h4>编辑配置信息</h4>
<label>图层层级 z(数字越大越上层)</label>
<input type="number" id="editZ" value="5">
<label>名称</label>
<input type="text" id="editName">
<label>自定义参数doing</label>
<input type="text" id="editDoing">
<label>温度(仅传感器生效)</label>
<input type="number" id="editTemp">
<button id="saveEdit">保存</button>
<button id="cancelEdit">取消</button>
</div>
<script>
$(function(){
const $mapWrap = $("#mapWrap");
const $bgImg = $("#bgImg");
const $heatLayer = $("#heatLayer");
const $popup = $("#detailPopup");
const $dragRect = $("#dragRect");
const $editModal = $("#editModal");
// 全局配置:厂房、传感器完全分开存储
let parkConfig = {
zones:[]
};
// 绘制模式:rectFactory / sensor / null
let drawMode = null;
let isDrag = false;
let dragStart = {x:0,y:0};
let editTarget = null;
// 删除标记:记录当前要删除的分区、下标、类型
let delInfo = {zoneIdx: -1, type: "", idx: -1};
let heatShow = false;
let sensorShow = false;
// 1.上传底图(仅前端预览,不存入JSON)
$("#uploadImg").on("change",function(e){
const file = e.target.files[0];
if(!file)return;
const reader = new FileReader();
reader.onload = function(res){
$bgImg.attr("src",res.target.result);
renderAll();
}
reader.readAsDataURL(file);
})
// 切换绘制模式
$("#drawRectFactory").click(()=>drawMode="rectFactory");
$("#drawSensor").click(()=>drawMode="sensor");
$("#stopDraw").click(()=>drawMode=null);
// 矩形拖拽绘制厂房
$mapWrap.mousedown(function(e){
if(drawMode !== "rectFactory") return;
const rect = $mapWrap[0].getBoundingClientRect();
dragStart.x = e.clientX - rect.left;
dragStart.y = e.clientY - rect.top;
isDrag = true;
$dragRect.css({left:dragStart.x,top:dragStart.y,width:0,height:0});
})
$mapWrap.mousemove(function(e){
if(!isDrag || drawMode !== "rectFactory") return;
const rect = $mapWrap[0].getBoundingClientRect();
const curX = e.clientX - rect.left;
const curY = e.clientY - rect.top;
const w = Math.abs(curX - dragStart.x);
const h = Math.abs(curY - dragStart.y);
const l = Math.min(curX, dragStart.x);
const t = Math.min(curY, dragStart.y);
$dragRect.css({left:l,top:t,width:w,height:h});
})
$mapWrap.mouseup(function(e){
if(drawMode !== "rectFactory" || !isDrag){
$popup.hide();
return;
}
isDrag = false;
const x1 = parseInt($dragRect.css("left"));
const y1 = parseInt($dragRect.css("top"));
const x2 = x1 + parseInt($dragRect.css("width"));
const y2 = y1 + parseInt($dragRect.css("height"));
$dragRect.css({width:0,height:0});
if(x2-x1 < 20 || y2-y1 <20)return;
editTarget = {
type:"rectFactory",
data:{x1,y1,x2,y2,z:5,name:"矩形厂房",doing:""}
};
showEditModal();
})
// 点击画布绘制传感器点位
$mapWrap.click(function(e){
if(drawMode === "sensor"){
const rect = $mapWrap[0].getBoundingClientRect();
const x = parseInt(e.clientX - rect.left);
const y = parseInt(e.clientY - rect.top);
editTarget = {
type:"sensor",
data:{x,y,z:8,name:"传感器",doing:"",temp:25}
};
showEditModal();
return;
}
if(!drawMode)$popup.hide();
})
// 弹出编辑框
function showEditModal(){
$("#editZ").val(editTarget.data.z||5);
$("#editName").val(editTarget.data.name);
$("#editDoing").val(editTarget.data.doing);
$("#editTemp").val(editTarget.data.temp||25);
$editModal.show();
}
$("#saveEdit").click(function(){
const z = Number($("#editZ").val()) || 5;
const name = $("#editName").val();
const doing = $("#editDoing").val();
const temp = parseFloat($("#editTemp").val())||25;
editTarget.data.z = z;
editTarget.data.name = name;
editTarget.data.doing = doing;
if(editTarget.type==="sensor") editTarget.data.temp = temp;
// 初始化默认分区
if(parkConfig.zones.length ===0){
parkConfig.zones.push({
zoneName:"默认园区分区",
zoneX1:0,zoneY1:0,zoneX2:1920,zoneY2:1080,
z:3,
factories:[],
sensors:[] // 传感器独立数组,与厂房完全分离
})
}
const zone = parkConfig.zones[0];
if(editTarget.type==="rectFactory"){
zone.factories.push(editTarget.data);
}else{
zone.sensors.push(editTarget.data);
}
$editModal.hide();
renderAll();
})
$("#cancelEdit").click(()=>$editModal.hide());
// 删除元素
$popup.on("click",".del-btn",function(){
const zone = parkConfig.zones[delInfo.zoneIdx];
if(delInfo.type === "factory"){
zone.factories.splice(delInfo.idx,1);
}else if(delInfo.type === "sensor"){
zone.sensors.splice(delInfo.idx,1);
}
$popup.hide();
renderAll();
})
// 温度配色工具
function getTempColor(temp){
if(temp<20)return "#2196F3";
else if(temp>30)return "#f44336";
return "#4CAF50";
}
function clearRender(){
$(".zone-wrap,.factory-hot,.summary-tag,.sensor-hot,.heat-block").remove();
}
// 全局渲染
function renderAll(){
clearRender();
parkConfig.zones.forEach((zone,zoneIdx)=>{
const zw = zone.zoneX2 - zone.zoneX1;
const zh = zone.zoneY2 - zone.zoneY1;
// 分区层级z
let $zone = $(`<div class="zone-wrap"><div class="zone-title">${zone.zoneName}</div></div>`)
.css({
left:zone.zoneX1,
top:zone.zoneY1,
width:zw,
height:zh,
zIndex: zone.z || 3
});
$mapWrap.append($zone);
// ========== 渲染厂房区域 ==========
zone.factories.forEach((fac,facIdx)=>{
const tw = fac.x2 - fac.x1;
const th = fac.y2 - fac.y1;
const cx = fac.x1 + tw/2;
const cy = fac.y1 + th/2;
// 厂房层级z
let $fac = $(`<div class="factory-hot"></div>`)
.css({
left:fac.x1,
top:fac.y1,
width:tw,
height:th,
zIndex: fac.z || 5
});
let $tag = $(`<div class="summary-tag">${fac.name} ${fac.doing}</div>`)
.css({
left:cx,
top:cy,
background:"#0066ff",
zIndex: (fac.z || 5) + 1
});
// 点击弹窗带删除按钮
$fac.add($tag).click(function(e){
e.stopPropagation();
delInfo = {zoneIdx, type:"factory", idx:facIdx};
let html = `<h4>${fac.name} 备注:${fac.doing} | 层级z:${fac.z}</h4>`;
html += `<button class="del-btn">删除此厂房</button>`;
$popup.html(html).css({left:e.pageX,top:e.pageY}).show();
})
$mapWrap.append($fac).append($tag);
// 热力色块
let $heat = $(`<div class="heat-block"></div>`)
.css({
left:fac.x1,
top:fac.y1,
width:tw,
height:th,
background:"#0066ff",
opacity:0.28
})
$heatLayer.append($heat);
})
// ========== 渲染独立传感器(单独数组,和厂房分离) ==========
zone.sensors.forEach((sen,senIdx)=>{
let c = getTempColor(sen.temp);
let $sen = $(`<div class="sensor-hot"><div class="sensor-dot" style="background:${c}"></div></div>`)
.css({
left:sen.x,
top:sen.y,
display:sensorShow?"block":"none",
zIndex: sen.z || 8
})
.click(function(e){
e.stopPropagation();
delInfo = {zoneIdx, type:"sensor", idx:senIdx};
let html = `
<h4>${sen.name}</h4>
<p>层级z:${sen.z}</p>
<p>doing:${sen.doing}</p>
<p>温度:${sen.temp}℃</p>
<button class="del-btn">删除此传感器</button>
`;
$popup.html(html).css({left:e.pageX,top:e.pageY}).show();
})
$mapWrap.append($sen);
})
})
}
// 热力图层开关
$("#toggleHeat").click(function(){
heatShow = !heatShow;
$heatLayer.css("display",heatShow?"block":"none");
$(this).text(heatShow?"关闭热力图层":"开启热力图层");
})
// 传感器显示开关
$("#toggleSensor").click(function(){
sensorShow = !sensorShow;
$(".sensor-hot").css("display",sensorShow?"block":"none");
$(this).text(sensorShow?"收起点位":"展开点位");
})
// 导出JSON:不含图片,厂房、传感器分开放
$("#exportJson").click(function(){
const output = JSON.parse(JSON.stringify(parkConfig));
const jsonStr = JSON.stringify(output,null,2);
const blob = new Blob([jsonStr],{type:"application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "园区地图配置.json";
a.click();
URL.revokeObjectURL(url);
})
// 导入JSON
$("#importJson").click(()=>$("#jsonFile").click());
$("#jsonFile").on("change",function(e){
const file = e.target.files[0];
if(!file)return;
const reader = new FileReader();
reader.onload = function(res){
try{
parkConfig = JSON.parse(res.target.result);
renderAll();
}catch(err){
alert("JSON格式错误");
}
}
reader.readAsText(file);
})
renderAll();
})
</script>
</body>
</html>
简配GIS 网页显示
容器中使用图片地图全覆盖
JSON文件加载,各个点位相对图片容器位置创建可点击点位,可点击点位扩大10px创建点击事件(直接用css标签 然后来实现),调用模态框,可以添加AJAX事件实现交互。
循环上面步骤 大地图嵌套模态框显示小地图,或者显示点击交互事件,小地图也可以在上面创建交互实现地图的联合交互创建。
地图的相对点位如何创建?
下面列子非常好显示 了其中内容,创建一个图片容器div,1920×1080 大小,不可缩放。
图片100%铺满,通过each 以及定义好的css 创建点击位置,和方法
区域定位.css({left:item.x,top:item.y})
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<style>
.map-box{
position:relative;
width:1920px;
height:1080px;
border:1px solid #999;
}
.factory-map{
width:1920px;
height:1080px;
display:block;
}
body{
overflow:auto;
}
/* 热区:宽高32px,四周多出10px点击范围 用于创建点击事件 巧妙*/
.point-hot{
position:absolute;
width:32px;
height:32px;
transform:translate(-50%,-50%);
cursor:pointer;
background:transparent;
}
.point-dot{
width:12px;
height:12px;
background:#f33;
border-radius:50%;
margin:10px;
}
.point-text{
position:absolute;
margin-top:-18px;
font-size:12px;
white-space:nowrap;
}
</style>
</head>
<body>
<div class="map-box" id="mapBox">
<img class="factory-map" src="factory.jpg">
</div>
<script>
$(function(){
let pointList = [
{x:300,y:220,name:"一号车间"},
{x:650,y:480,name:"水泵房"}
];
const $mapBox = $("#mapBox");
$.each(pointList,function(i,item){
let $hot = $("<div class='point-hot'></div>")
.css({left:item.x,top:item.y})
.append("<div class='point-dot'></div><div class='point-text'>"+item.name+"</div>");
// 点击热区任意位置,弹出点位名称
$hot.click(function(e){
e.stopPropagation();
alert(item.name);
});
$mapBox.append($hot);
})
// 空白区域点击只打印坐标,不弹窗
$mapBox.click(function(e){
let r = $mapBox[0].getBoundingClientRect();
let x = e.clientX - r.left,y = e.clientY - r.top;
console.log("X:"+parseInt(x)+" Y:"+parseInt(y));
})
})
</script>
</body>
</html>
创建区域
通过CSS来确定显示的样式
#CSS
/* 全局热力覆盖图层,底层、鼠标穿透不干扰点击 */
.heat-layer{
position:absolute;
left:0;
top:0;
width:100%;
height:100%;
z-index:2; /* 固定在厂房下方,底图上方 */
pointer-events:none; /* 关键:色块不拦截鼠标点击厂房/传感器 */
display:none;
}
/* 单块厂房热力色块 */
.heat-block{
position:absolute;
filter: blur(12px); /* 色块之间柔和渐变融合 */
border-radius:6px;
}
创建区域 【显示区域使用添加DIV中加入上面的CSS样式,后用js读取json厂区的宽高后设置宽高 】区域定位参考上面,宽高定位width high
// 循环所有分区
parkConfig.zones.forEach((zone,zoneIdx)=>{
// 循环当前分区全部厂房
zone.factories.forEach((fac,facIdx)=>{
// 厂房矩形宽高、坐标
const tw = fac.x2 - fac.x1;
const th = fac.y2 - fac.y1;
// 根据业务温度获取厂房对应颜色
function getTempColor(temp){
if(temp<20)return "#2196F3";
else if(temp>30)return "#f44336";
return "#4CAF50";
}
// 这里简化统一厂房底色,你可替换为区域平均温度色
const areaColor = "#0066ff";
// 创建厂房热力覆盖色块,放入独立热力图层
let $heat = $(`<div class="heat-block"></div>`)
.css({
left: fac.x1,
top: fac.y1,
width: tw,
height: th,
background: areaColor,
opacity: 0.28 // 透明度,不遮挡底图
})
// 追加到热力覆盖图层
$heatLayer.append($heat);
})
})
点击事件及点击后触发
使用AJAX去后台读取内容,使用模态框来显示回传信息,可以在其内部添加一个请求的url【包含请求信息】通过GET请求回传JSON信息。
实时点位显示
同步动态点位信息,比如巡检ID显示,直接定期AJAX请求,然后显示点,可以在顶部显示z-index,数值高。依然适用ajax点击事件显示。(也可以直接展示信息)
跟正点击信息
A 直接URL 通过AJAX后请求后模态框展示信息
B 直接展示信息,直接模态框展示信息
doing中JSON显示其中 有url和 info两个参数分别去读取。
热力图
一、CSS 部分(分 3 块:画布基础、热力容器、圆形热力块)
1. 画布基础容器(固定 1900×900,承载底图 + 所有图层)
css
.map-wrap{
position:relative;
width:1900px;
height:900px;
border:2px solid #666;
overflow:auto;
}
#bgImg{
position:absolute;
left:0;top:0;
width:1900px;height:900px;
object-fit:cover;
display:block;
}
作用:
- 固定画布尺寸 1900 宽 900 高,所有坐标基于这个画布;
position:relative给内部热力、厂房、点位提供定位基准。
2. 热力总容器 .heat-layer(整层统一控制)
css
.heat-layer{
position:absolute;
left:0;
top:0;
width:100%;
height:100%;
z-index:2; /* 层级:底图之上、厂房之下 */
pointer-events:none; /* 核心:鼠标穿透,不挡住点击厂房/传感器 */
display:none; /* 默认隐藏,开关按钮控制显示 */
}
关键属性解释:
position:absolute:铺满整个画布;z-index:2图层顺序:底图 (0) < 热力层 (2) < 厂房 (4) < 传感器 (8);pointer-events:none:鼠标操作会直接穿过热力色块,底层交互完全不受影响;display:none:一键开关只改这个容器的 display,不用删所有色块。
3. 单个圆形热力块 .heat-block(核心实现圆形 + 渐变融合)
css
.heat-block{
position:absolute;
border-radius:50%; /* 强制正方形div变成正圆形 */
filter: blur(20px); /* 高斯模糊,让圆形边缘向外虚化,多个圆融合渐变 */
}
两条核心:
border-radius:50%:宽高相等的 div,圆角 50% = 标准圆形;filter: blur(20px):圆形边缘向外扩散虚化 20 像素,相邻圆形色块重叠后自动形成平滑过渡,这就是你看到整片渐变的来源;
- 如果去掉
blur(20px),就能清晰看到一个个独立圆形热源。
二、JS 分步拆解(只看热力相关逻辑,分 5 步)
前置说明
数据结构:parkConfig.zones[0].sensors 是独立传感器数组,每个传感器包含 x,y,temp,热源只来源于这些点位。
步骤 1:工具函数 —— 根据温度返回热力颜色
js
运行
// 输入温度,返回半透明RGBA颜色
function tempToHeatColor(temp){
if(temp <= 20) return "rgba(33,150,243,0.28)"; // 低温蓝色
if(temp <= 30) return "rgba(76,175,80,0.28)"; // 常温绿色
return "rgba(244,67,54,0.28)"; // 高温红色
}
末尾 0.28 = 色块透明度,数值越低底色越淡,能看清底下厂区底图。
步骤 2:热力渲染主函数 renderHeatMap()(循环生成所有圆形色块)
js
运行
function renderHeatMap(){
// 第一步:清空上一轮所有热力圆形,防止重复叠加
$(".heat-block").remove();
const $heatLayer = $("#heatLayer");
// 遍历所有园区分区(当前只有1个默认分区)
parkConfig.zones.forEach(zone=>{
// 遍历【独立传感器数组】,每一个传感器生成1个圆形热力块
zone.sensors.forEach(sen=>{
// 1.获取当前传感器对应的温度颜色
const fillColor = tempToHeatColor(sen.temp);
// 2.根据温度计算圆形扩散半径:温度越高,圆越大
const spreadRadius = Math.max(40, Math.min(130, sen.temp * 2.3));
// Math.max(40,xxx):最小半径40,不会过小看不见
// Math.min(130,xxx):最大半径130,不会铺满整张图
// 3.创建圆形DOM热力块,定位到传感器坐标中心
$("<div class='heat-block'></div>")
.css({
// 圆心在sen.x sen.y,向左/上偏移半径,保证圆形中心对准传感器点位
left: sen.x - spreadRadius,
top: sen.y - spreadRadius,
// 宽高 = 两倍半径,形成完整圆形
width: spreadRadius * 2,
height: spreadRadius * 2,
background: fillColor
})
// 把圆形色块追加到热力总容器内
.appendTo($heatLayer);
})
})
}
坐标逻辑重点
- 传感器坐标
sen.x sen.y是圆形圆心; - 圆形宽高
2*spreadRadius; left = 圆心x - 半径、top = 圆心y - 半径,才能让圆形中心精准落在传感器绿点上。
步骤 3:全局渲染入口,每次重绘自动刷新热力
js
运行
function renderAll(){
clearRender(); // 清空厂房、点位、旧热力
// 渲染分区、厂房、传感器红点(原有逻辑省略)
renderHeatMap(); // 最后执行热力圆形生成,刷新整张温度图层
}
触发时机:新建 / 删除传感器、导入 JSON、修改温度后,执行 renderAll(),热力自动重新绘制。
步骤 4:热力图层开关按钮逻辑(控制整层显示隐藏)
js
运行
let heatShow = false;
$("#toggleHeat").click(function(){
heatShow = !heatShow;
// 只修改热力总容器display,所有圆形色块一起显示/隐藏
$heatLayer.css("display", heatShow ? "block" : "none");
$(this).text(heatShow ? "关闭温度热力图层" : "开启温度热力图层");
})
不用逐个删除圆形 DOM,只控制外层容器,性能更高。
步骤 5:补充:关闭模糊,看清原生圆形(可选按钮逻辑)
新增变量控制模糊,切换后能清晰看到单个圆形:
js
运行
let heatBlurOn = true;
$("#toggleBlur").click(function(){
heatBlurOn = !heatBlurOn;
renderAll();
})
// 修改renderHeatMap内css赋值,动态控制blur
let cssStyle = {
left: sen.x - spreadRadius,
top: sen.y - spreadRadius,
width: spreadRadius * 2,
height: spreadRadius * 2,
background: fillColor
};
if(heatBlurOn){
cssStyle.filter = "blur(20px)";
}
$("<div class='heat-block'></div>").css(cssStyle).appendTo($heatLayer);
heatBlurOn=true:开启模糊,圆形边缘融合,看不出单独圆形;heatBlurOn=false:无模糊,每个热源都是清晰独立圆形。
三、完整运行逻辑流程(从头到尾执行顺序)
- 上传厂区图片,填充
#bgImg作为画布底图; - 用户点击「绘制传感器点位」,在画布点击生成传感器,存入
zone.sensors; - 点击「开关热力图层」,
heatShow=true,.heat-layer变为display:block; - 页面执行
renderAll()→ 调用renderHeatMap(); - JS 循环读取所有独立传感器
x/y/temp; - 根据温度计算圆形半径、半透明颜色;
- 批量创建带
border-radius:50%的圆形 div,放入热力容器; - CSS
blur(20px)虚化所有圆形边缘,色块互相叠加融合,形成整张平滑温度图层; - 热力层
pointer-events:none,底层厂房、传感器删除、弹窗功能完全不受遮挡。
四、你疑惑 “看不到圆形” 的根本原因
- 每个热源确实是标准圆形 div;
- 代码默认开启
filter: blur(20px),圆形边缘向外大面积虚化扩散; - 多个圆形虚化区域重叠、颜色混合,肉眼看不到单独圆形轮廓,只看到整片渐变;
- 关闭 blur 模糊后,立刻能看到一个个独立圆形热源。
温湿度传感器 气压 气体
BME680
from bme680 import *
from machine import I2C, Pin
import time
bme = BME680_I2C(I2C(-1, scl=Pin(13), sda=Pin(14)))
print(bme.temperature, bme.humidity, bme.pressure, bme.gas)
关闭电加热丝 可以减少2摄氏度误差,修改源码bme680.py
# gas measurements enabled
#self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
# gas measurements disabled
self._write(_BME680_REG_CTRL_GAS, [0x00])
bmp280 AHT20 联合传感器 (温湿度)
from machine import I2C, Pin
import time
import ahtx0
from bmp280 import *
i2c=I2C(-1, scl=Pin(14), sda=Pin(13))
sensor = ahtx0.AHT10(i2c)
bmp = BMP280(i2c,addr=0x77)
#气压 温度 湿度
bmp.pressure
sensor.temperature
sensor.relative_humidity
资料
https://gitee.com/mirrors_robert-hh/bme680-micropython
BME680 全包
后期
BMP280+AHT20
SGP-41 氮氧化合物 CO气体检测