Added Espruino control (fix #417)

pull/513/head
Gordon Williams 2020-06-24 09:55:28 +01:00
parent f40e3a2549
commit c449a258f1
6 changed files with 319 additions and 0 deletions

View File

@ -1987,5 +1987,19 @@
"storage": [ "storage": [
{"name":"gpsautotime.wid.js","url":"widget.js"} {"name":"gpsautotime.wid.js","url":"widget.js"}
] ]
},
{ "id": "espruinoctrl",
"name": "Espruino Control",
"shortName":"Espruino Ctrl",
"icon": "app.png",
"version":"0.01",
"description": "Send commands to other Espruino devices via the Bluetooth UART interface. Customisable commands!",
"tags": "",
"readme": "README.md",
"custom": "custom.html",
"storage": [
{"name":"espruinoctrl.app.js"},
{"name":"espruinoctrl.img","url":"app-icon.js","evaluate":true}
]
} }
] ]

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1,28 @@
# Espruino Control
Send commands to other Espruino devices via the Bluetooth UART interface.
## Customising
Click the customise button and you can customise your commands
with 4 options:
* **Title** - The title of the menu item that will be displayed
* **Command** - The JS command to execute, eg. `LED.toggle()`
* **MAC Address** - If specified, of the form `aa:bb:cc:dd:ee:ff`. The device
with this address will be connected to directly. If not specified a menu
showing available Espruino devices is popped up.
* **RX** - If checked, the app will display any data received from the
device being connected to. Use this if you want to print data - eg: `print(E.getBattery())`
When done, click 'Upload'. Your changes will be saved to local storage
so they'll be remembered next time you upload from the same device.s
## Usage
Simply load the app and you'll see a menu with the menu items
you defined. Select one and you'll be able to connect to the device
and send the command.
If a command should wait for a response then

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwhH+AH4A/AH4A/AH4AFwIuuAAIllAAYIGF041IF34AKqwuuAANXF9QuCAANdGHqQgGBwvdGCIud5mjGB4udAAIwPFz3MSR61VFxQwNci4vGeh4uXGAguHGBK3WGA4AIegtXc69dGBxoBGAouWO4IwNe4gwZa4YwLFwikEFzAwLFwwwCFzQwKFw68YGB4AdF5AwmF5IwlF5QwkF5Yw/F8IwEL9WBB4IuuADwuzGxAugFAgliGBYutAH4A/AH4A/ADA="))

