Merge pull request #1 from shinys000114/master

Update schematic, Add example script
This commit is contained in:
Hardkernel Co., Ltd.
2025-11-19 16:39:01 +09:00
committed by GitHub
8 changed files with 288 additions and 56 deletions

41
example/logger/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Power Consumption Logger Example
Based on this script, you can monitor power consumption and implement graph plotting.
## How to Run the Script
### Install Python Virtual Environment
```shell
sudo apt install virtualenv
virtualenv venv
source venv/bin/activate
```
### Install require package
```shell
pip install grpcio-tools requests websockets protobuf pandas matplotlib
```
### Build `status_pb2.py`
```shell
python -m grpc_tools.protoc -I ../../proto --python_out=. status.proto
```
### Execute script
#### Power consumption collection
```shell
# python3 logger.py -u <username> -o <name.csv> -p <password> <address>
python3 logger.py -u admin -p password -o test.csv 192.168.30.5
```
#### Plot data
```shell
python3 csv_2_plot.py test.csv plot.png
```
![plot.png](plot.png)

View File

@@ -0,0 +1,95 @@
import argparse
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import os
import pandas as pd
def plot_power_data(csv_path, output_path):
"""
Reads power data from a CSV file and generates a plot image.
Args:
csv_path (str): The path to the input CSV file.
output_path (str): The path to save the output plot image.
"""
try:
# Read the CSV file into a pandas DataFrame
# The 'timestamp' column is parsed as dates
df = pd.read_csv(csv_path, parse_dates=['timestamp'])
print(f"Successfully loaded {len(df)} records from '{csv_path}'")
except FileNotFoundError:
print(f"Error: The file '{csv_path}' was not found.")
return
except Exception as e:
print(f"An error occurred while reading the CSV file: {e}")
return
# Create a figure and a set of subplots (3 rows, 1 column)
# sharex=True makes all subplots share the same x-axis (time)
fig, axes = plt.subplots(3, 1, figsize=(15, 18), sharex=True)
# --- Plot 1: Power (W) ---
ax1 = axes[0]
ax1.plot(df['timestamp'], df['vin_power'], label='VIN', color='red')
ax1.plot(df['timestamp'], df['main_power'], label='MAIN', color='green')
ax1.plot(df['timestamp'], df['usb_power'], label='USB', color='blue')
ax1.set_title('Power Consumption')
ax1.set_ylabel('Power (W)')
ax1.legend()
ax1.grid(True, which='both', linestyle='--', linewidth=0.5)
# --- Plot 2: Voltage (V) ---
ax2 = axes[1]
ax2.plot(df['timestamp'], df['vin_voltage'], label='VIN', color='red')
ax2.plot(df['timestamp'], df['main_voltage'], label='MAIN', color='green')
ax2.plot(df['timestamp'], df['usb_voltage'], label='USB', color='blue')
ax2.set_title('Voltage')
ax2.set_ylabel('Voltage (V)')
ax2.legend()
ax2.grid(True, which='both', linestyle='--', linewidth=0.5)
# --- Plot 3: Current (A) ---
ax3 = axes[2]
ax3.plot(df['timestamp'], df['vin_current'], label='VIN', color='red')
ax3.plot(df['timestamp'], df['main_current'], label='MAIN', color='green')
ax3.plot(df['timestamp'], df['usb_current'], label='USB', color='blue')
ax3.set_title('Current')
ax3.set_ylabel('Current (A)')
ax3.legend()
ax3.grid(True, which='both', linestyle='--', linewidth=0.5)
# --- Formatting the x-axis (Time) ---
# Improve date formatting on the x-axis
ax3.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
ax3.xaxis.set_major_locator(plt.MaxNLocator(15)) # Limit the number of ticks
plt.xlabel('Time')
plt.xticks(rotation=45)
# Add a main title to the figure
start_time = df['timestamp'].iloc[0].strftime('%Y-%m-%d %H:%M:%S')
end_time = df['timestamp'].iloc[-1].strftime('%H:%M:%S')
fig.suptitle(f'ODROID Power Log ({start_time} to {end_time})', fontsize=16, y=0.95)
# Adjust layout to prevent titles/labels from overlapping
plt.tight_layout(rect=[0, 0, 1, 0.94])
# --- Save the plot to a file ---
try:
plt.savefig(output_path, dpi=150)
print(f"Plot successfully saved to '{output_path}'")
except Exception as e:
print(f"An error occurred while saving the plot: {e}")
def main():
parser = argparse.ArgumentParser(description="Generate a plot from an Odroid PowerMate CSV log file.")
parser.add_argument("input_csv", help="Path to the input CSV log file.")
parser.add_argument("output_image", help="Path to save the output plot image (e.g., plot.png).")
args = parser.parse_args()
plot_power_data(args.input_csv, args.output_image)
if __name__ == "__main__":
main()

152
example/logger/logger.py Normal file
View File

