Compare commits

21 Commits

Author SHA1 Message Date
41bba5e492 Fix voltage monitor notifications and stability issues
Two critical fixes for the voltage monitoring system when 4G is active
on battery power:

1. Desktop Notifications Now Working:
   - Fixed D-Bus socket detection in voltage-alert-notify.sh
   - Changed from non-existent 'dbus-session' file to correct socket
     path /run/user/$USER_ID/bus
   - Notifications now properly appear when voltage drops below 3.45V
   - Added timestamp to notification message for better tracking
   - Made notification message more compact and actionable

2. Voltage Monitor Stability Fixed:
   - Added nohup when backgrounding monitor process in voltage-monitor-control.sh
   - Prevents SIGHUP signal when parent script exits
   - Monitor now remains stable and continuously detects low voltage
   - Rate-limited alerts working correctly (every 30 seconds)

Testing confirmed:
- Notifications display correctly with timestamp
- Monitor survives AC connect/disconnect cycles
- Low voltage detection working (threshold: 3.45V)
- Alerts sent successfully during battery drain with 4G active

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 13:29:38 +08:00
6a0bc9c4c8 Fix voltage alert desktop notifications by correcting D-Bus socket detection
The voltage monitoring system was detecting low battery voltage correctly
but desktop notifications were not appearing. The issue was in the D-Bus
session detection logic which searched for a non-existent "dbus-session"
file.

Changes:
- Use correct D-Bus socket path: /run/user/$USER_ID/bus
- Check socket existence with [ -S ] instead of searching for file
- Notifications now properly appear when voltage drops below 3.45V

This ensures users receive critical warnings when the 4G modem is active
on battery and voltage reaches dangerous levels that could cause system hangs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 12:39:43 +08:00
172eb6b046 Fix image caption formatting - add blank lines for proper display
Added blank lines between images and captions to ensure captions appear
on a new row below images instead of on the same line.

This follows Markdown best practices where a blank line creates a new
paragraph for the caption text.

Fixed in both STORY.md and STORY.zh-CN.md for all images.
2025-11-06 17:51:24 +08:00
a7ae5f6f88 Optimize all story images for faster loading
Image optimizations:
- Converted all PNG screenshots to JPG format
- Resized all images to max 800px width
- Reduced quality to 80% (optimal for web)
- Maintained visual clarity while reducing file sizes

Size reductions:
- uconsole-debugging-4g.jpg: 265KB → 119KB (55% smaller)
- feb4000-battery-percentage: 46KB → 28KB (39% smaller)
- feb4000-voltage-current: 69KB → 40KB (42% smaller)
- feb4000-energy-output-with-threshold: 70KB → 41KB (41% smaller)

Total savings: ~185KB combined (185KB → 228KB for all 4 images)
Faster page load times with no noticeable quality loss

Updated image references in both STORY.md and STORY.zh-CN.md
2025-11-06 17:45:51 +08:00
c14c8b63e8 Add uConsole debugging photo to story files
- Converted original 3.6MB iPhone photo to 265KB web-optimized JPG (93% reduction)
- Resized to 1200px width with 85% quality for fast loading
- Added to both English and Chinese story files
- Placed after introduction section to show vibe coding in action
- Caption emphasizes building on the device experiencing the problem

Image location: story-assets/uconsole-debugging-4g.jpg
2025-11-06 17:38:26 +08:00
5c179856af Completely rewrite STORY.zh-CN.md to align with English version
Major rewrite to match the voltage-focused narrative in STORY.md:

Content Changes:
- Removed outdated power budget calculations and theory
- Focused narrative on voltage drop discovery (not power delivery)
- Added voltage threshold analysis (3.45V minimum for 4G module)
- Included usable vs unusable capacity breakdown (54% vs 46%)
- Updated all component descriptions to unified power regulator
- Aligned power modes table and testing results
- Added event-driven AC detection mention