BIN
apps/espruinoctrl/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,275 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-exp.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<style>
.badgeerror[data-badge]::after { background-color: red; }
</style>
</head>
<body>
<p>Enter the menu items you'd like to see appear in the app below. <code>MAC Address</code> is the MAC address
of the device to use. If it isn't specified then a menu will be presented showing available devices.</p>
<div style="overflow-x:scroll">
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Command</th>
<th>MAC Address</th>
<th>RX</th>
<th></th>
</tr>
</thead>
<tbody id="tbody">
</tbody>
</table>
</div>
<p><button id="upload" class="btn btn-primary">Upload to Bangle.js</button></p>
<script src="../../lib/customize.js"></script>
<script>
var options;
try {
options = JSON.parse(localStorage.getItem("espruinoctrl"));
} catch (e) {}
if (!Array.isArray(options))
setDefaults();
function setDefaults() {
options = [
{"title":"Blink LED",cmd:"digitalPulse(LED1,1,100)",mac:"",rx:false},
{"title":"Restart",cmd:"load()",mac:"",rx:false},
{"title":"process.env",cmd:"print(process.env)",mac:"",rx:true},
{"title":"Battery",cmd:"print(`Battery ${E.getBattery()}%`)",mac:"",rx:true},
];
}
function changed() {
localStorage.setItem("espruinoctrl",JSON.stringify(options));
}
function isMACValid(mac) {
return mac.trim().match(/^[A-Fa-f0-9][A-Fa-f0-9]:[A-Fa-f0-9][A-Fa-f0-9]:[A-Fa-f0-9][A-Fa-f0-9]:[A-Fa-f0-9][A-Fa-f0-9]:[A-Fa-f0-9][A-Fa-f0-9]:[A-Fa-f0-9][A-Fa-f0-9]$/)!=null;
}
function refresh() {
var tbody = document.getElementById("tbody");
tbody.innerHTML = "";
options.forEach((option,idx)=>{
var tr = document.createElement("tr");
tr.innerHTML = `
<td><input type="text" name="title"></td>
<td><input type="text" name="command"></td>
<td><span data-badge="!"><input type="text" name="mac"></span></td>
<td><label class="form-switch"><input type="checkbox"><i class="form-icon"></i></label></td>
<td><button class="btn btn-action"><i class="icon icon-delete"></i></button></td>
`;
var titleInput = tr.children[0].firstChild;
titleInput.value = option.title;
titleInput.addEventListener("change", function(e) {
option.title = titleInput.value;
changed();
});
var cmdInput = tr.children[1].firstChild;
cmdInput.value = option.cmd;
cmdInput.addEventListener("change", function(e) {
option.cmd = cmdInput.value;
changed();
});
var warningspan = tr.children[2].firstChild;
var macInput = warningspan.firstChild;
macInput.value = option.mac;
macInput.addEventListener("change", function(e) {
if (isMACValid(macInput.value)) {
warningspan.classList.remove("badge");
warningspan.classList.remove("badgeerror");
} else {
warningspan.classList.add("badge");
warningspan.classList.add("badgeerror");
}
option.mac = macInput.value.trim();
changed();
});
var cmdRX = tr.children[3].firstChild.firstChild;
cmdRX.checked = option.rx;
cmdRX.addEventListener("change", function(e) {
option.rx = cmdRX.checked;
changed();
});
tr.children[4].firstChild.addEventListener("click", function() {
options.splice(idx,1);
changed();
refresh();
});
tbody.appendChild(tr);
});
var tr = document.createElement("tr");
tr.innerHTML = `
<td></td>
<td></td>
<td><button class="btn">Reset to Defaults</button></td>
<td></td>
<td><button class="btn btn-action"><i class="icon icon-plus"></i></button></td>
`;
tr.children[2].firstChild.addEventListener("click", function() {
setDefaults();
changed();
refresh();
});
tr.children[4].firstChild.addEventListener("click", function() {
options.push({"title":"",cmd:"",mac:""});
changed();
refresh();
});
tbody.appendChild(tr);
}
refresh();
document.getElementById("upload").addEventListener("click", function() {
var app = `
var menu = {
"" : {title:"Espruino Ctrl"},
${options.filter(o=>o.title.trim()!="").map(o=>
`${JSON.stringify(o.title)} : ()=>cmd(${JSON.stringify(o.cmd.trim())},${JSON.stringify(isMACValid(o.mac)?o.mac:undefined)},${!!o.rx})`
).join(",\n ")},
"< Back" : () => load()
};
var knownDevices = [
"Espruino","MDBT42Q","Puck.js","Bangle.js","Pixl.js"
];
Bangle.loadWidgets();
Bangle.drawWidgets();
E.showMenu(menu);
function cmd(cmd,mac,rx) {
cmd = "\\x03\\x10"+cmd+"\\n";
var send = rx ? sendCommandRX : sendCommand;
var m;
E.showMenu();
function onDone() {
E.showMenu(menu);
}
function err(msg) {
print("Error:",msg);
E.showAlert(msg.toString(),"Error").then(function() {
E.showMenu(m || menu);
});
}
if (mac) {
E.showMessage("Connecting\\n"+mac);
if (mac.length==17) mac+=" random";
NRF.connect(mac).then(dev=>send(dev,cmd,onDone)).catch(err);
} else {
E.showMessage("Scanning...");
NRF.findDevices(devices => {
m = { "" : {title:"Devices"} };
devices.filter(dev=>dev.name &&
knownDevices.some(n=>dev.name.startsWith(n))
).forEach(dev=>{
m[dev.name] = ()=>{
dev.gatt.connect().then(dev=>send(dev,cmd,onDone)).catch(err);
};
});
m["< Back"] = onDone;
E.showMenu(m);
},{active:true});
}
}
// Send a command to a connected device, get response
function sendCommandRX(device, text, callback) {
var service,rx,tx;
var timeout;
E.showMessage("Connected");
return new Promise((resolve,reject) => {
function done() {
Terminal.println("\\n============\\n Disconnected");
device.disconnect();
setTimeout(function() {
setWatch(function() {
if (callback) callback();
resolve();
}, BTN3);
g.reset().setFont("6x8",2).setFontAlign(0,0,1);
g.drawString("Back", g.getWidth()-10, g.getHeight()-50);
}, 200);
}
device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
service = s;
return service.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
}).then(function(c) {
tx = c;
return service.getCharacteristic("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
}).then(function(c) {
rx = c;
rx.on('characteristicvaluechanged', function(event) {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(done, 500);
Terminal.print(E.toString(event.target.value.buffer));
});
return rx.startNotifications();
}).then(function() {
E.showMessage("Sending...\\n");
function sender(resolve, reject) {
if (text.length) {
tx.writeValue(text.substr(0,20)).then(()=>{
sender(resolve, reject);
}).catch(reject);
text = text.substr(20);
} else {
resolve();
}
}
return new Promise(sender);
}).then(function() {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(done, 500);
}).catch(err => {
if (timeout) clearTimeout(timeout);
reject(err);
});
});
}
// Send a command to a connected device
function sendCommand(device, text, callback) {
E.showMessage("Connected");
return device.getPrimaryService("6e400001-b5a3-f393-e0a9-e50e24dcca9e").then(function(s) {
return s.getCharacteristic("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
}).then(function(c) {
E.showMessage("Sending...");
function sender(resolve, reject) {
if (text.length) {
c.writeValue(text.substr(0,20)).then(()=>{
sender(resolve, reject);
}).catch(reject);
text = text.substr(20);
} else {
resolve();
}
}
return new Promise(sender);
}).then(function() {
E.showMessage("Disconnect");
device.disconnect();
if (callback) callback();
});
}
`;
sendCustomizedApp({
storage:[
{name:"espruinoctrl.app.js", content:app},
]
});
});
</script>
</body>
</html>