Apply modifications to mass-produced boards

- 3 channel power sensor ina3221
- io expander pca9557
- Some gpio moves
- ...

Signed-off-by: YoungSoo Shin <shinys000114@gmail.com>
This commit is contained in:
2025-08-28 16:47:03 +09:00
parent 94e831adbf
commit 2dc5798b0a
33 changed files with 1017 additions and 3023 deletions

View File

@@ -71,7 +71,7 @@
<div class="mt-4">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
Main Power (12V)
Main Power
<div class="control-wrapper">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="main-power-toggle">
@@ -79,7 +79,7 @@
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center control-list-item">
USB Power (5V)
USB Power
<div class="control-wrapper">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="usb-power-toggle">
@@ -111,7 +111,7 @@
<div class="d-flex justify-content-end mb-3">
<a href="/datalog.csv" class="btn btn-primary" download="datalog.csv"><i class="bi bi-download me-1"></i> Download CSV</a>
</div>
<h5 class="card-title text-center mb-3">Power Input</h5>
<h5 class="card-title text-center mb-3">Power Metrics</h5>
<div class="row">
<div class="col-md-4 mb-3 mb-md-0">
<canvas id="powerChart" class="chart-canvas"></canvas>
@@ -251,7 +251,7 @@
<div id="ap-mode-config" style="display: none;">
<div class="mb-3">
<label for="ap-ssid" class="form-label">AP SSID</label>
<input type="text" class="form-control" id="ap-ssid" placeholder="ODROID-Remote-AP">
<input type="text" class="form-control" id="ap-ssid" placeholder="odroid-pm">
</div>
<div class="mb-3">
<label for="ap-password" class="form-label">AP Password</label>
@@ -278,6 +278,12 @@
<option value="1500000" selected>1500000</option>
</select>
</div>
<hr>
<div class="mb-3">
<label class="form-label">System Reboot</label>
<p class="text-muted small">This will restart the device. The reboot will occur after 3 seconds.</p>
<button type="button" class="btn btn-danger" id="reboot-button">Reboot Now</button>
</div>
<div class="d-flex justify-content-end pt-3 border-top mt-3">
<button type="button" class="btn btn-primary me-2" id="baud-rate-apply-button">Apply</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
@@ -314,6 +320,38 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const rebootButton = document.getElementById('reboot-button');
if (rebootButton) {
rebootButton.addEventListener('click', () => {
if (confirm('Are you sure you want to reboot the device?')) {
fetch('/api/reboot', {
method: 'POST',
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Reboot command sent:', data);
const settingsModalEl = document.getElementById('settingsModal');
const settingsModal = bootstrap.Modal.getInstance(settingsModalEl);
if (settingsModal) {
settingsModal.hide();
}
alert('Reboot command sent. The device will restart in 3 seconds.');
})
.catch(error => {
console.error('Error sending reboot command:', error);
alert('Failed to send reboot command.');
});
}
});
}
});
</script>
</body>
</html>

View File