Technical Updates:
- Changed from 4G Power Manager to uConsole Smart Power Regulator
- Updated installation commands to install-uconsole-power-regulator.sh
- Added voltage monitoring system components description
- Included power modes table (AC/Battery+4G/Battery)
- Updated community impact section with AC detection

Structure:
- Now 250 lines (previously 397) - cleaner, more focused
- Matches English version structure and flow
- Removed redundant technical explanations
- Kept essential voltage discovery story

Both English and Chinese versions now tell the same story with proper alignment.
2025-11-06 17:15:32 +08:00
05a3d2d309 Update STORY.md and STORY.zh-CN.md with unified power regulator
- Updated installation commands to use new install-uconsole-power-regulator.sh
- Changed references from '4G Power Manager' to 'uConsole Smart Power Regulator'
- Added power modes table showing AC/Battery+4G/Battery scenarios
- Updated feature list to include AC detection and voltage monitoring
- Clarified voltage-based issue vs power budget issue
- Added upgrade path mention

Both English and Chinese versions now aligned with unified system.
2025-11-06 17:05:07 +08:00
a4bc66a745 Implement unified uConsole Smart Power Regulator with AC detection
Major overhaul of power management system to address voltage-induced 4G
module hangs through intelligent, event-driven power regulation.

Key Features:
- Unified power regulation based on AC power + 4G modem state
- Event-driven AC detection via udev (power_supply subsystem)
- Three optimized power modes for different scenarios
- Automatic voltage monitoring with multi-method alerts

Power Modes:
- AC Connected: 2.4GHz (ondemand) - full performance
- Battery + 4G: 1.8GHz (powersave) - voltage monitoring enabled
- Battery Only: 2.0GHz (ondemand) - balanced performance

Technical Improvements:
- AC power state detection via udev events (not polling)
- Edge case handling (service starts with AC connected)
- Unified logging to /var/log/uconsole-power-regulator.log
- Upgrade path from old 4G Power Manager
- State tracking to avoid redundant regulator triggers

Components:
- uconsole-power-regulator.sh: Main power orchestrator
- uconsole-power-daemon.sh: Background state monitoring (5s interval)
- voltage-monitor.sh: Voltage checker (Battery + 4G only)
- voltage-alert-notify.sh: Multi-method alerts (desktop/audio/log/LED)
- voltage-monitor-control.sh: Monitor lifecycle controller
- 99-uconsole-power-regulator.rules: udev event triggers
- install-uconsole-power-regulator.sh: Installation + upgrade script

Documentation:
- Updated README.md with new system overview
- Created README_CN.md (Chinese translation)
- Updated TOOL-GUIDE.md with new architecture details
- Updated CLAUDE.md with unified system documentation

Breaking Changes:
- Replaces 4G Power Manager with unified regulator
- New service name: uconsole-power-regulator.service
- New log file: /var/log/uconsole-power-regulator.log
- Use upgrade script to migrate from old system

Installation:
- Fresh: sudo ./install-uconsole-power-regulator.sh install
- Upgrade: sudo ./install-uconsole-power-regulator.sh upgrade
- Uninstall: sudo ./install-uconsole-power-regulator.sh uninstall
2025-11-06 17:00:13 +08:00
399bd4b1db Add low voltage notification system for 4G module
Implements udev-triggered voltage monitoring that only runs when 4G module is active, providing multi-method alerts when battery voltage drops below 3.45V (4G module minimum).

**New Features:**
- Voltage monitoring activated automatically with 4G module
- Desktop notifications (notify-send with urgent priority)
- Audio alerts (paplay/beep fallback)
- Log file warnings
- LED blink indicators (if available)
- Rate-limited alerts (every 30 seconds)
- Zero overhead when 4G is inactive

**Architecture:**
- udev events trigger voltage monitor start/stop
- No continuous polling when 4G off (resource efficient)
- Lightweight monitoring loop (5s interval, ~0.1% CPU)
- Multi-method notifications for maximum visibility

