Audio Processing #1: Basic Terminologies

Low Power Design #3: Lighttpd, a light weight server solution for battery monitoring applications using raspberryPi

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 update
apk add lighttpd 

you can now start the lighttpd server and access it in your browser(remember the browser device 
should also be in the same network) by typing http:/ipaddress_of_the_device_running_lighttpd ser
ver
 
rc-service lighttpd start 
if 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 cgi
not needed on alpine linux 
  • Edit the lighttpd Configuration:

Open the lighttpd configuration file:

sudo vim /etc/lighttpd/lighttpd.conf
remove 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.cgi
chown 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/htdocs
    chmod 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 is
    looking for /data dir for soc and voltage info, please remember that in the lighttpd config
    file 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.


 

Comments