@@ -17,6 +17,7 @@ export const charts = {
current: null
};
const channelKeys = ['USB', 'MAIN', 'VIN'];
const CHART_DATA_POINTS = 30; // Number of data points to display on the chart
/**
@@ -53,10 +54,10 @@ function createChartOptions(title, minValue, maxValue) {
},
scales: {
x: { ticks: { autoSkipPadding: 10, maxRotation: 0, minRotation: 0 } },
y: {
y: {
min: minValue,
max: maxValue,
beginAtZero: false,
beginAtZero: true, // Start at zero for better comparison
ticks: {
stepSize: (maxValue - minValue) / 8
}
@@ -65,6 +66,23 @@ function createChartOptions(title, minValue, maxValue) {
};
}
/**
* Creates the dataset objects for a chart.
* @param {string} unit - The unit for the dataset label (e.g., 'W', 'V', 'A').
* @returns {Array<Object>} An array of Chart.js dataset objects.
*/
function createDatasets(unit) {
return channelKeys.map(channel => ({
label: `${channel} (${unit})`,
data: initialData(),
borderWidth: 2,
fill: false,
tension: 0.2,
pointRadius: 2
}));
}
/**
* Initializes all three charts (Power, Voltage, Current).
* If chart instances already exist, they are destroyed and new ones are created.
@@ -79,15 +97,12 @@ export function initCharts() {
// Create Power Chart
if (powerChartCtx) {
const powerOptions = createChartOptions('Power', 0, 120);
const powerOptions = createChartOptions('Power', 0, 50); // Adjusted max value
charts.power = new Chart(powerChartCtx, {
type: 'line',
data: {
labels: initialLabels(),
datasets: [
{ label: 'Power (W)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
{ label: 'Avg Power', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
]
datasets: createDatasets('W')
},
options: powerOptions
});
@@ -100,10 +115,7 @@ export function initCharts() {
type: 'line',
data: {
labels: initialLabels(),
datasets: [
{ label: 'Voltage (V)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
{ label: 'Avg Voltage', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
]
datasets: createDatasets('V')
},
options: voltageOptions
});
@@ -111,15 +123,12 @@ export function initCharts() {
// Create Current Chart
if (currentChartCtx) {
const currentOptions = createChartOptions('Current', 0, 7);
const currentOptions = createChartOptions('Current', 0, 5); // Adjusted max value
charts.current = new Chart(currentChartCtx, {
type: 'line',
data: {
labels: initialLabels(),
datasets: [
{ label: 'Current (A)', data: initialData(), borderWidth: 2, fill: false, tension: 0.2, pointRadius: 2 },
{ label: 'Avg Current', data: initialData(), borderWidth: 1.5, borderDash: [10, 5], fill: false, tension: 0, pointRadius: 0 }
]
datasets: createDatasets('A')
},
options: currentOptions
});
@@ -135,11 +144,14 @@ export function applyChartsTheme(themeName) {
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
const labelColor = isDark ? '#dee2e6' : '#212529';
const powerColor = getComputedStyle(htmlEl).getPropertyValue('--chart-power-color');
const voltageColor = getComputedStyle(htmlEl).getPropertyValue('--chart-voltage-color');
const currentColor = getComputedStyle(htmlEl).getPropertyValue('--chart-current-color');
// Define colors for each channel. These could be from CSS variables.
const channelColors = [
getComputedStyle(htmlEl).getPropertyValue('--chart-usb-color').trim() || '#0d6efd', // Blue
getComputedStyle(htmlEl).getPropertyValue('--chart-main-color').trim() || '#198754', // Green
getComputedStyle(htmlEl).getPropertyValue('--chart-vin-color').trim() || '#dc3545' // Red
];
const updateThemeForChart = (chart, color) => {
const updateThemeForChart = (chart) => {
if (!chart) return;
chart.options.scales.x.grid.color = gridColor;
chart.options.scales.y.grid.color = gridColor;
@@ -147,15 +159,17 @@ export function applyChartsTheme(themeName) {
chart.options.scales.y.ticks.color = labelColor;
chart.options.plugins.legend.labels.color = labelColor;
chart.options.plugins.title.color = labelColor;
chart.data.datasets[0].borderColor = color;
chart.data.datasets[1].borderColor = color;
chart.data.datasets[1].borderDash = [10, 5];
chart.data.datasets.forEach((dataset, index) => {
dataset.borderColor = channelColors[index];
});
chart.update('none');
};
updateThemeForChart(charts.power, powerColor);
updateThemeForChart(charts.voltage, voltageColor);
updateThemeForChart(charts.current, currentColor);
updateThemeForChart(charts.power);
updateThemeForChart(charts.voltage);
updateThemeForChart(charts.current);
}
/**
@@ -165,7 +179,7 @@ export function applyChartsTheme(themeName) {
export function updateCharts(data) {
const timeLabel = new Date(data.timestamp * 1000).toLocaleTimeString();
const updateSingleChart = (chart, value) => {
const updateSingleChart = (chart, metric) => {
if (!chart) return;
// Shift old data
@@ -174,18 +188,15 @@ export function updateCharts(data) {
// Push new data
chart.data.labels.push(timeLabel);
chart.data.datasets[0].data.push(value.toFixed(2));
// Calculate average
const dataArray = chart.data.datasets[0].data.filter(v => v !== null).map(v => parseFloat(v));
if (dataArray.length > 0) {
const sum = dataArray.reduce((acc, val) => acc + val, 0);
const avg = (sum / dataArray.length).toFixed(2);
chart.data.datasets[1].data.push(avg);
} else {
chart.data.datasets[1].data.push(null);
}
channelKeys.forEach((key, index) => {
if (data[key] && data[key][metric] !== undefined) {
const value = data[key][metric];
chart.data.datasets[index].data.push(value.toFixed(2));
} else {
chart.data.datasets[index].data.push(null); // Push null if data for a channel is missing
}
});
// Only update the chart if the tab is visible
if (graphTabPane.classList.contains('show')) {
@@ -193,9 +204,9 @@ export function updateCharts(data) {
}
};
updateSingleChart(charts.power, data.power);
updateSingleChart(charts.voltage, data.voltage);
updateSingleChart(charts.current, data.current);
updateSingleChart(charts.power, 'power');
updateSingleChart(charts.voltage, 'voltage');
updateSingleChart(charts.current, 'current');
}
/**

View File

@@ -34,7 +34,6 @@ function onWsOpen() {
if (term) {
term.write('\x1b[32mConnected to WebSocket Server\x1b[0m\r\n');
}
updateControlStatus();
}
/**
@@ -92,6 +91,9 @@ function initialize() {
const savedTheme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
applyTheme(savedTheme);
// Fetch initial status on page load
updateControlStatus();
// Establish the WebSocket connection with the defined handlers
initWebSocket({
onOpen: onWsOpen,

View File

@@ -1,15 +1,17 @@
:root {
--bs-body-font-family: 'Courier New', Courier, monospace;
--chart-power-color: #007bff;
--chart-voltage-color: #28a745;
--chart-current-color: #ffc107;
/* Chart Channel Colors */
--chart-usb-color: #0d6efd; /* Bootstrap Blue */
--chart-main-color: #198754; /* Bootstrap Green */
--chart-vin-color: #dc3545; /* Bootstrap Red */
}
[data-bs-theme="dark"] {
--chart-power-color: #569cd6;
--chart-voltage-color: #4ec9b0;
--chart-current-color: #dcdcaa;
/* Chart Channel Colors for Dark Theme */
--chart-usb-color: #569cd6; /* A lighter blue for dark backgrounds */
--chart-main-color: #4ec9b0; /* A teal/cyan for dark backgrounds */
--chart-vin-color: #d16969; /* A softer red for dark backgrounds */
}
body, .card, .modal-content, .list-group-item, .nav-tabs .nav-link {

View File

@@ -40,12 +40,18 @@ export function applyTheme(themeName) {
* @param {Object} data - The sensor data object from the WebSocket.
*/
export function updateSensorUI(data) {
dom.voltageDisplay.textContent = `${data.voltage.toFixed(2)} V`;
dom.currentDisplay.textContent = `${data.current.toFixed(2)} A`;
dom.powerDisplay.textContent = `${data.power.toFixed(2)} W`;
// Display VIN channel data in the header as a primary overview
if (data.VIN) {
dom.voltageDisplay.textContent = `${data.VIN.voltage.toFixed(2)} V`;
dom.currentDisplay.textContent = `${data.VIN.current.toFixed(2)} A`;
dom.powerDisplay.textContent = `${data.VIN.power.toFixed(2)} W`;
}
if (data.uptime_sec !== undefined) {
dom.uptimeDisplay.textContent = formatUptime(data.uptime_sec);
}
// Pass the entire multi-channel data object to the charts
updateCharts(data);
}