**New Components:**
- voltage-monitor.sh: Monitoring loop (checks voltage every 5s)
- voltage-alert-notify.sh: Multi-method notification handler
- voltage-monitor-control.sh: Start/stop controller
- Enhanced udev rules: Trigger monitoring on 4G state changes
- Updated installer: Dependencies and new scripts

**Dependencies Added:**
- libnotify-bin (desktop notifications)
- pulseaudio-utils (audio alerts)
- bc (voltage calculations - already required)

**Usage:**
After installation, voltage monitoring:
- Starts automatically when 4G module activates
- Stops automatically when 4G module deactivates
- Alerts user if voltage drops below 3.45V
- Repeats alerts every 30 seconds while low

Check status: sudo /usr/local/bin/voltage-monitor-control.sh status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 16:19:18 +08:00
75db60cff5 Split documentation into story and technical guide
**STORY.md (Main Article):**
- Focused on the 4G voltage discovery and vibe coding workflow
- Emphasizes the problem-solving narrative and methodology
- Highlights voltage drop as root cause, not power budget
- Shorter, more engaging format for casual readers
- Links to TOOL-GUIDE.md for technical details

**TOOL-GUIDE.md (Technical Reference):**
- Comprehensive user manual for battery monitor tool
- Complete API reference and endpoints
- Battery testing methodology and interpretation
- 4G power manager installation and tuning
- Troubleshooting guide and advanced usage
- Database schema and CSV format documentation

This split allows readers to:
1. Get inspired by the vibe coding story (STORY.md)
2. Dive deep into tool usage when needed (TOOL-GUIDE.md)

Both documents cross-reference each other appropriately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 15:49:35 +08:00
5b5c2dfa4e Rewrite story with voltage-focused 4G module analysis and add discharge curve visualization
Major story revision based on real-world testing and analysis:

**New Findings:**
- Root cause: Voltage drop below 3.45V (4G module minimum), not power budget
- SimTech SIM7600G-H operates at 3.4-4.2V; hangs below 3.45V threshold
- FEB-4000 battery test: 13.4 Wh usable (54%) above 3.45V threshold

**New Features:**
- Add 3.45V reference line to Energy Output vs. Voltage chart
- Visual indicator for 4G module minimum voltage threshold
- Real discharge curve analysis from 2h 48m FEB-4000 battery test

**Story Changes:**
- Focus shifted from power (watts) to voltage stability
- Include community discussion findings from ClockworkPi forums
- Add detailed FEB-4000 test results with screenshots
- Reframe 4G power manager as voltage-aware solution
- Add battery rating methodology based on usable capacity above 3.45V

**Screenshots Added:**
- feb4000-battery-percentage.png - Full discharge curve
- feb4000-voltage-current.png - Voltage/current trends
- feb4000-energy-output-with-threshold.png - Critical 3.45V analysis

This reflects the actual discovery: not insufficient wattage, but voltage sag under load causing 4G module brownout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 15:30:53 +08:00
9a6ceed18a Fix energy output chart rendering and performance issues
- Energy output chart now displays correctly for saved sessions by using first reading's capacityNow as baseline
- Improve chart label layout with proper spacing (removed cut-off issue)
- Eliminate 1s+ lag when editing session names by switching to uncontrolled input pattern
- Add keyboard shortcuts for session editing (Enter to save, Escape to cancel)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 14:55:58 +08:00
05cfac1088 Add 4th chart showing battery energy output vs voltage discharge curve
- Add initialCapacityRef to track starting battery capacity
- Transform historical data to calculate energy output in mWh using direct capacity method
- Reorganize charts into 2x2 grid layout with consistent 200px heights
- Add new chart: Energy Output (mWh) vs Voltage (V)
- Shows discharge curve for determining usable capacity at different voltage cutoffs
- Uses hardware fuel gauge readings for accuracy (no accumulation errors)
- Format voltage with 2 decimal places (.xx) and energy with 1 decimal place (.x)
- Maintains power-saving compatibility (hidden in background mode)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 10:19:00 +08:00
4dc62b07f2 Compact UI layout and reorder cards for better usability
Reduces visual clutter and improves information hierarchy by making
the battery status more dense and reordering important cards.

