App 功能一览
Android 端有 5 个 Tab,分别对应不同功能:
| Tab | 功能 | 效果 |
|---|---|---|
| 看板 | 实时传感器数据卡片 + 趋势图 + 均值 | 11 种传感器网格展示,无数据自动隐藏 |
| 发送 | LED 文本下发 | 多行独立配置,16 种动效,一键温湿度 |
| 报警 | 超限报警记录 | 分级显示(危险/预警),可清除 |
| 历史 | 历史数据表格 | 最近 120 条,支持 CSV 导出 |
| 设置 | 服务器连接 + 报警阈值 | IP 一键连接测试,配置自动保存 |
核心架构
MainActivity ├── Tab1: DashboardFragment (数据看板) ├── Tab2: SendFragment (LED发送) ├── Tab3: AlarmFragment (报警记录) ├── Tab4: HistoryFragment (历史数据) └── Tab5: SettingsFragment (系统设置) 共用组件: ├── ApiService (HTTP 单例) ├── SensorData (数据模型) └── TrendChart (自定义折线图)6 个 Java 文件,零第三方依赖,网络请求都用原生HttpURLConnection。
功能一:数据看板怎么实现的?
效果
- 11 张传感器卡片 2 列排列
- 彩色顶条区分类型(红=温度、蓝=湿度、紫=VOC)
- 数值 + 单位 + 更新时间
- 无数据的传感器自动隐藏卡片
- 温度/湿度独立折线图
- 10 点滑动均值统计
实现
数据获取— 每秒轮询一次:
private Runnable poller = new Runnable() { public void run() { if (running) { fetchData(); handler.postDelayed(this, 1000); // 1秒后再来 } } }; private void fetchData() { new Thread(() -> { String json = api.get("/api/data"); // HTTP GET SensorData d = SensorData.fromJson(new JSONObject(json)); getActivity().runOnUiThread(() -> updateUI(d)); // 切主线程更新UI }).start(); }卡片显隐— 用isNull()精确判断,不用> 0:
// SensorData.fromJson() String[] sensorKeys = {"temp","humi","pm10","pm25", "co2","noise","propane","benzene", "xylene","methane","hydrogen"}; for (String k : sensorKeys) d.hasData.put(k, !j.isNull(k)); // JSON null = 无数据 // DashboardFragment.updateUI() for (int i = 0; i < 11; i++) { if (Boolean.TRUE.equals(d.hasData.get(SENSOR_KEYS[i]))) { cards[i].setVisibility(View.VISIBLE); // 有数据显示 tvValues[i].setText(String.format("%.1f", vals[i])); tvTimes[i].setText(d.getTime(SENSOR_KEYS[i])); } else { cards[i].setVisibility(View.GONE); // 无数据隐藏 } }为什么不用
vals[i] > 0?因为温度 0℃ 是合法值,用> 0会误判为“无数据”。
自定义折线图 TrendChart— 纯 Canvas 绘制:
public class TrendChart extends View { private List<Float> data = new ArrayList<>(); private int maxPts = 30; @Override protected void onDraw(Canvas canvas) { // 1. 画网格线和 Y 轴刻度 // 2. 画折线 path // 3. 画半透明渐变填充 // 4. 画数据点圆点 // 5. 每 3 个点标注一次数值 } }均值统计— 10 点滑动窗口:
if (Boolean.TRUE.equals(d.hasData.get("temp"))) { tempHistory.add(d.temp); if (tempHistory.size() > 10) tempHistory.removeFirst(); tvTempAvg.setText(String.format("%.1f℃", avg(tempHistory))); }功能二:LED 文本发送怎么实现的?
效果
- 多行独立配置:文本、坐标、颜色、字号、动效、UID
- 全部发送:先切页 → 逐行下发
- 一键温湿度:自动拉取当前数据填到发送行
- 清除:发空格覆盖,不残留
实现
发送流程:
private void sendAll() { // 先切页 api.post("/api/qy-page", "{\"page\":" + page + "}"); Thread.sleep(300); // 等卡切换完毕 // 逐行下发(动画行排后面,避免被立即显示覆盖) lines.sort(立即显示优先); for (line : lines) { api.post("/api/send-text", line.toJSON()); Thread.sleep(200); // 间隔防止丢帧 } }请求 JSON 示例:
{ "text": "温度 21.9 ℃", "x": 4, "y": 0, "color": "white", "font_size": 24, "mode": 1, "uid": 41 }功能三:服务器连接怎么实现的?
效果
- 顶部 IP 输入框,格式
192.168.1.243:8086 - 点击连接 → 按钮变绿(成功)或变红(失败)
- 自动保存到 SharedPreferences
- 切回看板时自动读取并连接
实现
private void connect(Button btn) { api.setBaseUrl(etIp.getText().toString().trim()); btn.setText("..."); new Thread(() -> { try { api.get("/api/data"); // 测试连通性 runOnUiThread(() -> { btn.setText("已连接"); btn.setBackgroundColor(0xFF22C55E); // 绿色 }); } catch(Exception e) { runOnUiThread(() -> { btn.setText("连接失败"); btn.setBackgroundColor(0xFFE11D48); // 红色 }); } }).start(); }功能四:报警功能怎么实现的?
效果
- 超上限:红色标注 danger
- 低于下限:黄色标注 warn
- 时间、类型、值、等级列表展示
- 可一键清除
实现
alarms.forEach(a -> { String bg = a.level.equals("danger") ? "#fef2f2" : // 红色背景 a.level.equals("warn") ? "#fffbeb" : // 黄色背景 ""; // 渲染行... });功能五:历史数据怎么实现的?
效果
- 最近 120 条传感器数据表格
- 时间 + 每类传感器纵向列
- 导出为 CSV(带 BOM 的 UTF-8,Excel 直接打开不乱码)
实现
String csv = "时间," + 传感器列表 + "\n"; history.forEach(row -> { csv += row.time + ","; for (sensor : sensors) { csv += row.get(sensor) != null ? row.get(sensor) : "-"; csv += ","; } csv += "\n"; }); // \uFEFF = BOM,确保中文在 Excel 中正常显示 new Blob(['\uFEFF' + csv], {type: 'text/csv'});小结
Android 客户端的设计原则:
- 懒加载:切到哪个 Tab 才创建对应 Fragment
- 生命周期感知:onResume 时启动轮询,onPause 时停止轮询,避免后台耗电
- 原生网络:使用
HttpURLConnection,无需第三方库,减少包体积 - 数据驱动 UI:通过
SensorData模型统一管理数据,UI 根据数据状态自动更新 - 用户体验优先:无数据自动隐藏、连接状态视觉反馈、一键操作
通过以上设计,我们实现了一个轻量、高效、功能完整的 Android 客户端,能够稳定地与 MCU 端进行数据交互,并在 LED 大屏上实时展示环境监测信息。