什么是 DIS 数字化实验?
蓝牙 BLE
Web Bluetooth
兼容 phyphox
DIS(Digital Information System)数字化实验系统通过传感器实时采集物理数据,
经蓝牙传输到网页端,自动绘制图像并进行分析。无需安装任何 App,手机浏览器即可使用。
工作原理:ESP32 开发板连接传感器采集数据 → 通过蓝牙 BLE GATT 协议发送到浏览器 →
网页使用 Web Bluetooth API 接收数据 → Chart.js 实时绘制曲线。
兼容说明:本页面使用 phyphox 标准 BLE UUID,你现有的 ESP32 phyphox 代码无需任何修改,
网页和 phyphox App 可以同时接收数据。
当前浏览器不支持 Web Bluetooth API。请使用 Android 版 Chrome 或 Chrome/Edge 桌面版(需开启 chrome://flags/#enable-experimental-web-platform-features)。iOS Safari 暂不支持。
超声波测距
楞次定律
电容器充放电
光电门测速
牛顿第三定律
查理定律
未连接
等待数据(挡片通过光电门后自动记录,最多 5 次)
状态:等待连接两块力传感器板(设备名 ForceA、ForceB)
状态:等待连接 ESP32(设备名 Charles Law)
ESP32 MicroPython 代码(超声波测距 + OLED,兼容 phyphox)
你不需要修改 ESP32 代码!本页面使用与 phyphox 相同的 BLE UUID,网页和 phyphox App 可以同时连接。
下方是最新版本:增加 0.91 寸 OLED 本地显示、多重反射过滤算法,采样率提升到 50 Hz,蓝牙名为 Distance。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序 |
| 蓝牙设备名 | Distance |
| 采样率 | 约 50 Hz |
超声波 + OLED 接线
| 超声波 VCC | 3V3 |
| 超声波 GND | GND |
| 超声波 TRIG | GPIO 5 |
| 超声波 ECHO | GPIO 18 |
| OLED SCL | GPIO 22 |
| OLED SDA | GPIO 21 |
以下是 MicroPython + phyphoxBLE 库的超声波测距完整代码(直接刷入 ESP32 即可,phyphox App 和本网页可同时接收数据):
"""
超声波测距 + 0.91寸 OLED 显示 + phyphox BLE 传输
硬件: ESP32-WROOM / 超声波模块(内部 CS100A, 3.3V-5V 兼容) / SSD1306 0.91寸 OLED (I2C)
库: phyphoxBLE (需上传到板子的 /lib/phyphoxBLE/ 或根目录)
用法: 在 Thonny 中打开本文件, 点运行; 手机端打开 phyphox -> 右上角 "+" -> 选择蓝牙名 Distance
接线 (CS100A 模块, 3.3V 供电方案):
VCC -> 3V3
GND -> GND
TRIG-> GPIO5
ECHO-> GPIO18 (直连, 不用分压; 仅当 VCC 接 5V 时才需要 1k+2k 分压)
"""
from machine import Pin, I2C, time_pulse_us
import time
import ssd1306
from phyphoxBLE import PhyphoxBLE, Experiment
# ---------------- 引脚 ----------------
TRIG_PIN = 5 # 超声波 TRIG
ECHO_PIN = 18 # 超声波 ECHO (CS100A 用 3.3V 供电时直连即可, 无需分压)
SCL_PIN = 22 # OLED SCL
SDA_PIN = 21 # OLED SDA
trig = Pin(TRIG_PIN, Pin.OUT)
echo = Pin(ECHO_PIN, Pin.IN)
trig.value(0)
# ---------------- OLED (0.91寸 SSD1306) ----------------
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=400000)
oled = ssd1306.SSD1306_I2C(128, 32, i2c)
# ---------------- phyphox 实验定义 ----------------
p = PhyphoxBLE()
exp = Experiment()
exp.setTitle("超声波测距")
exp.setCategory("西大银翔DIS")
exp.setDescription("ESP32 + 超声波传感器测距,数据通过 BLE 实时传输到 phyphox")
# 视图1: 距离随时间变化的曲线 (X轴=时间CH0, Y轴=距离CH1)
view1 = Experiment.View()
view1.setLabel("距离-时间")
g = Experiment.Graph()
g.setLabel("距离")
g.setChannel(0, 1)
g.setLabelX("时间 t (s)")
g.setLabelY("距离 d (cm)")
g.setUnitX("s")
g.setUnitY("cm")
view1.addElement(g)
exp.addView(view1)
# 视图2: 实时数值
view2 = Experiment.View()
view2.setLabel("当前数值")
v = Experiment.Value()
v.setLabel("当前距离")
v.setUnit("cm")
v.setPrecision(1)
v.setChannel(1)
view2.addElement(v)
exp.addView(view2)
p.addExperiment(exp)
p.start(device_name="Distance")
print("BLE 已启动, 请在 phyphox 中连接 (蓝牙名: Distance)")
# ---------------- 超声波测距 ----------------
MAX_JUMP = 30 # 相邻采样允许的最大正常变化(cm)
CONFIRM_TOL = 8 # 大跳变后, 连续接近新值的容差(cm)
last_good = None # 已确认的基准距离
pending = None # 大跳变待确认值
pending_n = 0 # 待确认值连续出现的次数
def measure_distance():
# 先等上一条回波电平完全结束, 避免高速测量时读到上一条的残留
time_pulse_us(echo, 0, 1000)
trig.value(1)
time.sleep_us(10)
trig.value(0)
duration = time_pulse_us(echo, 1, 20000) # 20ms 超时 ≈ 3.4m 量程, 超范围不拖慢循环
if duration <= 0: # -1 超时 / -2 电平不对
return None
return duration / 2 / 29.1 # 声速 343m/s, 往返除2, 换算 cm
def plausible(dist):
"""区分'真实的大移动'和'多重反射杂波':
正常小变化直接采信; 大跳变需连续两帧都落在新值附近才采信,
单帧尖峰(杂波)会被丢弃。这样用手/小车挡住传感器也能及时更新。"""
global last_good, pending, pending_n
if last_good is None: # 第一帧直接采信
last_good = dist
return True
if abs(dist - last_good) <= MAX_JUMP: # 正常连续变化
last_good = dist
pending = None
pending_n = 0
return True
# 大跳变: 进入"待确认"状态
if pending is not None and abs(dist - pending) <= CONFIRM_TOL:
pending_n += 1
if pending_n >= 2: # 连续两次确认 = 真实移动
last_good = dist
pending = None
pending_n = 0
return True
return False # 才第一次, 再等一帧
pending = dist # 记为大跳变候选
pending_n = 1
return False
def send(value):
"""发送距离到 phyphox; 连接断开时捕获错误, 不让程序崩溃"""
try:
p.write(value)
except OSError:
print("BLE 连接已断开, 请在 phyphox 重新连接")
while True:
dist = measure_distance()
if dist is not None and plausible(dist):
send(dist) # 发送给 phyphox (写入 CH1)
oled.fill(0)
oled.text("Distance:", 0, 0)
oled.text("{:.1f} cm".format(dist), 0, 14)
oled.show()
print("Distance: {:.1f} cm".format(dist))
elif dist is not None:
# 杂波跳变: 丢弃, 用上一次有效值保持曲线连续
if last_good is not None:
send(last_good)
print("spike filtered: {:.1f} cm".format(dist))
else:
print("measurement timeout")
time.sleep_ms(20) # 20ms ≈ 50Hz, 反应更快
说明:本版本使用 CS100A 超声波模块(3.3V 供电,ECHO 直连 GPIO18,无需分压电阻)。
若用老款 HC-SR04 且 VCC 接 5V,则 ECHO 必须接 1kΩ+2kΩ 分压后再到 GPIO18,否则会烧坏 ESP32。
如果你使用的是自己写的 BLE 代码(非 phyphox 库),只要 Service UUID = cddf1001-30f7-4671-8b43-5e40ba53514a、
Characteristic UUID = cddf1002-30f7-4671-8b43-5e40ba53514a,数据按 float32 小端序发送,网页同样可以接收。
ESP32 MicroPython 代码(光电门测速,兼容 phyphox)
同样无需修改 phyphox 代码!网页和 phyphox App 都可以看到 5 个测速槽位。
下方代码基于 MicroPython + phyphoxBLE 库,每次挡片通过光电门自动测量并记录,第6次开始覆盖最早的记录。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序(多通道) |
| 通道数 | 1~5 个速度值 |
光电门模块接线
| VCC | 3.3V |
| OUT | GPIO 23 |
| GND | GND |
| 挡片宽度 | 需用尺子量准(默认 0.05 m) |
以下是 MicroPython + phyphoxBLE 库的光电门测速完整代码(直接刷入 ESP32 即可):
"""
光电门测速 + phyphox 蓝牙直连
官方 phyphoxBLE 库 API 构建实验
界面:
- 5 行带编号测速表格,按顺序填入:第1次测速=第一次测得的值(固定不动),
第2次测速=第二次,依次往下。
接线:模块 OUT -> GPIO 23,VCC -> 3.3V,GND -> GND
"""
from phyphoxBLE import PhyphoxBLE, Experiment
from machine import Pin
import time
# ---------- 测速参数 ----------
GATE_PIN = 23
BLOCKED = 1 # 挡光时 OUT 的电平(实测为 1)
FLAG_WIDTH_M = 0.05 # 挡片宽度,单位:米(用尺子量准再填)
MAX_HISTORY = 5 # 界面显示的测速次数
# ---------- 历史记录(全局,供清零回调清空) ----------
history = []
def on_config_write():
"""phyphox 连接时会写一次 config 到 ESP32 -> 清空历史,重新开始记录"""
global history
history = []
print(">> 已清零,重新开始记录")
# ---------- 1. 启动 BLE ----------
p = PhyphoxBLE()
# ---------- 2. 用库 API 构建实验界面 ----------
exp = Experiment()
exp.setTitle("光电门测速")
exp.setCategory("Physics")
exp.setDescription("ESP32 光电门测速实验:物体通过光电门时上报速度。")
view = Experiment.View()
view.setLabel("测速记录")
labels = ["第1次测速", "第2次测速", "第3次测速", "第4次测速", "第5次测速"]
for i in range(MAX_HISTORY):
vbox = Experiment.Value()
vbox.setLabel(labels[i])
vbox.setUnit("m/s")
vbox.setPrecision(2)
vbox.setChannel(i + 1) # 第 i+1 个数值框读 CH(i+1)
vbox.setXMLAttribute('size="2"') # 字体放大到 2 倍
view.addElement(vbox)
if i < MAX_HISTORY - 1: # 行间加分隔线
sep = Experiment.Separator()
sep.setHeight(0.5)
sep.setColor("606060")
view.addElement(sep)
exp.addView(view)
# ---------- 3. 广播 + 挂载实验 ----------
p.start("SpeedGate")
p.addExperiment(exp)
p._write_callback = on_config_write # 监听 phyphox 写入(连接/清零框)
print("BLE 广播中... 打开 phyphox 点 '+' 添加 SpeedGate")
# ---------- 4. 测速并发送 ----------
gate = Pin(GATE_PIN, Pin.IN, Pin.PULL_UP)
blocking = False
t_start = 0
print("就绪,请让物体带挡片通过光电门...")
while True:
level = gate.value()
if not blocking and level == BLOCKED:
blocking = True
t_start = time.ticks_us()
elif blocking and level != BLOCKED:
blocking = False
dt_us = time.ticks_diff(time.ticks_us(), t_start)
if dt_us > 0:
v = FLAG_WIDTH_M / (dt_us / 1_000_000.0)
history.append(v) # 按顺序记录,不覆盖前面的
history = history[-MAX_HISTORY:] # 只留最近 MAX_HISTORY 次
print("速度 %.2f m/s (累计 %d 条)" % (v, len(history)))
try:
p.write(*history) # CH1=第1次, CH2=第2次, ...
except Exception:
pass # 未连接/未订阅时忽略
使用提示:用尺子准确测量挡片实际宽度(米),修改代码中的 FLAG_WIDTH_M 常量。
挡光时 OUT 为高电平(1),若你的模块相反,把 BLOCKED = 0 即可。
ESP32 MicroPython 代码(电容器充放电,兼容 phyphox)
同样无需修改 phyphox 代码!网页和 phyphox App 可同时接收电容电压数据。
下方代码基于 MicroPython + phyphoxBLE 库,通过 ADC(GPIO34) 采集电容两端电压,以约 50 Hz 实时发送。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序(单个电压值) |
| 采样率 | 约 50 Hz |
电容电压采集接线
| ADC 输入 | GPIO 34(接电容正极节点 Uc) |
| 量程 | 0 ~ 3.3 V(12 位 ADC) |
| 充电按键 | 红键(接充电回路) |
| 放电按键 | 黑键(接放电回路) |
| 电容规格 | 2200 μF(可按实际修改) |
以下是 MicroPython + phyphoxBLE 库的电容器充放电完整代码(直接刷入 ESP32 即可):
"""
电容器充放电数字化实验教具 -- ESP32 (MicroPython / Thonny)
使用 phyphox 官方 MicroPython 库发送蓝牙数据
============================================================
依赖: ESP32 设备上需要两个库文件 (已加载)
- phyphoxBLE.py
- ble_advertising.py
(来自 https://github.com/phyphox/phyphox-micropython)
电路: GPIO34 接 Uc (电容正极节点), 2200uF 电容, 红键充电/黑键放电
用法:
1) 运行本程序, Shell 提示 BLE 已启动
2) 手机 phyphox 点 "+" 扫描, 连接设备后实验界面自动加载
3) 按住红键充电, 按住黑键放电
"""
from machine import Pin, ADC
from phyphoxBLE import PhyphoxBLE, Experiment
import time
PIN_ADC = 34 # 电容电压节点 Uc
adc = ADC(Pin(PIN_ADC))
adc.atten(ADC.ATTN_11DB) # 0 ~ 3.3V
adc.width(ADC.WIDTH_12BIT) # 12 位
VOLT_GAIN = 1.0 # 校准系数: 与万用表对比后如需修正, 微调此值
# ====== phyphox 实验界面 ======
exp = Experiment()
exp.setTitle("电容充放电")
exp.setCategory("西大银翔DIS")
exp.setDescription("2200uF 电容器充放电曲线。按住红键充电, 按住黑键放电, 松开保持电压。")
view = Experiment.View()
view.setLabel("充放电曲线")
graph = Experiment.Graph()
graph.setLabel("电压 U(t)")
graph.setLabelX("时间")
graph.setLabelY("电压")
graph.setUnitX("s")
graph.setUnitY("V")
graph.setChannel(0, 1) # 0=时间, 1=第一个数据(电压)
value = Experiment.Value()
value.setLabel("当前电压")
value.setUnit("V")
value.setChannel(1)
value.setPrecision(3)
view.addElement(graph)
view.addElement(value)
exp.addView(view)
p = PhyphoxBLE()
# 重要: 设备名必须纯 ASCII! 中文名会导致广播包长度错误, phyphox 一扫描就闪退。
p.start("DIS-Cap") # ≤26 字符, 纯 ASCII
p.addExperiment(exp) # 实验界面单独挂载, 连接后自动加载
print("BLE 已启动 (PhyphoxBLE), 用 phyphox 连接")
i = 0
while True:
try:
u = adc.read_uv() / 1_000_000.0 * VOLT_GAIN # 使用芯片出厂校准
except Exception:
u = adc.read() * 3.3 / 4095.0 * VOLT_GAIN # 老固件兜底换算
try:
p.write(u) # 发送给 phyphox
except OSError:
pass # 连接断开瞬间忽略, 避免主循环崩掉
if i % 25 == 0: # 约每秒打印 2 次
print(u)
i += 1
time.sleep_ms(20) # 采样/发送约 50 Hz
使用提示:先用万用表实测电容两端电压,微调代码中的 VOLT_GAIN 校准系数。
设备名必须为纯 ASCII(如 DIS-Cap),中文名会导致 BLE 广播包错误、phyphox 扫描闪退。
ESP32 MicroPython 代码(楞次定律,兼容 phyphox)
同样无需修改 ESP32 代码!本实验用 ADS1115 差分 ADC 测量线圈两端的感应电动势,
通过 BLE 同时发送到网页和 phyphox App。磁铁插入/拔出线圈时,可观察到正负峰值波形。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序(时间 t + 电压 u,共 8 字节) |
| 采样率 | 约 100 Hz |
| 设备名 | LenzLaw(纯 ASCII) |
ADS1115 接线
| VDD | 3V3 |
| GND | GND |
| SCL | GPIO 22 |
| SDA | GPIO 21 |
| ADDR | 接 GND(地址 0x48) |
| AIN0 / AIN1 | 线圈铜柱 A / B(差分输入) |
以下是 MicroPython + phyphoxBLE 库的楞次定律完整代码(直接刷入 ESP32,需同时上传 ads1x15.py 库):
# 楞次定律数字化实验教具
# ESP32 + ADS1115 + phyphox(BLE) + SSD1306 OLED
#
# 原理:磁铁运动 → 线圈磁通变化 → 产生感应电动势(楞次定律)。
# 本程序用 ADS1115 差分测量线圈两铜柱间的感应电压:
# - 有手机时:经 BLE 实时发送到 phyphox,画成"电压-时间"波形;
# - 有 OLED 时:独立线程在屏上滚动显示波形(英文状态文字)。
import time
import _thread
from machine import I2C, Pin
from phyphoxBLE import PhyphoxBLE, Experiment
from ads1x15 import ADS1115
# ============ 可调参数 ============
SDA_PIN = 21 # ADS1115 的 I2C 数据脚
SCL_PIN = 22 # ADS1115 的 I2C 时钟脚
PGA_VOLTS = 4.096 # ADS1115 满量程:4.096 或 6.144(信号削顶就改 6.144)
ADC_SPS = 860 # ADC 内部采样率:8/16/32/64/128/250/475/860
STREAM_HZ = 100 # 发送到 phyphox 的采样率(Hz)
ENABLE_OLED = True # 有 OLED 屏保持 True;没有就改 False
OLED_ADDR = 0x3C # OLED I2C 地址
OLED_SDA = 18 # OLED 的 SDA(建议用独立 I2C 总线)
OLED_SCL = 19 # OLED 的 SCL
OLED_REFRESH_MS = 80 # OLED 刷新间隔(ms)
# ==================================
_GAIN_INDEX = {6.144: 0, 4.096: 1, 2.048: 2, 1.024: 3, 0.512: 4, 0.256: 5}
_RATE_INDEX = {8: 0, 16: 1, 32: 2, 64: 3, 128: 4, 250: 5, 475: 6, 860: 7}
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=400000)
i2c_lock = _thread.allocate_lock()
ads = ADS1115(i2c, address=0x48, gain=_GAIN_INDEX[PGA_VOLTS])
def read_u():
"""读取 AIN0 - AIN1 差分电压(伏特)。"""
return ads.raw_to_v(ads.read(rate=_RATE_INDEX[ADC_SPS],
channel1=0, channel2=1))
# ---------- 构建 phyphox 实验界面 ----------
p = PhyphoxBLE()
p.debug = False
exp = Experiment()
exp.setTitle("楞次定律 - 感应电动势")
exp.setCategory("物理实验")
exp.setDescription("将磁铁快速插入/拔出线圈,观察感应电动势波形。")
view = Experiment.View()
view.setLabel("波形")
graph = Experiment.Graph()
graph.setLabel("感应电压")
graph.setLabelX("时间 t")
graph.setLabelY("电压 U")
graph.setUnitX("s")
graph.setUnitY("V")
graph.setXPrecision(3)
graph.setYPrecision(4)
graph.setColor("FF3B30")
graph.setChannel(1, 2) # x = CH1(时间), y = CH2(电压)
graph.setXMLAttribute('lineWidth="4"')
view.addElement(graph)
info = Experiment.InfoField()
info.setInfo("①点 ▶ 开始记录 ②快速插入/拔出磁铁 ③点 ■ 停止查看波形")
view.addElement(info)
voltage = Experiment.Value()
voltage.setLabel("当前电压")
voltage.setPrecision(4)
voltage.setUnit("V")
voltage.setChannel(2)
view.addElement(voltage)
exp.addView(view)
p.start("LenzLaw") # 设备名 ≤26字符
p.addExperiment(exp)
# ---------- 主循环:采样,连接时发送 ----------
interval_us = int(1_000_000 / STREAM_HZ)
last = time.ticks_us()
t0 = last
prev_connected = False
print("Lenz law experiment ready. 请在手机 phyphox 中连接设备 LenzLaw")
while True:
now = time.ticks_us()
if time.ticks_diff(now, last) >= interval_us:
last = now
with i2c_lock:
u = read_u()
connected = p.is_connected()
if connected:
if not prev_connected:
t0 = now
prev_connected = True
t = time.ticks_diff(now, t0) / 1_000_000
p.write(t, u)
else:
prev_connected = False
else:
time.sleep_ms(5)
使用提示:先将 ads1x15.py 和 phyphoxBLE/ 整个目录上传到 ESP32,再上传本文件。
磁铁插入线圈速度越快,峰值越大;插入/拔出波形方向相反;N 极 / S 极反向插入时波形也反号——这些就是楞次定律的直接体现。
ESP32 MicroPython 代码(双力传感器验证牛顿第三定律)
本实验需要两块 ESP32 + 两块 HX711 力传感器同时工作。
板 A(ForceA,红线,+F)和板 B(ForceB,黄线,−F)对称对拉,观察两条曲线镜像对称。
数据同步:两块板每次发送 采样时刻 + 力值(8 字节),
网页端按各自的采样时刻零时刻对齐,两条曲线时间轴精确同步,不受 BLE 传输延迟抖动影响。
注意:ESP32 的 BLE 一次只能被一个设备连接。
用网页连接前,请先在手机 phyphox / 系统蓝牙设置中断开 ForceA 和 ForceB,否则网页会提示"连接失败"。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序(单力值) |
| 蓝牙名 A | ForceA |
| 蓝牙名 B | ForceB |
| 采样率 | HX711 自身速率(红板约 10~80 Hz) |
| 网页连接数 | 同时连接 2 块 ESP32(双 BLE) |
双 HX711 接线(两块板引脚是反的!最易踩坑)
| 板 A VCC | 3V3(别接 5V,会烧 ESP32) |
| 板 A DOUT | GPIO 18 |
| 板 A SCK | GPIO 19 |
| 板 B VCC | 3V3(同样 3.3V) |
| 板 B DOUT | GPIO 19 |
| 板 B SCK | GPIO 18 |
| 应变梁接线 | 红→E+,黑→E−,绿/白→A±A−(方向不对可对调) |
① 板 A 代码(保存为板 A 的 main.py) —— 发送 +F,红线:
# 力传感器 A —— 发送 +F(网页/phyphox 红色线)
# 接线: HX711 VCC->3V3 GND->GND DOUT->GPIO18 CK->GPIO19
#
# 用法: 先运行 calibrate.py 得到 SCALE, 把 SCALE 填到下面, 然后作为板 A 的 main.py
from phyphoxBLE import PhyphoxBLE
from hx711 import HX711
import time
# ===== 配置 =====
DOUT = 18
SCK = 19
SCALE = -1.9275 # ← 用 calibrate.py 标定后填这里
SIGN = +1
NAME = "ForceA"
hx = HX711(DOUT, SCK)
p = PhyphoxBLE()
p.start(NAME)
print("去皮中,请勿碰传感器...")
time.sleep(2)
OFFSET = 0
for _ in range(10):
OFFSET += hx.read()
OFFSET /= 10
smooth = 0.0
ALPHA = 0.25
last = None
MAX_STEP = 3.0
while True:
raw = hx.read()
grams = (raw - OFFSET) / SCALE
n = grams / 1000.0 * 9.80665 * SIGN
if last is not None and abs(n - last) > MAX_STEP:
continue
last = n
smooth = n if smooth == 0 else smooth + ALPHA * (n - smooth)
try:
# 发送 (采样时刻秒, 力值) 两个 float32 —— 网页/phyphox 用它做精确时间轴,
# 两块板零时刻对齐后曲线严格同步(不再受 BLE 传输延迟抖动影响)
p.write(time.ticks_ms() / 1000.0, smooth)
except OSError:
pass
time.sleep_ms(1)
② 板 B 代码(保存为板 B 的 main.py) —— 发送 −F,黄色镜像线:
# 力传感器 B —— 发送 -F(网页/phyphox 黄色线,和 A 镜像)
# 接线: HX711 VCC->3V3 GND->GND DOUT->GPIO19 CK->GPIO18
# 注意:板 B 的 DOUT 和 SCK 跟板 A 反着用!
from phyphoxBLE import PhyphoxBLE
from hx711 import HX711
import time
# ===== 配置 =====
DOUT = 19
SCK = 18
SCALE = 662.325 # ← 用 calibrate.py 标定后填这里
SIGN = -1
NAME = "ForceB"
hx = HX711(DOUT, SCK)
p = PhyphoxBLE()
p.start(NAME)
print("去皮中,请勿碰传感器...")
time.sleep(2)
OFFSET = 0
for _ in range(10):
OFFSET += hx.read()
OFFSET /= 10
smooth = 0.0
ALPHA = 0.25
last = None
MAX_STEP = 3.0
while True:
raw = hx.read()
grams = (raw - OFFSET) / SCALE
n = grams / 1000.0 * 9.80665 * SIGN
if last is not None and abs(n - last) > MAX_STEP:
continue
last = n
smooth = n if smooth == 0 else smooth + ALPHA * (n - smooth)
try:
# 发送 (采样时刻秒, 力值) 两个 float32 —— 网页/phyphox 用它做精确时间轴,
# 两块板零时刻对齐后曲线严格同步(不再受 BLE 传输延迟抖动影响)
p.write(time.ticks_ms() / 1000.0, smooth)
except OSError:
pass
time.sleep_ms(1)
③ 标定脚本 calibrate.py(首次使用或更换传感器时跑一遍):
# 重新标定:把已知重量的物体挂上/放上,自动算出 SCALE
# 用前先改 DOUT/SCK 为当前板子的实际引脚!
from machine import Pin
from hx711 import HX711
import time
# ===== 配置 =====
DOUT = 19 # ← 改成当前板子的 DO 引脚
SCK = 18 # ← 改成当前板子的 CK 引脚
W = 200 # ← 已知重量(克)
hx = HX711(DOUT, SCK)
def read_raw(n=15):
vals = []
for _ in range(n):
vals.append(hx.read())
time.sleep_ms(3)
vals.sort()
return sum(vals[n // 4 : n - n // 4]) / max(1, (n - n // 4 - n // 4))
print("空载去皮中,请勿碰应变梁...")
time.sleep(2)
OFFSET = read_raw(30)
print("OFFSET =", OFFSET)
print("现在把 %d 克重物挂上, 等稳定后回车" % W)
input()
time.sleep(2)
SCALE = (read_raw(30) - OFFSET) / W
print("SCALE =", SCALE)
print("把 SCALE 填进 forceA.py 或 forceB.py")
使用步骤:① 两块板都先上传 hx711.py 和 phyphoxBLE/ 整个目录;
② 分别跑 calibrate.py 得到各自的 SCALE 并填进 forceA/forceB.py;
③ 上传 main.py 后通电,两块板应能在蓝牙列表中看到 ForceA / ForceB;
④ 切换到本页面"牛顿第三定律"选项卡,点击连接两块传感器,按提示依次选两块 ESP32;
⑤ 两根应变梁勾在一起对拉,红线(+F)和黄线(−F)应镜像对称 —— 这就是牛顿第三定律。
ESP32 MicroPython 代码(查理定律验证,兼容 phyphox)
同样无需修改 phyphox 代码!本实验使用 ESP32 + BMP280 气压温度传感器,
测量封闭容器内空气的压强 P 和热力学温度 T,体积不变时验证 P/T = 常数。
网页和 phyphox App 可同时接收数据;phyphox 里已配置好 P-T 散点图和实时曲线两个视图。
BLE 通信参数(与 phyphox 一致)
| Service UUID | cddf1001-30f7-... |
| Characteristic UUID | cddf1002-30f7-... |
| 数据格式 | float32 小端序(CH1=压强 Pa, CH2=温度 K) |
| 数据包长度 | 8 字节 |
| 蓝牙设备名 | Charles Law |
| 采样间隔 | 500 ms |
BMP280 接线
| VCC | 3.3V(模块带稳压可接 5V) |
| GND | GND |
| SCL | GPIO 22 |
| SDA | GPIO 21 |
| CSB | 3.3V(I2C 模式) |
| SDO | GND(地址 0x76)或 3.3V(地址 0x77) |
以下是 MicroPython + phyphoxBLE 库的查理定律完整代码(直接刷入 ESP32,需同时上传 bmp280.py 和 phyphoxBLE/ 库):
# -*- coding: utf-8 -*-
"""
查理定律验证实验
================
用 ESP32-WROOM + BMP280 测量封闭容器内空气的
热力学温度 T(单位 K) 和 压强 P(单位 Pa),
体积不变时 P/T = 常数,验证查理定律。
数据通过 BLE 实时发送到手机 phyphox App 显示成图像。
依赖库(需放在 ESP32 上):
- phyphoxBLE/ 文件夹(BLE 通信)
- bmp280.py 本机已下载的 micropython-bmp280 库
接线(BMP280 <-> ESP32):
VCC -> 3.3V
GND -> GND
SCL -> GPIO 22
SDA -> GPIO 21
CSB -> 3.3V(拉高 = I2C 模式)
SDC/SDO -> GND(I2C 地址 0x76)或 3.3V(地址 0x77)
"""
from machine import Pin, I2C
from phyphoxBLE import PhyphoxBLE, Experiment
from bmp280 import (BMP280, BMP280_CASE_WEATHER,
BMP280_TEMP_OS_2, BMP280_PRES_OS_16,
BMP280_IIR_FILTER_4)
import time
# ==================== 配置 ====================
I2C_SCL = 22
I2C_SDA = 21
SEND_INTERVAL_MS = 500
AVG_SAMPLES = 3
BLE_NAME = "Charles Law"
# ==================== BMP280 读取 ====================
def configure_sensor(bmp):
bmp.use_case(BMP280_CASE_WEATHER)
bmp.temp_os = BMP280_TEMP_OS_2
bmp.press_os = BMP280_PRES_OS_16
bmp.iir = BMP280_IIR_FILTER_4
def read_pair(bmp):
bmp.force_measure()
for _ in range(200):
if not bmp.is_measuring:
break
time.sleep_ms(1)
return bmp.temperature, bmp.pressure
# ==================== phyphox 实验界面 ====================
def build_experiment():
exp = Experiment()
exp.setTitle("查理定律验证")
exp.setCategory("热学")
exp.setDescription("封闭容器体积不变,测量空气的压强与热力学温度,验证 P/T = 常数")
view1 = Experiment.View()
view1.setLabel("P-T 图像")
graph = Experiment.Graph()
graph.setChannel(2, 1) # x = CH2 温度(K),y = CH1 压强(Pa)
graph.setLabel("P-T 散点")
graph.setLabelX("热力学温度 T")
graph.setUnitX("K")
graph.setLabelY("压强 P")
graph.setUnitY("Pa")
graph.setStyle("dots")
graph.setColor("ffff00")
graph.setXMLAttribute('size="12"')
view1.addElement(graph)
valP = Experiment.Value()
valP.setLabel("当前压强")
valP.setUnit("Pa")
valP.setChannel(1)
valP.setPrecision(1)
view1.addElement(valP)
valT = Experiment.Value()
valT.setLabel("当前温度")
valT.setUnit("K")
valT.setChannel(2)
valT.setPrecision(2)
view1.addElement(valT)
view2 = Experiment.View()
view2.setLabel("实时曲线")
gP = Experiment.Graph()
gP.setChannel(0, 1)
gP.setLabel("压强-时间")
gP.setLabelX("时间")
gP.setUnitX("s")
gP.setLabelY("压强")
gP.setUnitY("Pa")
gP.setStyle("lines")
gP.setColor("0000ff")
view2.addElement(gP)
gT = Experiment.Graph()
gT.setChannel(0, 2)
gT.setLabel("温度-时间")
gT.setLabelX("时间")
gT.setUnitX("s")
gT.setLabelY("温度")
gT.setUnitY("K")
gT.setStyle("lines")
gT.setColor("ff0000")
view2.addElement(gT)
exp.addView(view1)
exp.addView(view2)
return exp
# ==================== 主程序 ====================
def main():
i2c = I2C(0, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=100000)
addrs = i2c.scan()
print("I2C 扫描到设备:", [hex(a) for a in addrs])
found = None
chip_id = None
for addr in addrs:
for attempt in range(3):
try:
chip_id = i2c.readfrom_mem(addr, 0xD0, 1)[0]
print("地址 0x%02x 读到 chip ID = 0x%02x" % (addr, chip_id))
if chip_id in (0x58, 0x60):
found = addr
break
except OSError:
pass
time.sleep_ms(10)
if found:
break
if found is None:
raise RuntimeError("未找到 BMP280/BME280")
bmp = BMP280(i2c, found)
configure_sensor(bmp)
print("BMP280 已连接, I2C 地址 = 0x%02x" % found)
p = PhyphoxBLE()
p.addExperiment(build_experiment())
p.start(BLE_NAME)
print("BLE 已启动,请在 phyphox 中扫描并连接 \"%s\"" % BLE_NAME)
for _ in range(5):
read_pair(bmp)
time.sleep_ms(50)
while True:
t_sum = 0.0
p_sum = 0.0
for _ in range(AVG_SAMPLES):
temp_c, press_pa = read_pair(bmp)
t_sum += temp_c
p_sum += press_pa
time.sleep_ms(20)
temp_k = t_sum / AVG_SAMPLES + 273.15
press_pa = p_sum / AVG_SAMPLES
p.write(press_pa, temp_k) # CH1=压强, CH2=温度
time.sleep_ms(SEND_INTERVAL_MS)
if __name__ == "__main__":
main()
使用提示:准备一个带密封盖的塑料瓶或试管,把 BMP280 放进去并密封,
缓慢加热/冷却容器,观察 P-T 图像是否呈一条过原点的直线。可点"导出 CSV"用 Excel 做线性拟合求斜率和 R²。
使用步骤
1. 在上方选项卡选择实验类型(超声波测距、楞次定律、电容器充放电、光电门测速、牛顿第三定律 或 查理定律)
2. 将对应 ESP32 代码上传到开发板(现有 phyphox 代码无需修改)
3. 用 Android 手机 Chrome 浏览器打开本页面
4. 单实验点击「连接 ESP32」选择设备;牛顿第三定律点击「连接两块传感器」,按浏览器提示依次选 ForceA、ForceB
5. 连接成功后点击「开始采集」按钮,实时数据将自动显示
6. 点击「导出 CSV」可将数据保存为 Excel 可用格式