Battery Status Card Compacting:
- Reduced grid from 4 columns to 6 columns (more compact)
- Smaller text sizes: text-2xl → text-lg, text-xl → text-base
- Smaller labels: text-sm → text-xs
- Reduced spacing: space-y-4 → space-y-3, gap-4 → gap-3
- Thinner progress bar: default → h-2
- Split Voltage/Current into separate compact cells
- Shortened labels: "System Uptime" → "System Up"
- Capacity display: "X.XX / X.XX Wh" → "X.X/X.X Wh" (1 decimal)
- Removed redundant Health field (Status badge sufficient)
- Energy display: removed mWh subtext for cleaner look

Background Mode Banner:
- Changed from full Card to inline banner
- Reduced from py-6 to py-2 padding
- Smaller icons: h-6 → h-4
- Condensed text: "Background Mode Active" → "Background Mode:"
- Inline message: "Charts hidden, data collecting, recording active"
- Saves ~40px vertical space

Card Reordering:
- Moved "Recording Sessions" above "Export Custom Time Range"
- Sessions are used more frequently than custom exports
- Better flow: Status → Charts → Sessions → Export
- Sessions now appear before scrolling on most screens

Layout Benefits:
- More metrics visible without scrolling
- Faster scanning of key values
- Better information density
- Sessions card more prominent
- Cleaner, more professional appearance

Grid Responsiveness:
- Mobile: 2 columns
- Tablet (md): 4 columns
- Desktop (lg): 6 columns
- Adapts to screen size automatically
2025-11-05 23:25:32 +08:00
1360930783 Add system uptime and rename monitoring uptime to session time
Distinguishes between system/host uptime and monitoring session duration
for clearer metrics tracking.

System Uptime:
- Read from /proc/uptime (Linux system uptime)
- Shows how long the Raspberry Pi has been running
- Always visible in status card
- Format: Xh Ym Zs (e.g., "2h 15m 43s")
- Updates every 2 seconds with battery data

Session Time:
- Renamed from "Uptime" for clarity
- Tracks current monitoring session duration
- Only visible when monitoring is active
- Resets when Start Monitoring is clicked
- Independent of system uptime

Implementation:
- getSystemUptime() reads /proc/uptime first field (seconds)
- Added systemUptime to BatteryData interface
- Renamed uptime state to sessionTime
- Updated all references and UI labels

UI Layout:
Status card now shows (when monitoring):
├─ Battery metrics (6 cards)
├─ System Uptime (always visible)
├─ Session Time (monitoring only)
└─ Energy Consumed (monitoring only)

Benefits:
- System Uptime: Track system stability, detect unexpected reboots
- Session Time: Know exact monitoring duration for tests
- Clear distinction between host and application metrics
2025-11-05 23:16:51 +08:00
d4fa4ed1b9 Add uptime and energy consumption tracking
Implements real-time uptime and energy (Wh) tracking for monitoring
sessions, with live display and per-session historical calculations.

Real-Time Monitoring Display:
- Uptime counter updates every second (formatted as Xh Ym Zs)
- Energy consumed calculated using trapezoidal integration
- Formula: Energy += avg(Power[i-1], Power[i]) × time_delta
- Displays both Wh and mWh for precision
- Shows in status card when monitoring is active

Energy Calculation Algorithm:
- Uses trapezoidal rule for numerical integration
- Calculates average power between consecutive readings
- Accounts for variable time intervals
- Uses Math.abs() for absolute power consumption
- Accumulates energy from monitoring start

Per-Session Energy Display:
- calculateSessionEnergy() function in db.ts
- Processes all readings for a session
- Shows total Wh consumed during session
- Displays duration in human-readable format
- Updates session list with: "X readings • Xh Ym Zs • X.XXX Wh"

State Management:
- energyConsumed state tracks cumulative Wh
- uptime state tracks elapsed seconds
- lastPowerReadingRef stores previous reading for integration
- All reset on Start Monitoring
- All cleared on Stop Monitoring

Session Interface Updates:
- Added energy_wh?: number (optional for backward compatibility)
- Added duration_seconds?: number
- getMonitoringSessions() now calculates both automatically

UI Improvements:
- Two new cards in status grid during monitoring
- Uptime card with live seconds counter
- Energy card with Wh/mWh dual display
- Session list shows duration and energy per session
- formatUptime() helper for consistent time formatting

Performance:
- Energy calculation is O(n) over readings
- Cached in session query (no repeated calculations)
- Minimal overhead (~1ms per session)
2025-11-05 23:08:46 +08:00
054baa304f Add power-saving optimizations while preserving data granularity
Reverts the historicalData skip and adds multiple optimizations that
reduce power consumption without sacrificing data quality or reliability.

Data Preservation Changes:
- Revert: historicalData array now always updated (no data loss)
- Full granularity maintained in background mode
- Safe for power failure scenarios - last data point always preserved
- No changes to polling interval (still 2 seconds)

Power Optimizations (Applied to ALL modes):
1. Disable chart animations - isAnimationActive={false} on all charts
2. Remove CartesianGrid from all charts (~30% less SVG elements)
3. Lazy-load sessions list with Intersection Observer
   - Sessions only fetched when card scrolls into view
   - Eliminates initial load database query

Background Mode Benefits:
- Charts hidden (no rendering overhead)
- Same 2-second data collection
- historicalData array populated normally
- Database recording unchanged
- Combined optimizations save ~20-40% power
- Perfect for long-term monitoring and power testing

Live Mode Benefits:
- Full visualizations with cleaner, faster charts
- No animation overhead
- Simpler SVG rendering (no grid lines)
- Better performance on embedded systems

Technical Details:
- useEffect with IntersectionObserver for sessions lazy loading
- sessionsLoaded flag prevents multiple fetches
- threshold: 0.1 for early loading when user scrolls near
- All data arrays maintain full fidelity
- Monitoring buffer still caps at 1000 points

Documentation updates:
- Clarify full data granularity in background mode
- List all power optimizations with explanations
- Add note about power failure safety
2025-11-05 22:49:12 +08:00
824fdede86 Add Live and Background monitoring modes for battery optimization
Implements two display modes to reduce power consumption during monitoring
while maintaining consistent data collection quality.

Features:
- Live Mode: Full real-time charts with animations (default)
- Background Mode: Charts hidden, minimal UI updates (battery-saving)
- Same 2-second polling interval in both modes
- Database recording continues normally in both modes
- Mode toggle button appears when monitoring is active

Live Mode:
- All visualizations active (4 charts with animations)
- Real-time chart updates every 2 seconds
- Best for active monitoring and data analysis
- Higher power consumption from continuous SVG rendering

Background Mode:
- Charts completely hidden (no rendering overhead)
- Only current stats shown in status card
- Blue info card shows mode status
- Saves ~20-40% power by eliminating chart re-renders
- Best for long recording sessions
- historicalData state not updated (skips array manipulation)

UI improvements:
- Eye/EyeOff icons for mode toggle
- Background mode info card with status message
- Shows recording status in background mode message
- Charts still appear when viewing historical data

Technical optimizations:
- Skip setHistoricalData() updates in background mode
- Conditional chart rendering based on mode
- Buffer management continues in both modes
- No impact on data quality or database writes

