MP3 player may be useful in museum or exhibition to provide voice-based information to visitors on demand. This project shows how to control MP3 via webpage using PHPoC.
Demonstration
Things Used In This Project
- PHPoC Blue or Black
- Serial MP3 Player
- Speaker
Serial MP3 Player
Serial MP3 player have two interfaces:
- jack to speaker
- interface to micro-controller (in this project is PHPoC)
When receiving a command from micro-controller (e.g PLAY, PAUSE, VOLUME UP...), MP3 player read .mp3 file from SD card and perform action based on the command.
Before using, it need to copy .mp3 files to SD card and mount it to MP3 Player.
Wiring Diagram
Connect pin GND, VCC TX and RX of MP3 Player to pin GND, PWR5, U0RX and pin U0TX of PHPoC, respectively.
Data Flow
Web browser ---> PHPoC
Web app on web browser will send commands and data based on touch or click event to PHPoC via WebSocket. When receiving the command, PHPoC sends command to MP3 player according to the command received from web browser.
Command Set
Where, XX is volume value.
Source Code
Source code includes two files:
- index.php: This file is client side code. When receiving HTTP request from web browser, PHPoC interprets PHP script in this file, and then send the interpreted file to web browser. The interpreted file (contains html, css and JavaScript code) provides UI (User Interface), handling touch/click event from User and send command back to PHPoC via websocket.
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>PHPoC - IoT MP3 Player</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=0.7">
<style>
body { text-align: center; font-size: 15pt; }
h1 { font-weight: bold; font-size: 25pt; }
h2 { font-weight: bold; font-size: 15pt; }
button { font-weight: bold; font-size: 15pt; }
</style>
<script>
var canvas_width = 450, canvas_height = 250;
var trans_x = canvas_width/2, trans_y = canvas_height/2 - 30;
var plate_width = 300, plate_height = 80;
var volume_width = 300, volume_height = 8;
var circle_radius = 80;
var volume_y = circle_radius + 35;
var inner_radius;
var pause_width, pause_height;
var play_width, play_height;
var next_width, next_height, next_top, next_right;
var arrow_width;
var play_state = 0; // 0: pause. 1: playing
var volume = 20;
var click_state = 0; // 0: no click, 1: back button click, 2: next button click
var ws;
function init()
{
var canvas = document.getElementById("remote");
canvas.style.backgroundColor = "#999999";
canvas.width = canvas_width;
canvas.height = canvas_height;
canvas.addEventListener("touchstart", mouse_down);
canvas.addEventListener("touchend", mouse_up);
canvas.addEventListener("touchmove", mouse_move);
canvas.addEventListener("mousedown", mouse_down);
canvas.addEventListener("mouseup", mouse_up);
canvas.addEventListener("mousemove", mouse_move);
var ctx = canvas.getContext("2d");
ctx.translate(trans_x, trans_y);
inner_radius = circle_radius - 25;
next_height = Math.round(0.6 * plate_height);
arrow_width = Math.round(next_height * Math.cos(Math.PI/3.5));
next_width = 2*arrow_width - 7;
next_top = next_height / 2;
next_right = plate_width / 2 + 15;
update_view();
}
function connect_onclick()
{
if(ws == null)
{
var ws_host_addr = "<?echo _SERVER("HTTP_HOST")?>";
if((navigator.platform.indexOf("Win") != -1) && (ws_host_addr.charAt(0) == "["))
{
// network resource identifier to UNC path name conversion
ws_host_addr = ws_host_addr.replace(/[\[\]]/g, '');
ws_host_addr = ws_host_addr.replace(/:/g, "-");
ws_host_addr += ".ipv6-literal.net";
}
ws = new WebSocket("ws://" + ws_host_addr + "/remote_slide", "text.phpoc");
document.getElementById("ws_state").innerHTML = "CONNECTING";
ws.onopen = ws_onopen;
ws.onclose = ws_onclose;
ws.onmessage = ws_onmessage;
}
else
ws.close();
}
function ws_onopen()
{
document.getElementById("ws_state").innerHTML = "<font color='blue'>CONNECTED</font>";
document.getElementById("bt_connect").innerHTML = "Disconnect";
}
function ws_onclose()
{
document.getElementById("ws_state").innerHTML = "<font color='gray'>CLOSED</font>";
document.getElementById("bt_connect").innerHTML = "Connect";
ws.onopen = null;
ws.onclose = null;
ws.onmessage = null;
ws = null;
}
function ws_onmessage(e_msg)
{
e_msg = e_msg || window.event; // MessageEvent
alert("msg : " + e_msg.data);
}
function update_view()
{
var canvas = document.getElementById("remote");
var ctx = canvas.getContext("2d");
ctx.clearRect(-trans_x, -trans_y, canvas_width, canvas_height);
ctx.fillStyle="#404040";
ctx.beginPath();
ctx.arc(-plate_width / 2, 0, plate_height / 2, 0.5 * Math.PI, 1.5 * Math.PI);
ctx.lineTo(plate_width / 2, -plate_height / 2);
ctx.arc(plate_width / 2, 0, plate_height / 2, 1.5 * Math.PI, 0.5 * Math.PI);
ctx.lineTo(-plate_width / 2, plate_height / 2);
ctx.fill();
var gradient=ctx.createLinearGradient(0,-circle_radius,0,circle_radius);
gradient.addColorStop(0,"white");
gradient.addColorStop(0.5,"#cceeff");
gradient.addColorStop(1,"white");
ctx.fillStyle=gradient;
ctx.beginPath();
ctx.arc(0, 0, circle_radius, 0 , 2 * Math.PI);
ctx.fill();
var arrow1_right = next_right;
var arrow1_left = next_right - arrow_width;
var arrow2_left = next_right - next_width;
var arrow2_right = arrow2_left + arrow_width;
// Back button
if(click_state == 1)
ctx.fillStyle="#66ffff";
else
ctx.fillStyle=gradient;
ctx.beginPath();
ctx.lineTo(-arrow1_right, 0);
ctx.lineTo(-arrow1_left, next_height / 2);
ctx.lineTo(-arrow1_left, -next_height / 2);
ctx.fill();
ctx.beginPath();
ctx.lineTo(-arrow2_right, 0);
ctx.lineTo(-arrow2_left, next_height / 2);
ctx.lineTo(-arrow2_left, -next_height / 2);
ctx.fill();
// Next button
if(click_state == 2)
ctx.fillStyle="#66ffff";
else
ctx.fillStyle=gradient;
ctx.beginPath();
ctx.lineTo(arrow1_right, 0);
ctx.lineTo(arrow1_left, next_height / 2);
ctx.lineTo(arrow1_left, -next_height / 2);
ctx.fill();
ctx.beginPath();
ctx.lineTo(arrow2_right, 0);
ctx.lineTo(arrow2_left, next_height / 2);
ctx.lineTo(arrow2_left, -next_height / 2);
ctx.fill();
var x = Math.round(inner_radius * Math.cos(Math.PI/3));
var y = Math.round(inner_radius * Math.cos(Math.PI/6));
ctx.fillStyle="#2eb82e";
if(!play_state)
{
// Pausing button
ctx.beginPath();
ctx.lineTo(inner_radius, 0);
ctx.lineTo(-x, y);
ctx.lineTo(-x, -y);
ctx.fill();
}
else
{
// Playing button
x -= 3;
y -= 3;
var bar_width = 14;
ctx.beginPath();
ctx.lineTo(-x - bar_width, -y);
ctx.lineTo(-x - bar_width, y);
ctx.lineTo(-x + bar_width, y);
ctx.lineTo(-x + bar_width, -y);
ctx.fill();
ctx.beginPath();
ctx.lineTo(x - bar_width, -y);
ctx.lineTo(x - bar_width, y);
ctx.lineTo(x + bar_width, y);
ctx.lineTo(x + bar_width, -y);
ctx.fill();
}
// Volume
volume_height += 4;
ctx.fillStyle="#404040";
ctx.beginPath();
ctx.arc(-volume_width / 2, volume_y, volume_height / 2, 0.5 * Math.PI, 1.5 * Math.PI);
ctx.lineTo(volume_width / 2, volume_y - volume_height / 2);
ctx.arc(volume_width / 2, volume_y, volume_height / 2, 1.5 * Math.PI, 0.5 * Math.PI);
ctx.lineTo(-volume_width / 2, volume_y + volume_height / 2);
ctx.fill();
volume_height -= 6;
var temp = volume * volume_width / 30 - volume_width / 2;
ctx.fillStyle="#2eb82e";
ctx.beginPath();
ctx.arc(-volume_width / 2, volume_y, volume_height / 2, 0.5 * Math.PI, 1.5 * Math.PI);
ctx.lineTo(temp, volume_y - volume_height / 2);
ctx.arc(temp, volume_y, volume_height / 2, 1.5 * Math.PI, 0.5 * Math.PI);
ctx.lineTo(-volume_width / 2, volume_y + volume_height / 2);
ctx.fill();
volume_height += 2;
ctx.fillStyle="white";
ctx.beginPath();
ctx.arc(temp, volume_y, volume_height*2, 0, 2 * Math.PI);
ctx.fill();
}
function mouse_down()
{
if(ws == null)
return;
event.preventDefault();
var x, y;
if(event.offsetX)
{
x = event.offsetX - trans_x;
y = event.offsetY - trans_y;
}
else if(event.layerX)
{
x = event.layerX - trans_x;
y = event.layerY - trans_y;
}
else
{
x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - trans_x;
y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - trans_y;
}
var radius = Math.sqrt(x*x + y*y);
if(radius < inner_radius)
{
// pause/play button is click
play_state = (play_state + 1)%2;
if(play_state)
ws.send("P" + "0\r\n");
else
ws.send("S" + "0\r\n");
}
else if((y < next_top) && (y > -next_top))
{
if((x < next_right) && (x > (next_right-next_width)))
{
ws.send("N" + "0\r\n");
console.log("next button is pressed!");
click_state = 2;
}
else if((x > -next_right) && (x < -(next_right-next_width)))
{
ws.send("B" + "0\r\n");
console.log("back button is pressed!");
click_state = 1;
}
}
else
{
if((y > (volume_y - volume_height*3))
&& (y < (volume_y + volume_height*3))
&& (x < (volume_width / 2))
&& (x > -(volume_width / 2)))
{
volume = Math.round((volume_width / 2 + x)/ volume_width *30);
ws.send("V" + volume.toString() + "\r\n");
console.log("volume changed!");
}
}
update_view();
}
function mouse_up()
{
if(ws == null)
return;
event.preventDefault();
click_state = 0;
update_view();
}
function mouse_move()
{
if(ws == null)
return;
event.preventDefault();
var x, y;
if(event.offsetX)
{
x = event.offsetX - trans_x;
y = event.offsetY - trans_y;
}
else if(event.layerX)
{
x = event.layerX - trans_x;
y = event.layerY - trans_y;
}
else
{
x = (Math.round(event.touches[0].pageX - event.touches[0].target.offsetLeft)) - trans_x;
y = (Math.round(event.touches[0].pageY - event.touches[0].target.offsetTop)) - trans_y;
}
if((y > (volume_y - volume_height*3))
&& (y < (volume_y + volume_height*3))
&& (x < (volume_width / 2))
&& (x > -(volume_width / 2)))
{
volume = Math.round((volume_width / 2 + x)/ volume_width *30);
ws.send("V" + volume.toString() + "\r\n");
console.log("volume changed!");
}
update_view();
}
window.onload = init;
</script>
</head>
<body>
<p>
<h1>Web Remote Control / IoT MP3 Player</h1>
</p>
<canvas id="remote"></canvas>
<h2>
<p>
WebSocket : <span id="ws_state">null</span>
</p>
<button id="bt_connect" type="button" onclick="connect_onclick();">Connect</button>
</h2>
</body>
</html>
- task0.php: This file is server side code. It is run in infinite loop to receive and handle command from web browser, control MP3 player.
PHP Code:
<?php
if(_SERVER("REQUEST_METHOD"))
exit; // avoid php execution via http request
include "/lib/sd_340.php";
include "/lib/sn_tcp_ws.php";
define("CMD_PLAY_NEXT", 0x01);
define("CMD_PLAY_PREV", 0x02);
define("CMD_PLAY_W_INDEX", 0x03);
define("CMD_SET_VOLUME", 0x06);
define("CMD_SEL_DEV", 0x09);
define("CMD_PLAY_W_VOL", 0x22);
define("CMD_PLAY", 0x0D);
define("CMD_PAUSE", 0x0E);
define("CMD_SINGLE_CYCLE", 0x19);
define("DEV_TF", 0x02);
define("SINGLE_CYCLE_ON", 0x00);
define("SINGLE_CYCLE_OFF", 0x01);
function mp3_send_cmd($cmd, $data)
{
$cmd_buf = "\x7E"; /* $S - Every command should start with $(0x7E) */
$cmd_buf .= "\xFF"; /* VER - Version information */
$cmd_buf .= "\x06"; /* Len - The number of bytes of the command without starting byte and ending byte */
$cmd_buf .= int2bin($cmd, 1); /* CMD - Such as PLAY and PAUSE and so on */
$cmd_buf .= "\x00"; /* Feedback - 0x00 = not feedback, 0x01 = feedback */
$cmd_buf .= int2bin($data, 2, TRUE); /* data - usually it has two bytes */
$cmd_buf .= "\xEF"; /* $O - Ending byte of the command */
uart_write(0, $cmd_buf, 8);
}
uart_setup(0, 9600, "N81N");
ws_setup(0, "remote_slide", "text.phpoc");
$cmd_buf = "";
$rbuf = "";
while(1)
{
if(ws_state(0) == TCP_CONNECTED)
{
$rlen = ws_read_line(0, $rbuf);
if($rlen)
{
$name = $rbuf[0];
$value = (int)substr($rbuf, 1);
if($name == 'P')
{
echo "Play mp3\r\n";
mp3_send_cmd(CMD_PLAY, 0x0000);
}
if($name == 'S')
{
echo "Pause mp3\r\n";
mp3_send_cmd(CMD_PAUSE, 0x0000);
}
if($name == 'N')
{
echo "Play next mp3\r\n";
mp3_send_cmd(CMD_PLAY_NEXT, 0x0000);
}
if($name == 'B')
{
echo "Play previous mp3\r\n";
mp3_send_cmd(CMD_PLAY_PREV, 0x0000);
}
if($name == 'V')
{
echo "Change volume to $value\r\n";
mp3_send_cmd(CMD_SET_VOLUME, $value);
}
}
}
}
?>