@@ -0,0 +1,152 @@
import argparse
import asyncio
import csv
import requests
import websockets
from datetime import datetime
# Import the status_pb2.py file generated by `protoc`.
# This file must be in the same directory as logger.py.
import status_pb2
class OdroidPowerLogger:
"""
A class to connect to the Odroid Smart Power monitoring server and log power data.
1. Logs into the server via an HTTP POST request to obtain an authentication token.
2. Connects to the WebSocket using the obtained token.
3. Receives and decodes binary data in Protobuf format, then prints it.
"""
def __init__(self, host, username, password, output_file=None):
self.host = host
self.username = username
self.password = password
self.base_url = f"http://{self.host}"
self.ws_url = f"ws://{self.host}/ws"
self.output_file = output_file
self.token = None
def login(self):
"""Logs into the server to retrieve an authentication token."""
login_url = f"{self.base_url}/login"
payload = {"username": self.username, "password": self.password}
try:
print(f"Attempting to log in to '{login_url}'...")
response = requests.post(login_url, json=payload, timeout=5)
response.raise_for_status()
response_json = response.json()
if "token" in response_json:
self.token = response_json["token"]
print("Login successful! Token received.")
return True
else:
print("Login failed: No token in response.")
return False
except requests.exceptions.RequestException as e:
print(f"Error during login: {e}")
return False
async def listen_power_data(self):
"""Connects to the WebSocket to receive and log power data."""
if not self.token:
print("Cannot connect to WebSocket without an authentication token.")
return
# Add the authentication token as a query parameter
uri = f"{self.ws_url}?token={self.token}"
csv_file = None
csv_writer = None
try:
# --- CSV File Handling ---
if self.output_file:
try:
# Open the file in write mode, with newline='' to prevent extra blank rows
csv_file = open(self.output_file, 'w', newline='', encoding='utf-8')
csv_writer = csv.writer(csv_file)
# Write header
header = [
'timestamp', 'uptime_sec',
'vin_voltage', 'vin_current', 'vin_power',
'main_voltage', 'main_current', 'main_power',
'usb_voltage', 'usb_current', 'usb_power'
]
csv_writer.writerow(header)
print(f"Logging data to {self.output_file}")
except IOError as e:
print(f"Error opening CSV file: {e}")
# If file can't be opened, disable CSV writing
csv_file = None
csv_writer = None
# --- End CSV File Handling ---
async with websockets.connect(uri) as websocket:
print(f"Connected to WebSocket: {uri}")
while True:
# Receive binary message from the server
message_bytes = await websocket.recv()
# Decode the Protobuf message
status_message = status_pb2.StatusMessage()
status_message.ParseFromString(message_bytes)
# Process only if the payload type is 'sensor_data'
if status_message.WhichOneof('payload') == 'sensor_data':
sensor_data = status_message.sensor_data
ts_dt = datetime.fromtimestamp(sensor_data.timestamp)
ts_str = ts_dt.strftime('%Y-%m-%d %H:%M:%S')
print(f"--- {ts_str} (Uptime: {sensor_data.uptime_sec}s) ---")
# Print data for each channel
for name, channel in [('VIN', sensor_data.vin), ('MAIN', sensor_data.main),
('USB', sensor_data.usb)]:
print(
f" {name:<4}: {channel.voltage:5.2f} V | {channel.current:5.3f} A | {channel.power:5.2f} W")
# Write to CSV if enabled
if csv_writer:
row = [
ts_dt.isoformat(), sensor_data.uptime_sec,
sensor_data.vin.voltage, sensor_data.vin.current, sensor_data.vin.power,
sensor_data.main.voltage, sensor_data.main.current, sensor_data.main.power,
sensor_data.usb.voltage, sensor_data.usb.current, sensor_data.usb.power
]
csv_writer.writerow(row)
except websockets.exceptions.ConnectionClosed as e:
print(f"WebSocket connection closed: {e}")
except Exception as e:
print(f"Error during WebSocket processing: {e}")
finally:
if csv_file:
csv_file.close()
print(f"\nCSV file '{self.output_file}' saved.")
async def run(self):
"""Runs the logger."""
if self.login():
await self.listen_power_data()
async def main():
parser = argparse.ArgumentParser(description="Odroid Smart Power Data Logger")
parser.add_argument("host", help="Server's host address or IP (e.g., 192.168.1.10)")
parser.add_argument("-u", "--username", required=True, help="Login username")
parser.add_argument("-p", "--password", required=True, help="Login password")
parser.add_argument("-o", "--output", help="Path to the output CSV file.")
args = parser.parse_args()
logger = OdroidPowerLogger(host=args.host, username=args.username, password=args.password, output_file=args.output)
await logger.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nExiting program.")

BIN
example/logger/plot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@@ -31,8 +31,6 @@ void app_main(void)
}
ESP_ERROR_CHECK(ret);
storage_init();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

View File

@@ -1,6 +1,5 @@
dependencies:
espressif/led_indicator: ^1.1.1
joltwallet/littlefs: ==1.20.1
esp-idf-lib/ina3221: ^1.1.7
esp-idf-lib/pca9557: ^1.0.7
nikas-belogolov/nanopb: ^1.0.0

View File

@@ -1,53 +0,0 @@
#include "storage.h"
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "esp_littlefs.h"
#include "esp_log.h"
static const char* TAG = "datalog";
#define MAX_LOG_SIZE (700 * 1024)
void storage_init(void)
{
ESP_LOGI(TAG, "Initializing DataLog with LittleFS");
esp_vfs_littlefs_conf_t conf = {
.base_path = "/littlefs",
.partition_label = "littlefs",
.format_if_mount_failed = true,
.dont_mount = false,
};
esp_err_t ret = esp_vfs_littlefs_register(&conf);
if (ret != ESP_OK)
{
if (ret == ESP_FAIL)
{
ESP_LOGE(TAG, "Failed to mount or format filesystem");
}
else if (ret == ESP_ERR_NOT_FOUND)
{
ESP_LOGE(TAG, "Failed to find LittleFS partition");
}
else
{
ESP_LOGE(TAG, "Failed to initialize LittleFS (%s)", esp_err_to_name(ret));
}
return;
}
size_t total = 0, used = 0;
ret = esp_littlefs_info(conf.partition_label, &total, &used);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to get LittleFS partition information (%s)", esp_err_to_name(ret));
}
else
{
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
}

BIN
schematic.pdf Normal file

Binary file not shown.