Documentation updates:
- Add Display Modes section to CLAUDE.md
- Explain power savings and use cases for each mode
- Update feature list with two monitoring modes
2025-11-05 22:38:47 +08:00
b06ad685b0 Add incomplete session detection and repair functionality
When power dies during recording, all data is preserved but session
metadata is incomplete (end_time = start_time, reading_count = 0).
This adds automatic detection and repair of such sessions.

Features:
- Detect sessions where start_time equals end_time but has readings
- Visual indicators: yellow badge showing incomplete session count
- "Repair All" button in Recording Sessions card header
- Individual session badges showing "Incomplete" status
- Yellow highlighting for incomplete sessions in the list
- New API endpoint: /api/battery/sessions/repair

Database improvements:
- getIncompleteSessions() finds sessions needing repair
- repairSession() updates end_time to last reading timestamp
- repairAllIncompleteSessions() repairs all incomplete sessions
- Auto-calculates correct reading_count from actual data

UI improvements:
- AlertTriangle icon for incomplete session warnings
- RefreshCw icon for repair action
- Automatic incomplete session counting on session list load
- User-friendly repair confirmation with count of repaired sessions

Bug fix:
- Changed LEFT JOIN to INNER JOIN in incomplete detection query
- Use COUNT(r.id) instead of reading_count column for accuracy
2025-11-05 22:21:01 +08:00
b1141d1ee7 Fix record from start to actually save buffered data
When selecting "Record from start", the system now properly saves all
data points collected between clicking "Start Monitoring" and
"Start Recording".

Changes:
- Add new POST /api/battery/save endpoint for bulk saving readings
- Replace placeholder saveSingleReading with saveBufferedReadings
- Batch save all buffered readings in a single API call
- Log number of saved readings for verification

Previously, the saveSingleReading function was just a placeholder that
didn't actually save data to the database, causing data loss when users
selected "from start" option.
2025-11-05 22:00:27 +08:00
05eff3bf6a Add battery capacity tracking in Wh units
Extends battery monitoring to include energy capacity metrics:
- Read energy_full_design, energy_full, and energy_now from sysfs
- Convert µWh values to Wh for display and storage
- Add capacity columns to database with migration support
- Update UI to show current, full, and design capacity values
- Include capacity data in CSV exports

This provides better insight into actual battery health and remaining
energy beyond just percentage, useful for calculating runtime estimates.
2025-11-05 21:52:05 +08:00
31 changed files with 3200 additions and 817 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

102
CLAUDE.md
View File

@@ -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

View File

@@ -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
View 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 16App Router
- **语言**TypeScript
- **UI**React 19、shadcn/ui 组件、Tailwind CSS 4
- **图表**Recharts
- **数据库**SQLitebetter-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
View File

@@ -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.
![Debugging the 4G modem issue on uConsole itself](story-assets/uconsole-debugging-4g.jpg)
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:
![Energy Output vs Voltage with 3.45V Threshold](story-assets/feb4000-energy-output-with-threshold.jpg)
**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.
![Battery Monitor Interface](.playwright-mcp/battery-monitor-full-interface.png)
*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
![Battery Status Card](.playwright-mcp/screenshots/battery-status-live.png)
*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 Charts](.playwright-mcp/screenshots/charts-grid.png)
*Battery Percentage and Power Consumption charts showing real-time trends*
![Voltage and Current Chart](.playwright-mcp/screenshots/voltage-current-chart.png)
*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:
![Recording Sessions](.playwright-mcp/screenshots/recording-sessions.png)
*Saved recording sessions with download, edit, and delete options*
![Session Editing](.playwright-mcp/session-edit-mode.png)
*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:
![Export Custom Time Range](.playwright-mcp/export-custom-time-range.png)
*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

View File

