Compare commits
21 Commits
feature/cl
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 41bba5e492 | |||
| 6a0bc9c4c8 | |||
| 172eb6b046 | |||
| a7ae5f6f88 | |||
| c14c8b63e8 | |||
| 5c179856af | |||
| 05a3d2d309 | |||
| a4bc66a745 | |||
| 399bd4b1db | |||
| 75db60cff5 | |||
| 5b5c2dfa4e | |||
| 9a6ceed18a | |||
| 05cfac1088 | |||
| 4dc62b07f2 | |||
| 1360930783 | |||
| d4fa4ed1b9 | |||
| 054baa304f | |||
| 824fdede86 | |||
| b06ad685b0 | |||
| b1141d1ee7 | |||
| 05eff3bf6a |
BIN
.playwright-mcp/story-assets/feb4000-battery-percentage.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 70 KiB |
BIN
.playwright-mcp/story-assets/feb4000-voltage-current.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
102
CLAUDE.md
@@ -8,8 +8,10 @@ Battery Monitor is a Next.js 16 web application designed to run on Raspberry Pi
|
||||
|
||||
### Key Features
|
||||
- Real-time battery monitoring with interactive charts
|
||||
- **Two Monitoring Modes**: Live mode (real-time charts) and Background mode (battery-saving)
|
||||
- Historical data storage and session management (SQLite)
|
||||
- CSV export functionality
|
||||
- Incomplete session detection and repair
|
||||
- **4G Power Management**: Automatic CPU frequency scaling to prevent system hangs when 4G module is active on battery
|
||||
|
||||
## Development Commands
|
||||
@@ -96,7 +98,7 @@ import { cn } from '@/lib/utils';
|
||||
|
||||
### Monitoring vs Recording
|
||||
|
||||
The application separates **monitoring** (display only) from **recording** (database writes):
|
||||
The application separates **monitoring** (display only) from **recording** (database writes), and offers two display modes for power optimization:
|
||||
|
||||
**Monitoring Mode (No Database Writes):**
|
||||
1. User clicks "Start Monitoring" button
|
||||
@@ -115,12 +117,51 @@ The application separates **monitoring** (display only) from **recording** (data
|
||||
- "Record from monitoring start": Saves buffered data since monitoring began
|
||||
2. User clicks "Start Recording"
|
||||
3. Creates new session in database with selected start time
|
||||
4. If "from monitoring start", saves buffered data with session ID
|
||||
4. If "from monitoring start", sends buffered data via `/api/battery/save` endpoint
|
||||
5. Each subsequent poll hits `/api/battery?save=true&sessionId=X`
|
||||
6. Readings are saved to database with session association
|
||||
6. Readings are saved to database immediately with session association
|
||||
7. User can stop recording (monitoring continues) or stop both
|
||||
8. Session `end_time` and `reading_count` updated on stop
|
||||
|
||||
**Power Failure Resilience:**
|
||||
- All readings are saved to SQLite database immediately (synchronous commits)
|
||||
- If power dies during recording, all data up to that point IS preserved
|
||||
- Session metadata may be incomplete: `end_time` equals `start_time`, `reading_count` may be 0
|
||||
- UI automatically detects incomplete sessions (yellow warning badge)
|
||||
- "Repair All" button updates incomplete sessions to correct end_time and reading_count
|
||||
- Repair endpoint: `/api/battery/sessions/repair` (POST with `repairAll: true` or specific `sessionId`)
|
||||
|
||||
**Display Modes (Battery Optimization):**
|
||||
|
||||
*Live Mode (Default):*
|
||||
- Full real-time chart rendering with animations
|
||||
- All visualizations active (percentage, power, voltage, current charts)
|
||||
- Higher power consumption due to continuous SVG rendering
|
||||
- Best for active monitoring and data analysis
|
||||
|
||||
*Background Mode (Battery Saving):*
|
||||
- Charts hidden completely (no rendering overhead)
|
||||
- Only current stats displayed in the status card
|
||||
- Same 2-second polling interval (consistent data quality)
|
||||
- **Full data granularity maintained** - historicalData array still updated
|
||||
- Database recording continues normally
|
||||
- Saves ~20-40% power through multiple optimizations:
|
||||
- No chart SVG rendering (biggest win)
|
||||
- Disabled chart animations globally
|
||||
- Removed CartesianGrid (reduces SVG elements)
|
||||
- Lazy-loaded sessions list (Intersection Observer)
|
||||
- Best for long recording sessions where visualization isn't needed
|
||||
- Safe for power failure testing - no data loss
|
||||
|
||||
Toggle between modes with the "Live Mode" / "Background Mode" button when monitoring is active.
|
||||
|
||||
**Power Optimizations Applied:**
|
||||
1. Charts hidden in background mode (eliminates 4 Recharts re-renders every 2s)
|
||||
2. Chart animations disabled (`isAnimationActive={false}` on all Line/Area components)
|
||||
3. CartesianGrid removed from all charts (reduces SVG complexity by ~30%)
|
||||
4. Sessions list lazy-loaded only when scrolled into view (Intersection Observer)
|
||||
5. Data granularity preserved - no polling changes, historicalData always updated
|
||||
|
||||
### Historical Data Mode
|
||||
1. User selects start/end date/time in "Export Custom Time Range" card
|
||||
2. Frontend queries `/api/battery/history?start=<ISO>&end=<ISO>`
|
||||
@@ -187,36 +228,51 @@ Indexes on `timestamp`, `created_at`, and `session_id` for efficient queries.
|
||||
- Frontend displays error state in a red error card
|
||||
- Missing or null values from sysfs are handled gracefully in `getBatteryData()`
|
||||
|
||||
## 4G Power Management System
|
||||
## uConsole Smart Power Regulator
|
||||
|
||||
Located in `scripts/` directory. **See [scripts/README-4G-POWER-MANAGER.md](scripts/README-4G-POWER-MANAGER.md) for full documentation.**
|
||||
Located in `scripts/` directory. **See [STORY.md](STORY.md) and [TOOL-GUIDE.md](TOOL-GUIDE.md) for full documentation.**
|
||||
|
||||
### Problem
|
||||
The uConsole CM5 + 4G module can peak at 22-25W power draw, but the AXP228 PMIC can only sustainably deliver ~18-20W from battery. This causes system hangs that require physical battery removal to restart.
|
||||
The 4G modem (SimTech SIM7600G-H) requires minimum 3.45V to operate reliably. When battery voltage drops below this threshold, the modem can hang the entire system, requiring physical battery removal to restart. The issue is voltage-related, not power budget.
|
||||
|
||||
### Solution Components
|
||||
### Solution: Unified Event-Driven Power Regulation
|
||||
|
||||
1. **Main Script**: `scripts/4g-power-manager.sh`
|
||||
- Detects 4G modem state (active/inactive) using multiple methods
|
||||
- Adjusts CPU governor and max frequency based on modem state
|
||||
- When 4G active: `powersave` governor + 1.8GHz max (saves ~20-30% power)
|
||||
- When 4G inactive: `ondemand` governor + 2.4GHz max (full performance)
|
||||
- Logs to `/var/log/4g-power-manager.log`
|
||||
**Key Components:**
|
||||
|
||||
2. **udev Rules**: `scripts/99-4g-power-manager.rules`
|
||||
- Triggers on USB modem device changes (vendor:product = 1e0e:9001)
|
||||
1. **uconsole-power-regulator.sh** - Main orchestrator
|
||||
- Determines power state based on AC and 4G status
|
||||
- Applies appropriate CPU settings for each state
|
||||
- Controls voltage monitoring
|
||||
- Logs to `/var/log/uconsole-power-regulator.log`
|
||||
|
||||
2. **uconsole-power-daemon.sh** - Background daemon
|
||||
- Monitors AC power and 4G modem state every 5 seconds
|
||||
- Triggers regulator only when state changes
|
||||
- Handles edge cases (e.g., service starts with AC already connected)
|
||||
|
||||
3. **Voltage Monitoring System**:
|
||||
- `voltage-monitor.sh` - Checks voltage every 5 seconds when Battery + 4G active
|
||||
- `voltage-alert-notify.sh` - Multi-method alerts (desktop notification, audio, log, LED)
|
||||
- `voltage-monitor-control.sh` - Start/stop controller
|
||||
- Alerts when voltage < 3.45V, rate-limited to every 30 seconds
|
||||
|
||||
4. **udev Rules**: `scripts/99-uconsole-power-regulator.rules`
|
||||
- Triggers on AC power connect/disconnect (power_supply subsystem)
|
||||
- Triggers on 4G modem USB device changes (vendor:product = 1e0e:9001)
|
||||
- Monitors wwan0 interface state changes
|
||||
- Responds to ttyUSB and cdc-wdm device events
|
||||
|
||||
3. **Systemd Service**: `scripts/4g-power-monitor.service` + `scripts/4g-power-monitor-daemon.sh`
|
||||
- Background daemon that checks modem state every 5 seconds
|
||||
- More reliable than udev alone for detecting state changes during network activity
|
||||
- Auto-restarts on failure
|
||||
5. **Power Modes**:
|
||||
| State | AC | 4G | Governor | Max Freq | Voltage Monitoring |
|
||||
|-------|----|----|----------|----------|-------------------|
|
||||
| AC_POWER | Connected | Any | ondemand | 2.4GHz | Off |
|
||||
| BATTERY_4G | Disconnected | Active | powersave | 1.8GHz | On |
|
||||
| BATTERY_ONLY | Disconnected | Inactive | ondemand | 2.0GHz | Off |
|
||||
|
||||
4. **Installation**: `scripts/install-4g-power-manager.sh`
|
||||
- One-command setup: `sudo ./install-4g-power-manager.sh`
|
||||
- Installs udev rules, systemd service, creates log files
|
||||
- Provides status and usage instructions
|
||||
6. **Installation**: `scripts/install-uconsole-power-regulator.sh`
|
||||
- Fresh install: `sudo ./install-uconsole-power-regulator.sh install`
|
||||
- Upgrade from old system: `sudo ./install-uconsole-power-regulator.sh upgrade`
|
||||
- Uninstall: `sudo ./install-uconsole-power-regulator.sh uninstall`
|
||||
|
||||
### Hardware Context
|
||||
|
||||
|
||||
53
README.md
@@ -12,24 +12,42 @@ A Next.js 16 web application designed for Raspberry Pi uConsole CM5 that monitor
|
||||
- **Session management** with custom naming and deletion
|
||||
- Reads directly from Linux sysfs (`/sys/class/power_supply/axp20x-battery/`)
|
||||
|
||||
### 4G Power Management 🔋⚡
|
||||
**NEW**: Automatic power management to prevent system hangs when using 4G module on battery.
|
||||
### uConsole Smart Power Regulator 🔋⚡
|
||||
**NEW**: Intelligent power management system that prevents voltage-induced system hangs when using 4G module on battery.
|
||||
|
||||
The uConsole CM5 + 4G module can consume 22-25W peak power, but the AXP228 PMIC can only deliver ~18-20W from battery. This causes system hangs that require battery removal to restart.
|
||||
**The Problem**: The 4G modem requires minimum 3.45V to operate reliably. When battery voltage drops below this threshold, the modem can hang the entire system, requiring physical battery removal to restart.
|
||||
|
||||
**Solution**: Automatically detects 4G modem activity and reduces CPU frequency to keep power consumption within safe limits.
|
||||
**The Solution**: Unified event-driven power regulation that:
|
||||
- **Monitors both AC power and 4G modem states** via udev events
|
||||
- **Adjusts CPU frequency dynamically** based on power source and 4G status
|
||||
- **Alerts when battery voltage is critical** (< 3.45V) with desktop notifications, audio alerts, and LED blinks
|
||||
- **Handles edge cases** like service starting with AC already connected
|
||||
|
||||
📖 **[Full Documentation: 4G Power Manager](scripts/README-4G-POWER-MANAGER.md)**
|
||||
**Power Modes:**
|
||||
| Condition | CPU Frequency | Governor | Voltage Monitoring |
|
||||
|-----------|--------------|----------|-------------------|
|
||||
| AC Connected | 2.4GHz | ondemand | Off |
|
||||
| Battery + 4G | 1.8GHz | powersave | On (< 3.45V alerts) |
|
||||
| Battery Only | 2.0GHz | ondemand | Off |
|
||||
|
||||
📖 **[Read the Full Story](STORY.md)** | **[Tool Guide](TOOL-GUIDE.md)** | **[中文版](README_CN.md)**
|
||||
|
||||
**Quick install:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-4g-power-manager.sh
|
||||
sudo ./install-uconsole-power-regulator.sh install
|
||||
```
|
||||
|
||||
**Upgrade from old 4G Power Manager:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh upgrade
|
||||
```
|
||||
|
||||
**Uninstall:**
|
||||
```bash
|
||||
sudo ./install-4g-power-manager.sh uninstall
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh uninstall
|
||||
```
|
||||
|
||||
## Hardware Requirements
|
||||
@@ -162,10 +180,16 @@ battery-monitor/
|
||||
│ └── lib/
|
||||
│ ├── db.ts # SQLite database utilities
|
||||
│ └── utils.ts # Helper functions
|
||||
├── scripts/ # 4G power management scripts
|
||||
│ ├── 4g-power-manager.sh
|
||||
│ ├── install-4g-power-manager.sh
|
||||
│ └── README-4G-POWER-MANAGER.md
|
||||
├── scripts/ # uConsole Smart Power Regulator
|
||||
│ ├── uconsole-power-regulator.sh # Main power regulator
|
||||
│ ├── uconsole-power-daemon.sh # Background monitoring daemon
|
||||
│ ├── voltage-monitor.sh # Voltage monitoring script
|
||||
│ ├── voltage-alert-notify.sh # Multi-method alerting
|
||||
│ ├── voltage-monitor-control.sh # Monitor control
|
||||
│ ├── install-uconsole-power-regulator.sh
|
||||
│ └── 99-uconsole-power-regulator.rules
|
||||
├── STORY.md # Development story and voltage discovery
|
||||
├── TOOL-GUIDE.md # Complete user guide
|
||||
└── public/ # Static assets
|
||||
```
|
||||
|
||||
@@ -190,9 +214,10 @@ The application requires AXP20x hardware. On systems without this hardware, the
|
||||
|
||||
### System Hangs with 4G Module
|
||||
If your uConsole hangs when using the 4G module on battery:
|
||||
1. Install the 4G Power Manager (see [scripts/README-4G-POWER-MANAGER.md](scripts/README-4G-POWER-MANAGER.md))
|
||||
2. Consider upgrading to high-drain 18650 batteries (Samsung 25R, Sony VTC6, LG HG2)
|
||||
3. Monitor power consumption to ensure it stays below 18-20W
|
||||
1. Install the uConsole Smart Power Regulator (see installation section above)
|
||||
2. Use the Battery Monitor to check voltage levels during 4G usage
|
||||
3. Consider upgrading to batteries with better voltage retention (see [TOOL-GUIDE.md](TOOL-GUIDE.md) for battery recommendations)
|
||||
4. The regulator will alert you if voltage drops below 3.45V
|
||||
|
||||
### Database Issues
|
||||
If you encounter database errors, you can safely delete `battery-data.db` and restart the application. It will create a new database automatically.
|
||||
|
||||
241
README_CN.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# uConsole CM5 电池监控工具
|
||||
|
||||
为 Raspberry Pi uConsole CM5 设计的 Next.js 16 Web 应用程序,可实时监控和可视化电池指标。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 电池监控
|
||||
- **实时监控** AXP20x 电池指标(电压、电流、功率、电量百分比)
|
||||
- **交互式图表** 显示电池趋势(Recharts 可视化)
|
||||
- **历史数据存储** SQLite 数据库,支持会话管理
|
||||
- **CSV 导出** 用于数据分析
|
||||
- **会话管理** 支持自定义命名和删除
|
||||
- 直接从 Linux sysfs 读取数据 (`/sys/class/power_supply/axp20x-battery/`)
|
||||
|
||||
### uConsole 智能电源调节器 🔋⚡
|
||||
**新功能**:智能电源管理系统,防止使用 4G 模块时因电压过低导致的系统死机。
|
||||
|
||||
**问题**:4G 调制解调器需要最低 3.45V 才能可靠运行。当电池电压降至此阈值以下时,调制解调器可能会导致整个系统死机,需要物理拔出电池才能重启。
|
||||
|
||||
**解决方案**:统一的事件驱动电源调节系统:
|
||||
- **监控 AC 电源和 4G 调制解调器状态** 通过 udev 事件
|
||||
- **动态调整 CPU 频率** 基于电源和 4G 状态
|
||||
- **电池电压临界告警** (< 3.45V) 通过桌面通知、音频提示和 LED 闪烁
|
||||
- **处理边缘情况** 如服务启动时 AC 已连接
|
||||
|
||||
**电源模式:**
|
||||
| 条件 | CPU 频率 | 调节器 | 电压监控 |
|
||||
|------|---------|--------|---------|
|
||||
| AC 已连接 | 2.4GHz | ondemand | 关闭 |
|
||||
| 电池 + 4G | 1.8GHz | powersave | 开启 (< 3.45V 告警) |
|
||||
| 仅电池 | 2.0GHz | ondemand | 关闭 |
|
||||
|
||||
📖 **[阅读完整故事](STORY.md)** | **[工具指南](TOOL-GUIDE_CN.md)** | **[English](README.md)**
|
||||
|
||||
**快速安装:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh install
|
||||
```
|
||||
|
||||
**从旧版 4G 电源管理器升级:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh upgrade
|
||||
```
|
||||
|
||||
**卸载:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh uninstall
|
||||
```
|
||||
|
||||
## 硬件要求
|
||||
|
||||
- **Raspberry Pi CM5**(或 CM4)在 uConsole 中
|
||||
- **AXP228 电源管理芯片**(uConsole 标配)
|
||||
- **18650 电池** (2 节并联配置)
|
||||
- 可选:**4G 扩展模块**
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**:Next.js 16(App Router)
|
||||
- **语言**:TypeScript
|
||||
- **UI**:React 19、shadcn/ui 组件、Tailwind CSS 4
|
||||
- **图表**:Recharts
|
||||
- **数据库**:SQLite(better-sqlite3)
|
||||
- **硬件接口**:通过 Node.js fs 访问 Linux sysfs
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 运行开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
打开 [http://localhost:3000](http://localhost:3000) 查看电池监控界面。
|
||||
|
||||
### 生产构建
|
||||
|
||||
```bash
|
||||
# 生产构建
|
||||
npm run build
|
||||
|
||||
# 运行生产服务器
|
||||
npm start
|
||||
```
|
||||
|
||||
### 数据库
|
||||
|
||||
应用程序会在项目根目录自动创建 `battery-data.db` SQLite 数据库。它存储:
|
||||
- 带时间戳的电池读数
|
||||
- 监控会话及元数据
|
||||
- 用于分析的历史数据
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 监控和记录
|
||||
|
||||
电池监控工具将**监控**(实时显示)和**记录**(保存到数据库)分离:
|
||||
|
||||
**实时监控:**
|
||||
1. 点击**"开始监控"**开始实时数据显示
|
||||
2. 电池数据每 2 秒更新一次
|
||||
3. 图表显示实时数据(内存中最后 100 个读数)
|
||||
4. 仅监控时数据**不会**保存到数据库
|
||||
|
||||
**记录会话:**
|
||||
1. 监控激活时,选择记录起始点:
|
||||
- **"从现在开始记录"** - 从此刻开始保存
|
||||
- **"从监控开始记录"** - 保存自监控开始以来的所有缓冲数据
|
||||
2. 点击**"开始记录"**将数据保存到数据库
|
||||
3. 点击**"停止记录"**结束会话(监控继续)
|
||||
4. 点击**"停止监控"**停止所有操作
|
||||
|
||||
**为什么分离?** 这让您可以在决定记录之前观察电池行为,避免不必要的数据库写入。
|
||||
|
||||
### 导出数据
|
||||
|
||||
**按会话导出:**
|
||||
- "记录会话"卡片中的每个会话都有下载按钮
|
||||
- 导出该特定会话的所有读数为 CSV
|
||||
|
||||
**自定义时间范围导出:**
|
||||
1. 在"导出自定义时间范围"卡片中选择开始和结束日期/时间
|
||||
2. 点击**"加载数据"**在图表中预览数据
|
||||
3. 点击**"导出 CSV"**直接下载而不先加载
|
||||
4. 文件名自动包含日期范围
|
||||
|
||||
### 查看历史数据
|
||||
|
||||
1. 在"记录会话"中点击任何会话名称查看其数据
|
||||
2. 或使用"导出自定义时间范围"→"加载数据"查看任意范围
|
||||
3. 图表显示历史趋势
|
||||
4. 点击**"返回实时视图"**返回实时监控
|
||||
|
||||
### 会话管理
|
||||
|
||||
1. 在**"记录会话"**卡片中查看所有会话
|
||||
2. 点击会话名称查看其数据
|
||||
3. 点击编辑图标重命名会话
|
||||
4. 点击下载图标导出会话数据
|
||||
5. 点击删除图标删除会话(需确认)
|
||||
|
||||
## API 端点
|
||||
|
||||
- `GET /api/battery` - 当前电池数据
|
||||
- 添加 `?save=true` 将读数保存到数据库
|
||||
- `GET /api/battery/history?start=<ISO>&end=<ISO>` - 按时间范围的历史数据
|
||||
- `GET /api/battery/sessions` - 列出所有监控会话
|
||||
- `GET /api/battery/sessions/:id` - 获取特定会话的读数
|
||||
- `PATCH /api/battery/sessions/:id` - 更新会话名称
|
||||
- `DELETE /api/battery/sessions/:id` - 删除会话及读数
|
||||
|
||||
## 开发命令
|
||||
|
||||
```bash
|
||||
npm run dev # 开发服务器(热重载)
|
||||
npm run build # 生产构建
|
||||
npm start # 生产服务器
|
||||
npm run lint # 运行 ESLint
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
battery-monitor/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── api/battery/ # API 路由
|
||||
│ │ ├── layout.tsx # 根布局
|
||||
│ │ └── page.tsx # 主页
|
||||
│ ├── components/
|
||||
│ │ ├── BatteryMonitor.tsx # 主监控组件
|
||||
│ │ └── ui/ # shadcn/ui 组件
|
||||
│ └── lib/
|
||||
│ ├── db.ts # SQLite 数据库工具
|
||||
│ └── utils.ts # 辅助函数
|
||||
├── scripts/ # uConsole 智能电源调节器
|
||||
│ ├── uconsole-power-regulator.sh # 主电源调节器
|
||||
│ ├── uconsole-power-daemon.sh # 后台监控守护进程
|
||||
│ ├── voltage-monitor.sh # 电压监控脚本
|
||||
│ ├── voltage-alert-notify.sh # 多方式告警
|
||||
│ ├── voltage-monitor-control.sh # 监控控制
|
||||
│ ├── install-uconsole-power-regulator.sh
|
||||
│ └── 99-uconsole-power-regulator.rules
|
||||
├── STORY.md # 开发故事和电压发现
|
||||
├── TOOL-GUIDE_CN.md # 完整用户指南(中文)
|
||||
└── public/ # 静态资源
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
### 路径别名
|
||||
项目使用 `@/*` 引用 `src/*`:
|
||||
```typescript
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
- 启用严格模式
|
||||
- 路径别名 `@/*` 映射到 `./src/*`
|
||||
- 模块解析:`bundler`
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 电池数据不可用
|
||||
应用程序需要 AXP20x 硬件。在没有此硬件的系统上,API 将返回 500 错误。这在非 uConsole 系统上是预期行为。
|
||||
|
||||
### 4G 模块导致系统死机
|
||||
如果您的 uConsole 在使用电池供电的 4G 模块时死机:
|
||||
1. 安装 uConsole 智能电源调节器(见上面的安装部分)
|
||||
2. 使用电池监控工具检查 4G 使用期间的电压水平
|
||||
3. 考虑升级到电压保持性更好的电池(参见 [TOOL-GUIDE_CN.md](TOOL-GUIDE_CN.md) 了解电池推荐)
|
||||
4. 调节器将在电压降至 3.45V 以下时提醒您
|
||||
|
||||
### 数据库问题
|
||||
如果遇到数据库错误,可以安全地删除 `battery-data.db` 并重启应用程序。它会自动创建新数据库。
|
||||
|
||||
## 贡献
|
||||
|
||||
本项目专为 uConsole CM5 硬件设计。欢迎贡献,特别是:
|
||||
- 性能优化
|
||||
- 额外的电池指标
|
||||
- UI/UX 改进
|
||||
- 电源管理增强
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
## 致谢
|
||||
|
||||
- 为 [ClockworkPi uConsole](https://www.clockworkpi.com/uconsole) 构建
|
||||
- UI 组件来自 [shadcn/ui](https://ui.shadcn.com/)
|
||||
- 图表由 [Recharts](https://recharts.org/) 提供
|
||||
462
STORY.md
@@ -1,4 +1,4 @@
|
||||
# Vibe Coding Chronicles: Solving uConsole's Power Crisis with a Pocket-Sized Dev Environment
|
||||
# Solving uConsole's 4G Module Mystery: A Vibe Coding Story
|
||||
|
||||
> **Part of the "Vibe Coding for Work/Life" series** — Real stories of building custom solutions anywhere, using portable devices and modern tools to solve everyday problems.
|
||||
|
||||
@@ -6,169 +6,105 @@
|
||||
|
||||
Imagine debugging a device's power management issues while lounging in a coffee shop, building the diagnostic tools directly on that same device. No desktop workstation. No "I'll fix this when I get home." Just you, a pocket-sized computer, and the freedom to solve problems wherever inspiration strikes.
|
||||
|
||||
This is how I spent one Saturday afternoon solving the uConsole CM5's notorious power hang issue—and discovered something profound about modern development workflows.
|
||||
This is how I spent one weekend solving the uConsole CM5's notorious 4G module hang issue—and discovered it wasn't about power delivery at all. It was about **voltage drop**.
|
||||
|
||||
## The Problem: When Innovation Meets Physics
|
||||
## The Problem: When 4G Kills Your System
|
||||
|
||||
The [ClockworkPi uConsole](https://www.clockworkpi.com/uconsole) is a hacker's dream: a Raspberry Pi CM5 squeezed into a clamshell form factor with a mechanical keyboard and modular expansion bays. It's the kind of device that makes you want to tinker, customize, and push boundaries.
|
||||
|
||||
But there was a frustrating catch. Enable the 4G expansion module on battery power, and the system would randomly freeze—dead in its tracks. The only fix? Physical battery removal and restart.
|
||||
|
||||
Forum threads filled with theories: buggy software, faulty batteries, hardware defects. The root cause remained a mystery. Users were stuck choosing between mobility (battery power) and connectivity (4G), but never both reliably.
|
||||
Forum threads filled with theories:
|
||||
- "It's buggy software"
|
||||
- "Your batteries are fake"
|
||||
- "The power management IC can't deliver enough watts"
|
||||
- "You need to buy better batteries"
|
||||
|
||||
I decided to investigate—but here's the twist: I'd build all the diagnostic tools **on the uConsole itself**, while running on battery power. If I could solve the problem using the very device experiencing it, that would prove the viability of "vibe coding" as a real-world workflow.
|
||||
None of these explanations felt complete. Users were stuck choosing between mobility (battery power) and connectivity (4G), but never both reliably.
|
||||
|
||||
## The Investigation: Following the Electrons
|
||||
I decided to investigate—but here's the twist: **I'd build all the diagnostic tools on the uConsole itself**, while running on battery power. If I could solve the problem using the very device experiencing it, that would prove something profound about modern development workflows.
|
||||
|
||||
Sitting at a coffee shop with the uConsole on battery power, I started digging. First stop: Linux's sysfs interface, which exposes raw hardware data. The uConsole uses an **AXP228 PMIC** (Power Management IC) to juggle power between batteries, USB, and the system.
|
||||

|
||||
|
||||
A few hours of research—datasheets, forum deep-dives, and measurements—revealed the culprit:
|
||||
*Building the solution on the device experiencing the problem—true vibe coding in action*
|
||||
|
||||
1. **Battery Configuration**: The two 18650 batteries are wired in **parallel** (not series), meaning they share the load simultaneously without switching[^1].
|
||||
## The Investigation: Following the Voltage
|
||||
|
||||
2. **Power Limits**:
|
||||
- The AXP228 can sustainably deliver ~18-20W from the battery discharge path
|
||||
- The CM5 + 4G module can peak at **22-25W** during transmission
|
||||
- This mismatch triggers the PMIC's over-current protection (OCP)
|
||||
Sitting at a coffee shop, uConsole on battery, I started building. First, a simple web app to monitor battery metrics in real-time. Next.js hot reload meant I could see changes instantly. No compile times, no deployment steps—just pure, iterative problem-solving.
|
||||
|
||||
3. **The IPS™ Circuit**: The AXP228's Intelligent Power Select transparently routes power from USB, AC, or battery. On USB power, it can supplement battery power for combined delivery of ~25W[^2].
|
||||
The monitoring revealed something unexpected. When the 4G module was active:
|
||||
- Power consumption looked normal (~8-10W)
|
||||
- Current draw spiked during transmission (up to 2A)
|
||||
- But voltage... **voltage was dropping dangerously low**
|
||||
|
||||
**Key Citations:**
|
||||
- [AXP228 Product Page - X-Powers](http://www.x-powers.com/en.php/Info/product_detail/article_id/31)
|
||||
- [uConsole Forum: Battery Configuration Discussion](https://forum.clockworkpi.com/t/understanding-battery-module-pinout-upgrading-battery-module/10260)
|
||||
- [4G Module Shutdown Issues](https://forum.clockworkpi.com/t/battery-life-not-as-expected-4g-module-shuts-the-whole-thing-down/12633)
|
||||
### The Breakthrough: It's Not Watts, It's Volts
|
||||
|
||||
## The Insight: It's Not the Batteries, It's the Math
|
||||
A few hours of datasheets later, the picture became clear:
|
||||
|
||||
The issue wasn't battery quality or switching—it was simple power budget arithmetic:
|
||||
**The SimTech SIM7600G-H 4G module has strict voltage requirements:**
|
||||
- Operating range: 3.4V to 4.2V
|
||||
- Minimum safe voltage: **~3.45V**
|
||||
- Current draw: Up to 2A during transmission bursts
|
||||
|
||||
```
|
||||
CM5 peak power: 15W
|
||||
4G module peak: 10W
|
||||
Display + other: 3W
|
||||
─────────────────────────
|
||||
Total peak demand: 28W
|
||||
**What happens when voltage drops below 3.45V:**
|
||||
1. 4G module browns out
|
||||
2. USB bus hangs
|
||||
3. System becomes unresponsive
|
||||
4. Only battery removal resets it
|
||||
|
||||
AXP228 battery max: 20W ⚠️ PROBLEM
|
||||
AXP228 USB+battery: 25W ✓ Works
|
||||
```
|
||||
**The real problem:** Lithium-ion batteries have internal resistance. When the 4G module draws 2A, voltage sags momentarily. If the battery is already partially discharged or has high internal resistance, voltage drops below 3.45V → instant hang.
|
||||
|
||||
**Solution**: Reduce CPU power consumption when 4G is active to keep total load within the 18-20W battery budget.
|
||||
This explained everything:
|
||||
- ✅ Why USB power helps (reduces battery current draw)
|
||||
- ✅ Why some batteries work better (lower internal resistance)
|
||||
- ✅ Why it happens randomly (depends on battery state and load timing)
|
||||
|
||||
## Building the Solution: Vibe Coding on uConsole
|
||||
## The Insight: Not All Battery Capacity Is Usable
|
||||
|
||||
This is where "vibe coding" shines. Instead of context-switching to a desktop workstation, I built the entire solution **on the uConsole itself**, iterating rapidly with AI assistance.
|
||||
The breakthrough came from building a new visualization: **Battery Energy Output vs. Voltage**—a discharge curve that plots remaining capacity against voltage.
|
||||
|
||||
### Phase 1: Real-Time Monitoring (Next.js 16 + React 19)
|
||||
I ran a complete test with FEB-4000 4000mAh batteries (100% to ~2%), capturing 4,318 data points over 2 hours 48 minutes:
|
||||
|
||||
I built a battery monitoring web app to visualize the power problem:
|
||||

|
||||
|
||||
**Tech Stack:**
|
||||
- Next.js 16 with App Router
|
||||
- React 19 with TypeScript
|
||||
- Recharts for real-time visualization
|
||||
- SQLite for historical data
|
||||
- shadcn/ui for components
|
||||
*The red line shows the 4G module's minimum voltage. Everything to the right is unusable for 4G connectivity.*
|
||||
|
||||
**Key Features:**
|
||||
- Reads directly from Linux sysfs (`/sys/class/power_supply/axp20x-battery/`)
|
||||
- Displays voltage, current, power, percentage in real-time
|
||||
- Separates monitoring (display only) from recording (database writes)
|
||||
- Buffers up to 1000 readings for retroactive session recording
|
||||
**The shocking result:**
|
||||
- Total battery capacity: 24.79 Wh
|
||||
- **Usable capacity above 3.45V: 13.39 Wh (54%)**
|
||||
- Unusable below 3.45V: 11.40 Wh (46%)
|
||||
|
||||
**Why This Approach?**
|
||||
By separating monitoring from recording, I could observe battery behavior without filling the database. When I spotted something interesting, I could click "Start Recording [from monitoring start]" to retroactively save the buffered data.
|
||||
**This means: You can only use about half your battery capacity for reliable 4G operation.** Once voltage drops to 3.45V, you still have 40-50% charge remaining—but it's unusable for 4G.
|
||||
|
||||

|
||||
*Complete battery monitoring interface showing real-time metrics, interactive charts, and session management*
|
||||
## The Solution: Voltage-Aware Power Management
|
||||
|
||||
### Real-Time Monitoring Dashboard
|
||||
With the root cause identified, the fix became obvious: reduce current draw when 4G is active to prevent voltage sag.
|
||||
|
||||
The main interface displays live battery metrics with a clean, focused design:
|
||||
**The strategy:**
|
||||
- Detect when 4G module is active (modem state, network traffic)
|
||||
- Reduce CPU frequency from 2.4GHz to 1.8GHz
|
||||
- Lower current draw = less voltage sag
|
||||
- Keep voltage above 3.45V threshold
|
||||
|
||||

|
||||
*Real-time battery status showing charge level, voltage, current, power consumption, and monitoring controls*
|
||||
|
||||
The dashboard separates monitoring from recording—you can observe battery behavior live without writing to the database. When you spot something interesting, click "Start Recording" and choose whether to save from now or retroactively from when monitoring started.
|
||||
|
||||
### Interactive Charts
|
||||
|
||||
Three separate charts provide different perspectives on battery performance:
|
||||
|
||||

|
||||
*Battery Percentage and Power Consumption charts showing real-time trends*
|
||||
|
||||

|
||||
*Voltage & Current Trends with dual Y-axes for precise monitoring*
|
||||
|
||||
The charts update every 2 seconds, providing immediate visual feedback on power consumption patterns. This was crucial for diagnosing the 4G power issue—I could literally watch the power spike when the modem activated.
|
||||
|
||||
### Session Management
|
||||
|
||||
Recording sessions can be named, exported, and managed individually:
|
||||
|
||||

|
||||
*Saved recording sessions with download, edit, and delete options*
|
||||
|
||||

|
||||
*Inline editing of session names for easy identification*
|
||||
|
||||
Each session captures a complete picture of battery behavior during a specific time period. I used this to document the before/after behavior of the 4G power manager.
|
||||
|
||||
### Data Export
|
||||
|
||||
The app provides flexible export options:
|
||||
|
||||

|
||||
*Custom time range selection for targeted data export*
|
||||
|
||||
You can export per-session data or select custom time ranges for analysis in external tools like Excel or Python notebooks.
|
||||
|
||||
### Phase 2: Automatic Power Management (Bash + udev + systemd)
|
||||
|
||||
The monitoring app confirmed the power spikes. Next, I needed automatic mitigation:
|
||||
|
||||
**4G Power Manager Components:**
|
||||
|
||||
1. **Detection Script** (`4g-power-manager.sh`):
|
||||
- Detects 4G modem state using multiple methods:
|
||||
- `wwan0` interface status
|
||||
- ModemManager state (connected/registered)
|
||||
- Network traffic analysis
|
||||
- When 4G active: `powersave` governor + 1.8GHz max (saves 20-30% power)
|
||||
- When 4G inactive: `ondemand` governor + 2.4GHz max (full performance)
|
||||
|
||||
2. **udev Rules** (`99-4g-power-manager.rules`):
|
||||
- Triggers on USB modem device changes (vendor:product = 1e0e:9001)
|
||||
- Monitors `wwan0` interface state
|
||||
- Responds to ttyUSB and cdc-wdm device events
|
||||
|
||||
3. **Systemd Service** (`4g-power-monitor.service`):
|
||||
- Background daemon checking modem state every 5 seconds
|
||||
- More reliable than udev alone for network activity changes
|
||||
- Auto-restarts on failure
|
||||
|
||||
4. **Installation Script** (`install-4g-power-manager.sh`):
|
||||
- One-command setup with uninstall option
|
||||
- Creates logs, sets permissions, reloads services
|
||||
|
||||
**Testing Results:**
|
||||
```
|
||||
Before: CM5 @ 2.4GHz + 4G = 22-25W → System hangs
|
||||
After: CM5 @ 1.8GHz + 4G = 16-20W → Stable operation ✓
|
||||
```
|
||||
**Result:** Rock-solid 4G operation on battery power.
|
||||
|
||||
## Why Vibe Coding Works: The Magic of Zero Context Switching
|
||||
|
||||
Here's what blew my mind: this entire project—from "hmm, why does this crash" to "production-ready solution with installer scripts"—took about **8 hours**. One Saturday afternoon. No workstation. No separate test device. Just me, the uConsole, and a cup of coffee.
|
||||
Here's what blew my mind: this entire project—from "why does this crash?" to "production-ready solution with automated power management"—took about **10 hours across two days**.
|
||||
|
||||
No desktop workstation. No oscilloscope. No separate test device. Just:
|
||||
- The uConsole itself
|
||||
- A web browser for research
|
||||
- Claude Code for AI-assisted development
|
||||
- Coffee shop WiFi
|
||||
|
||||
**The secret sauce:**
|
||||
|
||||
1. **Immediate Feedback**: The problem is right in front of you. Build a chart, watch it live, spot the issue, fix it, repeat.
|
||||
2. **Modern Tooling**: Next.js hot reload means changes appear in seconds. TypeScript catches bugs before they run. AI assists with boilerplate and research.
|
||||
3. **Zero Context Loss**: No mental overhead switching between devices. The debugging environment IS the target environment.
|
||||
4. **Location Independence**: Coffee shop gets boring? Move to a park bench. Train ride? Perfect coding time.
|
||||
5. **Constraint-Driven Design**: Limited screen space forces you to build cleaner UIs. Battery consciousness makes you write efficient code.
|
||||
1. **Immediate Feedback Loop**: Build a chart → Watch it live → Spot the voltage drop → Fix it → Repeat
|
||||
2. **Zero Context Loss**: The debugging environment IS the target environment
|
||||
3. **Modern Tooling**: Next.js hot reload, TypeScript safety, AI-assisted coding
|
||||
4. **Location Independence**: Coffee shop → Park bench → Train ride → Couch
|
||||
5. **Constraint-Driven Design**: Limited resources force elegant solutions
|
||||
|
||||
**The workflow becomes circular:**
|
||||
```
|
||||
@@ -177,141 +113,84 @@ Observe issue → Research → Prototype → Test → Iterate
|
||||
All on the same device, anywhere
|
||||
```
|
||||
|
||||
This isn't just faster—it's more *fun*. You're in flow state, building exactly what you need, exactly when you need it.
|
||||
This isn't just faster—it's more *fun*. Pure flow state, building exactly what you need, exactly when you need it.
|
||||
|
||||
## What This Means for Everyone: Tools for Life, Not Just Work
|
||||
## What This Means: The Democratization of Problem-Solving
|
||||
|
||||
Ten years ago, solving this problem would've required:
|
||||
- An engineering workstation
|
||||
- Cross-compilation setup
|
||||
- Separate test hardware
|
||||
- Days of context-switching frustration
|
||||
Ten years ago, solving this would have required:
|
||||
- An engineering lab
|
||||
- $5,000+ of test equipment
|
||||
- Cross-compilation toolchains
|
||||
- Days of setup time
|
||||
- Dedicated workspace
|
||||
|
||||
Today? A $200 pocket computer and one focused afternoon.
|
||||
Today? A $200 pocket computer and one focused weekend.
|
||||
|
||||
**This isn't just about professional developers**. Vibe coding opens doors for:
|
||||
**This isn't just about professional developers.** Vibe coding opens doors for:
|
||||
- **Hobbyists** building battery-powered projects
|
||||
- **EV enthusiasts** testing battery performance
|
||||
- **Field researchers** debugging equipment on-site
|
||||
- **Makers** troubleshooting hardware in real-time
|
||||
- **Students** learning by building actual tools
|
||||
|
||||
- **Hobbyists**: Build custom dashboards for your home automation without a full dev setup
|
||||
- **Field Researchers**: Create data collection tools on-site, adapted to real conditions
|
||||
- **Sys Admins**: Prototype monitoring solutions while physically at the server rack
|
||||
- **Makers**: Debug hardware projects on the workbench, no desktop required
|
||||
- **Students**: Learn full-stack development on truly portable hardware
|
||||
The barrier to "scratch your own itch" development just collapsed.
|
||||
|
||||
The barrier to entry for "scratch your own itch" development just collapsed. If you can imagine a solution, you can probably build it—wherever you are right now.
|
||||
## The Battery Selection Problem Nobody Talks About
|
||||
|
||||
## The Code: Open Source and Production-Ready
|
||||
Users select batteries based on:
|
||||
- ❌ Capacity (mAh) alone
|
||||
- ❌ Price
|
||||
- ❌ "Recommended" lists
|
||||
|
||||
**Repository:** [battery-monitor on Gitea](https://hiwifi.denq.us:8418/denq/battery-monitor)
|
||||
**They should be selecting based on:**
|
||||
- ✅ **Continuous Discharge Rate (CDR)**: 15A minimum
|
||||
- ✅ **Internal Resistance**: Lower = better voltage stability
|
||||
- ✅ **Voltage retention under load**: Measured, not guessed
|
||||
|
||||
**Project Structure:**
|
||||
```
|
||||
battery-monitor/
|
||||
├── src/
|
||||
│ ├── app/api/battery/ # Next.js API routes
|
||||
│ ├── components/
|
||||
│ │ └── BatteryMonitor.tsx # Main React component
|
||||
│ └── lib/db.ts # SQLite utilities
|
||||
├── scripts/
|
||||
│ ├── 4g-power-manager.sh # Power management logic
|
||||
│ ├── 4g-power-monitor.service # Systemd service
|
||||
│ └── install-4g-power-manager.sh # Installation script
|
||||
└── README.md # Full documentation
|
||||
```
|
||||
The discharge curve visualization I built becomes a **battery rating tool**. Test your batteries, see how much usable capacity you actually have above 3.45V, make informed decisions.
|
||||
|
||||
**Installation (on uConsole CM5):**
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git
|
||||
cd battery-monitor
|
||||
**Recommended batteries for 4G:**
|
||||
- Samsung 25R (2500mAh, 20A CDR) - Excellent voltage stability
|
||||
- Samsung 30Q (3000mAh, 15A CDR) - Good balance
|
||||
- Sony VTC6 (3000mAh, 15A CDR) - Superb under load
|
||||
- LG HG2 (3000mAh, 20A CDR) - Best voltage retention
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the battery monitor
|
||||
npm run dev
|
||||
# Visit http://localhost:3000
|
||||
|
||||
# Install the 4G power manager (requires sudo)
|
||||
cd scripts
|
||||
sudo ./install-4g-power-manager.sh
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
|
||||
✅ **Real-time Monitoring**
|
||||
- Live battery metrics updated every 2 seconds
|
||||
- Interactive charts (percentage, power, voltage, current)
|
||||
- Visual indicators for charging/discharging state
|
||||
|
||||
✅ **Smart Recording**
|
||||
- Separate "Monitor" (display only) from "Record" (save to DB)
|
||||
- Option to record from "now" or retroactively from monitoring start
|
||||
- Session management with custom naming
|
||||
|
||||
✅ **Data Export**
|
||||
- Per-session CSV export
|
||||
- Custom time range export
|
||||
- Historical data viewing
|
||||
|
||||
✅ **4G Power Management**
|
||||
- Automatic CPU frequency scaling when 4G is active
|
||||
- Prevents system hangs on battery power
|
||||
- Transparent operation with logging
|
||||
|
||||
✅ **Easy Uninstall**
|
||||
```bash
|
||||
sudo ./install-4g-power-manager.sh uninstall
|
||||
```
|
||||
|
||||
## Lessons Learned: The Power of Constraints
|
||||
|
||||
Working exclusively on the uConsole taught me valuable lessons:
|
||||
|
||||
1. **Constraints Breed Creativity**: Limited screen space forced better UI design
|
||||
2. **Performance Matters**: Running on CM5 made me optimize database queries
|
||||
3. **User Testing is Instant**: I'm the user—feedback is immediate
|
||||
4. **Portability Enables Focus**: No distractions, just flow state coding
|
||||
|
||||
**Battery Recommendations:**
|
||||
For optimal performance, use high-drain 18650 cells:
|
||||
- Samsung 25R (2500mAh, 20A CDR)
|
||||
- Samsung 30Q (3000mAh, 15A CDR)
|
||||
- Sony VTC6 (3000mAh, 15A CDR)
|
||||
- LG HG2 (3000mAh, 20A CDR)
|
||||
|
||||
Avoid generic/unbranded cells with <10A discharge rating.
|
||||
|
||||
## What's Next: More Vibe Coding Stories
|
||||
|
||||
This is the first in a series exploring "vibe coding for work/life"—real projects built entirely on portable devices, solving real problems in real locations.
|
||||
|
||||
**Upcoming stories:**
|
||||
- Building a mesh network monitor while hiking remote trails
|
||||
- Creating a restaurant wait-time tracker from a coffee shop
|
||||
- Prototyping home energy management tools on the couch
|
||||
|
||||
The uConsole (and devices like it) represent something new: powerful enough for serious development, portable enough to carry everywhere, and capable enough to be your only computer.
|
||||
|
||||
**Vibe coding isn't about writing perfect code—it's about building useful tools with joy, wherever inspiration strikes.**
|
||||
|
||||
Want to share your vibe coding story? Tag it `#VibeCodingChronicles` and let's build a community of portable problem-solvers.
|
||||
Avoid generic/unbranded cells with <10A discharge rating—they'll hit 3.45V early.
|
||||
|
||||
## Try It Yourself
|
||||
|
||||
1. **Get the Hardware**: [uConsole CM5 from ClockworkPi](https://www.clockworkpi.com/uconsole)
|
||||
2. **Clone the Repo**: `git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git`
|
||||
3. **Follow the README**: Complete setup guide included
|
||||
4. **Share Your Mods**: The code is GPL v3—fork it, improve it, share it
|
||||
The entire solution is open source (GPL v3):
|
||||
|
||||
**Questions? Issues? Contributions?**
|
||||
- ClockworkPi Forum: [https://forum.clockworkpi.com/c/uconsole](https://forum.clockworkpi.com/c/uconsole)
|
||||
- Gitea Issues: [https://hiwifi.denq.us:8418/denq/battery-monitor/issues](https://hiwifi.denq.us:8418/denq/battery-monitor/issues)
|
||||
**Repository:** [battery-monitor on Gitea](https://hiwifi.denq.us:8418/denq/battery-monitor)
|
||||
|
||||
## The Joy of Building: Why This Matters
|
||||
**Quick Start:**
|
||||
```bash
|
||||
git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git
|
||||
cd battery-monitor
|
||||
npm install
|
||||
npm run dev # Visit http://localhost:3000
|
||||
|
||||
# Install uConsole Smart Power Regulator (optional)
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh install
|
||||
```
|
||||
|
||||
**What you get:**
|
||||
- Real-time battery voltage/current/power monitoring
|
||||
- Battery Energy Output vs. Voltage discharge curve
|
||||
- 3.45V threshold visualization for 4G compatibility
|
||||
- Intelligent power regulation (AC detection + 4G state monitoring)
|
||||
- Automatic voltage alerts when battery + 4G active
|
||||
- Session recording and CSV export
|
||||
- Complete battery testing toolkit
|
||||
|
||||
→ **Want detailed usage guide?** See [TOOL-GUIDE.md](TOOL-GUIDE.md)
|
||||
|
||||
## The Joy of Building
|
||||
|
||||
There's something profoundly satisfying about identifying a problem on a device and building the solution **on that very device**. No "I'll fix this later." No context switching. Just pure, focused problem-solving in the moment.
|
||||
|
||||
The uConsole CM5 now runs stably on battery with 4G active. The battery monitor provides real-time insights into power consumption. And the entire solution—from diagnostic web app to system-level power management—was built in one focused afternoon session.
|
||||
The uConsole CM5 now runs stably on battery with 4G active. The community has a new understanding of why some batteries work better than others. And the entire solution was built in one focused weekend of vibe coding.
|
||||
|
||||
**This is vibe coding**: Building custom solutions with joy, anywhere inspiration strikes. Not because you have to, but because you *can*.
|
||||
|
||||
@@ -319,71 +198,48 @@ The tools are here. The hardware is affordable. The only question is: what will
|
||||
|
||||
---
|
||||
|
||||
## Community Impact
|
||||
|
||||
Since sharing this discovery:
|
||||
- Multiple users confirmed voltage drop as root cause
|
||||
- Battery recommendations refined based on discharge testing
|
||||
- uConsole Smart Power Regulator adopted by several users
|
||||
- New battery test methodology for community
|
||||
- Event-driven AC detection added based on user feedback
|
||||
|
||||
**Share Your Results:**
|
||||
- Test your batteries with the discharge curve tool
|
||||
- Share findings in ClockworkPi forums
|
||||
- Help build community battery database
|
||||
|
||||
**Questions? Contributions?**
|
||||
- ClockworkPi Forum: [https://forum.clockworkpi.com/c/uconsole](https://forum.clockworkpi.com/c/uconsole)
|
||||
- Gitea Issues: [https://hiwifi.denq.us:8418/denq/battery-monitor/issues](https://hiwifi.denq.us:8418/denq/battery-monitor/issues)
|
||||
|
||||
---
|
||||
|
||||
*This story is part of the **Vibe Coding Chronicles** series, documenting real projects built entirely on portable devices. Follow along as we explore what's possible when development becomes truly mobile.*
|
||||
|
||||
---
|
||||
**Tags:** #uConsole #VibeCoding #4GModule #BatteryAnalysis #PortableComputing #AIAssistedDevelopment
|
||||
|
||||
## Technical Appendix
|
||||
|
||||
### Power Budget Analysis
|
||||
|
||||
| Component | Idle | Normal | Peak |
|
||||
|-----------|------|--------|------|
|
||||
| CM5 CPU | 2W | 8W | 15W |
|
||||
| 4G Module | 0.5W | 3W | 10W |
|
||||
| Display | 1W | 2W | 3W |
|
||||
| Other | 1W | 2W | 2W |
|
||||
| **Total** | **4.5W** | **15W** | **30W** |
|
||||
|
||||
**AXP228 Limits:**
|
||||
- Battery discharge path: ~18-20W sustained
|
||||
- USB + battery: ~25W sustained
|
||||
- Peak spikes: Handled briefly, then OCP triggers
|
||||
|
||||
**With 4G Active @ 1.8GHz:**
|
||||
- CM5: ~8-10W (reduced from 12-15W)
|
||||
- 4G: ~5-10W (during transmission)
|
||||
- Other: ~3W
|
||||
- **Total: ~16-23W** (within limits)
|
||||
|
||||
### API Endpoints
|
||||
|
||||
The battery monitor provides a RESTful API:
|
||||
|
||||
```
|
||||
GET /api/battery # Current battery data
|
||||
GET /api/battery?save=true # Save current reading to DB
|
||||
GET /api/battery/history # Query historical data
|
||||
GET /api/battery/sessions # List all sessions
|
||||
GET /api/battery/sessions/:id # Get specific session data
|
||||
POST /api/battery/sessions # Create new session
|
||||
PATCH /api/battery/sessions/:id # Update session name/end time
|
||||
DELETE /api/battery/sessions/:id # Delete session
|
||||
```
|
||||
|
||||
### BatteryData Interface
|
||||
|
||||
```typescript
|
||||
interface BatteryData {
|
||||
timestamp: string; // ISO 8601 format
|
||||
percentage: number; // 0-100
|
||||
voltage: number; // volts (converted from µV)
|
||||
current: number; // amps (converted from µA)
|
||||
power: number; // watts (voltage * current)
|
||||
status: string; // "Charging", "Discharging", "Full"
|
||||
health: string; // "Good", "Unknown"
|
||||
acConnected: boolean; // true if AC adapter connected
|
||||
}
|
||||
```
|
||||
**Next in series:** Building a mesh network monitor while hiking remote trails
|
||||
|
||||
---
|
||||
|
||||
**Tags:** #uConsole #RaspberryPi #VibeCoding #NextJS #BatteryManagement #PowerManagement #IoT #PortableComputing #AIAssistedDevelopment
|
||||
## Technical Summary
|
||||
|
||||
**Root Cause:** Battery voltage drops below 3.45V under 2A load from 4G module → brownout → system hang
|
||||
|
||||
**Solution:** CPU frequency scaling when 4G active → lower current → stable voltage
|
||||
|
||||
**Key Discovery:** ~50% of battery capacity unusable for 4G due to voltage requirements
|
||||
|
||||
**Tool Created:** Real-time battery monitor with discharge curve analysis for voltage-based battery rating
|
||||
|
||||
→ **For complete technical documentation:** [TOOL-GUIDE.md](TOOL-GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
**License:** GPL v3 (same as uConsole hardware designs)
|
||||
|
||||
---
|
||||
|
||||
[^1]: Battery Configuration - [ClockworkPi Forum Discussion](https://forum.clockworkpi.com/t/understanding-battery-module-pinout-upgrading-battery-module/10260)
|
||||
|
||||
[^2]: AXP228 Datasheet - [X-Powers Technology](http://www.x-powers.com/en.php/Info/product_detail/article_id/31)
|
||||
[^1]: SIM7600G-H Specifications - [FCC Filing](https://fcc.report/FCC-ID/2AJYU-8PYA004/4646631.pdf), Operating Voltage: 3.4V ~ 4.2V
|
||||
|
||||
500
STORY.zh-CN.md
@@ -1,4 +1,4 @@
|
||||
# Vibe Coding 编程纪事:用口袋大小的开发环境解决 uConsole 电源危机
|
||||
# 解决 uConsole 4G 模块之谜:Vibe Coding 编程纪事
|
||||
|
||||
> **「工作/生活中的 Vibe Coding」系列文章** —— 真实的故事,讲述如何在任何地方使用便携设备和现代工具构建定制化解决方案,解决日常问题。
|
||||
|
||||
@@ -6,384 +6,252 @@
|
||||
|
||||
想象一下,在咖啡馆里悠闲地调试设备的电源管理问题,直接在该设备上构建诊断工具。不需要台式工作站。不需要"等我回家再修"。只有你、一台口袋大小的电脑,以及在灵感迸发时随时随地解决问题的自由。
|
||||
|
||||
这就是我在某个周六下午解决 uConsole CM5 臭名昭著的电源死机问题的经历——同时也发现了关于现代开发工作流程的深刻见解。
|
||||
这就是我在某个周末解决 uConsole CM5 臭名昭著的 4G 模块死机问题的经历——结果发现问题根本不是功率传输,而是**电压下降**。
|
||||
|
||||
## 问题:当创新遭遇物理定律
|
||||
## 问题:当 4G 模块导致系统崩溃
|
||||
|
||||
[ClockworkPi uConsole](https://www.clockworkpi.com/uconsole) 是黑客的梦想:将树莓派 CM5 塞进翻盖式外壳,配备机械键盘和模块化扩展槽。这是那种让你想要折腾、定制和突破边界的设备。
|
||||
|
||||
但有一个令人沮丧的问题。在电池供电时启用 4G 扩展模块,系统会随机冻结——完全卡死。唯一的解决办法?物理移除电池并重启。
|
||||
|
||||
论坛帖子充满了各种理论:软件 bug、电池故障、硬件缺陷。根本原因仍是个谜。用户被迫在移动性(电池供电)和连接性(4G)之间做出选择,但无法可靠地同时拥有两者。
|
||||
论坛帖子充满了各种理论:
|
||||
- "这是软件 bug"
|
||||
- "你的电池是假货"
|
||||
- "电源管理芯片功率不够"
|
||||
- "你需要买更好的电池"
|
||||
|
||||
我决定调查——但有个特别之处:我将**在 uConsole 本身上**构建所有诊断工具,同时使用电池供电运行。如果我能用正在经历问题的设备来解决问题,那将证明"Vibe Coding"作为实际工作流程的可行性。
|
||||
这些解释都不完整。用户被迫在移动性(电池供电)和连接性(4G)之间做出选择,但无法可靠地同时拥有两者。
|
||||
|
||||
## 调查:追踪电子流
|
||||
我决定调查——但有个特别之处:**我将在 uConsole 本身上构建所有诊断工具**,同时使用电池供电运行。如果我能用正在经历问题的设备来解决问题,那将证明关于现代开发工作流程的一些深刻见解。
|
||||
|
||||
坐在咖啡馆里,uConsole 使用电池供电,我开始深入研究。第一站:Linux 的 sysfs 接口,它暴露原始硬件数据。uConsole 使用 **AXP228 PMIC**(电源管理集成电路)在电池、USB 和系统之间协调供电。
|
||||

|
||||
|
||||
几个小时的研究——数据手册、论坛深度挖掘和测量——揭示了罪魁祸首:
|
||||
*在遇到问题的设备上构建解决方案——真正的 vibe coding 实践*
|
||||
|
||||
1. **电池配置**:两块 18650 电池采用**并联**接线(而非串联),意味着它们同时分担负载,无需切换[^1]。
|
||||
## 调查:追踪电压
|
||||
|
||||
2. **功率限制**:
|
||||
- AXP228 从电池放电路径可持续提供约 18-20W
|
||||
- CM5 + 4G 模块在传输时峰值可达 **22-25W**
|
||||
- 这种不匹配会触发 PMIC 的过流保护(OCP)
|
||||
坐在咖啡馆里,uConsole 使用电池供电,我开始构建。首先是一个简单的 Web 应用程序来实时监控电池指标。Next.js 热重载意味着我可以立即看到变化。没有编译时间,没有部署步骤——只有纯粹的迭代式问题解决。
|
||||
|
||||
3. **IPS™ 电路**:AXP228 的智能电源选择透明地从 USB、AC 或电池路由电源。使用 USB 供电时,可以补充电池电源,组合提供约 25W[^2]。
|
||||
监控揭示了一些意想不到的东西。当 4G 模块激活时:
|
||||
- 功耗看起来正常(约 8-10W)
|
||||
- 传输期间电流峰值(高达 2A)
|
||||
- 但电压... **电压下降到危险的低水平**
|
||||
|
||||
**关键引用:**
|
||||
- [AXP228 产品页面 - X-Powers](http://www.x-powers.com/en.php/Info/product_detail/article_id/31)
|
||||
- [uConsole 论坛:电池配置讨论](https://forum.clockworkpi.com/t/understanding-battery-module-pinout-upgrading-battery-module/10260)
|
||||
- [4G 模块关机问题](https://forum.clockworkpi.com/t/battery-life-not-as-expected-4g-module-shuts-the-whole-thing-down/12633)
|
||||
### 突破:不是瓦特的问题,而是伏特的问题
|
||||
|
||||
## 洞察:问题不在电池,而在数学
|
||||
经过几个小时的数据手册研究,画面变得清晰:
|
||||
|
||||
问题不是电池质量或切换——而是简单的功率预算算术:
|
||||
**SimTech SIM7600G-H 4G 模块有严格的电压要求:**
|
||||
- 工作范围:3.4V 至 4.2V
|
||||
- 最低安全电压:**约 3.45V**
|
||||
- 电流消耗:传输突发期间高达 2A
|
||||
|
||||
```
|
||||
CM5 峰值功率: 15W
|
||||
4G 模块峰值: 10W
|
||||
显示屏及其他: 3W
|
||||
─────────────────────────
|
||||
总峰值需求: 28W
|
||||
**当电压降至 3.45V 以下时会发生什么:**
|
||||
1. 4G 模块欠压
|
||||
2. USB 总线挂起
|
||||
3. 系统无响应
|
||||
4. 只有移除电池才能重置
|
||||
|
||||
AXP228 电池最大: 20W ⚠️ 问题所在
|
||||
AXP228 USB+电池: 25W ✓ 可用
|
||||
```
|
||||
**真正的问题:** 锂离子电池有内阻。当 4G 模块抽取 2A 电流时,电压会瞬间下降。如果电池已经部分放电或内阻较高,电压就会降至 3.45V 以下 → 立即死机。
|
||||
|
||||
**解决方案**:当 4G 激活时降低 CPU 功耗,将总负载保持在 18-20W 电池预算内。
|
||||
这解释了一切:
|
||||
- ✅ 为什么 USB 供电有帮助(减少电池电流消耗)
|
||||
- ✅ 为什么有些电池效果更好(内阻更低)
|
||||
- ✅ 为什么会随机发生(取决于电池状态和负载时机)
|
||||
|
||||
## 构建解决方案:在 uConsole 上进行 Vibe Coding
|
||||
## 洞察:并非所有电池容量都可用
|
||||
|
||||
这就是"Vibe Coding"的闪光之处。我没有切换到桌面工作站,而是**在 uConsole 本身上**构建了整个解决方案,在 AI 辅助下快速迭代。
|
||||
突破来自构建一个新的可视化:**电池能量输出 vs 电压**——一条放电曲线,绘制剩余容量与电压的关系。
|
||||
|
||||
### 第一阶段:实时监控(Next.js 16 + React 19)
|
||||
我用 FEB-4000 4000mAh 电池进行了完整测试(100% 至约 2%),在 2 小时 48 分钟内捕获了 4,318 个数据点:
|
||||
|
||||
我构建了一个电池监控 Web 应用来可视化电源问题:
|
||||

|
||||
|
||||
**技术栈:**
|
||||
- Next.js 16 with App Router
|
||||
- React 19 with TypeScript
|
||||
- Recharts 实时可视化
|
||||
- SQLite 历史数据
|
||||
- shadcn/ui 组件
|
||||
*红线显示 4G 模块的最低电压。右侧的所有内容都无法用于 4G 连接。*
|
||||
|
||||
**关键特性:**
|
||||
- 直接从 Linux sysfs (`/sys/class/power_supply/axp20x-battery/`) 读取
|
||||
- 实时显示电压、电流、功率、百分比
|
||||
- 分离监控(仅显示)和记录(数据库写入)
|
||||
- 缓冲最多 1000 个读数以便追溯记录会话
|
||||
**令人震惊的结果:**
|
||||
- 电池总容量:24.79 Wh
|
||||
- **3.45V 以上可用容量:13.39 Wh (54%)**
|
||||
- 3.45V 以下不可用:11.40 Wh (46%)
|
||||
|
||||
**为什么采用这种方法?**
|
||||
通过分离监控和记录,我可以观察电池行为而不填满数据库。当我发现有趣的情况时,可以点击"开始记录 [从监控开始]"来追溯保存缓冲的数据。
|
||||
**这意味着:对于可靠的 4G 操作,你只能使用大约一半的电池容量。** 一旦电压降至 3.45V,你仍然有 40-50% 的电量剩余——但它对 4G 来说是无法使用的。
|
||||
|
||||

|
||||
*完整的电池监控界面,显示实时指标、交互式图表和会话管理*
|
||||
## 解决方案:电压感知电源管理
|
||||
|
||||
### 实时监控仪表板
|
||||
确定根本原因后,修复变得显而易见:在 4G 激活时降低电流消耗以防止电压下降。
|
||||
|
||||
主界面以简洁、专注的设计显示实时电池指标:
|
||||
**策略:**
|
||||
- 检测 4G 模块何时激活(调制解调器状态、网络流量)
|
||||
- 将 CPU 频率从 2.4GHz 降至 1.8GHz
|
||||
- 更低的电流消耗 = 更少的电压下降
|
||||
- 保持电压高于 3.45V 阈值
|
||||
|
||||

|
||||
*实时电池状态,显示充电水平、电压、电流、功耗和监控控制*
|
||||
**结果:** 电池供电下 4G 运行非常稳定。
|
||||
|
||||
仪表板将监控与记录分离——你可以实时观察电池行为而不写入数据库。当发现有趣的现象时,点击"开始记录"并选择是从现在保存还是从监控开始追溯保存。
|
||||
## 为什么 Vibe Coding 有效:零上下文切换的魔力
|
||||
|
||||
### 交互式图表
|
||||
让我震惊的是:整个项目——从"为什么会崩溃"到"带自动电源管理的生产就绪解决方案"——在两天内大约花了 **10 小时**。
|
||||
|
||||
三个独立图表从不同角度展示电池性能:
|
||||
|
||||

|
||||
*电池百分比和功耗图表,显示实时趋势*
|
||||
|
||||

|
||||
*电压和电流趋势,双 Y 轴精确监控*
|
||||
|
||||
图表每 2 秒更新一次,提供功耗模式的即时视觉反馈。这对诊断 4G 电源问题至关重要——我可以实时观察调制解调器激活时的功率峰值。
|
||||
|
||||
### 会话管理
|
||||
|
||||
记录会话可以命名、导出和单独管理:
|
||||
|
||||

|
||||
*保存的记录会话,带有下载、编辑和删除选项*
|
||||
|
||||

|
||||
*会话名称的内联编辑,便于识别*
|
||||
|
||||
每个会话捕获特定时间段内电池行为的完整画面。我用它来记录 4G 电源管理器的前后行为。
|
||||
|
||||
### 数据导出
|
||||
|
||||
应用提供灵活的导出选项:
|
||||
|
||||

|
||||
*自定义时间范围选择,用于定向数据导出*
|
||||
|
||||
你可以导出每个会话的数据,或选择自定义时间范围,在 Excel 或 Python notebook 等外部工具中分析。
|
||||
|
||||
### 第二阶段:自动电源管理(Bash + udev + systemd)
|
||||
|
||||
监控应用确认了功率峰值。接下来,我需要自动缓解:
|
||||
|
||||
**4G 电源管理器组件:**
|
||||
|
||||
1. **检测脚本** (`4g-power-manager.sh`):
|
||||
- 使用多种方法检测 4G 调制解调器状态:
|
||||
- `wwan0` 接口状态
|
||||
- ModemManager 状态(已连接/已注册)
|
||||
- 网络流量分析
|
||||
- 4G 激活时:`powersave` 调度器 + 1.8GHz 最大频率(节省 20-30% 功率)
|
||||
- 4G 未激活时:`ondemand` 调度器 + 2.4GHz 最大频率(全性能)
|
||||
|
||||
2. **udev 规则** (`99-4g-power-manager.rules`):
|
||||
- 在 USB 调制解调器设备变化时触发(供应商:产品 = 1e0e:9001)
|
||||
- 监控 `wwan0` 接口状态
|
||||
- 响应 ttyUSB 和 cdc-wdm 设备事件
|
||||
|
||||
3. **Systemd 服务** (`4g-power-monitor.service`):
|
||||
- 后台守护进程每 5 秒检查调制解调器状态
|
||||
- 比单独的 udev 更可靠,可检测网络活动期间的状态变化
|
||||
- 失败时自动重启
|
||||
|
||||
4. **安装脚本** (`install-4g-power-manager.sh`):
|
||||
- 一键设置,带卸载选项
|
||||
- 创建日志、设置权限、重新加载服务
|
||||
|
||||
**测试结果:**
|
||||
```
|
||||
之前:CM5 @ 2.4GHz + 4G = 22-25W → 系统死机
|
||||
之后:CM5 @ 1.8GHz + 4G = 16-20W → 稳定运行 ✓
|
||||
```
|
||||
|
||||
## Vibe Coding 为何有效:零上下文切换的魔力
|
||||
|
||||
让我震惊的是:整个项目——从"嗯,为什么会崩溃"到"带安装脚本的生产就绪解决方案"——大约花了 **8 小时**。一个周六下午。没有工作站。没有单独的测试设备。只有我、uConsole 和一杯咖啡。
|
||||
没有台式工作站。没有示波器。没有单独的测试设备。只有:
|
||||
- uConsole 本身
|
||||
- 用于研究的 Web 浏览器
|
||||
- Claude Code 用于 AI 辅助开发
|
||||
- 咖啡馆 WiFi
|
||||
|
||||
**秘诀:**
|
||||
|
||||
1. **即时反馈**:问题就在你眼前。构建图表,实时观察,发现问题,修复它,重复。
|
||||
2. **现代工具**:Next.js 热重载意味着更改在几秒钟内出现。TypeScript 在运行前捕获 bug。AI 辅助样板代码和研究。
|
||||
3. **零上下文丢失**:在设备之间切换没有心理开销。调试环境就是目标环境。
|
||||
4. **位置独立**:咖啡馆待腻了?移到公园长凳上。火车旅行?完美的编码时间。
|
||||
5. **约束驱动设计**:有限的屏幕空间迫使你构建更简洁的 UI。电池意识让你编写高效的代码。
|
||||
1. **即时反馈循环**:构建图表 → 实时观察 → 发现电压下降 → 修复 → 重复
|
||||
2. **零上下文丢失**:调试环境就是目标环境
|
||||
3. **现代工具**:Next.js 热重载、TypeScript 安全性、AI 辅助编码
|
||||
4. **专注的心流**:没有分心,没有切换,只有纯粹的问题解决
|
||||
|
||||
**工作流程变成循环:**
|
||||
**Vibe Coding 不仅仅是氛围——它是一种高效的、现代的开发工作流程**,适用于任何有互联网连接的地方。
|
||||
|
||||
## 构建的内容:实时电池监控 + 智能电源管理
|
||||
|
||||
### 第一阶段:诊断工具(Next.js + TypeScript + Recharts)
|
||||
|
||||
**电池监控 Web 应用:**
|
||||
- 实时指标(电压、电流、功率、百分比)
|
||||
- 4 个交互式图表,用于趋势分析
|
||||
- 会话记录到 SQLite 数据库
|
||||
- CSV 导出用于深入分析
|
||||
- **新增:电池能量输出 vs 电压图表**(带 3.45V 阈值可视化)
|
||||
|
||||
**关键发现的可视化:**
|
||||
|
||||

|
||||
|
||||
*完整的放电曲线从 100% 到约 2%*
|
||||
|
||||

|
||||
|
||||
*电压下降和电流峰值模式*
|
||||
|
||||

|
||||
|
||||
*关键图表显示 4G 可用容量 vs 不可用容量*
|
||||
|
||||
每个会话捕获特定时间段内电池行为的完整画面。我用它来记录智能电源调节器的前后行为。
|
||||
|
||||
### 第二阶段:自动电源管理(Bash + udev + systemd)
|
||||
|
||||
监控应用确认了电压下降。接下来,我需要自动缓解:
|
||||
|
||||
**uConsole 智能电源调节器组件:**
|
||||
|
||||
1. **主调节器** (`uconsole-power-regulator.sh`):
|
||||
- 基于 AC 和 4G 状态确定电源模式
|
||||
- 应用适当的 CPU 设置
|
||||
- 控制电压监控
|
||||
- 统一日志记录
|
||||
|
||||
2. **后台守护进程** (`uconsole-power-daemon.sh`):
|
||||
- 每 5 秒监控 AC 电源和 4G 调制解调器状态
|
||||
- 仅在状态变化时触发调节器
|
||||
- 处理边缘情况(如服务启动时 AC 已连接)
|
||||
|
||||
3. **电压监控系统**:
|
||||
- `voltage-monitor.sh` - 电池 + 4G 激活时每 5 秒检查电压
|
||||
- `voltage-alert-notify.sh` - 多方式告警(桌面/音频/日志/LED)
|
||||
- `voltage-monitor-control.sh` - 启动/停止控制器
|
||||
- 当电压 < 3.45V 时告警,速率限制为每 30 秒
|
||||
|
||||
4. **udev 规则** (`99-uconsole-power-regulator.rules`):
|
||||
- AC 电源连接/断开时触发(power_supply 子系统)
|
||||
- 4G 调制解调器 USB 设备变化
|
||||
- wwan0 接口状态变化
|
||||
- ttyUSB 和 cdc-wdm 设备事件
|
||||
|
||||
5. **安装脚本** (`install-uconsole-power-regulator.sh`):
|
||||
- 一键设置,带卸载和升级选项
|
||||
- 创建日志、设置权限、重新加载服务
|
||||
- 支持从旧版 4G 电源管理器升级
|
||||
|
||||
**电源模式:**
|
||||
```
|
||||
观察问题 → 研究 → 原型 → 测试 → 迭代
|
||||
↑_________________________________|
|
||||
都在同一设备上,任何地方
|
||||
AC 已连接:2.4GHz(完整性能)- 无电压监控
|
||||
电池 + 4G:1.8GHz(节能模式)- 启用电压监控和告警
|
||||
仅电池: 2.0GHz(平衡性能)- 无电压监控
|
||||
```
|
||||
|
||||
这不仅更快——而且更*有趣*。你处于心流状态,恰好在需要时构建恰好需要的东西。
|
||||
|
||||
## 这对每个人意味着什么:生活工具,不仅仅是工作
|
||||
|
||||
十年前,解决这个问题需要:
|
||||
- 工程工作站
|
||||
- 交叉编译设置
|
||||
- 单独的测试硬件
|
||||
- 数天的上下文切换挫折
|
||||
|
||||
今天?一台 200 美元的口袋电脑和一个专注的下午。
|
||||
|
||||
**这不仅仅关乎专业开发者**。Vibe Coding 为以下人群打开了大门:
|
||||
|
||||
- **爱好者**:为你的家庭自动化构建自定义仪表板,无需完整的开发设置
|
||||
- **现场研究人员**:在现场创建数据收集工具,适应真实条件
|
||||
- **系统管理员**:在服务器机架旁物理地原型监控解决方案
|
||||
- **创客**:在工作台上调试硬件项目,无需台式机
|
||||
- **学生**:在真正便携的硬件上学习全栈开发
|
||||
|
||||
"挠自己的痒处"开发的进入门槛刚刚崩溃。如果你能想象一个解决方案,你可能就能构建它——无论你现在在哪里。
|
||||
|
||||
## 代码:开源且生产就绪
|
||||
|
||||
**仓库:** [battery-monitor on Gitea](https://hiwifi.denq.us:8418/denq/battery-monitor)
|
||||
|
||||
**项目结构:**
|
||||
**测试结果:**
|
||||
```
|
||||
battery-monitor/
|
||||
├── src/
|
||||
│ ├── app/api/battery/ # Next.js API 路由
|
||||
│ ├── components/
|
||||
│ │ └── BatteryMonitor.tsx # 主 React 组件
|
||||
│ └── lib/db.ts # SQLite 工具
|
||||
├── scripts/
|
||||
│ ├── 4g-power-manager.sh # 电源管理逻辑
|
||||
│ ├── 4g-power-monitor.service # Systemd 服务
|
||||
│ └── install-4g-power-manager.sh # 安装脚本
|
||||
└── README.md # 完整文档
|
||||
之前:CM5 @ 2.4GHz + 4G = 电压降至 3.45V 以下 → 系统死机
|
||||
之后:CM5 @ 1.8GHz + 4G = 电压保持 > 3.45V → 稳定运行 ✓
|
||||
```
|
||||
|
||||
**安装(在 uConsole CM5 上):**
|
||||
## 试试看
|
||||
|
||||
**快速开始:**
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git
|
||||
cd battery-monitor
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
npm run dev # 访问 http://localhost:3000
|
||||
|
||||
# 运行电池监控器
|
||||
npm run dev
|
||||
# 访问 http://localhost:3000
|
||||
|
||||
# 安装 4G 电源管理器(需要 sudo)
|
||||
# 安装 uConsole 智能电源调节器(可选)
|
||||
cd scripts
|
||||
sudo ./install-4g-power-manager.sh
|
||||
sudo ./install-uconsole-power-regulator.sh install
|
||||
```
|
||||
|
||||
**关键特性:**
|
||||
**你将获得:**
|
||||
- 实时电池电压/电流/功率监控
|
||||
- 电池能量输出 vs 电压放电曲线
|
||||
- 4G 兼容性的 3.45V 阈值可视化
|
||||
- 智能电源调节(AC 检测 + 4G 状态监控)
|
||||
- 电池 + 4G 激活时自动电压告警
|
||||
- 会话记录和 CSV 导出
|
||||
- 完整的电池测试工具包
|
||||
|
||||
✅ **实时监控**
|
||||
- 每 2 秒更新的实时电池指标
|
||||
- 交互式图表(百分比、功率、电压、电流)
|
||||
- 充电/放电状态的可视化指示器
|
||||
→ **需要详细使用指南?** 参见 [TOOL-GUIDE.md](TOOL-GUIDE.md)
|
||||
|
||||
✅ **智能记录**
|
||||
- 分离"监控"(仅显示)和"记录"(保存到数据库)
|
||||
- 从"现在"或追溯从监控开始记录的选项
|
||||
- 带自定义命名的会话管理
|
||||
## 构建的乐趣
|
||||
|
||||
✅ **数据导出**
|
||||
- 每个会话的 CSV 导出
|
||||
- 自定义时间范围导出
|
||||
- 历史数据查看
|
||||
在设备上发现问题并**在同一设备上**构建解决方案,这种感觉非常令人满足。没有"我稍后再修"。没有上下文切换。只有纯粹的、专注的当下问题解决。
|
||||
|
||||
✅ **4G 电源管理**
|
||||
- 4G 激活时自动 CPU 频率调整
|
||||
- 防止电池供电时系统死机
|
||||
- 透明操作,带日志记录
|
||||
uConsole CM5 现在在电池供电下配合 4G 运行稳定。社区对为什么某些电池效果更好有了新的理解。整个解决方案都是在一个专注的周末通过 vibe coding 构建的。
|
||||
|
||||
✅ **轻松卸载**
|
||||
```bash
|
||||
sudo ./install-4g-power-manager.sh uninstall
|
||||
```
|
||||
**这就是 vibe coding**:在灵感迸发的任何地方,带着喜悦构建定制解决方案。不是因为你必须这样做,而是因为你*可以*这样做。
|
||||
|
||||
## 经验教训:约束的力量
|
||||
|
||||
专门在 uConsole 上工作教会了我宝贵的经验:
|
||||
|
||||
1. **约束孕育创造力**:有限的屏幕空间迫使更好的 UI 设计
|
||||
2. **性能很重要**:在 CM5 上运行让我优化数据库查询
|
||||
3. **用户测试是即时的**:我就是用户——反馈是即时的
|
||||
4. **可移植性实现专注**:没有干扰,只有心流状态编码
|
||||
|
||||
**电池推荐:**
|
||||
为获得最佳性能,使用高放电电流的 18650 电池:
|
||||
- Samsung 25R (2500mAh, 20A CDR)
|
||||
- Samsung 30Q (3000mAh, 15A CDR)
|
||||
- Sony VTC6 (3000mAh, 15A CDR)
|
||||
- LG HG2 (3000mAh, 20A CDR)
|
||||
|
||||
避免放电电流 <10A 的通用/无品牌电池。
|
||||
|
||||
## 接下来:更多 Vibe Coding 故事
|
||||
|
||||
这是"工作/生活中的 Vibe Coding"系列的第一篇——真实的项目,完全在便携设备上构建,在真实地点解决真实问题。
|
||||
|
||||
**即将推出的故事:**
|
||||
- 在远程徒步旅行中构建网格网络监控器
|
||||
- 在咖啡馆创建餐厅等待时间跟踪器
|
||||
- 在沙发上原型化家庭能源管理工具
|
||||
|
||||
uConsole(以及类似设备)代表了新事物:足够强大用于严肃开发,足够便携可随身携带,足够强大可成为你唯一的电脑。
|
||||
|
||||
**Vibe Coding 不是关于编写完美代码——而是关于在灵感迸发时随时随地构建有用工具的乐趣。**
|
||||
|
||||
想分享你的 Vibe Coding 故事?标记 `#VibeCodingChronicles`,让我们建立一个便携式问题解决者社区。
|
||||
|
||||
## 亲自尝试
|
||||
|
||||
1. **获取硬件**:[从 ClockworkPi 购买 uConsole CM5](https://www.clockworkpi.com/uconsole)
|
||||
2. **克隆仓库**:`git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git`
|
||||
3. **遵循 README**:包含完整设置指南
|
||||
4. **分享你的修改**:代码是 GPL v3——fork 它,改进它,分享它
|
||||
|
||||
**问题?Issues?贡献?**
|
||||
- ClockworkPi 论坛:[https://forum.clockworkpi.com/c/uconsole](https://forum.clockworkpi.com/c/uconsole)
|
||||
- Gitea Issues:[https://hiwifi.denq.us:8418/denq/battery-monitor/issues](https://hiwifi.denq.us:8418/denq/battery-monitor/issues)
|
||||
|
||||
## 构建的乐趣:为什么这很重要
|
||||
|
||||
在设备上识别问题并**在该设备本身上**构建解决方案有一种深刻的满足感。没有"我稍后修复"。没有上下文切换。只有当下纯粹、专注的问题解决。
|
||||
|
||||
uConsole CM5 现在在电池供电且 4G 激活时稳定运行。电池监控器提供功耗的实时洞察。整个解决方案——从诊断 Web 应用到系统级电源管理——都是在一个专注的下午会话中构建的。
|
||||
|
||||
**这就是 Vibe Coding**:在灵感迸发时随时随地构建定制解决方案的乐趣。不是因为你必须,而是因为你*能够*。
|
||||
|
||||
工具在这里。硬件价格实惠。唯一的问题是:你会构建什么?
|
||||
工具已经存在。硬件价格实惠。唯一的问题是:你将构建什么?
|
||||
|
||||
---
|
||||
|
||||
*本故事是 **Vibe Coding Chronicles(Vibe Coding 编程纪事)** 系列的一部分,记录完全在便携设备上构建的真实项目。关注我们,探索当开发真正移动化时的可能性。*
|
||||
## 社区影响
|
||||
|
||||
自从分享这一发现以来:
|
||||
- 多位用户确认电压下降是根本原因
|
||||
- 基于放电测试改进了电池推荐
|
||||
- 多位用户采用了 uConsole 智能电源调节器
|
||||
- 为社区提供了新的电池测试方法
|
||||
- 根据用户反馈添加了事件驱动的 AC 检测
|
||||
|
||||
**分享你的结果:**
|
||||
- 使用放电曲线工具测试你的电池
|
||||
- 在 ClockworkPi 论坛分享发现
|
||||
- 为电池推荐做出贡献
|
||||
|
||||
**改进和贡献:**
|
||||
- 改进电池推荐列表
|
||||
- 添加更多图表和可视化
|
||||
- 增强电源调节器逻辑
|
||||
- 支持其他 AXP2xx 设备
|
||||
|
||||
---
|
||||
|
||||
## 技术附录
|
||||
## 资源和参考
|
||||
|
||||
### 功率预算分析
|
||||
**硬件文档:**
|
||||
- [SimTech SIM7600 系列硬件设计](https://simcom.ee/documents/SIM7600x/SIM7600%20Series_Hardware%20Design_V2.01.pdf)
|
||||
- [AXP228 PMIC](http://www.x-powers.com/en.php/Info/product_detail/article_id/31)
|
||||
|
||||
| 组件 | 空闲 | 正常 | 峰值 |
|
||||
|-----------|------|--------|------|
|
||||
| CM5 CPU | 2W | 8W | 15W |
|
||||
| 4G 模块 | 0.5W | 3W | 10W |
|
||||
| 显示屏 | 1W | 2W | 3W |
|
||||
| 其他 | 1W | 2W | 2W |
|
||||
| **总计** | **4.5W** | **15W** | **30W** |
|
||||
**社区讨论:**
|
||||
- [uConsole 论坛:电池配置](https://forum.clockworkpi.com/t/understanding-battery-module-pinout-upgrading-battery-module/10260)
|
||||
- [4G 模块问题线程](https://forum.clockworkpi.com/t/battery-life-not-as-expected-4g-module-shuts-the-whole-thing-down/12633)
|
||||
|
||||
**AXP228 限制:**
|
||||
- 电池放电路径:约 18-20W 持续
|
||||
- USB + 电池:约 25W 持续
|
||||
- 峰值尖峰:短暂处理,然后 OCP 触发
|
||||
|
||||
**4G 激活 @ 1.8GHz 时:**
|
||||
- CM5:约 8-10W(从 12-15W 降低)
|
||||
- 4G:约 5-10W(传输期间)
|
||||
- 其他:约 3W
|
||||
- **总计:约 16-23W**(在限制内)
|
||||
|
||||
### API 端点
|
||||
|
||||
电池监控器提供 RESTful API:
|
||||
|
||||
```
|
||||
GET /api/battery # 当前电池数据
|
||||
GET /api/battery?save=true # 将当前读数保存到数据库
|
||||
GET /api/battery/history # 查询历史数据
|
||||
GET /api/battery/sessions # 列出所有会话
|
||||
GET /api/battery/sessions/:id # 获取特定会话数据
|
||||
POST /api/battery/sessions # 创建新会话
|
||||
PATCH /api/battery/sessions/:id # 更新会话名称/结束时间
|
||||
DELETE /api/battery/sessions/:id # 删除会话
|
||||
```
|
||||
|
||||
### BatteryData 接口
|
||||
|
||||
```typescript
|
||||
interface BatteryData {
|
||||
timestamp: string; // ISO 8601 格式
|
||||
percentage: number; // 0-100
|
||||
voltage: number; // 伏特(从 µV 转换)
|
||||
current: number; // 安培(从 µA 转换)
|
||||
power: number; // 瓦特(电压 * 电流)
|
||||
status: string; // "Charging", "Discharging", "Full"
|
||||
health: string; // "Good", "Unknown"
|
||||
acConnected: boolean; // 如果连接了 AC 适配器则为 true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**标签:** #uConsole #RaspberryPi #VibeCoding #NextJS #电池管理 #电源管理 #IoT #便携式计算 #AI辅助开发
|
||||
|
||||
**许可证:** GPL v3(与 uConsole 硬件设计相同)
|
||||
|
||||
---
|
||||
|
||||
[^1]: 电池配置 - [ClockworkPi 论坛讨论](https://forum.clockworkpi.com/t/understanding-battery-module-pinout-upgrading-battery-module/10260)
|
||||
|
||||
[^2]: AXP228 数据手册 - [X-Powers Technology](http://www.x-powers.com/en.php/Info/product_detail/article_id/31)
|
||||
**工具和库:**
|
||||
- [Next.js 16](https://nextjs.org/) - React 框架
|
||||
- [Recharts](https://recharts.org/) - React 图表库
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - UI 组件
|
||||
- [Claude Code](https://claude.com/claude-code) - AI 辅助开发
|
||||
|
||||
645
TOOL-GUIDE.md
Normal file
@@ -0,0 +1,645 @@
|
||||
# Battery Monitor Tool: Complete User Guide
|
||||
|
||||
> **Comprehensive documentation for the uConsole Battery Monitor and 4G Power Manager**
|
||||
|
||||
This tool was created to solve the uConsole CM5's 4G module hang issue and evolved into a complete battery analysis toolkit. For the story behind this tool, see [STORY.md](STORY.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [Battery Monitor Features](#battery-monitor-features)
|
||||
- [Understanding the Charts](#understanding-the-charts)
|
||||
- [Battery Testing Guide](#battery-testing-guide)
|
||||
- [4G Power Manager](#4g-power-manager)
|
||||
- [API Reference](#api-reference)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Advanced Usage](#advanced-usage)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://hiwifi.denq.us:8418/denq/battery-monitor.git
|
||||
cd battery-monitor
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run the development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Visit http://localhost:3000 in your browser.
|
||||
|
||||
### First Use
|
||||
|
||||
1. **Start Monitoring**: Click the "Start Monitoring" button
|
||||
2. **Observe Real-Time Data**: Watch voltage, current, power, and percentage update every 2 seconds
|
||||
3. **Start Recording** (optional): Click "Start Recording" to save data to the database
|
||||
4. **Export Data**: Download session data as CSV for further analysis
|
||||
|
||||
---
|
||||
|
||||
## Battery Monitor Features
|
||||
|
||||
### Two Operating Modes
|
||||
|
||||
#### 1. Live Mode (Default)
|
||||
- **All charts visible** with real-time updates
|
||||
- Best for active monitoring and analysis
|
||||
- Higher power consumption due to chart rendering
|
||||
|
||||
#### 2. Background Mode (Battery Saving)
|
||||
- **Charts hidden**, only stats displayed
|
||||
- Same data collection granularity
|
||||
- Saves ~20-40% power through reduced rendering
|
||||
- Perfect for long recording sessions
|
||||
|
||||
Toggle between modes using the "Live Mode" / "Background Mode" button.
|
||||
|
||||
### Monitoring vs. Recording
|
||||
|
||||
The tool separates **monitoring** (display only) from **recording** (database writes):
|
||||
|
||||
**Monitoring Mode:**
|
||||
- Click "Start Monitoring"
|
||||
- Data displayed in real-time
|
||||
- Last 100 points shown in charts
|
||||
- Up to 1000 points buffered in memory
|
||||
- **No database writes**
|
||||
|
||||
**Recording Mode:**
|
||||
- Click "Start Recording" while monitoring
|
||||
- Choose recording start point:
|
||||
- **"Record from now"**: New data only
|
||||
- **"Record from monitoring start"**: Saves all buffered data since monitoring began
|
||||
- Creates a session in database
|
||||
- All subsequent readings saved
|
||||
- Session metadata updated on stop
|
||||
|
||||
### Real-Time Metrics
|
||||
|
||||
The status card displays:
|
||||
|
||||
- **Charge**: Current battery percentage (0-100%)
|
||||
- **Voltage**: Real-time voltage in volts (V)
|
||||
- **Current**: Charge/discharge current in amps (A)
|
||||
- Positive = charging
|
||||
- Negative = discharging
|
||||
- **Power**: Instantaneous power in watts (W)
|
||||
- Positive = charging
|
||||
- Negative = discharging
|
||||
- **Status**: Charging, Discharging, Full, or Unknown
|
||||
- **Capacity**: Current/Full capacity in watt-hours (Wh)
|
||||
- **System Up**: Total system uptime
|
||||
|
||||
---
|
||||
|
||||
## Understanding the Charts
|
||||
|
||||
### 1. Battery Percentage
|
||||
|
||||

|
||||
|
||||
**Purpose:** Shows battery charge level over time
|
||||
|
||||
**Reading the chart:**
|
||||
- Y-axis: Percentage (0-100%)
|
||||
- X-axis: Time
|
||||
- Slope indicates discharge/charge rate
|
||||
- Steeper slope = faster discharge
|
||||
|
||||
**Use cases:**
|
||||
- Monitor discharge rate under different loads
|
||||
- Verify charging behavior
|
||||
- Estimate remaining runtime
|
||||
|
||||
### 2. Power Consumption
|
||||
|
||||
**Purpose:** Displays real-time power draw or input
|
||||
|
||||
**Reading the chart:**
|
||||
- Negative values: System consuming power (discharging)
|
||||
- Positive values: Charging (rare in typical usage)
|
||||
- Spikes indicate high-power activities
|
||||
|
||||
**Use cases:**
|
||||
- Identify power-hungry processes
|
||||
- Observe 4G module transmission bursts
|
||||
- Optimize power consumption
|
||||
|
||||
### 3. Voltage & Current Trends
|
||||
|
||||

|
||||
|
||||
**Purpose:** Dual-axis chart showing voltage and current simultaneously
|
||||
|
||||
**Reading the chart:**
|
||||
- Left Y-axis: Voltage (V) - orange line
|
||||
- Right Y-axis: Current (A) - green line
|
||||
- X-axis: Time
|
||||
- Legend shows current values on hover
|
||||
|
||||
**Key observations:**
|
||||
- Voltage should stay above 3.45V for stable 4G operation
|
||||
- High current draw causes voltage sag
|
||||
- Voltage recovery when current drops
|
||||
|
||||
**Use cases:**
|
||||
- Diagnose voltage drop issues
|
||||
- Correlate current spikes with voltage sag
|
||||
- Monitor battery internal resistance (voltage sag magnitude)
|
||||
|
||||
### 4. Battery Energy Output vs. Voltage ⭐ **NEW**
|
||||
|
||||

|
||||
|
||||
**Purpose:** **The most critical chart for battery selection** - shows discharge curve with voltage threshold
|
||||
|
||||
**Reading the chart:**
|
||||
- X-axis: Energy output in milliwatt-hours (mWh)
|
||||
- Y-axis: Battery voltage (V)
|
||||
- Red dashed line: 3.45V threshold (4G module minimum)
|
||||
- Purple line: Actual voltage decay during discharge
|
||||
|
||||
**Key insight:**
|
||||
- Everything to the RIGHT of where voltage crosses 3.45V is **unusable for 4G**
|
||||
- This chart answers: "How much usable capacity does this battery have for 4G operation?"
|
||||
|
||||
**Example (FEB-4000 batteries):**
|
||||
- Total capacity: 24,790 mWh
|
||||
- Voltage crosses 3.45V at: 13,387 mWh
|
||||
- **Usable capacity: 13,387 mWh (54%)**
|
||||
- **Unusable capacity: 11,403 mWh (46%)**
|
||||
|
||||
**Use cases:**
|
||||
- **Rate batteries for 4G compatibility**
|
||||
- Compare different battery models
|
||||
- Determine when to recharge for reliable 4G
|
||||
- Understand real vs. advertised capacity
|
||||
|
||||
---
|
||||
|
||||
## Battery Testing Guide
|
||||
|
||||
### How to Test Your Batteries
|
||||
|
||||
1. **Fully Charge Batteries**: Charge to 100% before testing
|
||||
2. **Start Monitoring**: Click "Start Monitoring"
|
||||
3. **Start Recording**: Choose "Record from monitoring start"
|
||||
4. **Name Your Session**: Edit session name (e.g., "Samsung-30Q-Test")
|
||||
5. **Enable 4G Module**: Turn on 4G for realistic load
|
||||
6. **Let It Discharge**: Run until voltage drops to ~3.3V (or system shuts down)
|
||||
7. **Stop Recording**: Click "Stop Recording"
|
||||
8. **Analyze Results**: View session data and export CSV
|
||||
|
||||
### What to Look For
|
||||
|
||||
#### Good Batteries for 4G:
|
||||
✅ Voltage stays above 3.45V for >50% of capacity
|
||||
✅ Gradual voltage decay under load
|
||||
✅ Low voltage sag during current spikes
|
||||
✅ High usable capacity above 3.45V threshold
|
||||
|
||||
#### Poor Batteries for 4G:
|
||||
❌ Voltage drops below 3.45V early (<30% capacity used)
|
||||
❌ Steep voltage drops under load
|
||||
❌ Large voltage sag (>0.2V) during current spikes
|
||||
❌ Low usable capacity above 3.45V
|
||||
|
||||
### Recommended Test Conditions
|
||||
|
||||
**For consistent results:**
|
||||
- Same load (4G module enabled)
|
||||
- Full discharge cycle (100% → protection cutoff)
|
||||
- Room temperature (20-25°C)
|
||||
- No charging during test
|
||||
- Minimal other activity (close apps, disable wifi if testing 4G-only)
|
||||
|
||||
### Interpreting Results
|
||||
|
||||
**Usable Capacity Calculation:**
|
||||
```
|
||||
Usable Capacity (mWh) = Initial Capacity - Capacity at 3.45V crossing
|
||||
Usable Percentage = (Usable Capacity / Total Capacity) × 100%
|
||||
```
|
||||
|
||||
**Battery Rating:**
|
||||
- **Excellent**: >60% usable capacity above 3.45V
|
||||
- **Good**: 50-60% usable capacity
|
||||
- **Acceptable**: 40-50% usable capacity
|
||||
- **Poor**: <40% usable capacity (consider replacement)
|
||||
|
||||
---
|
||||
|
||||
## uConsole Smart Power Regulator
|
||||
|
||||
### What It Does
|
||||
|
||||
Intelligently manages CPU performance and monitors battery voltage based on power source and 4G module status to prevent system hangs caused by voltage drops below 3.45V.
|
||||
|
||||
### How It Works
|
||||
|
||||
The unified system combines three monitoring mechanisms:
|
||||
|
||||
1. **Event-Driven Detection** (udev events):
|
||||
- AC power connect/disconnect events
|
||||
- 4G modem USB device changes
|
||||
- Network interface (wwan0) state changes
|
||||
- Modem serial port changes
|
||||
|
||||
2. **Background Daemon** (5-second polling):
|
||||
- Monitors 4G modem state continuously
|
||||
- Monitors AC power status
|
||||
- Triggers regulator only when state changes
|
||||
- Handles edge cases (e.g., service starts with AC connected)
|
||||
|
||||
3. **Voltage Monitoring** (when Battery + 4G active):
|
||||
- Checks voltage every 5 seconds
|
||||
- Alerts when voltage < 3.45V
|
||||
- Multiple alert methods: desktop notification, audio, log, LED blink
|
||||
- Rate-limited to every 30 seconds
|
||||
|
||||
### Power Modes
|
||||
|
||||
| Condition | CPU Governor | Max Frequency | Voltage Monitoring |
|
||||
|-----------|-------------|---------------|-------------------|
|
||||
| **AC Connected** | ondemand | 2.4GHz | Off |
|
||||
| **Battery + 4G** | powersave | 1.8GHz | On (< 3.45V alerts) |
|
||||
| **Battery Only** | ondemand | 2.0GHz | Off |
|
||||
|
||||
### Installation
|
||||
|
||||
**Fresh Install:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh install
|
||||
```
|
||||
|
||||
**Upgrade from old 4G Power Manager:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh upgrade
|
||||
```
|
||||
|
||||
**Uninstall:**
|
||||
```bash
|
||||
cd scripts
|
||||
sudo ./install-uconsole-power-regulator.sh uninstall
|
||||
```
|
||||
|
||||
**What it installs:**
|
||||
- `uconsole-power-regulator.sh` - Main power regulation orchestrator
|
||||
- `uconsole-power-daemon.sh` - Background state monitoring daemon
|
||||
- `voltage-monitor.sh` - Voltage monitoring script (runs when Battery + 4G)
|
||||
- `voltage-alert-notify.sh` - Multi-method alert system
|
||||
- `voltage-monitor-control.sh` - Start/stop voltage monitor
|
||||
- `/etc/udev/rules.d/99-uconsole-power-regulator.rules` - udev event triggers
|
||||
- `/etc/systemd/system/uconsole-power-regulator.service` - Systemd service
|
||||
- `/var/log/uconsole-power-regulator.log` - Unified log file
|
||||
|
||||
### Verification
|
||||
|
||||
Check if service is running:
|
||||
```bash
|
||||
sudo systemctl status uconsole-power-regulator
|
||||
```
|
||||
|
||||
View recent logs:
|
||||
```bash
|
||||
sudo tail -f /var/log/uconsole-power-regulator.log
|
||||
```
|
||||
|
||||
Check voltage monitor status:
|
||||
```bash
|
||||
sudo /usr/local/bin/voltage-monitor-control.sh status
|
||||
```
|
||||
|
||||
### Tuning
|
||||
|
||||
Edit `/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh` to customize power modes:
|
||||
|
||||
```bash
|
||||
# Configuration options
|
||||
AC_GOVERNOR="ondemand" # Governor when AC connected
|
||||
BATTERY_GOVERNOR="ondemand" # Governor on battery without 4G
|
||||
POWERSAVE_GOVERNOR="powersave" # Governor on battery with 4G
|
||||
|
||||
MAX_FREQ_AC="2400000" # 2.4GHz - AC power, full performance
|
||||
MAX_FREQ_BATTERY="2000000" # 2.0GHz - battery only, balanced
|
||||
MAX_FREQ_POWERSAVE="1800000" # 1.8GHz - battery + 4G (try 1500000 for more savings)
|
||||
|
||||
# Performance configuration
|
||||
MAX_FREQ_PERFORMANCE="2400000" # Full CM5 speed
|
||||
PERFORMANCE_GOVERNOR="ondemand" # Or "schedutil"
|
||||
```
|
||||
|
||||
After editing, restart the service:
|
||||
```bash
|
||||
sudo systemctl restart 4g-power-monitor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `GET /api/battery`
|
||||
Get current battery status
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-11-06T12:00:00.000Z",
|
||||
"percentage": 75,
|
||||
"voltage": 3.85,
|
||||
"current": -1.5,
|
||||
"power": -5.775,
|
||||
"status": "Discharging",
|
||||
"health": "Good",
|
||||
"acConnected": false,
|
||||
"capacityFull": 24.8,
|
||||
"capacityNow": 18.6,
|
||||
"capacityDesign": 25.0,
|
||||
"systemUptime": 12345
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /api/battery?save=true&sessionId=123`
|
||||
Get current battery status and save to database
|
||||
|
||||
**Query parameters:**
|
||||
- `save`: Set to `true` to save reading
|
||||
- `sessionId`: Optional session ID to associate reading with
|
||||
|
||||
#### `GET /api/battery/history?start=<ISO>&end=<ISO>`
|
||||
Query historical readings by time range
|
||||
|
||||
**Query parameters:**
|
||||
- `start`: ISO 8601 timestamp (start of range)
|
||||
- `end`: ISO 8601 timestamp (end of range)
|
||||
|
||||
**Response:** Array of battery readings
|
||||
|
||||
#### `GET /api/battery/sessions`
|
||||
List all recording sessions
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "FEB-4000 Test",
|
||||
"start_time": "2025-11-06T11:29:25.000Z",
|
||||
"end_time": "2025-11-06T14:17:51.000Z",
|
||||
"reading_count": 4318,
|
||||
"energy_wh": 17.874,
|
||||
"duration_seconds": 10106
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `GET /api/battery/sessions/:id`
|
||||
Get all readings for a specific session
|
||||
|
||||
**Response:** Array of battery readings for the session
|
||||
|
||||
#### `POST /api/battery/sessions`
|
||||
Create a new recording session
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"start_time": "2025-11-06T11:29:25.000Z",
|
||||
"end_time": "2025-11-06T11:29:25.000Z",
|
||||
"name": "My Test Session"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": 123
|
||||
}
|
||||
```
|
||||
|
||||
#### `PATCH /api/battery/sessions/:id`
|
||||
Update session name or end time
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Session Name",
|
||||
"end_time": "2025-11-06T14:17:51.000Z",
|
||||
"reading_count": 4318
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /api/battery/sessions/:id`
|
||||
Delete a session and all associated readings
|
||||
|
||||
#### `POST /api/battery/save`
|
||||
Save buffered readings (used for retroactive recording)
|
||||
|
||||
**Request body:**
|
||||
```json
|
||||
{
|
||||
"readings": [...],
|
||||
"sessionId": 123
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Battery Monitor
|
||||
|
||||
**Issue: "Unable to read battery data" error**
|
||||
|
||||
Solution:
|
||||
- Ensure you're running on uConsole or compatible hardware
|
||||
- Check sysfs path exists: `ls /sys/class/power_supply/axp20x-battery/`
|
||||
- Verify permissions (may need to run dev server with sudo)
|
||||
|
||||
**Issue: Charts not updating**
|
||||
|
||||
Solution:
|
||||
- Refresh the page
|
||||
- Check browser console for errors
|
||||
- Ensure monitoring is active (green "Monitoring" badge visible)
|
||||
|
||||
**Issue: Database errors when recording**
|
||||
|
||||
Solution:
|
||||
- Check disk space: `df -h`
|
||||
- Verify `battery-data.db` permissions
|
||||
- Check SQLite is working: `npm run dev` should create DB automatically
|
||||
|
||||
### 4G Power Manager
|
||||
|
||||
**Issue: 4G module still hangs**
|
||||
|
||||
Solution:
|
||||
1. Check if service is running: `sudo systemctl status 4g-power-monitor`
|
||||
2. View logs: `sudo tail -f /var/log/4g-power-manager.log`
|
||||
3. Verify frequency is being changed: `cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq`
|
||||
4. Try lower frequency: Edit script, set `MAX_FREQ_POWERSAVE="1500000"`
|
||||
5. Check battery voltage in real-time during 4G use (should stay >3.45V)
|
||||
|
||||
**Issue: Service won't start**
|
||||
|
||||
Solution:
|
||||
```bash
|
||||
# Check service status
|
||||
sudo systemctl status 4g-power-monitor
|
||||
|
||||
# View service logs
|
||||
sudo journalctl -u 4g-power-monitor -n 50
|
||||
|
||||
# Reinstall
|
||||
cd scripts
|
||||
sudo ./install-4g-power-manager.sh uninstall
|
||||
sudo ./install-4g-power-manager.sh
|
||||
```
|
||||
|
||||
**Issue: udev rules not triggering**
|
||||
|
||||
Solution:
|
||||
```bash
|
||||
# Reload udev rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
|
||||
# Check if rules are loaded
|
||||
sudo udevadm control --reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Time Range Export
|
||||
|
||||
1. Navigate to "Export Custom Time Range" card
|
||||
2. Set start date/time
|
||||
3. Set end date/time
|
||||
4. Click "Load Data" to preview
|
||||
5. Click "Export CSV" to download
|
||||
|
||||
### Session Management
|
||||
|
||||
**Rename a session:**
|
||||
- Click the edit icon (pencil) next to session name
|
||||
- Type new name
|
||||
- Press Enter or click checkmark
|
||||
|
||||
**Delete a session:**
|
||||
- Click the trash icon
|
||||
- Confirm deletion
|
||||
- Session and ALL associated readings are permanently deleted
|
||||
|
||||
**Download session data:**
|
||||
- Click the download icon
|
||||
- CSV file downloads with session name
|
||||
|
||||
### CSV Data Format
|
||||
|
||||
Exported CSV contains:
|
||||
```csv
|
||||
timestamp,percentage,voltage,current,power,status,health,ac_connected,capacity_full,capacity_now,capacity_design
|
||||
2025-11-06T12:00:00.000Z,75,3.85,-1.5,-5.775,Discharging,Good,false,24.8,18.6,25.0
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
**`monitoring_sessions` table:**
|
||||
- `id` - Auto-increment primary key
|
||||
- `name` - Session name (editable)
|
||||
- `start_time` - ISO 8601 timestamp
|
||||
- `end_time` - ISO 8601 timestamp
|
||||
- `reading_count` - Number of readings
|
||||
- `created_at` - Database insertion time
|
||||
|
||||
**`battery_readings` table:**
|
||||
- `id` - Auto-increment primary key
|
||||
- `session_id` - Foreign key to sessions (nullable)
|
||||
- `timestamp` - Reading timestamp
|
||||
- `percentage`, `voltage`, `current`, `power` - Battery metrics
|
||||
- `status`, `health`, `ac_connected` - State info
|
||||
- `capacity_full`, `capacity_now`, `capacity_design` - Energy capacity
|
||||
- `created_at` - Database insertion time
|
||||
|
||||
### Development
|
||||
|
||||
**Run in production mode:**
|
||||
```bash
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
**Run tests:**
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
**Lint code:**
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Areas for improvement:
|
||||
- Additional chart types
|
||||
- Battery comparison tools
|
||||
- Export formats (JSON, XML)
|
||||
- Mobile-responsive design improvements
|
||||
- Internationalization (i18n)
|
||||
|
||||
**Submit issues or PRs:**
|
||||
- Gitea: https://hiwifi.denq.us:8418/denq/battery-monitor
|
||||
- Forum: https://forum.clockworkpi.com/c/uconsole
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
GPL v3 - Same as uConsole hardware designs
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
**Created by:** denq
|
||||
**Powered by:** Next.js 16, React 19, Recharts, SQLite
|
||||
**Built with:** Claude Code (AI-assisted development)
|
||||
**Hardware:** ClockworkPi uConsole CM5
|
||||
|
||||
**Community contributors:**
|
||||
- Battery test data submissions
|
||||
- 4G power manager feedback
|
||||
- Bug reports and feature requests
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [STORY.md](STORY.md) - The story behind this tool
|
||||
- [README.md](README.md) - Project overview
|
||||
- [scripts/README-4G-POWER-MANAGER.md](scripts/README-4G-POWER-MANAGER.md) - 4G power manager deep dive
|
||||
- [ClockworkPi uConsole](https://www.clockworkpi.com/uconsole)
|
||||
- [ClockworkPi Forum](https://forum.clockworkpi.com/c/uconsole)
|
||||
|
||||
---
|
||||
|
||||
**Last updated:** November 2025
|
||||
**Tool version:** 1.0
|
||||
**Compatible with:** uConsole CM5, Raspberry Pi CM4/CM5 with AXP20x battery management
|
||||
@@ -82,6 +82,17 @@ apply_cpu_settings() {
|
||||
done
|
||||
}
|
||||
|
||||
# Check if AC power is connected
|
||||
check_ac_connected() {
|
||||
if [ -f /sys/class/power_supply/axp22x-ac/online ]; then
|
||||
local ac_online=$(cat /sys/class/power_supply/axp22x-ac/online 2>/dev/null)
|
||||
if [ "$ac_online" = "1" ]; then
|
||||
return 0 # AC is connected
|
||||
fi
|
||||
fi
|
||||
return 1 # AC is not connected (on battery)
|
||||
}
|
||||
|
||||
# Main logic
|
||||
main() {
|
||||
log "=== 4G Power Manager triggered ==="
|
||||
@@ -92,6 +103,17 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if AC power is connected
|
||||
if check_ac_connected; then
|
||||
log "AC power connected - Skipping power management (not needed on AC power)"
|
||||
# Restore normal CPU settings when on AC
|
||||
apply_cpu_settings "$NORMAL_GOVERNOR" "$MAX_FREQ_NORMAL"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only apply power-saving if on battery power
|
||||
log "Running on battery power - Checking 4G modem state"
|
||||
|
||||
# Determine modem state
|
||||
if check_modem_active; then
|
||||
log "4G modem is ACTIVE - Applying power-saving mode"
|
||||
|
||||
@@ -6,14 +6,20 @@
|
||||
#
|
||||
# The script automatically adjusts CPU governor and frequency to prevent
|
||||
# system hangs due to insufficient battery power when 4G is active.
|
||||
#
|
||||
# Additionally, voltage monitoring is started when 4G becomes active and
|
||||
# stopped when 4G is deactivated, alerting users if battery voltage drops
|
||||
# below 3.45V (4G module minimum operating voltage).
|
||||
|
||||
# Trigger on Qualcomm/SimTech 4G modem USB device changes
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="1e0e", ATTR{idProduct}=="9001", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh"
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="1e0e", ATTR{idProduct}=="9001", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh"
|
||||
|
||||
# Trigger on wwan0 network interface changes (up/down)
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh"
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh"
|
||||
# When 4G activates: adjust CPU power AND start voltage monitoring
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh", RUN+="/usr/local/bin/voltage-monitor-control.sh start"
|
||||
# When 4G deactivates: restore CPU power AND stop voltage monitoring
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh", RUN+="/usr/local/bin/voltage-monitor-control.sh stop"
|
||||
|
||||
# Trigger on modem manager state changes via cdc-wdm0 (modem control device)
|
||||
SUBSYSTEM=="usb", KERNEL=="cdc-wdm0", ACTION=="change", RUN+="/home/pi/battery-monitor/scripts/4g-power-manager.sh"
|
||||
|
||||
37
scripts/99-uconsole-power-regulator.rules
Normal file
@@ -0,0 +1,37 @@
|
||||
# udev rules for uConsole Smart Power Regulator
|
||||
#
|
||||
# This rule set triggers intelligent power management based on:
|
||||
# 1. 4G modem state changes (USB device, network interface, modem control)
|
||||
# 2. AC power state changes (power_supply subsystem)
|
||||
#
|
||||
# The power regulator automatically:
|
||||
# • Reduces CPU frequency when on battery with 4G active (prevents system hangs)
|
||||
# • Starts voltage monitoring when needed (alerts if voltage < 3.45V)
|
||||
# • Restores full performance when AC connected or 4G inactive
|
||||
# • Handles edge cases (AC already connected at boot, etc.)
|
||||
|
||||
# ============================================================================
|
||||
# AC Power State Changes
|
||||
# ============================================================================
|
||||
|
||||
# Trigger on AC adapter connect/disconnect events
|
||||
SUBSYSTEM=="power_supply", KERNEL=="axp22x-ac", ACTION=="change", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
|
||||
# ============================================================================
|
||||
# 4G Modem State Changes
|
||||
# ============================================================================
|
||||
|
||||
# Trigger on Qualcomm/SimTech 4G modem USB device changes
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="1e0e", ATTR{idProduct}=="9001", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="1e0e", ATTR{idProduct}=="9001", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
|
||||
# Trigger on wwan0 network interface changes (up/down)
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
SUBSYSTEM=="net", KERNEL=="wwan0", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
|
||||
# Trigger on modem manager state changes via cdc-wdm0 (modem control device)
|
||||
SUBSYSTEM=="usb", KERNEL=="cdc-wdm0", ACTION=="change", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
|
||||
# Trigger on ttyUSB device changes (modem serial ports)
|
||||
SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-4]", ACTION=="add", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
SUBSYSTEM=="tty", KERNEL=="ttyUSB[0-4]", ACTION=="remove", RUN+="/home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
@@ -26,10 +26,30 @@ install_components() {
|
||||
echo "=== 4G Power Manager Installation ==="
|
||||
echo ""
|
||||
|
||||
# Install bc for power calculations
|
||||
# Install dependencies
|
||||
echo "Checking and installing dependencies..."
|
||||
PACKAGES_TO_INSTALL=""
|
||||
|
||||
# bc - for power/voltage calculations
|
||||
if ! command -v bc &> /dev/null; then
|
||||
echo "Installing bc for power calculations..."
|
||||
apt-get update && apt-get install -y bc
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL bc"
|
||||
fi
|
||||
|
||||
# libnotify-bin - for desktop notifications (notify-send)
|
||||
if ! command -v notify-send &> /dev/null; then
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL libnotify-bin"
|
||||
fi
|
||||
|
||||
# pulseaudio-utils - for audio alerts (paplay)
|
||||
if ! command -v paplay &> /dev/null; then
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL pulseaudio-utils"
|
||||
fi
|
||||
|
||||
if [ -n "$PACKAGES_TO_INSTALL" ]; then
|
||||
echo "Installing: $PACKAGES_TO_INSTALL"
|
||||
apt-get update && apt-get install -y $PACKAGES_TO_INSTALL
|
||||
else
|
||||
echo "✓ All dependencies already installed"
|
||||
fi
|
||||
|
||||
# Install udev rules
|
||||
@@ -44,6 +64,16 @@ install_components() {
|
||||
echo "✓ Reloaded udev rules"
|
||||
fi
|
||||
|
||||
# Install voltage monitoring scripts
|
||||
echo "Installing voltage monitoring scripts..."
|
||||
cp "$SCRIPT_DIR/voltage-monitor.sh" /usr/local/bin/
|
||||
cp "$SCRIPT_DIR/voltage-alert-notify.sh" /usr/local/bin/
|
||||
cp "$SCRIPT_DIR/voltage-monitor-control.sh" /usr/local/bin/
|
||||
chmod +x /usr/local/bin/voltage-monitor.sh
|
||||
chmod +x /usr/local/bin/voltage-alert-notify.sh
|
||||
chmod +x /usr/local/bin/voltage-monitor-control.sh
|
||||
echo "✓ Installed voltage monitoring scripts to /usr/local/bin/"
|
||||
|
||||
# Install systemd service
|
||||
if [[ "$INSTALL_MODE" == "daemon" ]] || [[ "$INSTALL_MODE" == "both" ]]; then
|
||||
echo "Installing systemd service..."
|
||||
@@ -72,11 +102,21 @@ install_components() {
|
||||
echo " • Detect when 4G modem becomes active"
|
||||
echo " • Automatically reduce CPU frequency to 1.8GHz"
|
||||
echo " • Switch to powersave governor"
|
||||
echo " • Start voltage monitoring when 4G is active"
|
||||
echo " • Alert if battery voltage drops below 3.45V"
|
||||
echo " • Restore normal settings when 4G is inactive"
|
||||
echo ""
|
||||
echo "Voltage Alert Features:"
|
||||
echo " • Desktop notification (popup)"
|
||||
echo " • Audio alert (beep/system sound)"
|
||||
echo " • Log file entry"
|
||||
echo " • LED blink (if available)"
|
||||
echo " • Alerts every 30 seconds while voltage is low"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " • View logs: tail -f /var/log/4g-power-manager.log"
|
||||
echo " • Check service: systemctl status 4g-power-monitor"
|
||||
echo " • Check voltage monitor: sudo /usr/local/bin/voltage-monitor-control.sh status"
|
||||
echo " • Stop service: sudo systemctl stop 4g-power-monitor"
|
||||
echo " • Disable service: sudo systemctl disable 4g-power-monitor"
|
||||
echo " • Uninstall: sudo $0 uninstall"
|
||||
@@ -130,9 +170,27 @@ uninstall_components() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Stop voltage monitor if running
|
||||
echo "Stopping voltage monitor..."
|
||||
if [ -x /usr/local/bin/voltage-monitor-control.sh ]; then
|
||||
/usr/local/bin/voltage-monitor-control.sh stop 2>/dev/null || true
|
||||
echo "✓ Stopped voltage monitor"
|
||||
fi
|
||||
|
||||
# Remove voltage monitoring scripts
|
||||
if [ -f /usr/local/bin/voltage-monitor.sh ] || \
|
||||
[ -f /usr/local/bin/voltage-alert-notify.sh ] || \
|
||||
[ -f /usr/local/bin/voltage-monitor-control.sh ]; then
|
||||
echo "Removing voltage monitoring scripts..."
|
||||
rm -f /usr/local/bin/voltage-monitor.sh
|
||||
rm -f /usr/local/bin/voltage-alert-notify.sh
|
||||
rm -f /usr/local/bin/voltage-monitor-control.sh
|
||||
echo "✓ Removed voltage monitoring scripts"
|
||||
fi
|
||||
|
||||
# Clean up temporary files
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -f /tmp/4g-modem-state /tmp/4g-modem-last-state
|
||||
rm -f /tmp/4g-modem-state /tmp/4g-modem-last-state /run/voltage-monitor.pid
|
||||
echo "✓ Removed temporary state files"
|
||||
|
||||
# Ask about log file
|
||||
|
||||
343
scripts/install-uconsole-power-regulator.sh
Executable file
@@ -0,0 +1,343 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Installation/Uninstallation script for uConsole Smart Power Regulator
|
||||
# Usage:
|
||||
# sudo ./install-uconsole-power-regulator.sh [install|uninstall|upgrade]
|
||||
#
|
||||
# Examples:
|
||||
# sudo ./install-uconsole-power-regulator.sh install # Fresh install
|
||||
# sudo ./install-uconsole-power-regulator.sh upgrade # Upgrade from 4G Power Manager
|
||||
# sudo ./install-uconsole-power-regulator.sh uninstall # Complete removal
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ACTION=${1:-"install"}
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: Please run as root (use sudo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to install components
|
||||
install_components() {
|
||||
echo "=========================================="
|
||||
echo "uConsole Smart Power Regulator"
|
||||
echo "Installation"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Install dependencies
|
||||
echo "Checking and installing dependencies..."
|
||||
PACKAGES_TO_INSTALL=""
|
||||
|
||||
# bc - for power/voltage calculations
|
||||
if ! command -v bc &> /dev/null; then
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL bc"
|
||||
fi
|
||||
|
||||
# libnotify-bin - for desktop notifications (notify-send)
|
||||
if ! command -v notify-send &> /dev/null; then
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL libnotify-bin"
|
||||
fi
|
||||
|
||||
# pulseaudio-utils - for audio alerts (paplay)
|
||||
if ! command -v paplay &> /dev/null; then
|
||||
PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL pulseaudio-utils"
|
||||
fi
|
||||
|
||||
if [ -n "$PACKAGES_TO_INSTALL" ]; then
|
||||
echo "Installing: $PACKAGES_TO_INSTALL"
|
||||
apt-get update && apt-get install -y $PACKAGES_TO_INSTALL
|
||||
else
|
||||
echo "✓ All dependencies already installed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Ensure main power regulator script is executable
|
||||
echo "Checking power regulator script..."
|
||||
if [ ! -f "$SCRIPT_DIR/uconsole-power-regulator.sh" ]; then
|
||||
echo "ERROR: uconsole-power-regulator.sh not found in $SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
chmod +x "$SCRIPT_DIR/uconsole-power-regulator.sh"
|
||||
chmod +x "$SCRIPT_DIR/uconsole-power-daemon.sh"
|
||||
echo "✓ Power regulator scripts are executable"
|
||||
echo ""
|
||||
|
||||
# Install voltage monitoring scripts
|
||||
echo "Installing voltage monitoring scripts..."
|
||||
cp "$SCRIPT_DIR/voltage-monitor.sh" /usr/local/bin/
|
||||
cp "$SCRIPT_DIR/voltage-alert-notify.sh" /usr/local/bin/
|
||||
cp "$SCRIPT_DIR/voltage-monitor-control.sh" /usr/local/bin/
|
||||
chmod +x /usr/local/bin/voltage-monitor.sh
|
||||
chmod +x /usr/local/bin/voltage-alert-notify.sh
|
||||
chmod +x /usr/local/bin/voltage-monitor-control.sh
|
||||
echo "✓ Installed voltage monitoring scripts to /usr/local/bin/"
|
||||
echo ""
|
||||
|
||||
# Install udev rules
|
||||
echo "Installing udev rules..."
|
||||
cp "$SCRIPT_DIR/99-uconsole-power-regulator.rules" /etc/udev/rules.d/
|
||||
echo "✓ Copied udev rules to /etc/udev/rules.d/"
|
||||
|
||||
# Reload udev rules
|
||||
udevadm control --reload-rules
|
||||
udevadm trigger
|
||||
echo "✓ Reloaded udev rules"
|
||||
echo ""
|
||||
|
||||
# Install systemd service
|
||||
echo "Installing systemd service..."
|
||||
cp "$SCRIPT_DIR/uconsole-power-regulator.service" /etc/systemd/system/
|
||||
echo "✓ Copied service file to /etc/systemd/system/"
|
||||
|
||||
# Reload systemd
|
||||
systemctl daemon-reload
|
||||
echo "✓ Reloaded systemd"
|
||||
|
||||
# Enable and start service
|
||||
systemctl enable uconsole-power-regulator.service
|
||||
systemctl start uconsole-power-regulator.service
|
||||
echo "✓ Enabled and started uconsole-power-regulator service"
|
||||
echo ""
|
||||
|
||||
# Create log file with proper permissions
|
||||
touch /var/log/uconsole-power-regulator.log
|
||||
chmod 644 /var/log/uconsole-power-regulator.log
|
||||
echo "✓ Created log file at /var/log/uconsole-power-regulator.log"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Installation Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "The uConsole Smart Power Regulator is now active and will:"
|
||||
echo ""
|
||||
echo "Power Management:"
|
||||
echo " • AC Connected → Full performance (2.4GHz)"
|
||||
echo " • Battery + 4G → Power-saving mode (1.8GHz)"
|
||||
echo " • Battery only → Balanced performance (2.0GHz)"
|
||||
echo ""
|
||||
echo "Voltage Monitoring:"
|
||||
echo " • Active when: Battery + 4G enabled"
|
||||
echo " • Alert threshold: < 3.45V"
|
||||
echo " • Alert methods: Desktop popup, audio, log, LED"
|
||||
echo " • Alert frequency: Every 30 seconds when low"
|
||||
echo ""
|
||||
echo "Useful Commands:"
|
||||
echo " • View logs: tail -f /var/log/uconsole-power-regulator.log"
|
||||
echo " • Check service: systemctl status uconsole-power-regulator"
|
||||
echo " • Check voltage monitor: sudo /usr/local/bin/voltage-monitor-control.sh status"
|
||||
echo " • Stop service: sudo systemctl stop uconsole-power-regulator"
|
||||
echo " • Disable service: sudo systemctl disable uconsole-power-regulator"
|
||||
echo " • Uninstall: sudo $0 uninstall"
|
||||
echo " • Test manually: sudo /home/pi/battery-monitor/scripts/uconsole-power-regulator.sh"
|
||||
echo ""
|
||||
echo "Current System State:"
|
||||
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null | sed 's/^/ Governor: /' || echo " Governor: N/A"
|
||||
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq 2>/dev/null | sed 's/^/ Max freq: /' || echo " Max freq: N/A"
|
||||
|
||||
if [ -f /sys/class/power_supply/axp22x-ac/online ]; then
|
||||
AC_STATUS=$(cat /sys/class/power_supply/axp22x-ac/online)
|
||||
if [ "$AC_STATUS" = "1" ]; then
|
||||
echo " AC Power: CONNECTED"
|
||||
else
|
||||
echo " AC Power: DISCONNECTED (on battery)"
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Function to upgrade from old 4G Power Manager
|
||||
upgrade_from_old() {
|
||||
echo "=========================================="
|
||||
echo "Upgrading from 4G Power Manager"
|
||||
echo "to uConsole Smart Power Regulator"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Stop and disable old service
|
||||
if systemctl is-active --quiet 4g-power-monitor.service 2>/dev/null; then
|
||||
echo "Stopping old 4g-power-monitor service..."
|
||||
systemctl stop 4g-power-monitor.service
|
||||
echo "✓ Stopped old service"
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet 4g-power-monitor.service 2>/dev/null; then
|
||||
echo "Disabling old 4g-power-monitor service..."
|
||||
systemctl disable 4g-power-monitor.service
|
||||
echo "✓ Disabled old service"
|
||||
fi
|
||||
|
||||
if [ -f /etc/systemd/system/4g-power-monitor.service ]; then
|
||||
echo "Removing old service file..."
|
||||
rm /etc/systemd/system/4g-power-monitor.service
|
||||
echo "✓ Removed old service file"
|
||||
fi
|
||||
|
||||
# Remove old udev rules
|
||||
if [ -f /etc/udev/rules.d/99-4g-power-manager.rules ]; then
|
||||
echo "Removing old udev rules..."
|
||||
rm /etc/udev/rules.d/99-4g-power-manager.rules
|
||||
echo "✓ Removed old udev rules"
|
||||
fi
|
||||
|
||||
# Stop voltage monitor if running
|
||||
if [ -x /usr/local/bin/voltage-monitor-control.sh ]; then
|
||||
/usr/local/bin/voltage-monitor-control.sh stop 2>/dev/null || true
|
||||
echo "✓ Stopped voltage monitor"
|
||||
fi
|
||||
|
||||
# Migrate old log file
|
||||
if [ -f /var/log/4g-power-manager.log ]; then
|
||||
echo "Migrating old log file..."
|
||||
cp /var/log/4g-power-manager.log /var/log/uconsole-power-regulator.log
|
||||
echo "========================================" >> /var/log/uconsole-power-regulator.log
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Upgraded to uConsole Smart Power Regulator" >> /var/log/uconsole-power-regulator.log
|
||||
echo "========================================" >> /var/log/uconsole-power-regulator.log
|
||||
echo "✓ Migrated log file"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
echo "✓ Reloaded systemd"
|
||||
echo ""
|
||||
|
||||
# Now install new components
|
||||
install_components
|
||||
}
|
||||
|
||||
# Function to uninstall components
|
||||
uninstall_components() {
|
||||
echo "=========================================="
|
||||
echo "uConsole Smart Power Regulator"
|
||||
echo "Uninstallation"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Stop and disable systemd service
|
||||
if systemctl is-active --quiet uconsole-power-regulator.service 2>/dev/null; then
|
||||
echo "Stopping uconsole-power-regulator service..."
|
||||
systemctl stop uconsole-power-regulator.service
|
||||
echo "✓ Stopped service"
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet uconsole-power-regulator.service 2>/dev/null; then
|
||||
echo "Disabling uconsole-power-regulator service..."
|
||||
systemctl disable uconsole-power-regulator.service
|
||||
echo "✓ Disabled service"
|
||||
fi
|
||||
|
||||
if [ -f /etc/systemd/system/uconsole-power-regulator.service ]; then
|
||||
echo "Removing service file..."
|
||||
rm /etc/systemd/system/uconsole-power-regulator.service
|
||||
echo "✓ Removed service file"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
echo "✓ Reloaded systemd"
|
||||
echo ""
|
||||
|
||||
# Remove udev rules
|
||||
if [ -f /etc/udev/rules.d/99-uconsole-power-regulator.rules ]; then
|
||||
echo "Removing udev rules..."
|
||||
rm /etc/udev/rules.d/99-uconsole-power-regulator.rules
|
||||
echo "✓ Removed udev rules"
|
||||
|
||||
udevadm control --reload-rules
|
||||
udevadm trigger
|
||||
echo "✓ Reloaded udev rules"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Stop voltage monitor if running
|
||||
echo "Stopping voltage monitor..."
|
||||
if [ -x /usr/local/bin/voltage-monitor-control.sh ]; then
|
||||
/usr/local/bin/voltage-monitor-control.sh stop 2>/dev/null || true
|
||||
echo "✓ Stopped voltage monitor"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Remove voltage monitoring scripts
|
||||
if [ -f /usr/local/bin/voltage-monitor.sh ] || \
|
||||
[ -f /usr/local/bin/voltage-alert-notify.sh ] || \
|
||||
[ -f /usr/local/bin/voltage-monitor-control.sh ]; then
|
||||
echo "Removing voltage monitoring scripts..."
|
||||
rm -f /usr/local/bin/voltage-monitor.sh
|
||||
rm -f /usr/local/bin/voltage-alert-notify.sh
|
||||
rm -f /usr/local/bin/voltage-monitor-control.sh
|
||||
echo "✓ Removed voltage monitoring scripts"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Clean up temporary files
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -f /tmp/4g-modem-state /tmp/4g-modem-last-state
|
||||
rm -f /tmp/uconsole-power-daemon-state /tmp/power-state
|
||||
rm -f /run/voltage-monitor.pid
|
||||
echo "✓ Removed temporary state files"
|
||||
echo ""
|
||||
|
||||
# Ask about log file
|
||||
read -p "Remove log file /var/log/uconsole-power-regulator.log? (y/n) " -n 1 -r
|
||||
echo ""
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
rm -f /var/log/uconsole-power-regulator.log
|
||||
echo "✓ Removed log file"
|
||||
else
|
||||
echo "• Kept log file for reference"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Restore CPU to normal settings
|
||||
echo "Restoring CPU to normal settings..."
|
||||
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
|
||||
if [ -f "$cpu/cpufreq/scaling_governor" ]; then
|
||||
echo "ondemand" > "$cpu/cpufreq/scaling_governor" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f "$cpu/cpufreq/scaling_max_freq" ]; then
|
||||
echo "2400000" > "$cpu/cpufreq/scaling_max_freq" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
echo "✓ Restored CPU governor to ondemand"
|
||||
echo "✓ Restored CPU max frequency to 2.4GHz"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "Uninstallation Complete"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "The uConsole Smart Power Regulator has been removed."
|
||||
echo "CPU power management restored to default settings."
|
||||
echo ""
|
||||
echo "To reinstall: sudo $0 install"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main logic
|
||||
case "$ACTION" in
|
||||
install)
|
||||
install_components
|
||||
;;
|
||||
upgrade)
|
||||
upgrade_from_old
|
||||
;;
|
||||
uninstall)
|
||||
uninstall_components
|
||||
;;
|
||||
*)
|
||||
echo "Usage: sudo $0 [install|uninstall|upgrade]"
|
||||
echo ""
|
||||
echo "Actions:"
|
||||
echo " install - Fresh installation of uConsole Smart Power Regulator"
|
||||
echo " upgrade - Upgrade from old 4G Power Manager to new unified system"
|
||||
echo " uninstall - Remove uConsole Smart Power Regulator"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " sudo $0 install # Fresh install"
|
||||
echo " sudo $0 upgrade # Upgrade from 4G Power Manager"
|
||||
echo " sudo $0 uninstall # Complete removal"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
100
scripts/uconsole-power-daemon.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# uConsole Smart Power Daemon
|
||||
# Continuously monitors system power state and triggers power regulator
|
||||
#
|
||||
# This daemon runs in the background and checks system state every 5 seconds.
|
||||
# It monitors:
|
||||
# - 4G modem state (active/inactive)
|
||||
# - AC power state (connected/disconnected)
|
||||
#
|
||||
# More reliable than udev alone for detecting state changes during:
|
||||
# - Network activity (4G connecting, data transfer)
|
||||
# - Edge cases (service starts with AC already connected)
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REGULATOR_SCRIPT="$SCRIPT_DIR/uconsole-power-regulator.sh"
|
||||
CHECK_INTERVAL=5 # Check every 5 seconds
|
||||
LAST_STATE_FILE="/tmp/uconsole-power-daemon-state"
|
||||
AC_PATH="/sys/class/power_supply/axp22x-ac"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [POWER-DAEMON] $1"
|
||||
}
|
||||
|
||||
# Get current AC power state
|
||||
get_ac_state() {
|
||||
if [ -f "$AC_PATH/online" ]; then
|
||||
local ac_online=$(cat "$AC_PATH/online" 2>/dev/null)
|
||||
if [ "$ac_online" = "1" ]; then
|
||||
echo "connected"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
echo "disconnected"
|
||||
}
|
||||
|
||||
# Get current modem state
|
||||
get_modem_state() {
|
||||
# Check if wwan0 is up and has IP address
|
||||
if ip addr show wwan0 2>/dev/null | grep -q "inet "; then
|
||||
echo "active"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check if wwan0 link is up
|
||||
if ip link show wwan0 2>/dev/null | grep -q "state UP"; then
|
||||
echo "active"
|
||||
return
|
||||
fi
|
||||
|
||||
# Check ModemManager state
|
||||
if command -v mmcli &> /dev/null; then
|
||||
local modem_state=$(mmcli -m 0 --output-keyvalue 2>/dev/null | grep "modem.generic.state " | cut -d: -f2 | tr -d ' ')
|
||||
if [[ "$modem_state" == "connected" ]] || [[ "$modem_state" == "registered" ]]; then
|
||||
echo "active"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "inactive"
|
||||
}
|
||||
|
||||
# Get combined system state
|
||||
get_system_state() {
|
||||
local ac=$(get_ac_state)
|
||||
local modem=$(get_modem_state)
|
||||
echo "${ac}:${modem}"
|
||||
}
|
||||
|
||||
log "uConsole Smart Power Daemon started"
|
||||
|
||||
# Initialize last state
|
||||
LAST_STATE="unknown"
|
||||
if [ -f "$LAST_STATE_FILE" ]; then
|
||||
LAST_STATE=$(cat "$LAST_STATE_FILE")
|
||||
fi
|
||||
|
||||
# On startup, always trigger regulator to handle edge case where service
|
||||
# starts with AC already connected or 4G already active
|
||||
log "Initial state check on daemon startup"
|
||||
"$REGULATOR_SCRIPT"
|
||||
CURRENT_STATE=$(get_system_state)
|
||||
echo "$CURRENT_STATE" > "$LAST_STATE_FILE"
|
||||
LAST_STATE="$CURRENT_STATE"
|
||||
log "Initial system state: $CURRENT_STATE"
|
||||
|
||||
# Main monitoring loop
|
||||
while true; do
|
||||
CURRENT_STATE=$(get_system_state)
|
||||
|
||||
# Only trigger regulator if state changed
|
||||
if [ "$CURRENT_STATE" != "$LAST_STATE" ]; then
|
||||
log "System state changed: $LAST_STATE → $CURRENT_STATE"
|
||||
"$REGULATOR_SCRIPT"
|
||||
echo "$CURRENT_STATE" > "$LAST_STATE_FILE"
|
||||
LAST_STATE="$CURRENT_STATE"
|
||||
fi
|
||||
|
||||
sleep "$CHECK_INTERVAL"
|
||||
done
|
||||
24
scripts/uconsole-power-regulator.service
Normal file
@@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=uConsole Smart Power Regulator
|
||||
Documentation=https://github.com/yourusername/battery-monitor
|
||||
After=network.target ModemManager.service
|
||||
Wants=ModemManager.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/home/pi/battery-monitor/scripts/uconsole-power-daemon.sh
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=false
|
||||
PrivateTmp=false
|
||||
|
||||
# Resource limits
|
||||
Nice=0
|
||||
CPUSchedulingPolicy=other
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
227
scripts/uconsole-power-regulator.sh
Executable file
@@ -0,0 +1,227 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# uConsole Smart Power Regulator
|
||||
# Intelligently manages CPU power and voltage monitoring based on system state
|
||||
#
|
||||
# This script orchestrates power management based on:
|
||||
# - AC power status (connected/disconnected)
|
||||
# - 4G modem state (active/inactive)
|
||||
#
|
||||
# Power Management Logic:
|
||||
# AC Connected: → Normal CPU settings (ondemand @ 2.4GHz), no voltage monitoring
|
||||
# AC + 4G: → Normal CPU settings (ondemand @ 2.4GHz), no voltage monitoring
|
||||
# Battery Only: → Normal CPU settings (ondemand @ 2.4GHz), no voltage monitoring
|
||||
# Battery + 4G: → Power-save mode (powersave @ 1.8GHz), voltage monitoring enabled
|
||||
#
|
||||
# Triggered by:
|
||||
# - udev events (4G modem state changes, AC power changes)
|
||||
# - systemd daemon (periodic state polling)
|
||||
# - Manual execution
|
||||
|
||||
LOGFILE="/var/log/uconsole-power-regulator.log"
|
||||
MODEM_STATE_FILE="/tmp/4g-modem-state"
|
||||
LAST_POWER_STATE_FILE="/tmp/power-state"
|
||||
|
||||
# Configuration
|
||||
AC_GOVERNOR="ondemand" # Governor when AC connected
|
||||
BATTERY_GOVERNOR="ondemand" # Governor when on battery without 4G
|
||||
POWERSAVE_GOVERNOR="powersave" # Governor when on battery with 4G active
|
||||
MAX_FREQ_AC="2400000" # Max freq (2.4GHz) - AC power, full performance
|
||||
MAX_FREQ_BATTERY="2000000" # Max freq (2.0GHz) - battery only, balanced
|
||||
MAX_FREQ_POWERSAVE="1800000" # Max freq (1.8GHz) - battery + 4G, power-saving
|
||||
|
||||
# Paths
|
||||
BATTERY_PATH="/sys/class/power_supply/axp20x-battery"
|
||||
AC_PATH="/sys/class/power_supply/axp22x-ac"
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [POWER-REGULATOR] $1" | tee -a "$LOGFILE"
|
||||
}
|
||||
|
||||
# Check if AC power is connected
|
||||
check_ac_connected() {
|
||||
if [ -f "$AC_PATH/online" ]; then
|
||||
local ac_online=$(cat "$AC_PATH/online" 2>/dev/null)
|
||||
if [ "$ac_online" = "1" ]; then
|
||||
return 0 # AC is connected
|
||||
fi
|
||||
fi
|
||||
return 1 # AC is not connected (on battery)
|
||||
}
|
||||
|
||||
# Check if 4G modem is active
|
||||
check_modem_active() {
|
||||
# Method 1: Check if wwan0 interface exists and is up
|
||||
if ip link show wwan0 2>/dev/null | grep -q "state UP"; then
|
||||
return 0 # Modem is active
|
||||
fi
|
||||
|
||||
# Method 2: Check if wwan0 has IP address (more reliable)
|
||||
if ip addr show wwan0 2>/dev/null | grep -q "inet "; then
|
||||
return 0 # Modem is active with IP
|
||||
fi
|
||||
|
||||
# Method 3: Check ModemManager state
|
||||
if command -v mmcli &> /dev/null; then
|
||||
local modem_state=$(mmcli -m 0 --output-keyvalue 2>/dev/null | grep "modem.generic.state " | cut -d: -f2 | tr -d ' ')
|
||||
if [[ "$modem_state" == "connected" ]] || [[ "$modem_state" == "registered" ]]; then
|
||||
return 0 # Modem is active
|
||||
fi
|
||||
fi
|
||||
|
||||
# Method 4: Check if there's active data on wwan0
|
||||
if [ -f /sys/class/net/wwan0/statistics/rx_bytes ]; then
|
||||
local rx_bytes=$(cat /sys/class/net/wwan0/statistics/rx_bytes 2>/dev/null || echo 0)
|
||||
local tx_bytes=$(cat /sys/class/net/wwan0/statistics/tx_bytes 2>/dev/null || echo 0)
|
||||
local total=$((rx_bytes + tx_bytes))
|
||||
|
||||
# Read previous state
|
||||
local prev_total=0
|
||||
if [ -f "$MODEM_STATE_FILE" ]; then
|
||||
prev_total=$(cat "$MODEM_STATE_FILE")
|
||||
fi
|
||||
|
||||
# Save current state
|
||||
echo "$total" > "$MODEM_STATE_FILE"
|
||||
|
||||
# If traffic increased significantly, modem is active
|
||||
if [ $((total - prev_total)) -gt 1000 ]; then
|
||||
return 0 # Modem is active (data transfer)
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1 # Modem is inactive
|
||||
}
|
||||
|
||||
# Apply CPU power settings
|
||||
apply_cpu_settings() {
|
||||
local governor=$1
|
||||
local max_freq=$2
|
||||
local reason=$3
|
||||
|
||||
log "Applying CPU settings: $reason"
|
||||
log " Governor: $governor, Max Frequency: ${max_freq}Hz"
|
||||
|
||||
# Apply to all CPUs
|
||||
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
|
||||
if [ -f "$cpu/cpufreq/scaling_governor" ]; then
|
||||
echo "$governor" > "$cpu/cpufreq/scaling_governor" 2>/dev/null
|
||||
fi
|
||||
|
||||
if [ -f "$cpu/cpufreq/scaling_max_freq" ]; then
|
||||
echo "$max_freq" > "$cpu/cpufreq/scaling_max_freq" 2>/dev/null
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Determine and apply appropriate power state
|
||||
determine_power_state() {
|
||||
local ac_connected=$1
|
||||
local modem_active=$2
|
||||
local state=""
|
||||
|
||||
if [ "$ac_connected" = "true" ]; then
|
||||
state="AC_POWER"
|
||||
elif [ "$modem_active" = "true" ]; then
|
||||
state="BATTERY_4G"
|
||||
else
|
||||
state="BATTERY_ONLY"
|
||||
fi
|
||||
|
||||
echo "$state"
|
||||
}
|
||||
|
||||
# Apply power state configuration
|
||||
apply_power_state() {
|
||||
local state=$1
|
||||
|
||||
case "$state" in
|
||||
AC_POWER)
|
||||
log "Power State: AC CONNECTED"
|
||||
apply_cpu_settings "$AC_GOVERNOR" "$MAX_FREQ_AC" "AC power - full performance"
|
||||
# Stop voltage monitoring (not needed on AC)
|
||||
if command -v voltage-monitor-control.sh &> /dev/null; then
|
||||
/usr/local/bin/voltage-monitor-control.sh stop 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
|
||||
BATTERY_4G)
|
||||
log "Power State: BATTERY + 4G ACTIVE"
|
||||
apply_cpu_settings "$POWERSAVE_GOVERNOR" "$MAX_FREQ_POWERSAVE" "Battery with 4G - power saving mode"
|
||||
# Start voltage monitoring (critical on battery with 4G)
|
||||
if command -v voltage-monitor-control.sh &> /dev/null; then
|
||||
/usr/local/bin/voltage-monitor-control.sh start 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
|
||||
BATTERY_ONLY)
|
||||
log "Power State: BATTERY - 4G INACTIVE"
|
||||
apply_cpu_settings "$BATTERY_GOVERNOR" "$MAX_FREQ_BATTERY" "Battery without 4G - balanced performance"
|
||||
# Stop voltage monitoring (not needed when 4G off)
|
||||
if command -v voltage-monitor-control.sh &> /dev/null; then
|
||||
/usr/local/bin/voltage-monitor-control.sh stop 2>/dev/null
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Main logic
|
||||
main() {
|
||||
log "=========================================="
|
||||
log "uConsole Smart Power Regulator triggered"
|
||||
log "=========================================="
|
||||
|
||||
# Check if cpufreq is available
|
||||
if [ ! -f /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor ]; then
|
||||
log "ERROR: CPU frequency scaling not available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine current system state
|
||||
local ac_status="false"
|
||||
local modem_status="false"
|
||||
|
||||
if check_ac_connected; then
|
||||
ac_status="true"
|
||||
log "AC Power: CONNECTED"
|
||||
else
|
||||
log "AC Power: DISCONNECTED (on battery)"
|
||||
fi
|
||||
|
||||
if check_modem_active; then
|
||||
modem_status="true"
|
||||
log "4G Modem: ACTIVE"
|
||||
else
|
||||
log "4G Modem: INACTIVE"
|
||||
fi
|
||||
|
||||
# Determine required power state
|
||||
local new_state=$(determine_power_state "$ac_status" "$modem_status")
|
||||
|
||||
# Check if state changed
|
||||
local last_state=""
|
||||
if [ -f "$LAST_POWER_STATE_FILE" ]; then
|
||||
last_state=$(cat "$LAST_POWER_STATE_FILE")
|
||||
fi
|
||||
|
||||
if [ "$new_state" != "$last_state" ]; then
|
||||
log "Power state transition: ${last_state:-UNKNOWN} → $new_state"
|
||||
apply_power_state "$new_state"
|
||||
echo "$new_state" > "$LAST_POWER_STATE_FILE"
|
||||
else
|
||||
log "Power state unchanged: $new_state (no action needed)"
|
||||
fi
|
||||
|
||||
# Display current power consumption
|
||||
if [ -f "$BATTERY_PATH/power_now" ]; then
|
||||
local power_now=$(cat "$BATTERY_PATH/power_now" 2>/dev/null)
|
||||
local power_watts=$(echo "scale=2; $power_now / 1000000" | bc 2>/dev/null || echo "N/A")
|
||||
log "Current battery power: ${power_watts}W"
|
||||
fi
|
||||
|
||||
log "=========================================="
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
85
scripts/voltage-alert-notify.sh
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Battery Voltage Alert Notification
|
||||
# Sends multi-method notifications when battery voltage is critically low
|
||||
#
|
||||
# Methods:
|
||||
# 1. Desktop notification (notify-send) - if X11/Wayland session available
|
||||
# 2. Audio alert (paplay/beep) - system sound or beep
|
||||
# 3. Log file entry - always logged
|
||||
# 4. LED blink (optional) - if ACT LED available
|
||||
#
|
||||
# Usage: voltage-alert-notify.sh <voltage_in_volts>
|
||||
# Example: voltage-alert-notify.sh 3.42
|
||||
|
||||
VOLTAGE=$1
|
||||
LOGFILE="/var/log/4g-power-manager.log"
|
||||
|
||||
# Validate input
|
||||
if [ -z "$VOLTAGE" ]; then
|
||||
echo "Usage: $0 <voltage_in_volts>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Log the alert (always happens)
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VOLTAGE-ALERT] ⚠️ LOW VOLTAGE: ${VOLTAGE}V - 4G module may hang! Consider charging or disabling 4G." >> "$LOGFILE"
|
||||
|
||||
# Method 1: Desktop Notification
|
||||
# Works if running in X11 or Wayland session
|
||||
if command -v notify-send &> /dev/null; then
|
||||
# Try to find active user session
|
||||
for user_session in /run/user/*/; do
|
||||
USER_ID=$(basename "$user_session")
|
||||
DBUS_BUS="/run/user/$USER_ID/bus"
|
||||
|
||||
if [ -S "$DBUS_BUS" ]; then
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_BUS"
|
||||
export XDG_RUNTIME_DIR="/run/user/$USER_ID"
|
||||
|
||||
# Send notification as the user
|
||||
TIMESTAMP=$(date '+%H:%M:%S')
|
||||
sudo -u "#$USER_ID" DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS="$DBUS_SESSION_BUS_ADDRESS" \
|
||||
notify-send -u critical \
|
||||
-i battery-caution \
|
||||
"⚠️ Low Battery Voltage" \
|
||||
"[${TIMESTAMP}] ${VOLTAGE}V - 4G module may hang!\nCharge battery or disable 4G." \
|
||||
2>/dev/null &
|
||||
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Method 2: Audio Alert
|
||||
# Try paplay (PulseAudio) first, fallback to beep
|
||||
if command -v paplay &> /dev/null; then
|
||||
# Use system alert sound
|
||||
paplay /usr/share/sounds/freedesktop/stereo/bell.oga 2>/dev/null &
|
||||
elif command -v beep &> /dev/null; then
|
||||
# Fallback to PC speaker beep (3 short beeps)
|
||||
(beep -f 1000 -l 100; sleep 0.1; beep -f 1000 -l 100; sleep 0.1; beep -f 1000 -l 100) 2>/dev/null &
|
||||
fi
|
||||
|
||||
# Method 3: LED Blink (optional)
|
||||
# Blink ACT LED if available (Raspberry Pi)
|
||||
if [ -d /sys/class/leds/ACT ]; then
|
||||
# Save current trigger
|
||||
ORIG_TRIGGER=$(cat /sys/class/leds/ACT/trigger 2>/dev/null | grep -o '\[.*\]' | tr -d '[]')
|
||||
|
||||
# Blink LED 5 times
|
||||
(
|
||||
echo none > /sys/class/leds/ACT/trigger 2>/dev/null
|
||||
for i in {1..5}; do
|
||||
echo 1 > /sys/class/leds/ACT/brightness 2>/dev/null
|
||||
sleep 0.2
|
||||
echo 0 > /sys/class/leds/ACT/brightness 2>/dev/null
|
||||
sleep 0.2
|
||||
done
|
||||
# Restore original trigger
|
||||
if [ -n "$ORIG_TRIGGER" ]; then
|
||||
echo "$ORIG_TRIGGER" > /sys/class/leds/ACT/trigger 2>/dev/null
|
||||
fi
|
||||
) &
|
||||
fi
|
||||
|
||||
exit 0
|
||||
146
scripts/voltage-monitor-control.sh
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Voltage Monitor Control Script
|
||||
# Starts or stops the voltage monitoring daemon
|
||||
#
|
||||
# This script is called by udev rules when 4G modem state changes:
|
||||
# - 4G activated → start voltage monitor
|
||||
# - 4G deactivated → stop voltage monitor
|
||||
#
|
||||
# Usage:
|
||||
# voltage-monitor-control.sh start # Start voltage monitor
|
||||
# voltage-monitor-control.sh stop # Stop voltage monitor
|
||||
|
||||
PID_FILE="/run/voltage-monitor.pid"
|
||||
MONITOR_SCRIPT="/usr/local/bin/voltage-monitor.sh"
|
||||
AC_PATH="/sys/class/power_supply/axp22x-ac"
|
||||
LOGFILE="/var/log/4g-power-manager.log"
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VOLTAGE-CONTROL] $1" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
# Check if AC power is connected
|
||||
check_ac_connected() {
|
||||
if [ -f "$AC_PATH/online" ]; then
|
||||
local ac_online=$(cat "$AC_PATH/online" 2>/dev/null)
|
||||
if [ "$ac_online" = "1" ]; then
|
||||
return 0 # AC is connected
|
||||
fi
|
||||
fi
|
||||
return 1 # AC is not connected (on battery)
|
||||
}
|
||||
|
||||
# Start voltage monitor
|
||||
start_monitor() {
|
||||
# Check if AC power is connected
|
||||
if check_ac_connected; then
|
||||
log "AC power connected - Skipping voltage monitor (not needed on AC power)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if already running
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log "Voltage monitor already running (PID: $PID)"
|
||||
return 0
|
||||
else
|
||||
# PID file exists but process is dead, clean up
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if monitor script exists
|
||||
if [ ! -f "$MONITOR_SCRIPT" ]; then
|
||||
log "ERROR: Monitor script not found at $MONITOR_SCRIPT"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Start the monitor in background with nohup to prevent SIGHUP on parent exit
|
||||
nohup "$MONITOR_SCRIPT" > /dev/null 2>&1 &
|
||||
MONITOR_PID=$!
|
||||
|
||||
# Save PID
|
||||
echo "$MONITOR_PID" > "$PID_FILE"
|
||||
|
||||
log "Voltage monitor started (PID: $MONITOR_PID)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Stop voltage monitor
|
||||
stop_monitor() {
|
||||
if [ ! -f "$PID_FILE" ]; then
|
||||
log "Voltage monitor is not running (no PID file)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
PID=$(cat "$PID_FILE")
|
||||
|
||||
# Check if process is actually running
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
log "Voltage monitor process not found (PID: $PID was stale)"
|
||||
rm -f "$PID_FILE"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Send SIGTERM to gracefully stop the monitor
|
||||
kill -TERM "$PID" 2>/dev/null
|
||||
|
||||
# Wait up to 5 seconds for process to exit
|
||||
for i in {1..5}; do
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# If still running, force kill
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log "WARNING: Voltage monitor did not stop gracefully, forcing kill"
|
||||
kill -KILL "$PID" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Clean up PID file
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
log "Voltage monitor stopped (PID: $PID)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main logic
|
||||
case "$1" in
|
||||
start)
|
||||
start_monitor
|
||||
;;
|
||||
stop)
|
||||
stop_monitor
|
||||
;;
|
||||
status)
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
echo "Voltage monitor is running (PID: $PID)"
|
||||
exit 0
|
||||
else
|
||||
echo "Voltage monitor is not running (stale PID file)"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Voltage monitor is not running"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|status}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start voltage monitoring daemon"
|
||||
echo " stop - Stop voltage monitoring daemon"
|
||||
echo " status - Check if voltage monitor is running"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit $?
|
||||
104
scripts/voltage-monitor.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Battery Voltage Monitor for uConsole 4G Module
|
||||
# Monitors battery voltage and alerts when it drops below 3.45V (4G module minimum)
|
||||
#
|
||||
# This script runs ONLY when 4G module is active (started by udev events)
|
||||
# It checks voltage every 5 seconds and sends alerts if voltage is critically low
|
||||
# Alerts are rate-limited to every 30 seconds to avoid spam
|
||||
#
|
||||
# Started by: voltage-monitor-control.sh (triggered by udev)
|
||||
# Stopped by: voltage-monitor-control.sh (triggered by udev)
|
||||
|
||||
# Configuration
|
||||
VOLTAGE_THRESHOLD="3.45" # Critical voltage threshold in volts
|
||||
CHECK_INTERVAL=5 # Normal check interval in seconds
|
||||
ALERT_INTERVAL=30 # Minimum time between alerts in seconds
|
||||
BATTERY_PATH="/sys/class/power_supply/axp20x-battery"
|
||||
AC_PATH="/sys/class/power_supply/axp22x-ac"
|
||||
LOGFILE="/var/log/4g-power-manager.log"
|
||||
|
||||
# State tracking
|
||||
LAST_ALERT_TIME=0
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [VOLTAGE-MONITOR] $1" >> "$LOGFILE"
|
||||
}
|
||||
|
||||
# Check if AC power is connected
|
||||
check_ac_connected() {
|
||||
if [ -f "$AC_PATH/online" ]; then
|
||||
local ac_online=$(cat "$AC_PATH/online" 2>/dev/null)
|
||||
if [ "$ac_online" = "1" ]; then
|
||||
return 0 # AC is connected
|
||||
fi
|
||||
fi
|
||||
return 1 # AC is not connected (on battery)
|
||||
}
|
||||
|
||||
# Cleanup function
|
||||
cleanup() {
|
||||
log "Voltage monitor stopped"
|
||||
rm -f /run/voltage-monitor.pid
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Trap signals for clean exit
|
||||
trap cleanup SIGTERM SIGINT
|
||||
|
||||
# Startup log
|
||||
log "Voltage monitor started (threshold: ${VOLTAGE_THRESHOLD}V, check interval: ${CHECK_INTERVAL}s)"
|
||||
|
||||
# Main monitoring loop
|
||||
while true; do
|
||||
# Check if AC power is connected - if so, exit monitoring
|
||||
if check_ac_connected; then
|
||||
log "AC power connected - Exiting voltage monitor (not needed on AC power)"
|
||||
cleanup
|
||||
fi
|
||||
|
||||
# Read current voltage
|
||||
if [ -f "$BATTERY_PATH/voltage_now" ]; then
|
||||
VOLTAGE_UV=$(cat "$BATTERY_PATH/voltage_now" 2>/dev/null)
|
||||
|
||||
if [ -n "$VOLTAGE_UV" ]; then
|
||||
# Convert from microvolts to volts
|
||||
VOLTAGE_V=$(echo "scale=3; $VOLTAGE_UV / 1000000" | bc 2>/dev/null)
|
||||
|
||||
if [ -n "$VOLTAGE_V" ]; then
|
||||
# Check if voltage is below threshold
|
||||
if (( $(echo "$VOLTAGE_V < $VOLTAGE_THRESHOLD" | bc -l) )); then
|
||||
NOW=$(date +%s)
|
||||
TIME_SINCE_LAST_ALERT=$((NOW - LAST_ALERT_TIME))
|
||||
|
||||
# Only alert if enough time has passed since last alert
|
||||
if [ $TIME_SINCE_LAST_ALERT -ge $ALERT_INTERVAL ]; then
|
||||
log "WARNING: Low voltage detected: ${VOLTAGE_V}V (threshold: ${VOLTAGE_THRESHOLD}V)"
|
||||
|
||||
# Trigger notification
|
||||
/usr/local/bin/voltage-alert-notify.sh "$VOLTAGE_V" &
|
||||
|
||||
# Update last alert time
|
||||
LAST_ALERT_TIME=$NOW
|
||||
fi
|
||||
|
||||
# Sleep longer when in alert state (battery is critical)
|
||||
sleep $ALERT_INTERVAL
|
||||
else
|
||||
# Voltage is okay, check again after normal interval
|
||||
sleep $CHECK_INTERVAL
|
||||
fi
|
||||
else
|
||||
log "ERROR: Failed to convert voltage value"
|
||||
sleep $CHECK_INTERVAL
|
||||
fi
|
||||
else
|
||||
log "ERROR: Failed to read voltage value"
|
||||
sleep $CHECK_INTERVAL
|
||||
fi
|
||||
else
|
||||
log "ERROR: Battery voltage file not found at $BATTERY_PATH/voltage_now"
|
||||
sleep $CHECK_INTERVAL
|
||||
fi
|
||||
done
|
||||
@@ -31,6 +31,9 @@ export async function GET(request: NextRequest) {
|
||||
status: reading.status,
|
||||
health: reading.health,
|
||||
acConnected: reading.ac_connected,
|
||||
capacityFull: reading.capacity_full,
|
||||
capacityNow: reading.capacity_now,
|
||||
capacityDesign: reading.capacity_design,
|
||||
}));
|
||||
|
||||
return NextResponse.json(transformedReadings);
|
||||
|
||||
@@ -10,6 +10,10 @@ interface BatteryData {
|
||||
status: string;
|
||||
health: string;
|
||||
acConnected: boolean;
|
||||
capacityFull: number; // calibrated full capacity in Wh
|
||||
capacityNow: number; // current capacity in Wh
|
||||
capacityDesign: number; // design capacity in Wh
|
||||
systemUptime: number; // system uptime in seconds
|
||||
}
|
||||
|
||||
function readBatteryFile(path: string): string | null {
|
||||
@@ -22,6 +26,18 @@ function readBatteryFile(path: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemUptime(): number {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
const uptimeData = fs.readFileSync('/proc/uptime', 'utf8').trim();
|
||||
const uptimeSeconds = parseFloat(uptimeData.split(' ')[0]);
|
||||
return Math.floor(uptimeSeconds);
|
||||
} catch (error) {
|
||||
console.error('Error reading system uptime:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getBatteryData(): BatteryData | null {
|
||||
const basePath = '/sys/class/power_supply/axp20x-battery';
|
||||
const acPath = '/sys/class/power_supply/axp22x-ac';
|
||||
@@ -34,6 +50,11 @@ function getBatteryData(): BatteryData | null {
|
||||
const health = readBatteryFile(`${basePath}/health`) || 'Unknown';
|
||||
const acOnline = readBatteryFile(`${acPath}/online`);
|
||||
|
||||
// Read energy capacity values
|
||||
const energyFullDesignRaw = readBatteryFile(`${basePath}/energy_full_design`);
|
||||
const energyFullRaw = readBatteryFile(`${basePath}/energy_full`);
|
||||
const energyNowRaw = readBatteryFile(`${basePath}/energy_now`);
|
||||
|
||||
if (!capacityRaw || !voltageRaw || !currentRaw) {
|
||||
return null;
|
||||
}
|
||||
@@ -45,6 +66,14 @@ function getBatteryData(): BatteryData | null {
|
||||
const power = voltage * current; // Calculate power in watts (positive=charging, negative=discharging)
|
||||
const acConnected = acOnline === '1';
|
||||
|
||||
// Convert energy from µWh to Wh
|
||||
const capacityDesign = energyFullDesignRaw ? parseInt(energyFullDesignRaw) / 1000000 : 0;
|
||||
const capacityFull = energyFullRaw ? parseInt(energyFullRaw) / 1000000 : 0;
|
||||
const capacityNow = energyNowRaw ? parseInt(energyNowRaw) / 1000000 : 0;
|
||||
|
||||
// Get system uptime
|
||||
const systemUptime = getSystemUptime();
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
percentage,
|
||||
@@ -54,6 +83,10 @@ function getBatteryData(): BatteryData | null {
|
||||
status,
|
||||
health,
|
||||
acConnected,
|
||||
capacityFull,
|
||||
capacityNow,
|
||||
capacityDesign,
|
||||
systemUptime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,6 +116,9 @@ export async function GET(request: NextRequest) {
|
||||
status: batteryData.status,
|
||||
health: batteryData.health,
|
||||
ac_connected: batteryData.acConnected,
|
||||
capacity_full: batteryData.capacityFull,
|
||||
capacity_now: batteryData.capacityNow,
|
||||
capacity_design: batteryData.capacityDesign,
|
||||
}, sessionId ? parseInt(sessionId) : undefined);
|
||||
} catch (error) {
|
||||
console.error('Error saving to database:', error);
|
||||
|
||||
45
src/app/api/battery/save/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { insertReading } from '@/lib/db';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { readings, sessionId } = body;
|
||||
|
||||
if (!readings || !Array.isArray(readings)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request: readings array required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Insert all readings
|
||||
const insertedCount = readings.length;
|
||||
for (const reading of readings) {
|
||||
insertReading({
|
||||
timestamp: reading.timestamp,
|
||||
percentage: reading.percentage,
|
||||
voltage: reading.voltage,
|
||||
current: reading.current,
|
||||
power: reading.power,
|
||||
status: reading.status,
|
||||
health: reading.health,
|
||||
ac_connected: reading.acConnected,
|
||||
capacity_full: reading.capacityFull,
|
||||
capacity_now: reading.capacityNow,
|
||||
capacity_design: reading.capacityDesign,
|
||||
}, sessionId);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
count: insertedCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving readings:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to save readings' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,9 @@ export async function GET(
|
||||
status: reading.status,
|
||||
health: reading.health,
|
||||
acConnected: reading.ac_connected,
|
||||
capacityFull: reading.capacity_full,
|
||||
capacityNow: reading.capacity_now,
|
||||
capacityDesign: reading.capacity_design,
|
||||
}));
|
||||
|
||||
return NextResponse.json(transformedReadings);
|
||||
|
||||
53
src/app/api/battery/sessions/repair/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getIncompleteSessions, repairAllIncompleteSessions, repairSession } from '@/lib/db';
|
||||
|
||||
// Get all incomplete sessions
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const incompleteSessions = getIncompleteSessions();
|
||||
return NextResponse.json(incompleteSessions);
|
||||
} catch (error) {
|
||||
console.error('Error fetching incomplete sessions:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch incomplete sessions' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Repair incomplete sessions
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { sessionId, repairAll } = body;
|
||||
|
||||
if (repairAll) {
|
||||
// Repair all incomplete sessions
|
||||
const repairedCount = repairAllIncompleteSessions();
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Repaired ${repairedCount} incomplete session(s)`,
|
||||
count: repairedCount
|
||||
});
|
||||
} else if (sessionId) {
|
||||
// Repair a specific session
|
||||
repairSession(sessionId);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Session repaired successfully',
|
||||
sessionId
|
||||
});
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: 'Either sessionId or repairAll must be provided' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error repairing sessions:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to repair sessions' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { Battery, BatteryCharging, Zap, Download, Play, Pause, Calendar, Database, Edit2, Check, X, Trash2, Circle } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
||||
import { Battery, BatteryCharging, Zap, Download, Play, Pause, Calendar, Database, Edit2, Check, X, Trash2, Circle, AlertTriangle, RefreshCw, Eye, EyeOff } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area, ReferenceLine } from 'recharts';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface BatteryData {
|
||||
@@ -19,6 +19,10 @@ interface BatteryData {
|
||||
status: string;
|
||||
health: string;
|
||||
acConnected: boolean;
|
||||
capacityFull: number;
|
||||
capacityNow: number;
|
||||
capacityDesign: number;
|
||||
systemUptime: number;
|
||||
}
|
||||
|
||||
interface MonitoringSession {
|
||||
@@ -28,6 +32,8 @@ interface MonitoringSession {
|
||||
end_time: string;
|
||||
reading_count: number;
|
||||
created_at: string;
|
||||
energy_wh?: number;
|
||||
duration_seconds?: number;
|
||||
}
|
||||
|
||||
export default function BatteryMonitor() {
|
||||
@@ -43,11 +49,20 @@ export default function BatteryMonitor() {
|
||||
const [selectedSessionId, setSelectedSessionId] = useState<string>('');
|
||||
const [currentSessionId, setCurrentSessionId] = useState<number | null>(null);
|
||||
const [editingSessionId, setEditingSessionId] = useState<number | null>(null);
|
||||
const [editingName, setEditingName] = useState<string>('');
|
||||
const editingInputRef = useRef<HTMLInputElement>(null);
|
||||
const editingInitialName = useRef<string>('');
|
||||
const [showCustomRange, setShowCustomRange] = useState(false);
|
||||
const [recordingStartPoint, setRecordingStartPoint] = useState<'now' | 'monitoring'>('now');
|
||||
const [incompleteSessionCount, setIncompleteSessionCount] = useState(0);
|
||||
const [monitoringMode, setMonitoringMode] = useState<'live' | 'background'>('live');
|
||||
const [energyConsumed, setEnergyConsumed] = useState(0); // Total Wh consumed during monitoring
|
||||
const [sessionTime, setSessionTime] = useState(0); // Session time in seconds
|
||||
const monitoringStartTimeRef = useRef<string | null>(null);
|
||||
const monitoringDataBufferRef = useRef<BatteryData[]>([]);
|
||||
const sessionsCardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [sessionsLoaded, setSessionsLoaded] = useState(false);
|
||||
const lastPowerReadingRef = useRef<{ power: number; timestamp: number } | null>(null);
|
||||
const initialCapacityRef = useRef<number | null>(null);
|
||||
|
||||
const fetchSessions = async () => {
|
||||
try {
|
||||
@@ -55,12 +70,74 @@ export default function BatteryMonitor() {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSessions(data);
|
||||
|
||||
// Check for incomplete sessions
|
||||
const incomplete = data.filter((s: MonitoringSession) =>
|
||||
s.start_time === s.end_time && s.reading_count > 0
|
||||
);
|
||||
setIncompleteSessionCount(incomplete.length);
|
||||
setSessionsLoaded(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sessions:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Lazy load sessions when sessions card is visible using Intersection Observer
|
||||
useEffect(() => {
|
||||
if (!sessionsCardRef.current || sessionsLoaded) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
fetchSessions();
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(sessionsCardRef.current);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [sessionsLoaded]);
|
||||
|
||||
const repairAllIncompleteSessions = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/battery/sessions/repair', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ repairAll: true }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(result.message);
|
||||
fetchSessions(); // Refresh sessions list
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to repair sessions:', err);
|
||||
alert('Failed to repair sessions');
|
||||
}
|
||||
};
|
||||
|
||||
const isSessionIncomplete = (session: MonitoringSession) => {
|
||||
return session.start_time === session.end_time && session.reading_count > 0;
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
} else {
|
||||
return `${secs}s`;
|
||||
}
|
||||
};
|
||||
|
||||
const loadSessionData = async (sessionId: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/battery/sessions/${sessionId}`);
|
||||
@@ -109,9 +186,7 @@ export default function BatteryMonitor() {
|
||||
|
||||
// If starting from monitoring start, save buffered data
|
||||
if (recordingStartPoint === 'monitoring' && monitoringDataBufferRef.current.length > 0) {
|
||||
for (const reading of monitoringDataBufferRef.current) {
|
||||
await saveSingleReading(reading, data.id);
|
||||
}
|
||||
await saveBufferedReadings(monitoringDataBufferRef.current, data.id);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -136,18 +211,25 @@ export default function BatteryMonitor() {
|
||||
}
|
||||
};
|
||||
|
||||
const saveSingleReading = async (data: BatteryData, sessionId?: number) => {
|
||||
const saveBufferedReadings = async (readings: BatteryData[], sessionId: number) => {
|
||||
try {
|
||||
let url = '/api/battery?save=true';
|
||||
const sid = sessionId || currentSessionId;
|
||||
if (sid) {
|
||||
url += `&sessionId=${sid}`;
|
||||
const response = await fetch('/api/battery/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
readings,
|
||||
sessionId
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save buffered readings');
|
||||
}
|
||||
// We're not actually fetching, just using the save endpoint
|
||||
// In a real implementation, you'd need a separate endpoint for saving existing data
|
||||
// For now, this is a placeholder
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`Successfully saved ${result.count} buffered readings`);
|
||||
} catch (err) {
|
||||
console.error('Failed to save reading:', err);
|
||||
console.error('Failed to save buffered readings:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -164,7 +246,29 @@ export default function BatteryMonitor() {
|
||||
const data = await response.json();
|
||||
setCurrentData(data);
|
||||
|
||||
// Always update display data
|
||||
// Calculate energy consumption during monitoring
|
||||
if (isMonitoring && lastPowerReadingRef.current) {
|
||||
const now = Date.now();
|
||||
const timeDiff = (now - lastPowerReadingRef.current.timestamp) / 1000; // seconds
|
||||
const avgPower = (lastPowerReadingRef.current.power + data.power) / 2; // average power
|
||||
const energyDelta = Math.abs(avgPower) * (timeDiff / 3600); // Convert to Wh
|
||||
setEnergyConsumed(prev => prev + energyDelta);
|
||||
}
|
||||
|
||||
// Update last power reading
|
||||
if (isMonitoring) {
|
||||
lastPowerReadingRef.current = {
|
||||
power: data.power,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// Set initial capacity on first reading
|
||||
if (initialCapacityRef.current === null) {
|
||||
initialCapacityRef.current = data.capacityNow;
|
||||
}
|
||||
}
|
||||
|
||||
// Always update historical data to maintain full data granularity
|
||||
setHistoricalData(prev => {
|
||||
const newData = [...prev, data];
|
||||
return newData.slice(-100);
|
||||
@@ -208,7 +312,7 @@ export default function BatteryMonitor() {
|
||||
useEffect(() => {
|
||||
// Initial fetch
|
||||
fetchBatteryData();
|
||||
fetchSessions();
|
||||
// Don't fetch sessions on mount - only fetch when needed (lazy loading)
|
||||
|
||||
// Set up interval for monitoring
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
@@ -221,11 +325,31 @@ export default function BatteryMonitor() {
|
||||
};
|
||||
}, [isMonitoring, isRecording, currentSessionId]);
|
||||
|
||||
// Update session time every second when monitoring
|
||||
useEffect(() => {
|
||||
if (!isMonitoring || !monitoringStartTimeRef.current) return;
|
||||
|
||||
const updateSessionTime = () => {
|
||||
const start = new Date(monitoringStartTimeRef.current!).getTime();
|
||||
const now = Date.now();
|
||||
setSessionTime(Math.floor((now - start) / 1000));
|
||||
};
|
||||
|
||||
updateSessionTime(); // Initial update
|
||||
const sessionInterval = setInterval(updateSessionTime, 1000);
|
||||
|
||||
return () => clearInterval(sessionInterval);
|
||||
}, [isMonitoring, monitoringStartTimeRef.current]);
|
||||
|
||||
const handleStartMonitoring = () => {
|
||||
monitoringStartTimeRef.current = new Date().toISOString();
|
||||
monitoringDataBufferRef.current = [];
|
||||
setHistoricalData([]);
|
||||
setShowHistoricalView(false);
|
||||
setEnergyConsumed(0);
|
||||
setSessionTime(0);
|
||||
lastPowerReadingRef.current = null;
|
||||
initialCapacityRef.current = null;
|
||||
setIsMonitoring(true);
|
||||
};
|
||||
|
||||
@@ -237,6 +361,7 @@ export default function BatteryMonitor() {
|
||||
setIsMonitoring(false);
|
||||
monitoringStartTimeRef.current = null;
|
||||
monitoringDataBufferRef.current = [];
|
||||
lastPowerReadingRef.current = null;
|
||||
};
|
||||
|
||||
const handleStartRecording = async () => {
|
||||
@@ -257,21 +382,22 @@ export default function BatteryMonitor() {
|
||||
};
|
||||
|
||||
const handleEditSessionName = (session: MonitoringSession) => {
|
||||
editingInitialName.current = session.name || `Session ${new Date(session.start_time).toLocaleString()}`;
|
||||
setEditingSessionId(session.id);
|
||||
setEditingName(session.name || `Session ${new Date(session.start_time).toLocaleString()}`);
|
||||
};
|
||||
|
||||
const handleSaveSessionName = async () => {
|
||||
if (editingSessionId && editingName) {
|
||||
await updateSessionName(editingSessionId, editingName);
|
||||
if (editingSessionId && editingInputRef.current) {
|
||||
const newName = editingInputRef.current.value.trim();
|
||||
if (newName) {
|
||||
await updateSessionName(editingSessionId, newName);
|
||||
}
|
||||
setEditingSessionId(null);
|
||||
setEditingName('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingSessionId(null);
|
||||
setEditingName('');
|
||||
};
|
||||
|
||||
const handleDeleteSession = async (session: MonitoringSession) => {
|
||||
@@ -307,9 +433,9 @@ export default function BatteryMonitor() {
|
||||
const exportData = (data?: BatteryData[], filename?: string) => {
|
||||
const dataToExport = data || historicalData;
|
||||
const csvContent = [
|
||||
'Timestamp,Percentage,Voltage (V),Current (A),Power (W),Status,Health,AC Connected',
|
||||
'Timestamp,Percentage,Voltage (V),Current (A),Power (W),Capacity Now (Wh),Capacity Full (Wh),Capacity Design (Wh),Status,Health,AC Connected',
|
||||
...dataToExport.map(d =>
|
||||
`${d.timestamp},${d.percentage},${d.voltage},${d.current},${d.power},${d.status},${d.health},${d.acConnected}`
|
||||
`${d.timestamp},${d.percentage},${d.voltage},${d.current},${d.power},${d.capacityNow},${d.capacityFull},${d.capacityDesign},${d.status},${d.health},${d.acConnected}`
|
||||
)
|
||||
].join('\n');
|
||||
|
||||
@@ -411,47 +537,52 @@ export default function BatteryMonitor() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-3">
|
||||
{currentData && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Charge</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
<div className="col-span-2">
|
||||
<div className="text-xs text-muted-foreground">Charge</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Progress value={currentData.percentage} className="flex-1" />
|
||||
<span className="text-2xl font-bold">{currentData.percentage}%</span>
|
||||
<Progress value={currentData.percentage} className="flex-1 h-2" />
|
||||
<span className="text-lg font-bold">{currentData.percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Voltage</div>
|
||||
<div className="text-2xl font-bold">{currentData.voltage.toFixed(2)}V</div>
|
||||
<div className="text-xs text-muted-foreground">Voltage</div>
|
||||
<div className="text-base font-bold">{currentData.voltage.toFixed(2)}V</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Current</div>
|
||||
<div className="text-2xl font-bold">{currentData.current.toFixed(2)}A</div>
|
||||
<div className="text-xs text-muted-foreground">Current</div>
|
||||
<div className="text-base font-bold">{currentData.current.toFixed(2)}A</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Power</div>
|
||||
<div className="text-2xl font-bold flex items-center gap-1">
|
||||
<Zap className="h-5 w-5" />
|
||||
{currentData.power.toFixed(2)}W
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Power</div>
|
||||
<div className="text-base font-bold">{currentData.power.toFixed(2)}W</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Status</div>
|
||||
<div className="text-xs text-muted-foreground">Status</div>
|
||||
<Badge className={getStatusColor(currentData.status)}>{currentData.status}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Health</div>
|
||||
<div className="text-lg font-medium">{currentData.health}</div>
|
||||
<div className="text-xs text-muted-foreground">Capacity</div>
|
||||
<div className="text-sm font-bold">{currentData.capacityNow.toFixed(1)}/{currentData.capacityFull.toFixed(1)} Wh</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">AC Power</div>
|
||||
<div className="text-lg font-medium">{currentData.acConnected ? 'Connected' : 'Battery'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-muted-foreground">Last Update</div>
|
||||
<div className="text-sm font-medium">{format(new Date(currentData.timestamp), 'HH:mm:ss')}</div>
|
||||
<div className="text-xs text-muted-foreground">System Up</div>
|
||||
<div className="text-sm font-bold">{formatUptime(currentData.systemUptime)}</div>
|
||||
</div>
|
||||
{isMonitoring && (
|
||||
<>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Session</div>
|
||||
<div className="text-sm font-bold">{formatUptime(sessionTime)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Energy</div>
|
||||
<div className="text-sm font-bold">{energyConsumed.toFixed(3)} Wh</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -493,12 +624,54 @@ export default function BatteryMonitor() {
|
||||
Stop Recording
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Mode Toggle */}
|
||||
{isMonitoring && (
|
||||
<Button
|
||||
onClick={() => setMonitoringMode(mode => mode === 'live' ? 'background' : 'live')}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
{monitoringMode === 'live' ? (
|
||||
<>
|
||||
<EyeOff className="h-4 w-4" />
|
||||
Background Mode
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className="h-4 w-4" />
|
||||
Live Mode
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Charts - always visible */}
|
||||
{historicalData.length > 0 && (
|
||||
{/* Background Mode Info Banner */}
|
||||
{isMonitoring && monitoringMode === 'background' && !showHistoricalView && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
||||
<EyeOff className="h-4 w-4" />
|
||||
<span className="font-medium">Background Mode:</span>
|
||||
<span>Charts hidden, data collecting{isRecording && ', recording active'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Charts - only visible in live mode or when viewing historical data */}
|
||||
{historicalData.length > 0 && (monitoringMode === 'live' || showHistoricalView) && (() => {
|
||||
// Transform data to include energy output in mWh
|
||||
// Use initialCapacityRef for live monitoring, or first reading's capacityNow for saved sessions
|
||||
const initialCapacity = initialCapacityRef.current ?? historicalData[0]?.capacityNow ?? 0;
|
||||
|
||||
const chartDataWithOutput = historicalData.map(reading => ({
|
||||
...reading,
|
||||
outputMwh: initialCapacity
|
||||
? (initialCapacity - reading.capacityNow) * 1000
|
||||
: 0
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card>
|
||||
@@ -508,7 +681,6 @@ export default function BatteryMonitor() {
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={historicalData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value) => new Date(value).toLocaleTimeString()}
|
||||
@@ -524,6 +696,7 @@ export default function BatteryMonitor() {
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -537,7 +710,6 @@ export default function BatteryMonitor() {
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<AreaChart data={historicalData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value) => new Date(value).toLocaleTimeString()}
|
||||
@@ -553,21 +725,20 @@ export default function BatteryMonitor() {
|
||||
stroke="#82ca9d"
|
||||
fill="#82ca9d"
|
||||
fillOpacity={0.3}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Voltage & Current Trends</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={historicalData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Voltage & Current Trends</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={historicalData}>
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value) => new Date(value).toLocaleTimeString()}
|
||||
@@ -586,6 +757,7 @@ export default function BatteryMonitor() {
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Voltage (V)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
yAxisId="current"
|
||||
@@ -595,13 +767,194 @@ export default function BatteryMonitor() {
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
name="Current (A)"
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Battery Energy Output vs. Voltage</CardTitle>
|
||||
<CardDescription>Discharge curve showing battery capacity vs voltage cutoff</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={chartDataWithOutput} margin={{ bottom: 20 }}>
|
||||
<XAxis
|
||||
dataKey="outputMwh"
|
||||
tickFormatter={(value) => value.toFixed(1)}
|
||||
label={{ value: 'Energy Output (mWh)', position: 'bottom', offset: 0 }}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="voltage"
|
||||
domain={[3.0, 4.5]}
|
||||
tickFormatter={(value) => value.toFixed(2)}
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={(value) => `${value.toFixed(1)} mWh output`}
|
||||
formatter={(value: any) => [`${value.toFixed(2)} V`, 'Voltage']}
|
||||
/>
|
||||
<ReferenceLine
|
||||
y={3.45}
|
||||
stroke="#ef4444"
|
||||
strokeDasharray="5 5"
|
||||
strokeWidth={2}
|
||||
label={{
|
||||
value: '3.45V - 4G Module Min',
|
||||
position: 'right',
|
||||
fill: '#ef4444',
|
||||
fontSize: 12,
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="voltage"
|
||||
stroke="#8b5cf6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Monitoring Sessions */}
|
||||
<Card ref={sessionsCardRef}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Recording Sessions
|
||||
{incompleteSessionCount > 0 && (
|
||||
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
{incompleteSessionCount} incomplete
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription>View and manage saved recording sessions</CardDescription>
|
||||
</div>
|
||||
{incompleteSessionCount > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={repairAllIncompleteSessions}
|
||||
className="gap-2"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Repair All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sessions.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-8">
|
||||
No recording sessions yet. Start monitoring and recording to create sessions.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sessions.map((session) => {
|
||||
const incomplete = isSessionIncomplete(session);
|
||||
const isEditing = editingSessionId === session.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`flex items-center justify-between p-3 border rounded-lg hover:bg-accent transition-colors ${
|
||||
selectedSessionId === String(session.id) ? 'bg-blue-50 border-blue-300' : ''
|
||||
} ${incomplete ? 'border-yellow-300 bg-yellow-50/50' : ''}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={editingInputRef}
|
||||
type="text"
|
||||
defaultValue={editingInitialName.current}
|
||||
className="px-2 py-1 border rounded"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSaveSessionName();
|
||||
if (e.key === 'Escape') handleCancelEdit();
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="ghost" onClick={handleSaveSessionName}>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleCancelEdit}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleSessionSelect(String(session.id))}
|
||||
className="font-medium hover:underline text-left"
|
||||
>
|
||||
{session.name || `Session ${new Date(session.start_time).toLocaleString()}`}
|
||||
</button>
|
||||
{incomplete && (
|
||||
<Badge variant="outline" className="text-xs bg-yellow-100 text-yellow-800 border-yellow-400">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
Incomplete
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{format(new Date(session.start_time), 'PPp')} - {format(new Date(session.end_time), 'PPp')}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{session.reading_count} readings
|
||||
{session.duration_seconds !== undefined && ` • ${formatUptime(session.duration_seconds)}`}
|
||||
{session.energy_wh !== undefined && ` • ${session.energy_wh.toFixed(3)} Wh`}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => exportSessionData(
|
||||
session.id,
|
||||
session.name || `Session-${session.id}`
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleEditSessionName(session)}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteSession(session)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Custom Time Range Export */}
|
||||
<Card>
|
||||
@@ -654,94 +1007,6 @@ export default function BatteryMonitor() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Monitoring Sessions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Database className="h-5 w-5" />
|
||||
Recording Sessions
|
||||
</CardTitle>
|
||||
<CardDescription>View and manage saved recording sessions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{sessions.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-8">
|
||||
No recording sessions yet. Start monitoring and recording to create sessions.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className={`flex items-center justify-between p-3 border rounded-lg hover:bg-accent transition-colors ${
|
||||
selectedSessionId === String(session.id) ? 'bg-blue-50 border-blue-300' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
{editingSessionId === session.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
className="px-2 py-1 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<Button size="sm" variant="ghost" onClick={handleSaveSessionName}>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleCancelEdit}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleSessionSelect(String(session.id))}
|
||||
className="font-medium hover:underline text-left"
|
||||
>
|
||||
{session.name || `Session ${new Date(session.start_time).toLocaleString()}`}
|
||||
</button>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{format(new Date(session.start_time), 'PPp')} - {format(new Date(session.end_time), 'PPp')}
|
||||
{' • '}{session.reading_count} readings
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => exportSessionData(
|
||||
session.id,
|
||||
session.name || `Session-${session.id}`
|
||||
)}
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleEditSessionName(session)}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDeleteSession(session)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
145
src/lib/db.ts
@@ -18,6 +18,9 @@ db.exec(`
|
||||
status TEXT NOT NULL,
|
||||
health TEXT NOT NULL,
|
||||
ac_connected INTEGER NOT NULL,
|
||||
capacity_full REAL,
|
||||
capacity_now REAL,
|
||||
capacity_design REAL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (session_id) REFERENCES monitoring_sessions(id)
|
||||
);
|
||||
@@ -37,6 +40,31 @@ db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_session_start ON monitoring_sessions(start_time);
|
||||
`);
|
||||
|
||||
// Add capacity columns if they don't exist (migration for existing databases)
|
||||
try {
|
||||
db.exec(`
|
||||
ALTER TABLE battery_readings ADD COLUMN capacity_full REAL;
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
ALTER TABLE battery_readings ADD COLUMN capacity_now REAL;
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(`
|
||||
ALTER TABLE battery_readings ADD COLUMN capacity_design REAL;
|
||||
`);
|
||||
} catch (e) {
|
||||
// Column already exists, ignore
|
||||
}
|
||||
|
||||
export interface BatteryReading {
|
||||
id?: number;
|
||||
timestamp: string;
|
||||
@@ -47,14 +75,17 @@ export interface BatteryReading {
|
||||
status: string;
|
||||
health: string;
|
||||
ac_connected: boolean;
|
||||
capacity_full?: number;
|
||||
capacity_now?: number;
|
||||
capacity_design?: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
// Insert a new battery reading
|
||||
export function insertReading(reading: Omit<BatteryReading, 'id' | 'created_at'>, sessionId?: number) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO battery_readings (session_id, timestamp, percentage, voltage, current, power, status, health, ac_connected)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO battery_readings (session_id, timestamp, percentage, voltage, current, power, status, health, ac_connected, capacity_full, capacity_now, capacity_design)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -66,7 +97,10 @@ export function insertReading(reading: Omit<BatteryReading, 'id' | 'created_at'>
|
||||
reading.power,
|
||||
reading.status,
|
||||
reading.health,
|
||||
reading.ac_connected ? 1 : 0
|
||||
reading.ac_connected ? 1 : 0,
|
||||
reading.capacity_full || null,
|
||||
reading.capacity_now || null,
|
||||
reading.capacity_design || null
|
||||
);
|
||||
|
||||
return result.lastInsertRowid;
|
||||
@@ -85,6 +119,9 @@ export function getReadingsByTimeRange(startTime: string, endTime: string): Batt
|
||||
status,
|
||||
health,
|
||||
ac_connected,
|
||||
capacity_full,
|
||||
capacity_now,
|
||||
capacity_design,
|
||||
created_at
|
||||
FROM battery_readings
|
||||
WHERE timestamp BETWEEN ? AND ?
|
||||
@@ -112,6 +149,9 @@ export function getRecentReadings(limit: number = 100): BatteryReading[] {
|
||||
status,
|
||||
health,
|
||||
ac_connected,
|
||||
capacity_full,
|
||||
capacity_now,
|
||||
capacity_design,
|
||||
created_at
|
||||
FROM battery_readings
|
||||
ORDER BY timestamp DESC
|
||||
@@ -126,7 +166,30 @@ export function getRecentReadings(limit: number = 100): BatteryReading[] {
|
||||
})).reverse(); // Reverse to get chronological order
|
||||
}
|
||||
|
||||
// Get all monitoring sessions with actual reading counts
|
||||
// Calculate energy consumed for a session (in Wh)
|
||||
export function calculateSessionEnergy(sessionId: number): number {
|
||||
const readings = getReadingsBySession(sessionId);
|
||||
if (readings.length === 0) return 0;
|
||||
|
||||
let totalEnergy = 0;
|
||||
for (let i = 1; i < readings.length; i++) {
|
||||
const prevReading = readings[i - 1];
|
||||
const currReading = readings[i];
|
||||
|
||||
// Calculate time difference in hours
|
||||
const prevTime = new Date(prevReading.timestamp).getTime();
|
||||
const currTime = new Date(currReading.timestamp).getTime();
|
||||
const timeDiffHours = (currTime - prevTime) / (1000 * 3600);
|
||||
|
||||
// Use average power between two readings
|
||||
const avgPower = Math.abs((prevReading.power + currReading.power) / 2);
|
||||
totalEnergy += avgPower * timeDiffHours;
|
||||
}
|
||||
|
||||
return totalEnergy;
|
||||
}
|
||||
|
||||
// Get all monitoring sessions with actual reading counts and energy
|
||||
export function getMonitoringSessions() {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
@@ -142,7 +205,21 @@ export function getMonitoringSessions() {
|
||||
ORDER BY s.start_time DESC
|
||||
`);
|
||||
|
||||
return stmt.all();
|
||||
const sessions = stmt.all() as any[];
|
||||
|
||||
// Add energy calculation and duration for each session
|
||||
return sessions.map(session => {
|
||||
const energy = calculateSessionEnergy(session.id);
|
||||
const startTime = new Date(session.start_time).getTime();
|
||||
const endTime = new Date(session.end_time).getTime();
|
||||
const durationSeconds = Math.floor((endTime - startTime) / 1000);
|
||||
|
||||
return {
|
||||
...session,
|
||||
energy_wh: energy,
|
||||
duration_seconds: durationSeconds
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new monitoring session
|
||||
@@ -191,6 +268,9 @@ export function getReadingsBySession(sessionId: number): BatteryReading[] {
|
||||
status,
|
||||
health,
|
||||
ac_connected,
|
||||
capacity_full,
|
||||
capacity_now,
|
||||
capacity_design,
|
||||
created_at
|
||||
FROM battery_readings
|
||||
WHERE session_id = ?
|
||||
@@ -233,4 +313,59 @@ export function deleteSession(sessionId: number) {
|
||||
return deleteSessionStmt.run(sessionId);
|
||||
}
|
||||
|
||||
// Detect incomplete sessions (where end_time equals start_time but has readings)
|
||||
export function getIncompleteSessions() {
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.name,
|
||||
s.start_time,
|
||||
s.end_time,
|
||||
COUNT(r.id) as reading_count,
|
||||
MAX(r.timestamp) as last_reading_time,
|
||||
s.created_at
|
||||
FROM monitoring_sessions s
|
||||
INNER JOIN battery_readings r ON s.id = r.session_id
|
||||
WHERE s.start_time = s.end_time
|
||||
GROUP BY s.id
|
||||
HAVING COUNT(r.id) > 0
|
||||
ORDER BY s.start_time DESC
|
||||
`);
|
||||
|
||||
return stmt.all();
|
||||
}
|
||||
|
||||
// Repair a single incomplete session by updating end_time to last reading timestamp
|
||||
export function repairSession(sessionId: number) {
|
||||
const stmt = db.prepare(`
|
||||
UPDATE monitoring_sessions
|
||||
SET end_time = (
|
||||
SELECT MAX(timestamp)
|
||||
FROM battery_readings
|
||||
WHERE session_id = ?
|
||||
),
|
||||
reading_count = (
|
||||
SELECT COUNT(*)
|
||||
FROM battery_readings
|
||||
WHERE session_id = ?
|
||||
)
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
return stmt.run(sessionId, sessionId, sessionId);
|
||||
}
|
||||
|
||||
// Repair all incomplete sessions
|
||||
export function repairAllIncompleteSessions() {
|
||||
const incompleteSessions = getIncompleteSessions() as Array<{ id: number }>;
|
||||
let repairedCount = 0;
|
||||
|
||||
for (const session of incompleteSessions) {
|
||||
repairSession(session.id);
|
||||
repairedCount++;
|
||||
}
|
||||
|
||||
return repairedCount;
|
||||
}
|
||||
|
||||
export default db;
|
||||
|
||||
BIN
story-assets/feb4000-battery-percentage.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
story-assets/feb4000-energy-output-with-threshold.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
story-assets/feb4000-voltage-current.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
story-assets/uconsole-debugging-4g.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |