Low Power Design #3: Lighttpd, a light weight server solution for battery monitoring applications using raspberryPi
- Get link
- X
- Other Apps
Introduction:
Using lighttpd
as a lightweight web server is an excellent idea for your Raspberry Pi setup. lighttpd
is very efficient, has a small memory footprint, and is well-suited for serving static files and handling simple dynamic content. Here’s how you can set up lighttpd
to serve your battery monitoring data:
Since i am using Alpine linux as a root that's why you won't be seeing sudo here.
1. Install and Configure lighttpd
First, you need to install lighttpd
on your Raspberry Pi:
apk updateapk add lighttpdyou can now start the lighttpd server and access it in your browser(remember the browser deviceshould also be in the same network) by typing http:/ipaddress_of_the_device_running_lighttpd serverrc-service lighttpd startif your browser says 403:forbidden that mean index.html is not present at the desired location.
2. Set Up CGI for Dynamic Content: configuration
To allow lighttpd
to execute Python scripts, you can enable CGI (Common Gateway Interface).
Enable CGI Module:
- Enable CGI Module:
sudo lighttpd-enable-mod cginot needed on alpine linux
- Edit the
lighttpd
Configuration:
Open the lighttpd
configuration file:
sudo vim /etc/lighttpd/lighttpd.confremove all the contents in it and then put the below contents in it
Add below to enable CGI and set the path for Python scripts:
server.modules = (
"mod_indexfile",
"mod_access",
"mod_alias",
"mod_accesslog"
)
server.document-root = "/var/www/localhost/htdocs"
server.upload-dirs = ("/var/cache/lighttpd/uploads")
server.errorlog = "/var/log/lighttpd/error.log"
server.pid-file = "/run/lighttpd/lighttpd.pid"
server.username = "lighttpd"
server.groupname = "lighttpd"
index-file.names = ( "index.html" )
server.modules += ( "mod_cgi" )
cgi.assign = ( ".cgi" => "/bin/sh", ".py" => "/usr/bin/python3" )
alias.url += ( "/data" => "/usr/local/bin/soc_data.py" )
Save and exit the file (Ctrl + X
, then Y
, then Enter
).
- Create the CGI Directory:
Remember these CGI scripts can be accress from http://ip/test.cgi from any mobile device
on the same network.
vim /var/www/localhost/htdocs/test.cgi
chmod +x /var/www/localhost/htdocs/test.cgichown lighttpd:lighttpd /var/www/localhost/htdocs/test.cgi
sample CGI file that will run in shell: put this in test.cgi
#!/bin/sh
echo "Content-Type: text/plain"
echo ""
echo "Hello, World!"this will be called when the http://ip/test.cgi is hit from the browser it can server some
other purpose at the same time.
3. Write the Python CGI Script to sample SOC and Voltage
Create a Python script that reads the battery data and returns it as JSON.
- Create the Python Script:vim /usr/local/bin/soc_data.py
chmod +x /usr/local/bin/soc_data.py
mkdir -p /var/www/localhost/htdocs
ln -s /usr/local/bin/soc_data.py /var/www/localhost/htdocs/data
chmod 755 /var/www/localhost/htdocschmod 755 /var/www/localhost
mkdir -p /var/cache/lighttpd/uploads (optional)
chown lighttpd:lighttpd /var/cache/lighttpd/uploads(optional) chmod 755 /var/cache/lighttpd/uploads (optional)To create the symbolic link to the data dir in the htdocs, this is becasue Javascript islooking for /data dir for soc and voltage info, please remember that in the lighttpd configfile we have alias for /data to the /usr/local/bin/soc_data.py
Add the following code (remember this code is at some different location so either you should move it to the location where your index.html file is present or create a symbolic link :
#!/usr/bin/env python3
import smbus2
import json
import cgi
print("Content-Type: application/json\n")
MAX17048_ADDRESS = 0x36
bus = smbus2.SMBus(1)
def read_voltage():
raw_voltage = bus.read_word_data(MAX17048_ADDRESS, 0x02)
voltage = ((raw_voltage & 0xFF) << 8) | (raw_voltage >> 8)
voltage = voltage * 1.25 / 1000
return voltage
def read_soc():
raw_soc = bus.read_word_data(MAX17048_ADDRESS, 0x04)
soc = ((raw_soc & 0xFF) << 8) | (raw_soc >> 8)
soc = soc / 256.0
return soc
if __name__ == "__main__":
voltage = read_voltage()
soc = read_soc()
data = {'voltage': voltage, 'soc': soc}
print(json.dumps(data))OR use below script that will simulate the SOC and V of a battery#!/usr/bin/env python3
import json
import random
import time
import os
from datetime import datetime
# Simulate stored values between requests
def load_battery_state():
if os.path.exists("/tmp/battery_state.json"):
with open("/tmp/battery_state.json", "r") as f:
return json.load(f)
# Ensure history is present in the state
if "history" not in state:
state["history"] = []
return state
else:
# If no state is saved, start with full battery
return {"soc": 100, "voltage": 4.2, "history": []}
def save_battery_state(state):
with open("/tmp/battery_state.json", "w") as f:
json.dump(state, f)
def simulate_battery_discharge(state):
# Initialize history if it's missing
if "history" not in state:
state["history"] = []
# Reduce SOC by 0.5% per request
state["soc"] = max(0, state["soc"] - 0.5) # Prevent SOC from going below 0%
# Simulate voltage drop based on SOC
if state["soc"] > 20:
state["voltage"] = 4.2 - (100 - state["soc"]) * 0.007 # Slowly decrease voltage
else:
state["voltage"] = 3.5 + (state["soc"] / 20) * 0.7 # Faster drop below 20% SOC
# Ensure voltage doesn't go below 3.5V
state["voltage"] = max(3.5, round(state["voltage"], 2))
# Track SOC history with timestamps
timestamp = time.time() # Current timestamp in seconds since epoch
state["history"].append({"soc": state["soc"], "timestamp": timestamp})
# Keep only the last 20 history points to avoid too much data
if len(state["history"]) > 20:
state["history"].pop(0)
# Reset when fully discharged (optional)
if state["soc"] == 0:
state["soc"] = 100 # Reset SOC to 100%
state["voltage"] = 4.2 # Reset voltage to 4.2V
state["history"] = [] # Reset history after full discharge
return state
def calculate_avg_soc_change_per_hour(state):
history = state["history"]
if len(history) < 2:
return 0 # Not enough data to calculate the rate
# Calculate the time difference and SOC difference between the oldest and newest entry
soc_start = history[0]["soc"]
soc_end = history[-1]["soc"]
time_start = history[0]["timestamp"]
time_end = history[-1]["timestamp"]
# Calculate the change in SOC and time (convert seconds to hours)
soc_change = soc_start - soc_end
time_diff_hours = (time_end - time_start) / 3600 # Time difference in hours
# Calculate average change per hour
if time_diff_hours > 0:
avg_soc_change_per_hour = soc_change / time_diff_hours
return avg_soc_change_per_hour
else:
return 0
def estimate_battery_life(state, avg_soc_change_per_hour):
if avg_soc_change_per_hour > 0:
remaining_hours = state["soc"] / avg_soc_change_per_hour
return round(remaining_hours, 2)
else:
return "N/A"
# Load current battery state
battery_state = load_battery_state()
print("Loaded battery state:", battery_state) # Debug line
# Load current battery state
#battery_state = load_battery_state()
# Simulate the discharge process
battery_state = simulate_battery_discharge(battery_state)
# Save the updated state
save_battery_state(battery_state)
# Calculate average SOC change per hour
avg_soc_change_per_hour = calculate_avg_soc_change_per_hour(battery_state)
# Estimate remaining battery life
estimated_battery_life = estimate_battery_life(battery_state, avg_soc_change_per_hour)
# Output as JSON
print("Content-Type: application/json")
print()
data = {
"soc": f"{battery_state['soc']}%",
"voltage": f"{battery_state['voltage']}V",
"avg_soc_change_per_hour": f"{round(avg_soc_change_per_hour, 2)}%" if avg_soc_change_per_hour else "N/A",
"estimated_battery_life_hours": estimated_battery_life
}
print(json.dumps(data))
Make the script executable:
sudo chmod +x /var/www/cgi-bin/battery_monitor.py
4. Create the Frontend HTML File to display SOC and Voltage data
Now create an HTML file that will use JS to fetch and display the data.
- Create the HTML File:
vim /var/www/localhost/htdocs/index.html
Add the following code (this code will receive the simulated battery voltage and SOC):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Battery SOC and Voltage</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
}
#chart-container {
position: relative;
width: 90%; /* Make the chart take 90% of the screen width */
max-width: 2000px; /* Limit max size for large screens */
height: 400px; /* Set height */
margin: 20px auto;
}
canvas {
width: 100% !important; /* Force the canvas to take full width */
height: auto !important; /* Maintain aspect ratio */
}
</style>
</head>
<body>
<h1>Battery SOC and Voltage</h1>
<!-- New elements for Avg SOC Change per Hour and Estimated Battery Life -->
<h3 id="avg-change">Avg SOC Change per Hour: N/A</h3>
<h3 id="remaining-life">Estimated Battery Life: N/A hours</h3>
<div id="chart-container">
<canvas id="myChart"></canvas>
</div>
<script>
// Get reference to chart element
const ctx = document.getElementById('myChart').getContext('2d');
// Initial data and chart setup
const data = {
labels: [], // This will hold time or index of each update
datasets: [
{
label: 'SOC (%)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
data: [],
yAxisID: 'y',
},
{
label: 'Voltage (V)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderColor: 'rgba(255, 99, 132, 1)',
data: [],
yAxisID: 'y1',
}
]
};
// Create the chart
const myChart = new Chart(ctx, {
type: 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false, // Allow chart to resize
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'SOC (%)',
}
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Voltage (V)',
}
}
}
}
});
/*
// Function to fetch data and update the chart
async function fetchData() {
try {
const response = await fetch('/data'); // Make sure /data points to your CGI script
const jsonData = await response.json();
const soc = parseFloat(jsonData.soc.replace('%', '')); // Remove % symbol from SOC
const voltage = parseFloat(jsonData.voltage.replace('V', '')); // Remove V symbol from Voltage
// Get the current timestamp for plotting
const timestamp = new Date().toLocaleTimeString();
// Update the chart with the new data
if (data.labels.length > 500) { // Keep only the latest 20 data points
data.labels.shift();
data.datasets[0].data.shift();
data.datasets[1].data.shift();
}
data.labels.push(timestamp); // Add timestamp for each update
data.datasets[0].data.push(soc); // Add SOC
data.datasets[1].data.push(voltage); // Add Voltage
myChart.update(); // Redraw the chart
} catch (error) {
console.error('Error fetching data:', error);
}
}
// Fetch data every 2 seconds
setInterval(fetchData, 2000);
*/
// Function to fetch data and update the chart
async function fetchData() {
try {
const response = await fetch('/data'); // Make sure /data points to your CGI script
const jsonData = await response.json();
const soc = parseFloat(jsonData.soc.replace('%', '')); // Remove % symbol from SOC
const voltage = parseFloat(jsonData.voltage.replace('V', '')); // Remove V symbol from Voltage
const avgSocChange = jsonData.avg_soc_change_per_hour; // Average SOC change per hour
const estimatedLife = jsonData.estimated_battery_life_hours; // Estimated remaining hours
// Update HTML elements with the new data (for example, add these values somewhere in your HTML page)
document.getElementById('avg-change').innerText = `Avg SOC Change per Hour: ${avgSocChange}`;
document.getElementById('remaining-life').innerText = `Estimated Battery Life: ${estimatedLife} hours`;
// Get the current timestamp for plotting
const timestamp = new Date().toLocaleTimeString();
// Update the chart with the new data
if (data.labels.length > 500) { // Keep only the latest 20 data points
data.labels.shift();
data.datasets[0].data.shift();
data.datasets[1].data.shift();
}
data.labels.push(timestamp); // Add timestamp for each update
data.datasets[0].data.push(soc); // Add SOC
data.datasets[1].data.push(voltage); // Add Voltage
myChart.update(); // Redraw the chart
} catch (error) {
console.error('Error fetching data:', error);
}
}
// Fetch data every 2 seconds
setInterval(fetchData, 2000);
</script>
</body>
</html>
5. Restart lighttpd
After making all these changes, restart the lighttpd
server:
rc-service lighttpd restart
6. Access the Application
Now, open a web browser on your mobile device and navigate to the IP address of your Raspberry Pi:
http://<your_raspberry_pi_ip>/
This should display the battery voltage and state of charge in real time.
- Get link
- X
- Other Apps
Comments
Post a Comment