@@ -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 和系统之间协调供电。
![uConsole 上调试 4G 调制解调器问题](story-assets/uconsole-debugging-4g.jpg)
几个小时的研究——数据手册、论坛深度挖掘和测量——揭示了罪魁祸首:
*在遇到问题的设备上构建解决方案——真正的 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 应用来可视化电源问题:
![能量输出 vs 电压(带 3.45V 阈值)](story-assets/feb4000-energy-output-with-threshold.jpg)
**技术栈:**
- 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 来说是无法使用的。
![电池监控界面](.playwright-mcp/battery-monitor-full-interface.png)
*完整的电池监控界面,显示实时指标、交互式图表和会话管理*
## 解决方案:电压感知电源管理
### 实时监控仪表板
确定根本原因后,修复变得显而易见:在 4G 激活时降低电流消耗以防止电压下降。
主界面以简洁、专注的设计显示实时电池指标:
**策略:**
- 检测 4G 模块何时激活(调制解调器状态、网络流量)
- 将 CPU 频率从 2.4GHz 降至 1.8GHz
- 更低的电流消耗 = 更少的电压下降
- 保持电压高于 3.45V 阈值
![电池状态卡片](.playwright-mcp/screenshots/battery-status-live.png)
*实时电池状态,显示充电水平、电压、电流、功耗和监控控制*
**结果:** 电池供电下 4G 运行非常稳定。
仪表板将监控与记录分离——你可以实时观察电池行为而不写入数据库。当发现有趣的现象时,点击"开始记录"并选择是从现在保存还是从监控开始追溯保存。
## 为什么 Vibe Coding 有效:零上下文切换的魔力
### 交互式图表
让我震惊的是:整个项目——从"为什么会崩溃"到"带自动电源管理的生产就绪解决方案"——在两天内大约花了 **10 小时**
三个独立图表从不同角度展示电池性能
![电池图表](.playwright-mcp/screenshots/charts-grid.png)
*电池百分比和功耗图表,显示实时趋势*
![电压和电流图表](.playwright-mcp/screenshots/voltage-current-chart.png)
*电压和电流趋势,双 Y 轴精确监控*
图表每 2 秒更新一次,提供功耗模式的即时视觉反馈。这对诊断 4G 电源问题至关重要——我可以实时观察调制解调器激活时的功率峰值。
### 会话管理
记录会话可以命名、导出和单独管理:
![记录会话](.playwright-mcp/screenshots/recording-sessions.png)
*保存的记录会话,带有下载、编辑和删除选项*
![会话编辑](.playwright-mcp/session-edit-mode.png)
*会话名称的内联编辑,便于识别*
每个会话捕获特定时间段内电池行为的完整画面。我用它来记录 4G 电源管理器的前后行为。
### 数据导出
应用提供灵活的导出选项:
![导出自定义时间范围](.playwright-mcp/export-custom-time-range.png)
*自定义时间范围选择,用于定向数据导出*
你可以导出每个会话的数据,或选择自定义时间范围,在 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 阈值可视化)
**关键发现的可视化:**
![FEB-4000 电池百分比](story-assets/feb4000-battery-percentage.jpg)
*完整的放电曲线从 100% 到约 2%*
![FEB-4000 电压和电流](story-assets/feb4000-voltage-current.jpg)
*电压下降和电流峰值模式*
![FEB-4000 能量输出(带阈值)](story-assets/feb4000-energy-output-with-threshold.jpg)
*关键图表显示 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(完整性能)- 无电压监控
电池 + 4G1.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 ChroniclesVibe 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
View 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
![Battery Percentage Chart](story-assets/feb4000-battery-percentage.png)
**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
![Voltage & Current Chart](story-assets/feb4000-voltage-current.png)
**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**
![Energy Output vs Voltage](story-assets/feb4000-energy-output-with-threshold.png)
**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

View File

@@ -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"

View File

@@ -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"

View 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"

View File

@@ -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

View 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
View 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

View 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

View 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
View 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

View 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
View 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

View File

@@ -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);

View File

@@ -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);

View 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 }
);
}
}

View File

@@ -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);

View 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 }
);
}
}

View File

@@ -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>
);
}

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB