Game Instance


Let the games begin

Dual NiMH Battery Smart Charger

A Web based interface

I was emphasizing the fact that the Arduino Web Console can be extended to fit the needs of your project. This article will center on how to do just that, exemplifying on the Smart Charger I built and wrote about few posts back. A minimal understanding of HTML and Javascript will be required.

Arduino Web Console modified to interface the NiMH smart charger project. Battery parameters are monitored in real-time. Charger configuration changes can be made on-the-fly. Arduino Web Console modified to interface the NiMH smart charger project. Both devices are slow-charging old, high internal resistance NiMH batteries.

First

Get the latest version of the Web Console from Github. Open the script with your favorite editor/IDE, look for <span id="status"></span><br /><br /> and paste the following snippet after it:

<legend>Batteries</legend>
<table style="width: 100%">
	<tr>
		<td bgcolor="#FFFFCC">
			<b>#1</b> - <i><span id="battery_1_status"></span></i><br />
			<canvas id="cnv_voltage1" width="160" height="160"></canvas>
			<canvas id="cnv_current1" width="160" height="160"></canvas>
			<canvas id="cnv_temperature1" width="160" height="160"></canvas>
			<canvas id="cnv_capacity1" width="160" height="160"></canvas>
			<br />
			<legend>Device configuration</legend>
			Mode: <select id="device_mode1">
				<option value="1">Discharge only</option>
				<option value="2">Charge only</option>
				<option value="3" selected>Dischage, then charge</option>
			</select>
			&nbsp; 
			Slow Charging current: <select id="slow_charging_current1">
				<option value="150">150 mA</option>
				<option value="240" selected>240 mA</option>
				<option value="360">360 mA</option>
			</select>
			&nbsp; 
			Fast Charging current: <select id="fast_charging_current1">
				<option value="650">650 mA</option>
				<option value="800" selected>800 mA</option>
				<option value="1000">1000 mA</option>
			</select>
			&nbsp; 
			<input type="button" value="Apply" onclick="ApplyChanges(1);" class="pure-button pure-button-primary" />
		</td>
		<td style="width: 20px;">
		</td>
		<td bgcolor="#FFFFCC">
			<b>#2</b> - <i><span id="battery_2_status"></span></i><br />
			<canvas id="cnv_voltage2" width="160" height="160"></canvas>
			<canvas id="cnv_current2" width="160" height="160"></canvas>
			<canvas id="cnv_temperature2" width="160" height="160"></canvas>
			<canvas id="cnv_capacity2" width="160" height="160"></canvas>
			<br />
			<legend>Device configuration</legend>
			Mode: <select id="device_mode2">
				<option value="1">Discharge only</option>
				<option value="2">Charge only</option>
				<option value="3" selected>Dischage, then charge</option>
			</select>
			&nbsp; 
			Slow Charging current: <select id="slow_charging_current2">
				<option value="150">150 mA</option>
				<option value="240" selected>240 mA</option>
				<option value="360">360 mA</option>
			</select>
			&nbsp; 
			Fast Charging current: <select id="fast_charging_current2">
				<option value="650">650 mA</option>
				<option value="800" selected>800 mA</option>
				<option value="1000">1000 mA</option>
			</select>
			&nbsp; 
			<input type="button" value="Apply" onclick="ApplyChanges(2);" class="pure-button pure-button-primary" />
		</td>
	</tr>
</table>
<br /><br />

You've just added the HTML code that will help interacting with the charger. There are a total four gauges for each charging device indicating the cell voltage, charging current, cell temperature and absorbed charge. Besides them, the interface allows changing the real-time charging parameters such as function mode and charging currents. However, the code you've just added won't work unless you add the Javascript part that drives it. Look for the Send function definition and paste the following lines after it:

var gauge = function (cnv_id, signal, markers) {
	//
	var el = document.getElementById(cnv_id);
	var cnv = el.getContext('2d');
	var width = el.width;
	var height = el.height;
	var margin_x = 30;
	var margin_y = 30;
	var w_inner = width - 2 * margin_x;
	var h_inner = height - 2 * margin_y;
	
	// clear canvas
	cnv.clearRect(0, 0, width, height);
	cnv.save();
	
	cnv.strokeStyle = "#333333";
	cnv.lineWidth = 2;
	cnv.beginPath();
	var size = w_inner > h_inner ? h_inner : w_inner;
	var cx = size / 2, cy = size / 2;
	var r = size * 7 / 8 / 2;
	var da = 3 / 2 * Math.PI;
	var start_a = 3 * Math.PI / 4;
	var end_a = start_a + da;
	var a = start_a - (signal.value - signal.min) / (signal.max - signal.min) * da;
	var x1 = r * 3 / 10 * Math.sin(a);
	var y1 = r * 3 / 10 * Math.cos(a);
	var x2 = r * 9 / 10 * Math.sin(a);
	var y2 = r * 9 / 10 * Math.cos(a);
	var text_size = size > 120 ? size / 10 : 12;
	
	// gauge background
	cnv.arc(margin_x + cx, margin_y + cy, r, start_a, end_a);
	
	// push min, max values as markers
	var marker_font_size = 12;
	if (typeof markers == 'undefined') {
		// 
		var markers = [];
	}
	markers.push(signal.min);
	markers.push(signal.max);

	// markers
	if (typeof markers == 'object') {
		//
		markers.sort();
		var max_length = markers[markers.length - 1].toString.length;
		var height_to_width_ratio = 8 / 6;
		cnv.font = "bold italic " + marker_font_size + "px sans-serif";
		for (var i = 0; i < markers.length; i ++) {
			//
			var marker_value = markers[i];
			var ma = start_a - (marker_value - signal.min) / (signal.max - signal.min) * da;
			var mr = r + height_to_width_ratio * marker_font_size * max_length; //marker_value.toString().length;
			cnv.fillText(
				marker_value, 
				margin_x + cx - mr * Math.sin(ma) - height_to_width_ratio / 2 * marker_font_size * max_length, 
				margin_y + cy - mr * Math.cos(ma) + 2 * marker_font_size / 3
			);
		}
	}
	cnv.stroke();

	// gauge indicator
	cnv.strokeStyle = "#008F00";
	cnv.beginPath();
	cnv.moveTo(margin_x + cx - x1, margin_y + cy - y1);
	cnv.lineTo(margin_x + cx - x2, margin_y + cy - y2);
	cnv.stroke();

	// value
	cnv.font = "bold italic " + text_size + "px sans-serif";
	var value_str = signal.value.toFixed(2);
	cnv.fillText(
		value_str, 
		margin_x + cx - value_str.toString().length / 2 * 3 * text_size / 4, 
		margin_y + cy + text_size / 2
	);

	// unit
	if (typeof signal.unit != 'undefined') {
		// 
		var unit_text_size = 3 * text_size / 4;
		cnv.font = "bold " + unit_text_size + "px sans-serif";
		cnv.fillText(
			signal.unit, 
			margin_x + cx - signal.unit.toString().length / 2 * 3 * unit_text_size / 4, 
			margin_y + cy + unit_text_size / 2 + r / 2
		);
	}
};

var Signal = function (value, unit, min, max) {
	// 
	this.value = value || 0;
	this.unit = unit || "";
	this.min = min || 0;
	this.max = max || this.min + 10;
};

var voltage1 = new Signal(1.0, "V", 0.4, 2.0);
var current1 = new Signal(0, "A", -1.0, 1.0);
var temperature1 = new Signal(20, "deg C", 0, 50);
var capacity1 = new Signal(0, "Ah", 0, 3);

var voltage2 = new Signal(1.0, "V", 0.4, 2.0);
var current2 = new Signal(0, "A", -1.0, 1.0);
var temperature2 = new Signal(20, "deg C", 0, 50);
var capacity2 = new Signal(0, "Ah", 0, 3);

var Extract = function (str, begins, ends) {
	//
	var start = str.indexOf(begins);
	if (start < 0) {
		//
		return "";
	}
	var end = str.indexOf(ends, start + 1);
	if (end < 0) {
		// 
		return "";
	}
	return str.substr(start + begins.length, end - start - begins.length);
};

var Process = function () {
	// 
	var textarea = document.getElementById('log');
	var lines = textarea.value.split("\n");
	var last_line = lines[lines.length - 2];
	var items = last_line.split(" ");
	var battery_1_status = document.getElementById('battery_1_status');
	var battery_2_status = document.getElementById('battery_2_status');
	var voltage_value = parseFloat(Extract(last_line, "Vbat = ", " V"));
	var current_value = parseFloat(Extract(last_line, "Ishunt = ", " A"));
	var temperature_value = parseFloat(Extract(last_line, "Tbat = ", " degC"));
	var capacity_value = parseFloat(Extract(last_line, "Cout = ", " mAh")) / 1000;
	var status;
	var voltage_marks = [0.6, 0.8, 1, 1.2, 1.4, 1.6, 1.8];
	var current_marks = [-0.5, 0, 0.5];
	var temperature_marks = [10, 20, 30, 40];
	var capacity_marks = [0.5, 1, 1.5, 2, 2.5];
	if (items.length >= 38) {
		// 
		status = ' <font color="green">Fast charging</font>';
	} else if (items.length >= 35) {
		//
		status = ' <font color="brown">Discharging</font>';
	} else if (items.length >= 33) {
		// 
		status = ' <font color="green">Slow charging</font>';
	} else if (items.length >= 10) {
		// 
		status = ' <font color="blue">Please remove the battery</font>';
	}
	if (last_line.substr(0, 8) == 'DEVICE 1') {
		// 
		battery_1_status.innerHTML = status;
		if (voltage_value != '') {
			// 
			voltage1.value = parseFloat(voltage_value);
			gauge("cnv_voltage1", voltage1, voltage_marks);
		}
		if (current_value != '') {
			// 
			current1.value = parseFloat(current_value);
			gauge("cnv_current1", current1, current_marks);
		}
		if (temperature_value != '') {
			// 
			temperature1.value = parseFloat(temperature_value);
			gauge("cnv_temperature1", temperature1, temperature_marks);
		}
		if (capacity_value != '') {
			// 
			capacity1.value = parseFloat(capacity_value);
			gauge("cnv_capacity1", capacity1, capacity_marks);
		}
	}
	else if (last_line.substr(0, 8) == 'DEVICE 2') {
		// 
		battery_2_status.innerHTML = status;
		if (voltage_value != '') {
			// 
			voltage2.value = parseFloat(voltage_value);
			gauge("cnv_voltage2", voltage2, voltage_marks);
		}
		if (current_value != '') {
			// 
			current2.value = parseFloat(current_value);
			gauge("cnv_current2", current2, current_marks);
		}
		if (temperature_value != '') {
			// 
			temperature2.value = parseFloat(temperature_value);
			gauge("cnv_temperature2", temperature2, temperature_marks);
		}
		if (capacity_value != '') {
			// 
			capacity2.value = parseFloat(capacity_value);
			gauge("cnv_capacity2", capacity2, capacity_marks);
		}
	}
};

var ApplyChanges = function (device_id) {
	// 
	var mode = document.getElementById('device_mode' + device_id).value;
	var slow_charging_current = document.getElementById('slow_charging_current' + device_id).value;
	var fast_charging_current = document.getElementById('fast_charging_current' + device_id).value;
	var command = "config:" + device_id + ":" + mode + ":" + slow_charging_current + ":" + fast_charging_current + ":";
	jx.load('/serial|write|' + command + '|', function (data) {
		// 
		if (data == 'Ok') {
			// 
		} else {
			// 
			alert(data);
		}
	});
};

You also need to hook-up the new functionality to the temporized Ajax call handler. You will have to replace the Probe function definition with this:

var Probe = function () {
	// 
	var ts = last_ts > 0 ? last_ts : 0;
	jx.load('/serial|read|' + ts + '|', function (data) {
		// 
		var obj = JSON.parse(data);
		var textarea = document.getElementById('log');
		textarea.value += obj.data;
		last_ts = obj.ts;
		if (document.getElementById('autoscroll').checked) {
			// 
			textarea.scrollTop = textarea.scrollHeight;
		}
		Process();
	});
};

Restart the script and refresh the page http://localhost:8000/. It should work without restarting it but that brings peace of mind, so why not. Place a battery on any of the charger slots and check-out the real-time parameters. You can perform config changes and even apply them. None will work, however, unless you change the Arduino code of your project to accept such serial commands. How to do that is out of the scope of this article but I invite you to read the Arduino Irrigation System with Bluetooth post. You'll find there all you need to implement that.

This concludes

the demonstration on how to extend the Arduino Web Console and solves the interfacing limitation the NiMH battery charger had. Two in one shot. Hidden in the code there's a function called gauge that you might want to check out. It draws a canvas-based minimalistic gauge that could come handy anytime you need to monitor live parameters.