1
0
Fork 0

Merge pull request #10 from espruino/master

update 20200618
master
ps-igel 2020-06-18 22:34:09 +02:00 committed by GitHub
commit 4a643016fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1983 additions and 72 deletions

View File

@ -2,7 +2,7 @@
{ "id": "boot",
"name": "Bootloader",
"icon": "bootloader.png",
"version":"0.18",
"version":"0.19",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"tags": "tool,system",
"type":"bootloader",
@ -53,7 +53,7 @@
{ "id": "about",
"name": "About",
"icon": "app.png",
"version":"0.05",
"version":"0.06",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"tags": "tool,system",
"allow_emulator":true,
@ -403,7 +403,7 @@
{ "id": "files",
"name": "App Manager",
"icon": "files.png",
"version":"0.05",
"version":"0.06",
"description": "Show currently installed apps, free space, and allow their deletion from the watch",
"tags": "tool,system,files",
"storage": [
@ -414,7 +414,7 @@
{ "id": "weather",
"name": "Weather",
"icon": "icon.png",
"version":"0.02",
"version":"0.03",
"description": "Show Gadgetbridge weather report",
"readme": "readme.md",
"tags": "widget,outdoors",
@ -869,7 +869,7 @@
"name": "Large Digit Blob Clock",
"shortName" : "Blob Clock",
"icon": "clock-blob.png",
"version":"0.03",
"version":"0.04",
"description": "A clock with big digits",
"tags": "clock",
"type":"clock",
@ -1332,7 +1332,7 @@
"name": "Numerals Clock",
"shortName": "Numerals Clock",
"icon": "numerals.png",
"version":"0.05",
"version":"0.06",
"description": "A simple big numerals clock",
"tags": "numerals,clock",
"type":"clock",
@ -1605,7 +1605,7 @@
"id": "largeclock",
"name": "Large Clock",
"icon": "largeclock.png",
"version": "0.03",
"version": "0.06",
"description": "A readable and informational digital watch, with date, seconds and moon phase",
"readme": "README.md",
"tags": "clock",
@ -1792,11 +1792,25 @@
"icon": "app.png",
"description": "A simple fortune telling app",
"tags": "game",
"version": "0.03",
"storage": [
{ "name": "jbm8b.app.js", "url": "app.js" },
{ "name": "jbm8b.img", "url": "app-icon.js", "evaluate": true }
],
"version": "0.03"
]
},
{ "id": "BLEcontroller",
"name": "BLE Customisable Controller with Joystick",
"shortName": "BLE Controller",
"icon": "BLEcontroller.png",
"version": "0.01",
"description": "A configurable controller for BLE devices and robots, with a basic four direction joystick. Designed to be easy to customise so you can add your own menus.",
"tags": "tool,bluetooth",
"readme": "README.md",
"allow_emulator":false,
"storage": [
{ "name": "BLEcontroller.app.js", "url": "app.js" },
{ "name": "BLEcontroller.img", "url": "app-icon.js", "evaluate": true }
]
},
{ "id": "widviz",
"name": "Widget Visibility Widget",
@ -1905,5 +1919,44 @@
{"name":"life.app.js","url":"life.min.js"},
{"name":"life.img","url":"life-icon.js","evaluate":true}
]
},
{ "id": "magnav",
"name": "Navigation Compass",
"icon": "magnav.png",
"version":"0.03",
"description": "Compass with linear display as for GPSNAV. Has Tilt compensation and remembers calibration.",
"readme": "README.md",
"tags": "tool,outdoors",
"storage": [
{"name":"magnav.app.js","url":"magnav.min.js"},
{"name":"magnav.img","url":"magnav-icon.js","evaluate":true}
],
"data":[{"name":"magnav.json"}]
},
{ "id": "gpspoilog",
"name": "GPS POI Logger",
"shortName":"GPS POI Log",
"icon": "app.png",
"version":"0.01",
"description": "A simple app to log points of interest with their GPS coordinates and read them back onto your PC. Based on the https://www.espruino.com/Bangle.js+Storage tutorial",
"tags": "outdoors",
"interface": "interface.html",
"storage": [
{"name":"gpspoilog.app.js","url":"app.js"},
{"name":"gpspoilog.img","url":"app-icon.js","evaluate":true}
]
},
{ "id": "miclock2",
"name": "Mixed Clock 2",
"icon": "clock-mixed.png",
"version":"0.01",
"description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.",
"tags": "clock",
"type":"clock",
"allow_emulator":true,
"storage": [
{"name":"miclock2.app.js","url":"clock-mixed.js"},
{"name":"miclock2.img","url":"clock-mixed-icon.js","evaluate":true}
]
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,50 @@
# BLE Customisable Controller with Joystick
A highly customisable state machine driven user interface that will communicate with another BLE device. The controller uses the three buttons and the left and right hand side of the watch to provide a flexible and attractive BLE interface.
Amaze your friends by controlling your robot, your house or any other BLE device from your watch!
<iframe width="560" height="315" src="https://www.youtube.com/embed/acQxcoFe0W0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
To keep the messages small, commands are sent from the Controller to the BLE target in a text string. This is made up of a comma delimited string of the following elements:
* message number (up to the least significant four digits)
* screen name (up to four characters)
* object name (up to four characters)
* value/status (up to four characters)
The combination of these variables will uniquely identify the status change requested from the watch to the target device that can then be programmed to respond appropriately.
Gordon Williams' EspruinoHub is an excellent way to transform thse BLE advertisements into MQTT messages for further processing. They can be subscribed to via the following MQTT topic (change the watchaddress, to the MAC address of your Bangle.js)
/ble/advertise/wa:tc:ha:dd:re:ss/espruino/#
## Usage
The application can be configured at will by changing the definitions of the screens, events, icons and buttons.
Most changes are possible via data, rather than code change.
## Features
The default package contains three configurations:
* a simple home light and sockets controller UI (app.js)
* a robot controller UI with joystick (app-joy.js)
* a simple static assistant controller (app-ex2.js)
You can try out the other configurations by deleting app.js and renaming the file you want to try as app.js.
I have tested out the application to as many as eight screens without problems, but four screens are usually enough for most situations.
## Controls
The controls will vary by screen, but I suggest a convention of using BTN3 (the bottom button) for moving backwards up the menu stack.
I have used the convention of red/green for buttons that are switches and blue buttons that provide single function operation (such as navigating a menu or executing a on-off activity)
## Requests
In the first instance, please consult my blog post on this application [here](https://k9-build.blogspot.com/2020/05/controlling-k9-using-bluetooth-ble-from.html)
## Creator
Richard Hopkins, FIET CEng
May 2020

View File

@ -0,0 +1,450 @@
/*
==========================================================
Simple event based robot controller that enables robot
to switch into automatic or manual control modes. Behaviours
are controlled via a simple finite state machine.
In automatic mode the
robot will look after itself. In manual mode, the watch
will provide simple forward, back, left and right commands.
The messages will be transmitted to a partner BLE Espruino
using BLE
Written by Richard Hopkins, May 2020
==========================================================
declare global variables for watch button statuses */
top_btn = false;
middle_btn = false;
left_btn= false; // the left side of the touch screen
right_btn = false; // the right side of the touch screen
bottom_btn = false;
msgNum = 0; // message number
/*
CONFIGURATION AREA - STATE VARIABLES
declare global variables for the toggle button
statuses; if you add an additional toggle button
you should declare it and initiase it here */
var status_spk = {value: true};
var status_face = {value: true};
var status_iris_light = {value: false};
var status_iris = {value: false};
var status_hover = {value: false};
var status_dome = {value: false};
/* trsnsmit message
where
s = first character of state,
o = first three character of object name
v = value of state.object
*/
const transmit = (state,object,status) => {
msgNum ++;
msg = {
n: msgNum.toString().slice(-4),
s: state.substr(0,4),
o: object.substr(0,4),
v: status.substr(0,4),
};
message= msg.n + "," + msg.s + "," + msg.o + "," + msg.v;
NRF.setAdvertising({},{
showName: false,
manufacturer: 0x0590,
manufacturerData: JSON.stringify(message)});
};
/*
CONFIGURATION AREA - ICON DEFINITIONS
Retrieve 30px PNG icons from:
https://icons8.com/icon/set/speak/ios-glyphs
Create icons using:
https://www.espruino.com/Image+Converter
Use compression: true
Transparency: true
Diffusion: flat
Colours: 16bit RGB
Ouput as: Image Object
Add an additional element to the icons array
with a unique name and the data from the Image Object
*/
const icons = [
{
name: "back",
data: "gEBAP4B/AP4B/AKgADHPI71HP45/HP45/HP45/HP45/Hf49/Hv49/Hv49/Hv49/Hv497He4B/AP4B/AJAA=="
},
{
name: "spk_on",
data: "gEBAP4B/AP4Bic/YAFPP4v1HrYZRVJo7ZDKp5jMJYvZHaYAHVL4LHACZrhADLBTJKI7dPLI7/Hf47/HeZBVFqZHZRJp1lAJ47LOtZTnHbIZDKLpHNAL69ZANp1tQbY5/AP4B/ANQ"
},
{
name: "spk_off",
data: "gEBAPhB7P/o9rFKI9pFKY9tXNYZNHrZXfMaoAHPOZhNF7LdXHpKpZEJpvPDZK1ZAB49NPLo9jHdI9NHd49PHebvxEJY9NI6I7dHpaDXcKqfPHLKjZHcpTjHbIZDKa73JHa4BXGY45xe5Y7zV+o9/Hv49JHe4BEA="
},
{
name: "facerecog",
data: "gEBAP4BSLuozNH9YpTHsolXPsYfdDraZhELIZhHeLtJELY1VC4Y7HHqoXJABYdNHa5bJDrLvfHfbrPZJI7nGZpdVNJ4lRIpaznRqp1hCq55ZC6IRPd8oPjW8Y5jSr45dEJppNHcIjLHZY5ja6rrhFK45pVqI5rGI4AHHNpx3ANA="
},
{
name: "sleep",
data: "gEBAP4B/AP4B2ACY7/Quq95HP45/HP4APOdY7fACZfnHcaZZAL45/HP45/E7YAHCaZFZHfbh/HP45/HOoAHHf4B/AP4B/AP4BIA="
},
{
name: "awake",
data: "gEBAP4B/AKyb7HfIAFHPI77Ov451Hf453Hf453HdoAbHf45/Hf5HrHNY7NHNo7/HO47/HO47HHPJ1/Heo51HfoB/ALg="
},
{
name: "happy",
data: "gEBAP4B/AP4BKa+oAXHNITfHK4ZtD5JZfHOojZaMYlXHMYnXHfI5nFaYPLaaIRNHf47/d/47/HtInTCZrfZHa4vNABYlVKLI3PbLrzfD7qTXDLaphHMIpLAB45hIKY1pAP4B/AMA"
},
{
name: "sad",
data: "gEBAP4B/AP4BKa+oAXHNITfHK4ZtD5JZfHOojZaMYlXHMYnXHfI5nFaYPLaaIRNHf47/d/47/CK4njCZ4APHcIVJBbbdTecYjZHr4fdSa4ZbEZ4lNCaY9dAB45hIKY1pAP4B/AMA"
},
{
name: "hover",
data: "gEBAP4B/AP7NedL4fZK7ojNHeJ35DJI7vC5Y7tVMI7XHNYnNYro7hHKI7lAK47/HdoAhHPI7/Hf47/Hf4AtHPI7/Hf47/Hd45LAP4B/ANwA="
},
{
name: "light",
data: "gEBAP4B/APi/Na67lfACZ/nNaI9lE6o9jEbI9hD7Y7dDsJZ3D6YRJHdIJHHfaz7Hf5Z/Hf4hZHMIjFEqIVVHsY5hDpI7TEqL1jVsqlTdM55THOJvHOuY7/HfI9JHOI9HHOoBgA=="
},
{
name: "speak",
data: "gEBAP4B/AP4BIbO4AXG+4/hAEY55HqoArHPI9PHfIAzHf47/Hf47/HeY9xHJI79Hto5NHtY5RHc45THco5VHcI3XHJpHRG7I7LEro5ZG+IB/AP4BwA=="
},
{
name: "dalek",
data: "gEBAP4B/AP4B/AJMQwQBBGucIoMAkADBhFhAoZBcAAQfJhEgB45BCHYMBjGiB4ZLCK5APDFpphBC5AbEJosY0YfCG4IAEJIYdGFYR5LHJYlEAI0Y4cY8YXMOpQBFlNFlMkOZA7MKII7JOAXkE4T1UERKtFHoxJBABY5QiGiD5kANYTnCiFiWIJVOgDZCOra3FoKxFDKI7hADQ7PkEIaoIHEaKYfJAoKPFAJcIGYIJHkI7UgMY8ZFHC5rVDKIZTCDIJhBA4ILBBoYFHC4QBEBogpBjHDdsJJEAoYAHKoTxWWb5tNWZOiHZRbBHbwtLF5ynBL7wtLjHjd6oAZkHkI5JJKAAZ3TkAjJhALBsJ5K0a/KkLvfkMEFpVhO8hrIU4QLGG4QAzkCdVAP4B/AP4Bb"
}
];
/* finds icon data by name in the icon array and returns an image object*/
const drawIcon = (name) => {
for (var icon of icons) {
if (icon.name == name) {
image = {
width : 30, height : 30, bpp : 16,
transparent : 1,
buffer: require("heatshrink").decompress(atob(icon.data))
};
return image;}
}
};
/*
CONFIGURATION AREA - BUTTON DEFINITIONS
for a simple button, just define a primary colour
and an icon name from the icon array and
the text to display beneath the button
for toggle buttons, additionally provide secondary
colours, icon name and text. Also provide a reference
to a global variable for the value of the button.
The global variable should be declared at the start of
the program and it may be adviable to use the 'status_name'
format to ensure it is clear.
*/
var happyBtn = {
primary_colour: 0x653E,
primary_text: 'Speak',
primary_icon: 'happy',
};
var sadBtn = {
primary_colour: 0x33F9,
primary_text: 'Speak',
primary_icon: 'sad',
};
var speakBtn = {
primary_colour: 0x33F9,
primary_text: 'Speak',
primary_icon: 'speak',
};
var faceBtn = {
primary_colour: 0xE9C7,
primary_text: 'Off',
primary_icon: 'facerecog',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'On',
secondary_icon : 'facerecog',
value: status_face
};
var irisLightBtn = {
primary_colour: 0xE9C7,
primary_text: 'Off',
primary_icon: 'light',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'On',
secondary_icon : 'light',
value: status_iris_light
};
var irisBtn = {
primary_colour: 0xE9C7,
primary_text: 'Closed',
primary_icon: 'sleep',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Open',
secondary_icon : 'awake',
value: status_iris
};
var hoverBtn = {
primary_colour: 0xE9C7,
primary_text: 'Off',
primary_icon: 'hover',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'On',
secondary_icon : 'hover',
value: status_hover
};
var domeBtn = {
primary_colour: 0xE9C7,
primary_text: 'Off',
primary_icon: 'dalek',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'On',
secondary_icon : 'dalek',
value: status_dome
};
/*
CONFIGURATION AREA - SCREEN DEFINITIONS
a screen can have a button (as defined above)
on the left and/or the right of the screen.
in adddition a screen can optionally have
an icon for each of the three buttons on
the left hand side of the screen. These
are defined as btn1, bt2 and bt3. The
values are names from the icon array.
*/
const menuScreen = {
left: faceBtn,
right: speakBtn,
btn1: "hover",
btn2: "light",
};
const speakScreen = {
left: happyBtn,
right: sadBtn,
btn3: "back"
};
const irisScreen = {
left: irisBtn,
right: irisLightBtn,
btn3: "back"
};
const lightsScreen = {
left: hoverBtn,
right: domeBtn,
btn3: "back"
};
/* base state definition
Each of the screens correspond to a state;
this class provides a constuctor for each
of the states
*/
class State {
constructor(params) {
this.state = params.state;
this.events = params.events;
this.screen = params.screen;
}
}
/*
CONFIGURATION AREA - BUTTON BEHAVIOURS/STATE TRANSITIONS
This area defines how each screen behaves.
Each screen corresponds to a different State of the
state machine. This makes it much easier to isolate
behaviours between screens.
The state value is transmitted whenever a button is pressed
to provide context (so the receiving device, knows which
button was pressed on which screen).
The screens are defined above.
The events section identifies if a particular button has been
pressed and released on the screen and an action can then be taken.
The events function receives a notification from a mySetWatch which
provides an event object that identifies which button and whether
it has been pressed down or released. Actions can then be taken.
The events function will always return a State object.
If the events function returns different State from the current
one, then the state machine will change to that new State and redrsw
the screen appropriately.
To add in additional capabilities for button presses, simply add
an additional 'if' statement.
For toggle buttons, the value of the sppropiate status object is
inversed and the new value transmitted.
*/
/* The Home State/Page is where the application beings */
const Home = new State({
state: "DalekMenu",
screen: menuScreen,
events: (event) => {
if ((event.object == "top") && (event.status == "end")) {
return Lights;
}
if ((event.object == "middle") && (event.status == "end")) {
return Iris;
}
if ((event.object == "right") && (event.status == "end")) {
return Speak;
}
if ((event.object == "left") && (event.status == "end")) {
status_face.value = !status_face.value;
transmit(this.state, "face", onOff(status_face.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const Speak = new State({
state: "Speak",
screen: speakScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const Iris = new State({
state: "Iris",
screen: irisScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
if ((event.object == "right") && (event.status == "end")) {
status_iris_light.value = !status_iris_light.value;
transmit(this.state, "light", onOff(status_iris_light.value));
return this;
}
if ((event.object == "left") && (event.status == "end")) {
status_iris.value = !status_iris.value;
transmit(this.state, "servo", onOff(status_iris.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const Lights = new State({
state: "Lights",
screen: lightsScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
if ((event.object == "right") && (event.status == "end")) {
status_dome.value = !status_dome.value;
transmit(this.state, "dome", onOff(status_dome.value));
return this;
}
if ((event.object == "left") && (event.status == "end")) {
status_hover.value = !status_hover.value;
transmit(this.state, "hover", onOff(status_hover.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
/* translate button status into english */
const startEnd = status => status ? "start" : "end";
/* translate status into english */
const onOff= status => status ? "on" : "off";
/* create watching functions that will change the global
button status when pressed or released
This is actuslly the hesrt of the program. When a button
is not being pressed, nothing is happening (no loops).
This makes the progrsm more battery efficient.
When a setWatch event is raised, the custom callbacks defined
here will be called. These then fired as events to the current
state/screen of the state mschine.
Some events, will result in the stste of the state machine
chsnging, which is why the screen is redrswn after each
button press.
*/
const setMyWatch = (params) => {
setWatch(() => {
params.bool=!params.bool;
machine = machine.events({object: params.label, status: startEnd(params.bool)});
drawScreen(machine.screen);
}, params.btn, {repeat:true, edge:"both"});
};
/* object array used to set up the watching functions
*/
const buttons = [
{bool : bottom_btn, label : "bottom",btn : BTN3},
{bool : middle_btn, label : "middle",btn : BTN2},
{bool : top_btn, label : "top",btn : BTN1},
{bool : left_btn, label : "left",btn : BTN4},
{bool : right_btn, label : "right",btn : BTN5}
];
/* set up watchers for buttons */
for (var button of buttons)
{setMyWatch(button);}
/* Draw various kinds of buttons */
const drawButton = (params,side) => {
g.setFontAlign(0,1);
icon = drawIcon(params.primary_icon);
text = params.primary_text;
g.setColor(params.primary_colour);
const x = (side == "left") ? 0 : 120;
if ((params.toggle) && (params.value.value)) {
g.setColor(params.secondary_colour);
text = params.secondary_text;
icon = drawIcon(params.secondary_icon);
}
g.fillRect(0+x,24,119+x, 239);
g.setColor(0x000);
g.setFont("Vector",15);
g.setFontAlign(0,0.0);
g.drawString(text,60+x,160);
options = {rotate: 0, scale:2};
g.drawImage(icon,x+60,120,options);
};
/* Draw the pages corresponding to the states */
const drawScreen = (params) => {
drawButton(params.left,'left');
drawButton(params.right,'right');
g.setColor(0x000);
if (params.btn1) {g.drawImage(drawIcon(params.btn1),210,40);}
if (params.btn2) {g.drawImage(drawIcon(params.btn2),210,125);}
if (params.btn3) {g.drawImage(drawIcon(params.btn3),210,195);}
};
machine = Home; // instantiate the state machine at Home
Bangle.drawWidgets(); // draw active widgets
drawScreen(machine.screen); // draw the screen

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("MDCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAAAAAAAABERIQAAAAAAAAAAAAAAAAAAAAAAAAABERIRpiEQAAAAAAAAAAAAAAAAAAAAAAEREhOiImEqYAAAAAAAAAAAAAAAAAAAABESP///8zOFEQAAAAAAAAAAAAAAAAAAARI//////z8zp6AAAAAAAAAAAAAAAAAAES////////PzOFEAAAAAAAAAAAAAAAABEv////////8/MyGgAAAAAAAAAAAAAAARI//////////z8zIRAAAAAAAAAAAAAAARP/////8/P///M/IRAAAAAAAAAAAAAAES/////zgzM///M/OFEAAAAAAAAAAAAAET////84/zMzP/8z8hEAAAAAAAAAAAAAEf////M///gzP/8/MyEAAAAAAAAAAAABEv///zP/8zjzM/8/M4UQAAAAAAAAAAABEv///zP/M48zj//zPyEQAAAAAAAAAAABE////zP/ODMzj//zPyEQAAAAAAAAAAABE////zMzjyM48//zPyEQAAAAAAAAAAABE/////ODMzOPP/8/M4QQAAAAAAAAAAABE/////MzjzOD///zPyEQAAAAAAAAAAABE/////8zOPMz///zMzEQAAAAAAAAAAABEj//////PzP///8/MxhQAAAAAAAAAAARES////////////8/MRpyAAAAAAAAAAASMRP////////////zMRMhAAAAAAAAAAARMR////////////8/IRMhAAAAAAAAAAARES/////////////zMhp6AAAAAAAAAAEREv////////////8/MyEacAAAAAAAABERIv/////////////zPyERGAAAAAAAABEBE//////////////zPyEQEQAAAAAAAREBE//////////////zPyEQERAAAAABERABE//////////////zPyEQAREQAAEREgABE//////////////zPyEQABERIAESESABE//////////////zPyEQARESEAEfIRABE//////////////zPyEQAREBEAEREgABE//////z//////8/MzEQABERIAAREQABE/8/8z/zPz/z8/M/MzGAABERAAABEQABE/8/8/8/Pz/z8/PzPyEQABEQAAAAEQABE//////////////zPyEQABEAAAAAERAAETMzMzMzMjMzMzODIxEAAREAAAAAARAAEREhGmIRGhESaiYRKmEAARAAAAAAEREgABERIaYhEaERJqEmEQABERIAAAABERIaAAAAAAEREhGmIREAAAARESGgAAABEAARAAAAAAEREhGmIRGgAAARAAEQAAABEAARAAABERIRpiERoREgAAARAAEQAAAAAAAAAAABERIRpiERoREgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="))

View File

@ -0,0 +1,446 @@
/*
==========================================================
Simple event based robot controller that enables robot
to switch into automatic or manual control modes. Behaviours
are controlled via a simple finite state machine.
In automatic mode the
robot will look after itself. In manual mode, the watch
will provide simple forward, back, left and right commands.
The messages will be transmitted to a partner BLE Espruino
using BLE
Written by Richard Hopkins, May 2020
==========================================================
declare global variables for watch button statuses */
top_btn = false;
middle_btn = false;
left_btn= false; // the left side of the touch screen
right_btn = false; // the right side of the touch screen
bottom_btn = false;
msgNum = 0; // message number
NRF.setConnectionInterval(100);
Bangle.loadWidgets();
Bangle.drawWidgets();
/*
CONFIGURATION AREA - STATE VARIABLES
declare global variables for the toggle button
statuses; if you add an additional toggle button
you should declare it and initiase it here */
var status_auto = {value: false};
var status_chess = {value: false};
var status_wake = {value: false};
/* trsnsmit message
where
s = first character of state,
o = first three character of object name
v = value of state.object
*/
const transmit = (state,object,status) => {
msgNum ++;
msg = {
n: msgNum.toString().slice(-4),
s: state.substr(0,4),
o: object.substr(0,4),
v: status.substr(0,4),
};
message= msg.n + "," + msg.s + "," + msg.o + "," + msg.v;
NRF.setAdvertising({},{
showName: false,
manufacturer: 0x0590,
manufacturerData: JSON.stringify(message)});
};
/*
CONFIGURATION AREA - ICON DEFINITIONS
Retrieve 30px PNG icons from:
https://icons8.com/icon/set/speak/ios-glyphs
Create icons using:
https://www.espruino.com/Image+Converter
Use compression: true
Transparency: true
Diffusion: flat
Colours: 16bit RGB
Ouput as: Image Object
Add an additional element to the icons array
with a unique name and the data from the Image Object
*/
const icons = [
{
name: "walk",
data: "gEBAP4B/ALyh7b/YALHfY9tACY55HfYdNHto7pHpIbXbL5fXAD6VlHuYAjHf47/Hf47tHK47LDa45zHc4NHHeILJHeonTO9o9rHf47/eOoB/ANg="
},
{
name: "sit",
data: "gEBAP4B/AP4BacO4ANHPI/rACp1/Hf49rGtI5/He7n3ACY55HcYAZHf45/Hf45rHe4XHGbI7/Va47zZZrpbHfbtXD5Y/vHcYB/AP4BmA"
},
{
name: "joystick",
data: "gEBAP4B/AP4BMavIALHPI9vHf47/eP45vHpY5xHo451Hf47/FuYAHHNItHABa33AP6xpAD455HqY7/Hf47/Hd49pHKIB/AP4B/AMwA=="
},
{
name: "left",
data: "gEBAP4B/AP4BKa9ojHAC5pfHJKDTUsYdZHb6ZfO+I9dABabdLbIBdHf473PP47NJdY7/ePIB/RJop5Ys7t/AP6PvD7o7fP8Y1zTZoHPf/4B/AP4B+A=="
},
{
name: "right",
data: "gEBAP4B/AP4BKa+oAXDo45hCaqFbUbLBfbbo7bHMojTR7Y5LHa51ZALo75Ov47/FeY77AP4B5WdbF3dv4B/R94fdHb5/jGuabNA57//AP4B/APw="
},
{
name: "forward",
data: "gEBAP4B/AKSX5avIALHPI9tACY55HsoAbHPI9fHfZFVGMo7/Hf47/Hf47/Hf47/Hf47/Hf47/Hf47/Hf49XHOIB/ALw="
},
{
name: "backward",
data: "gEBAP4B/AKCZ5a/Y7/Hf47/Hf47/Hf47/Hf47/Hf47/Hf47/HfIAfHf491W/L15HMo9THNI9PHNo9LHOI9HHOoB/ALg="
},
{
name: "back",
data: "gEBAP4B/AP4B/AKgADHPI71HP45/HP45/HP45/HP45/Hf49/Hv49/Hv49/Hv49/Hv497He4B/AP4B/AJAA=="
},
{
name: "mic_on",
data: "gEBAP4B/AKCZ5a/Y7/Hf47/Hf47/Hf47/GbY7TIcY7/Hf47/Hf47/HdY9NCpp5lCb57fOdYvNeJo91HNrlvHf7tVIdY77AP4BiA="
},
{
name: "comms",
data: "gEBAP4B+QvbF7ABo7/He49tACI7/Hf47zHtI7jJq47lRqoAVEqY7nHsoAZGJo71HrKxfQaY7bdKo7/Hdqz5B5Y7zHK47RD55FRHao3XHKo7JG7L1NHeJTbHboB/AP4BG"
},
{
name: "pawn",
data: "gEBAP4B/AP4B/AP4BEAA455HuY7/Hf47xAB47/PuI1xPZY7/Hf47/G9Y/zHfIATHPI9nHfYB/AOYAfHf4B/AP4B/APA="
},
{
name: "sleep",
data: "gEBAP4B/AP4B2ACY7/Quq95HP45/HP4APOdY7fACZfnHcaZZAL45/HP45/E7YAHCaZFZHfbh/HP45/HOoAHHf4B/AP4B/AP4BIA="
},
{
name: "awake",
data: "gEBAP4B/AKyb7HfIAFHPI77Ov451Hf453Hf453HdoAbHf45/Hf5HrHNY7NHNo7/HO47/HO47HHPJ1/Heo51HfoB/ALg="
},
{
name: "wag_h",
data: "gEBAP4B/AP4B/AP4B/AP4B/AMwADD+oAFHb4hTHMIlXHMopTHNItPAG47/WfY9tFKY9lEq49hELY7ja8YB/AP4B/AP4B/AP4B/AP4BCA"
},
{
name: "wag_v",
data: "gEBAP4B/AP4BOafIAHHPI9xAB45vd449rFZIHLHsonJBKa7rGNo7/Hf47/Hf47/Hf47/Hf4xlBKY7hFIoHLQM4rHApK7rAB71xHOo9LHOI9HHOoB/AP4BYA="
}
];
/* finds icon data by name in the icon array and returns an image object*/
const drawIcon = (name) => {
for (var icon of icons) {
if (icon.name == name) {
image = {
width : 30, height : 30, bpp : 16,
transparent : 1,
buffer: require("heatshrink").decompress(atob(icon.data))
};
return image;}
}
};
/*
CONFIGURATION AREA - BUTTON DEFINITIONS
for a simple button, just define a primary colour
and an icon name from the icon array and
the text to display beneath the button
for toggle buttons, additionally provide secondary
colours, icon name and text. Also provide a reference
to a global variable for the value of the button.
The global variable should be declared at the start of
the program and it may be adviable to use the 'status_name'
format to ensure it is clear.
*/
var joystickBtn = {
primary_colour: 0x653E,
primary_icon: 'joystick',
primary_text: 'Joystick',
};
var turnLeftBtn = {
primary_colour: 0x653E,
primary_text: 'Left',
primary_icon: 'left',
};
var turnRightBtn = {
primary_colour: 0x33F9,
primary_text: 'Right',
primary_icon: 'right',
};
var tailHBtn = {
primary_colour: 0x653E,
primary_text: 'Wag Tail',
primary_icon: 'wag_h',
};
var tailVBtn = {
primary_colour: 0x33F9,
primary_text: 'Wag Tail',
primary_icon: 'wag_v',
};
var chessBtn = {
primary_colour: 0xE9C7,
primary_text: 'Off',
primary_icon: 'pawn',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'On',
secondary_icon : 'pawn',
value: status_chess
};
var wakeBtn = {
primary_colour: 0xE9C7,
primary_text: 'Sleeping',
primary_icon: 'sleep',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Awake',
secondary_icon : 'awake',
value: status_wake
};
var autoBtn = {
primary_colour: 0xE9C7,
primary_text: 'Stop',
primary_icon: 'sit',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Move',
secondary_icon : 'walk',
value: status_auto
};
/*
CONFIGURATION AREA - SCREEN DEFINITIONS
a screen can have a button (as defined above)
on the left and/or the right of the screen.
in adddition a screen can optionally have
an icon for each of the three buttons on
the left hand side of the screen. These
are defined as btn1, bt2 and bt3. The
values are names from the icon array.
*/
const menuScreen = {
left: wakeBtn,
right: joystickBtn,
btn1: "pawn",
btn2: "wag_v",
};
const joystickScreen = {
left: turnLeftBtn,
right: turnRightBtn,
btn1: "forward",
btn2: "backward",
btn3: "back"
};
const tailScreen = {
left: tailHBtn,
right: tailVBtn,
btn3: "back"
};
const chessScreen = {
left: chessBtn,
right: autoBtn,
btn3: "back"
};
/* base state definition
Each of the screens correspond to a state;
this class provides a constuctor for each
of the states
*/
class State {
constructor(params) {
this.state = params.state;
this.events = params.events;
this.screen = params.screen;
}
}
/*
CONFIGURATION AREA - BUTTON BEHAVIOURS/STATE TRANSITIONS
This area defines how each screen behaves.
Each screen corresponds to a different State of the
state machine. This makes it much easier to isolate
behaviours between screens.
The state value is transmitted whenever a button is pressed
to provide context (so the receiving device, knows which
button was pressed on which screen).
The screens are defined above.
The events section identifies if a particular button has been
pressed and released on the screen and an action can then be taken.
The events function receives a notification from a mySetWatch which
provides an event object that identifies which button and whether
it has been pressed down or released. Actions can then be taken.
The events function will always return a State object.
If the events function returns different State from the current
one, then the state machine will change to that new State and redrsw
the screen appropriately.
To add in additional capabilities for button presses, simply add
an additional 'if' statement.
For toggle buttons, the value of the appropiate status object is
inversed and the new value transmitted.
*/
/* The Home State/Page is where the application beings */
const Home = new State({
state: "K9Menu",
screen: menuScreen,
events: (event) => {
if ((event.object == "top") && (event.status == "end")) {
return Chess;
}
if ((event.object == "middle") && (event.status == "end")) {
return Tail;
}
if ((event.object == "right") && (event.status == "end")) {
return Joystick;
}
if ((event.object == "left") && (event.status == "end")) {
status_wake.value = !status_wake.value;
transmit(this.state, "wake", onOff(status_wake.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const Chess = new State({
state: "Chess",
screen: chessScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
if ((event.object == "right") && (event.status == "end")) {
status_auto.value = !status_auto.value;
transmit(this.state, "follow", onOff(status_auto.value));
return this;
}
if ((event.object == "left") && (event.status == "end")) {
status_chess.value = !status_chess.value;
transmit(this.state, "chess", onOff(status_chess.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const Tail = new State({
state: "Tail",
screen: tailScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
transmit(this.state, event.object, event.status);
return this;
}
});
/* Joystick page state */
const Joystick = new State({
state: "Joystick",
screen: joystickScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
transmit("Joystick", "joystick", "off");
return Home;
}
transmit(this.state, event.object, event.status);
return this;
}
});
/* translate button status into english */
const startEnd = status => status ? "start" : "end";
/* translate status into english */
const onOff= status => status ? "on" : "off";
/* create watching functions that will change the global
button status when pressed or released
This is actuslly the hesrt of the program. When a button
is not being pressed, nothing is happening (no loops).
This makes the progrsm more battery efficient.
When a setWatch event is raised, the custom callbacks defined
here will be called. These then fired as events to the current
state/screen of the state mschine.
Some events, will result in the stste of the state machine
chsnging, which is why the screen is redrswn after each
button press.
*/
const setMyWatch = (params) => {
setWatch(() => {
params.bool=!params.bool;
machine = machine.events({object: params.label, status: startEnd(params.bool)});
drawScreen(machine.screen);
}, params.btn, {repeat:true, edge:"both"});
};
/* object array used to set up the watching functions
*/
const buttons = [
{bool : bottom_btn, label : "bottom",btn : BTN3},
{bool : middle_btn, label : "middle",btn : BTN2},
{bool : top_btn, label : "top",btn : BTN1},
{bool : left_btn, label : "left",btn : BTN4},
{bool : right_btn, label : "right",btn : BTN5}
];
/* set up watchers for buttons */
for (var button of buttons)
{setMyWatch(button);}
/* Draw various kinds of buttons */
const drawButton = (params,side) => {
g.setFontAlign(0,1);
icon = drawIcon(params.primary_icon);
text = params.primary_text;
g.setColor(params.primary_colour);
const x = (side == "left") ? 0 : 120;
if ((params.toggle) && (params.value.value)) {
g.setColor(params.secondary_colour);
text = params.secondary_text;
icon = drawIcon(params.secondary_icon);
}
g.fillRect(0+x,28,119+x, 239);
g.setColor(0x000);
g.setFont("Vector",15);
g.setFontAlign(0,0.0);
g.drawString(text,60+x,160);
options = {rotate: 0, scale:2};
g.drawImage(icon,x+60,120,options);
};
/* Draw the pages corresponding to the states */
const drawScreen = (params) => {
drawButton(params.left,'left');
drawButton(params.right,'right');
g.setColor(0x000);
if (params.btn1) {g.drawImage(drawIcon(params.btn1),210,40);}
if (params.btn2) {g.drawImage(drawIcon(params.btn2),210,125);}
if (params.btn3) {g.drawImage(drawIcon(params.btn3),210,195);}
};
machine = Home; // instantiate the state machine at Home
Bangle.drawWidgets(); // draw active widgets
drawScreen(machine.screen); // draw the screen

368
apps/BLEcontroller/app.js Normal file
View File

@ -0,0 +1,368 @@
/*
==========================================================
Simple event based robot controller that enables robot
to switch into automatic or manual control modes. Behaviours
are controlled via a simple finite state machine.
In automatic mode the
robot will look after itself. In manual mode, the watch
will provide simple forward, back, left and right commands.
The messages will be transmitted to a partner BLE Espruino
using BLE
Written by Richard Hopkins, May 2020
==========================================================
declare global variables for watch button statuses */
top_btn = false;
middle_btn = false;
left_btn= false; // the left side of the touch screen
right_btn = false; // the right side of the touch screen
bottom_btn = false;
msgNum = 0; // message number
NRF.setConnectionInterval(100);
Bangle.loadWidgets();
Bangle.drawWidgets();
/*
CONFIGURATION AREA - STATE VARIABLES
declare global variables for the toggle button
statuses; if you add an additional toggle button
you should declare it and initiase it here */
var status_printer = {value: false};
var status_tv = {value: false};
var status_light_hall = {value: false};
var status_light_study = {value: false};
/* trsnsmit message
where
s = first character of state,
o = first three character of object name
v = value of state.object
*/
const transmit = (state,object,status) => {
msgNum ++;
msg = {
n: msgNum.toString().slice(-4),
s: state.substr(0,4),
o: object.substr(0,4),
v: status.substr(0,4),
};
message= msg.n + "," + msg.s + "," + msg.o + "," + msg.v;
NRF.setAdvertising({},{
showName: false,
manufacturer: 0x0590,
manufacturerData: JSON.stringify(message)});
};
/*
CONFIGURATION AREA - ICON DEFINITIONS
Retrieve 30px PNG icons from:
https://icons8.com/icon/set/speak/ios-glyphs
Create icons using:
https://www.espruino.com/Image+Converter
Use compression: true
Transparency: true
Diffusion: flat
Colours: 16bit RGB
Ouput as: Image Object
Add an additional element to the icons array
with a unique name and the data from the Image Object
*/
const icons = [
{
name: "switch",
data: "gEBAP4B/AP4B/AP4B/AMgA3HPJdlVvI7/Hf47/Hf47/Hf47/Hf47/Hf4AvIPKRXAP4B/AP4B/AP4B/AJgA=="
},
{
name: "light",
data: "gEBAP4B/APi/Na67lfACZ/nNaI9lE6o9jEbI9hD7Y7dDsJZ3D6YRJHdIJHHfaz7Hf5Z/Hf4hZHMIjFEqIVVHsY5hDpI7TEqL1jVsqlTdM55THOJvHOuY7/HfI9JHOI9HHOoBgA=="
},
{
name: "back",
data: "gEBAP4B/AP4B/AKgADHPI71HP45/HP45/HP45/HP45/Hf49/Hv49/Hv49/Hv49/Hv497He4B/AP4B/AJAA=="
}
];
/* finds icon data by name in the icon array and returns an image object*/
const drawIcon = (name) => {
for (var icon of icons) {
if (icon.name == name) {
image = {
width : 30, height : 30, bpp : 16,
transparent : 1,
buffer: require("heatshrink").decompress(atob(icon.data))
};
return image;}
}
};
/*
CONFIGURATION AREA - BUTTON DEFINITIONS
for a simple button, just define a primary colour
and an icon name from the icon array and
the text to display beneath the button
for toggle buttons, additionally provide secondary
colours, icon name and text. Also provide a reference
to a global variable for the value of the button.
The global variable should be declared at the start of
the program and it may be adviable to use the 'status_name'
format to ensure it is clear.
*/
var lightBtn = {
primary_colour: 0x653E,
primary_text: 'Lights',
primary_icon: 'light',
};
var socketsBtn = {
primary_colour: 0x33F9,
primary_text: 'Sockets',
primary_icon: 'switch',
};
var lightHallBtn = {
primary_colour: 0xE9C7,
primary_text: 'Hall Off',
primary_icon: 'light',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Hall On',
secondary_icon : 'light',
value: status_light_hall
};
var lightStudyBtn = {
primary_colour: 0xE9C7,
primary_text: 'Study Off',
primary_icon: 'light',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Study On',
secondary_icon : 'light',
value: status_light_study
};
var socketTVBtn = {
primary_colour: 0xE9C7,
primary_text: 'TV Off',
primary_icon: 'switch',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'TV On',
secondary_icon : 'switch',
value: status_tv
};
var socketPrinterBtn = {
primary_colour: 0xE9C7,
primary_text: 'Printer Off',
primary_icon: 'switch',
toggle: true,
secondary_colour: 0x3F48,
secondary_text: 'Printer On',
secondary_icon : 'switch',
value: status_printer
};
/*
CONFIGURATION AREA - SCREEN DEFINITIONS
a screen can have a button (as defined above)
on the left and/or the right of the screen.
in adddition a screen can optionally have
an icon for each of the three buttons on
the left hand side of the screen. These
are defined as btn1, bt2 and bt3. The
values are names from the icon array.
*/
const homeScreen = {
left: lightBtn,
right: socketsBtn,
};
const lightsScreen = {
left: lightHallBtn,
right: lightStudyBtn,
btn3: "back"
};
const socketsScreen = {
left: socketTVBtn,
right: socketPrinterBtn,
btn3: "back"
};
/* base state definition
Each of the screens correspond to a state;
this class provides a constuctor for each
of the states
*/
class State {
constructor(params) {
this.state = params.state;
this.events = params.events;
this.screen = params.screen;
}
}
/*
CONFIGURATION AREA - BUTTON BEHAVIOURS/STATE TRANSITIONS
This area defines how each screen behaves.
Each screen corresponds to a different State of the
state machine. This makes it much easier to isolate
behaviours between screens.
The state value is transmitted whenever a button is pressed
to provide context (so the receiving device, knows which
button was pressed on which screen).
The screens are defined above.
The events section identifies if a particular button has been
pressed and released on the screen and an action can then be taken.
The events function receives a notification from a mySetWatch which
provides an event object that identifies which button and whether
it has been pressed down or released. Actions can then be taken.
The events function will always return a State object.
If the events function returns different State from the current
one, then the state machine will change to that new State and redrsw
the screen appropriately.
To add in additional capabilities for button presses, simply add
an additional 'if' statement.
For toggle buttons, the value of the appropiate status object is
inversed and the new value transmitted.
*/
/* The Home State/Page is where the application beings */
const Home = new State({
state: "Home",
screen: homeScreen,
events: (event) => {
if ((event.object == "right") && (event.status == "end")) {
return SocketsMenu;
}
if ((event.object == "left") && (event.status == "end")) {
return LightsMenu;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const LightsMenu = new State({
state: "LightsMenu",
screen: lightsScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
if ((event.object == "right") && (event.status == "end")) {
status_light_study.value = !status_light_study.value;
transmit(this.state, "study", onOff(status_light_study.value));
return this;
}
if ((event.object == "left") && (event.status == "end")) {
status_light_hall.value = !status_light_hall.value;
transmit(this.state, "hall", onOff(status_light_hall.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
const SocketsMenu = new State({
state: "SocketsMenu",
screen: socketsScreen,
events: (event) => {
if ((event.object == "bottom") && (event.status == "end")) {
return Home;
}
if ((event.object == "right") && (event.status == "end")) {
status_printer.value = !status_printer.value;
transmit(this.state, "printer", onOff(status_printer.value));
return this;
}
if ((event.object == "left") && (event.status == "end")) {
status_tv.value = !status_tv.value;
transmit(this.state, "tv", onOff(status_tv.value));
return this;
}
transmit(this.state, event.object, event.status);
return this;
}
});
/* translate button status into english */
const startEnd = status => status ? "start" : "end";
/* translate status into english */
const onOff= status => status ? "on" : "off";
/* create watching functions that will change the global
button status when pressed or released
This is actuslly the hesrt of the program. When a button
is not being pressed, nothing is happening (no loops).
This makes the progrsm more battery efficient.
When a setWatch event is raised, the custom callbacks defined
here will be called. These then fired as events to the current
state/screen of the state mschine.
Some events, will result in the stste of the state machine
chsnging, which is why the screen is redrswn after each
button press.
*/
const setMyWatch = (params) => {
setWatch(() => {
params.bool=!params.bool;
machine = machine.events({object: params.label, status: startEnd(params.bool)});
drawScreen(machine.screen);
}, params.btn, {repeat:true, edge:"both"});
};
/* object array used to set up the watching functions
*/
const buttons = [
{bool : bottom_btn, label : "bottom",btn : BTN3},
{bool : middle_btn, label : "middle",btn : BTN2},
{bool : top_btn, label : "top",btn : BTN1},
{bool : left_btn, label : "left",btn : BTN4},
{bool : right_btn, label : "right",btn : BTN5}
];
/* set up watchers for buttons */
for (var button of buttons)
{setMyWatch(button);}
/* Draw various kinds of buttons */
const drawButton = (params,side) => {
g.setFontAlign(0,1);
icon = drawIcon(params.primary_icon);
text = params.primary_text;
g.setColor(params.primary_colour);
const x = (side == "left") ? 0 : 120;
if ((params.toggle) && (params.value.value)) {
g.setColor(params.secondary_colour);
text = params.secondary_text;
icon = drawIcon(params.secondary_icon);
}
g.fillRect(0+x,28,119+x, 239);
g.setColor(0x000);
g.setFont("Vector",15);
g.setFontAlign(0,0.0);
g.drawString(text,60+x,160);
options = {rotate: 0, scale:2};
g.drawImage(icon,x+60,120,options);
};
/* Draw the pages corresponding to the states */
const drawScreen = (params) => {
drawButton(params.left,'left');
drawButton(params.right,'right');
g.setColor(0x000);
if (params.btn1) {g.drawImage(drawIcon(params.btn1),210,40);}
if (params.btn2) {g.drawImage(drawIcon(params.btn2),210,125);}
if (params.btn3) {g.drawImage(drawIcon(params.btn3),210,195);}
};
machine = Home; // instantiate the state machine at Home
Bangle.drawWidgets(); // draw active widgets
drawScreen(machine.screen); // draw the screen

View File

@ -3,3 +3,4 @@
0.03: Actual pixels as of 5 Mar 2020
0.04: Actual pixels as of 9 Mar 2020
0.05: Actual pixels as of 27 Apr 2020
0.06: Actual pixels as of 12 Jun 2020

File diff suppressed because one or more lines are too long

View File

@ -2,3 +2,4 @@
Only draw widgets after clearing screen - they update automatically
Remove 'faceUp' check as it's automatic
0.03: Modified for use with new bootloader and firmware
0.04: Modified to account for changes in the behavior of Graphics.fillPoly

View File

@ -18,8 +18,8 @@ function flip() {
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),buffer:buf.buffer},55,26);
}
function drawPixel(ox,oy,x,y,r,p) {
let x1 = ox+x*(r*2+1);
let y1 = oy+y*(r*2+1);
let x1 = ox+x*(r*2);
let y1 = oy+y*(r*2);
let xmid = x1+r;
let ymid = y1+r;
let x2 = xmid+r;
@ -27,14 +27,14 @@ function drawPixel(ox,oy,x,y,r,p) {
if (p > 0) {
if (p > 1) {
buf.setColor(0,0,0);
buf.fillRect(x1,y1,x2,y2);
buf.fillPoly([x1,y1,x2,y1,x2,y2,x1,y2]);
}
buf.setColor(1,1,1);
} else {
buf.setColor(0,0,0);
}
if (p < 2) {
buf.fillRect(x1,y1,x2,y2);
buf.fillPoly([x1,y1,x2,y1,x2,y2,x1,y2]);
} else if (p === 2) {
buf.fillPoly([xmid,y1,x2,y1,x2,y2,x1,y2,x1,ymid]);
} else if (p === 3) {

View File

@ -17,3 +17,4 @@
0.16: Detect out of memory errors and draw them onto the bottom of the screen in red
0.17: Don't modify beep/buzz behaviour if firmware does it automatically
0.18: Fix 'GPS time' checks for western hemisphere
0.19: Tweaks to simplify code and lower memory usage

View File

@ -42,13 +42,14 @@ if (!s.timeout) Bangle.setLCDPower(1);
E.setTimeZone(s.timezone);
delete s;
// Draw out of memory errors onto the screen
E.on('errorFlag', function(errorFlags) { g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:",errorFlags);
E.on('errorFlag', function(errorFlags) {
g.reset(1).setColor("#ff0000").setFont("6x8").setFontAlign(0,1).drawString(errorFlags,g.getWidth()/2,g.getHeight()-1).flip();
print("Interpreter error:", errorFlags);
E.getErrorFlags(); // clear flags so we get called next time
});
// stop users doing bad things!
global.save = function() { throw new Error("You can't use save() on Bangle.js without overwriting the bootloader!"); }
// Load *.boot.js files
require('Storage').list(/\.boot\.js/).map(bootFile=>{
require('Storage').list(/\.boot\.js/).forEach(bootFile=>{
eval(require('Storage').read(bootFile));
});

View File

@ -1,24 +1,28 @@
// This runs after a 'fresh' boot
var settings=require("Storage").readJSON('setting.json',1)||{};
// load clock if specified
var clockApp = settings.clock;
var clockApp=(require("Storage").readJSON("setting.json",1)||{}).clock;
if (clockApp) clockApp = require("Storage").read(clockApp);
if (!clockApp) {
var clockApps = require("Storage").list(/\.info$/).map(app=>require("Storage").readJSON(app,1)||{}).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder);
if (clockApps && clockApps.length)
clockApp = require("Storage").read(clockApps[0].src);
delete clockApps;
clockApp = require("Storage").list(/\.info$/)
.map(file => {
const app = require("Storage").readJSON(file,1);
if (app && app.type == "clock") {
return app;
}
})
.filter(x=>x)
.sort((a, b) => a.sortorder - b.sortorder)[0];
if (clockApp)
clockApp = require("Storage").read(clockApp.src);
}
if (!clockApp) clockApp=`E.showMessage("No Clock Found");
setWatch(() => {
Bangle.showLauncher();
}, BTN2, {repeat:false,edge:"falling"});)
`;
delete settings;
// check to see if our clock is wrong - if it is use GPS time
if ((new Date()).getFullYear()<2000) {
E.showMessage("Searching for\nGPS time");
Bangle.on('GPS',function cb(g) {
Bangle.on("GPS",function cb(g) {
Bangle.setGPSPower(0);
Bangle.removeListener("GPS",cb);
if (!g.time || (g.time.getFullYear()<2000) ||

View File

@ -2,3 +2,4 @@
0.03: Add support for data files
0.04: Add functionality to sort apps manually or alphabetically ascending/descending.
0.05: Tweaks to help with memory usage
0.06: Reduce memory usage

View File

@ -45,13 +45,13 @@ function globToRegex(pattern) {
return new RegExp('^'+regex+'$');
}
function eraseFiles(app) {
app.files.split(",").forEach(f=>store.erase(f));
function eraseFiles(info) {
info.files.split(",").forEach(f=>store.erase(f));
}
function eraseData(app) {
if(!app.data) return;
const d=app.data.split(';'),
function eraseData(info) {
if(!info.data) return;
const d=info.data.split(';'),
files=d[0].split(','),
sFiles=(d[1]||'').split(',');
let erase = f=>store.erase(f);
@ -68,8 +68,9 @@ function eraseData(app) {
}
function eraseApp(app, files,data) {
E.showMessage('Erasing\n' + app.name + '...');
if (files) eraseFiles(app);
if (data) eraseData(app);
var info = store.readJSON(app.id + ".info", 1)||{};
if (files) eraseFiles(info);
if (data) eraseData(info);
}
function eraseOne(app, files,data){
E.showPrompt('Erase\n'+app.name+'?').then((v) => {
@ -86,8 +87,7 @@ function eraseAll(apps, files,data) {
E.showPrompt('Erase all?').then((v) => {
if (v) {
Bangle.buzz(100, 1);
for(var n = 0; n<apps.length; n++)
eraseApp(apps[n], files,data);
apps.forEach(app => eraseApp(app, files, data));
}
showApps();
});
@ -100,7 +100,7 @@ function showAppMenu(app) {
},
'< Back': () => showApps(),
};
if (app.data) {
if (app.hasData) {
appmenu['Erase Completely'] = () => eraseOne(app, true, true);
appmenu['Erase App,Keep Data'] = () => eraseOne(app, true, false);
appmenu['Only Erase Data'] = () => eraseOne(app, false, true);
@ -120,11 +120,10 @@ function showApps() {
var list = store.list(/\.info$/).filter((a)=> {
return a !== 'setting.info';
}).sort().map((app) => {
var ret = store.readJSON(app,1)||{};
ret[''] = app;
return ret;
});
}).map((a)=> {
let app = store.readJSON(a, 1) || {};
return {id: app.id, name: app.name, hasData: !!app.data};
}).sort(sortHelper());
if (list.length > 0) {
list.reduce((menu, app) => {

1
apps/gpspoilog/ChangeLog Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AEeGAAwttGMotLGMItPGLwuTGDQuVGDIfHq9RAAgvfDw+AFwtRq4weFZYAIwAvYJIguPSowvavl8F8qpFF6wwSF44AFvl9vo3GF8l80+t1unGAovkvovDvovrAAQvqR4IACR9TviGAovHABIuXF/4vgGAmAFx+AFzDxGACYvVGDAuWF+AwWFzAvwGCguaGCYucF+AwQFzwvwGBwugGBouiF+AwKF0gwJF0wwHF1AvwGAguqGAYusAH4A/AFI="))

67
apps/gpspoilog/app.js Normal file
View File

@ -0,0 +1,67 @@
var menuItems = {
"":{title:"GPS POI Log"},
" ":{value:"No Fix"},
"Tree" : ()=>addItem("Tree"),
"Gate" : ()=>addItem("Gate"),
"Flower" : ()=>addItem("Flower"),
"Plant" : ()=>addItem("Plant"),
"Bus Stop" : ()=>addItem("Bus Stop"),
"Pub" : ()=>addItem("Pub")
};
var menu = E.showMenu(menuItems);
var gps = { fix : 0};
var gpsCount = 0;
var file = require("Storage").open("gpspoilog.csv","a");
function setStatus(msg) {
menuItems[" "].value = msg;
menu.draw();
}
Bangle.on('GPS',function(g) {
gps = g;
gpsCount++;
var msg;
if (g.fix) {
msg = g.satellites + " Satellites";
} else {
msg = "No Fix";
}
setStatus(msg+" "+"-\\|/"[gpsCount&3]);
});
function addItem(name) {
if (!gps.fix) {
setStatus("Ignored - no fix");
return; // don't do anything as no fix
}
// The fields we want to put in out CSV file
var csv = [
0|getTime(), // Time to the nearest second
gps.lat,
gps.lon,
gps.alt,
name
];
// Write data here
file.write(csv.join(",")+"\n");
setStatus("Written");
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setGPSPower(1);
function getData(callback) {
var f = require("Storage").open("gpspoilog.csv","r");
var l = f.readLine();
while (l!==undefined) {
callback(l);
l = f.readLine();
}
}

BIN
apps/gpspoilog/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,69 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<div id="data"></div>
<button class="btn btn-default" id="btnSave">Save</button>
<button class="btn btn-default" id="btnDelete">Delete</button>
<script src="../../lib/interface.js"></script>
<script>
var dataElement = document.getElementById("data");
var csvData = "";
function getData() {
// show loading window
Util.showModal("Loading...");
// get the data
dataElement.innerHTML = "";
Util.readStorageFile(`gpspoilog.csv`,data=>{
csvData = data.trim();
// remove window
Util.hideModal();
// If no data, report it and exit
if (data.length==0) {
dataElement.innerHTML = "<b>No data found</b>";
return;
}
// Otherwise parse the data and output it as a table
dataElement.innerHTML = `<table>
<tr>
<th>Time</th>
<th>Lat</th>
<th>Lon</th>
<th>Alt</th>
<th>Type</th>
</tr>`+data.trim().split("\n").map(l=>{
l = l.split(",");
return `<tr>
<td>${(new Date(l[0]*1000)).toLocaleString()}</td>
<td>${l[1]}</td>
<td>${l[2]}</td>
<td>${l[3]}</td>
<td>${l[4]}</td>
</tr>`
}).join("\n")+"</table>";
});
}
// You can call a utility function to save the data
document.getElementById("btnSave").addEventListener("click", function() {
Util.saveCSV("gpsdata", csvData);
});
// Or you can also delete the file
document.getElementById("btnDelete").addEventListener("click", function() {
Util.showModal("Deleting...");
Util.eraseStorageFile("gpspoilog.csv", function() {
Util.hideModal();
getData();
});
});
// Called when app starts
function onInit() {
getData();
}
</script>
</body>
</html>

View File

@ -1,3 +1,6 @@
0.01: Init
0.02: fix 3/4 moon orientation
0.03: Change `largeclock.json` to 'data' file to allow settings to be preserved
0.04: Adjust layout to account for new vector font
0.05: Add support for 12 hour time
0.06: Allow to disable BTN1 and BTN3 buttons

View File

@ -11,7 +11,7 @@ A readable and informational digital watch, with date, seconds and moon phase an
## How to use it
- The clock can be used as any other one, if you like it just set it as the default clock app in settings > select clock
- In setting > large clock you can select which app is to be open by BTN1 and BTN3
- In setting > large clock you can select which app, if any, is to be open by BTN1 and BTN3
## Credits

View File

@ -4,11 +4,13 @@ let interval;
let lastMoonPhase;
let lastMinutes;
const is12Hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"];
const moonR = 12;
const moonX = 215;
const moonY = 50;
const moonY = is12Hour ? 90 : 50;
const settings = require("Storage").readJSON("largeclock.json", 1);
const settings = require("Storage").readJSON("largeclock.json", 1)||{};
const BTN1app = settings.BTN1 || "";
const BTN3app = settings.BTN3 || "";
@ -118,33 +120,41 @@ function drawMoon(d) {
function drawTime(d) {
const da = d.toString().split(" ");
const time = da[4].substr(0, 5).split(":");
const time = da[4].split(":");
const dow = da[0];
const month = da[1];
const day = da[2];
const year = da[3];
const hours = time[0];
const hours = is12Hour ? ("0" + (((d.getHours() + 11) % 12) + 1)).substr(-2) : time[0];
const meridian = d.getHours() < 12 ? "AM" : "PM";
const minutes = time[1];
const seconds = d.getSeconds();
const seconds = time[2];
if (minutes != lastMinutes) {
if (is12Hour) {
g.setFont("Vector", 18);
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.clearRect(195, 34, 240, 44);
g.drawString(meridian, 217, 34);
}
g.clearRect(0, 24, moonX - moonR - 10, 239);
g.setColor(1, 1, 1);
g.setFontAlign(-1, -1);
g.setFont("Vector", 100);
g.drawString(hours, 50, 24, true);
g.drawString(hours, 40, 24, true);
g.setColor(1, 50, 1);
g.drawString(minutes, 50, 135, true);
g.drawString(minutes, 40, 135, true);
g.setFont("Vector", 20);
g.setRotation(3);
g.drawString(`${dow} ${day} ${month}`, 50, 15, true);
g.drawString(year, 75, 205, true);
g.drawString(`${dow} ${day} ${month}`, 50, 10, true);
g.drawString(year, is12Hour ? 46 : 75, 205, true);
lastMinutes = minutes;
}
g.setRotation(0);
g.setFont("Vector", 20);
g.setColor(1, 1, 1);
g.setFontAlign(0, -1);
g.clearRect(200, 210, 240, 240);
g.clearRect(195, 210, 240, 240);
g.drawString(seconds, 215, 215);
}

View File

@ -1,4 +1,4 @@
{
"BTN1": "timer.app.js",
"BTN3": "calendar.app.js"
"BTN1": "",
"BTN3": ""
}

View File

@ -21,6 +21,10 @@
if (a.n > b.n) return 1;
return 0;
});
apps.push({
n: "NONE",
src: ""
});
const settings = s.readJSON("largeclock.json", 1) || {
BTN1: "",

View File

@ -486,7 +486,7 @@ var locales = {
temperature: "°C",
ampm: { 0: "dop.", 1: "pop." },
timePattern: { 0: "%HH:%MM:%SS", 1: "%HH:%MM" },
datePattern: { 0: "%d. %b %Y", 1: "%d.%m.%Y" }, // "30. jan. 2020" // "30.01.2020"(short)
datePattern: { 0: "%-d. %b %Y", 1: "%-d.%-m.%Y" }, // "3. jan. 2020" // "3.1.2020"(short)
abmonth: "jan.,feb.,mar.,apr.,maj,jun.,jul.,avg.,sep.,okt.,nov.,dec.",
month: "januar,februar,marec,april,maj,junij,julij,avgust,september,oktober,november,december",
abday: "ned.,pon.,tor.,sre.,čet.,pet.,sob.",

4
apps/magnav/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: New App!
0.02: Course marker
0.03: Tilt compensation and calibration

26
apps/magnav/README.md Normal file
View File

@ -0,0 +1,26 @@
# Navigation Compass
This is a tilt and roll compensated compass with a linear display. The compass will display the same direction that it shows when flat as when it is tilted (rotation around the W-S axis) or rolled (rotation around the N-S) axis. *Even with compensation, it would be beyond foolish to rely solely on this app for any serious navigational purpose.*
![](screenshot.jpg)
## Calibration
Correct operation of this app depends critically on calibration. When first run on a Bangle, the app will request calibration. This lasts for 30 seconds during which you should move the watch slowly through figures of 8. It is important that during calibration the watch is fully rotated around each of it axes. If the app does give the correct direction heading or is not stable with respect to tilt and roll - redo the calibration by pressing *BTN3*. Calibration data is recorded in a storage file named `magnav.json`.
## Controls
*BTN1* - switches to your selected clock app.
*BTN2* - switches to the app launcher.
*BTN3* - invokes calibration ( can be cancelled if pressed accidentally)
*Touch Left* - marks the current heading with a blue circle - see screen shot. This can be used to take a bearing and then follow it.
*Touch Right* - cancels the marker (blue circle not displayed).
## Support
Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev).

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/ACMBiIACiAWQCoYADCyEimf/mczkQYOBwMj/4ABC4MzmQYMLYMvCwQXDDARjKFogXFDAQuJiQWEC4szkIwIIooXHGBAuHC4wwIFwXcp4XKGA8Rn//7vMAYIXImYXFIwc97nNDAQXHJAsBj4RB/owBDAQXHmIXERofz7vcDAQXHMApeCAAPdGAIYBC45gFUok9GAXM4hgIXpBgBGAQYIPAcBibTEC4IwC4fzPBIXGJAk/C5amCJAnM74VBVQwXKVIPMbAQXRMAIXEJAoXLnpdDC5Z3FMAPTB4LhCC6ApEC5bXEDAwwBDwjvDgAXHCQgwFC4kRKoQAFGAgXDiIXEl4XKSIkyC4ioHC4qsCOwh4KRYoFCkIXEMBIXEn5eGMBQXGLwpIKC4hGIGBIWFFw4wJFxwwCkYXJFxIwCJIoWFFxIwCGIgWEFxQYEkTRDkQWODAYAFCxxjDAARbLAH4AFA=="))

220
apps/magnav/magnav.js Normal file
View File

@ -0,0 +1,220 @@
const Yoff = 80;
var pal2color = new Uint16Array([0x0000,0xffff,0x07ff,0xC618],0,2);
var buf = Graphics.createArrayBuffer(240,50,2,{msb:true});
Bangle.setLCDTimeout(30);
function flip(b,y) {
g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer, palette:pal2color},0,y);
b.clear();
}
const labels = ["N","NE","E","SE","S","SW","W","NW"];
var brg=null;
function drawCompass(course) {
buf.setColor(1);
buf.setFont("Vector",16);
var start = course-90;
if (start<0) start+=360;
buf.fillRect(28,45,212,49);
var xpos = 30;
var frag = 15 - start%15;
if (frag<15) xpos+=frag; else frag = 0;
for (var i=frag;i<=180-frag;i+=15){
var res = start + i;
if (res%90==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-8,0);
buf.fillRect(xpos-2,25,xpos+2,45);
} else if (res%45==0) {
buf.drawString(labels[Math.floor(res/45)%8],xpos-12,0);
buf.fillRect(xpos-2,30,xpos+2,45);
} else if (res%15==0) {
buf.fillRect(xpos,35,xpos+1,45);
}
xpos+=15;
}
if (brg) {
var bpos = brg - course;
if (bpos>180) bpos -=360;
if (bpos<-180) bpos +=360;
bpos+=120;
if (bpos<30) bpos = 14;
if (bpos>210) bpos = 226;
buf.setColor(2);
buf.fillCircle(bpos,40,8);
}
flip(buf,Yoff);
}
var heading = 0;
function newHeading(m,h){
var s = Math.abs(m - h);
var delta = (m>h)?1:-1;
if (s>=180){s=360-s; delta = -delta;}
if (s<2) return h;
var hd = h + delta*(1 + Math.round(s/5));
if (hd<0) hd+=360;
if (hd>360)hd-= 360;
return hd;
}
var candraw = false;
var CALIBDATA = require("Storage").readJSON("magnav.json",1)||null;
function tiltfixread(O,S){
var start = Date.now();
var m = Bangle.getCompass();
var g = Bangle.getAccel();
m.dx =(m.x-O.x)*S.x; m.dy=(m.y-O.y)*S.y; m.dz=(m.z-O.z)*S.z;
var d = Math.atan2(-m.dx,m.dy)*180/Math.PI;
if (d<0) d+=360;
var phi = Math.atan(-g.x/-g.z);
var cosphi = Math.cos(phi), sinphi = Math.sin(phi);
var theta = Math.atan(-g.y/(-g.x*sinphi-g.z*cosphi));
var costheta = Math.cos(theta), sintheta = Math.sin(theta);
var xh = m.dy*costheta + m.dx*sinphi*sintheta + m.dz*cosphi*sintheta;
var yh = m.dz*sinphi - m.dx*cosphi;
var psi = Math.atan2(yh,xh)*180/Math.PI;
if (psi<0) psi+=360;
return psi;
}
// Note actual mag is 360-m, error in firmware
function reading() {
var d = tiltfixread(CALIBDATA.offset,CALIBDATA.scale);
heading = newHeading(d,heading);
drawCompass(heading);
buf.setColor(1);
buf.setFont("6x8",2);
buf.setFontAlign(-1,-1);
buf.drawString("o",170,0);
buf.setFont("Vector",40);
var course = Math.round(heading);
var cs = course.toString();
cs = course<10?"00"+cs : course<100 ?"0"+cs : cs;
buf.drawString(cs,70,10);
flip(buf,Yoff+80);
}
function calibrate(){
var max={x:-32000, y:-32000, z:-32000},
min={x:32000, y:32000, z:32000};
var ref = setInterval(()=>{
var m = Bangle.getCompass();
max.x = m.x>max.x?m.x:max.x;
max.y = m.y>max.y?m.y:max.y;
max.z = m.z>max.z?m.z:max.z;
min.x = m.x<min.x?m.x:min.x;
min.y = m.y<min.y?m.y:min.y;
min.z = m.z<min.z?m.z:min.z;
}, 100);
return new Promise((resolve) => {
setTimeout(()=>{
if(ref) clearInterval(ref);
var offset = {x:(max.x+min.x)/2,y:(max.y+min.y)/2,z:(max.z+min.z)/2};
var delta = {x:(max.x-min.x)/2,y:(max.y-min.y)/2,z:(max.z-min.z)/2};
var avg = (delta.x+delta.y+delta.z)/3;
var scale = {x:avg/delta.x, y:avg/delta.y, z:avg/delta.z};
resolve({offset:offset,scale:scale});
},30000);
});
}
function docalibrate(e,first){
const title = "Calibrate";
const msg = "takes 30 seconds";
function action(b){
if (b) {
buf.setColor(1);
buf.setFont("Vector",24);
buf.setFontAlign(0,-1);
buf.drawString("Fig 8s to",120,0);
buf.drawString("Calibrate",120,26);
flip(buf,Yoff);
calibrate().then((r)=>{
require("Storage").write("magnav.json",r);
CALIBDATA = r;
startdraw();
setButtons();
});
} else {
startdraw();
setTimeout(setButtons,1000);
}
}
if (first===undefined) first=false;
stopdraw();
clearWatch();
if (first)
E.showAlert(msg,title).then(action.bind(null,true));
else
E.showPrompt(msg,{title:title,buttons:{"Start":true,"Cancel":false}}).then(action);
}
Bangle.on('touch', function(b) {
if(!candraw) return;
if(b==1) brg=heading;
if(b==2) brg=null;
});
var intervalRef;
function startdraw(){
g.clear();
g.setColor(1,0.5,0.5);
g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);
g.setColor(1,1,1);
Bangle.drawWidgets();
candraw = true;
intervalRef = setInterval(reading,200);
}
function stopdraw() {
candraw=false;
if(intervalRef) {clearInterval(intervalRef);}
}
function setButtons(){
setWatch(()=>{load();}, BTN1, {repeat:false,edge:"falling"});
setWatch(Bangle.showLauncher, BTN2, {repeat:false,edge:"falling"});
setWatch(docalibrate, BTN3, {repeat:false,edge:"falling"});
}
var SCREENACCESS = {
withApp:true,
request:function(){
this.withApp=false;
stopdraw();
clearWatch();
},
release:function(){
this.withApp=true;
startdraw();
setButtons();
}
};
Bangle.on('lcdPower',function(on) {
if (!SCREENACCESS.withApp) return;
if (on) {
startdraw();
} else {
stopdraw();
}
});
Bangle.on('kill',()=>{Bangle.setCompassPower(0);});
Bangle.loadWidgets();
Bangle.setCompassPower(1);
if (!CALIBDATA)
docalibrate({},true);
else {
startdraw();
setButtons();
}

10
apps/magnav/magnav.min.js vendored Normal file
View File

@ -0,0 +1,10 @@
var Yoff=80,pal2color=new Uint16Array([0,65535,2047,50712],0,2),buf=Graphics.createArrayBuffer(240,50,2,{msb:!0});Bangle.setLCDTimeout(30);function flip(b,c){g.drawImage({width:240,height:50,bpp:2,buffer:b.buffer,palette:pal2color},0,c);b.clear()}var labels="N NE E SE S SW W NW".split(" "),brg=null;
function drawCompass(b){buf.setColor(1);buf.setFont("Vector",16);var c=b-90;0>c&&(c+=360);buf.fillRect(28,45,212,49);var a=30,d=15-c%15;15>d?a+=d:d=0;for(var e=d;e<=180-d;e+=15){var f=c+e;0==f%90?(buf.drawString(labels[Math.floor(f/45)%8],a-8,0),buf.fillRect(a-2,25,a+2,45)):0==f%45?(buf.drawString(labels[Math.floor(f/45)%8],a-12,0),buf.fillRect(a-2,30,a+2,45)):0==f%15&&buf.fillRect(a,35,a+1,45);a+=15}brg&&(b=brg-b,180<b&&(b-=360),-180>b&&(b+=360),b+=120,30>b&&(b=14),210<b&&(b=226),buf.setColor(2),
buf.fillCircle(b,40,8));flip(buf,Yoff)}var heading=0;function newHeading(b,c){var a=Math.abs(b-c),d=b>c?1:-1;180<=a&&(a=360-a,d=-d);if(2>a)return c;a=c+d*(1+Math.round(a/5));0>a&&(a+=360);360<a&&(a-=360);return a}var candraw=!1,CALIBDATA=require("Storage").readJSON("magnav.json",1)||null;
function tiltfixread(b,c){Date.now();var a=Bangle.getCompass(),d=Bangle.getAccel();a.dx=(a.x-b.x)*c.x;a.dy=(a.y-b.y)*c.y;a.dz=(a.z-b.z)*c.z;var e=Math.atan(-d.x/-d.z),f=Math.cos(e);e=Math.sin(e);d=Math.atan(-d.y/(-d.x*e-d.z*f));var k=Math.sin(d);a=180*Math.atan2(a.dz*e-a.dx*f,a.dy*Math.cos(d)+a.dx*e*k+a.dz*f*k)/Math.PI;0>a&&(a+=360);return a}
function reading(){var b=tiltfixread(CALIBDATA.offset,CALIBDATA.scale);heading=newHeading(b,heading);drawCompass(heading);buf.setColor(1);buf.setFont("6x8",2);buf.setFontAlign(-1,-1);buf.drawString("o",170,0);buf.setFont("Vector",40);b=Math.round(heading);var c=b.toString();buf.drawString(10>b?"00"+c:100>b?"0"+c:c,70,10);flip(buf,Yoff+80)}
function calibrate(){var b=-32E3,c=-32E3,a=-32E3,d=32E3,e=32E3,f=32E3,k=setInterval(function(){var h=Bangle.getCompass();b=h.x>b?h.x:b;c=h.y>c?h.y:c;a=h.z>a?h.z:a;d=h.x<d?h.x:d;e=h.y<e?h.y:e;f=h.z<f?h.z:f},100);return new Promise(function(h){setTimeout(function(){k&&clearInterval(k);var m=(b-d)/2,n=(c-e)/2,p=(a-f)/2,l=(m+n+p)/3;h({offset:{x:(b+d)/2,y:(c+e)/2,z:(a+f)/2},scale:{x:l/m,y:l/n,z:l/p}})},3E4)})}
function docalibrate(b,c){function a(a){a?(buf.setColor(1),buf.setFont("Vector",24),buf.setFontAlign(0,-1),buf.drawString("Fig 8s to",120,0),buf.drawString("Calibrate",120,26),flip(buf,Yoff),calibrate().then(function(a){require("Storage").write("magnav.json",a);CALIBDATA=a;startdraw();setButtons()})):(startdraw(),setTimeout(setButtons,1E3))}void 0===c&&(c=!1);stopdraw();clearWatch();c?E.showAlert("takes 30 seconds","Calibrate").then(a.bind(null,!0)):E.showPrompt("takes 30 seconds",{title:"Calibrate",
buttons:{Start:!0,Cancel:!1}}).then(a)}Bangle.on("touch",function(b){candraw&&(1==b&&(brg=heading),2==b&&(brg=null))});var intervalRef;function startdraw(){g.clear();g.setColor(1,.5,.5);g.fillPoly([120,Yoff+50,110,Yoff+70,130,Yoff+70]);g.setColor(1,1,1);Bangle.drawWidgets();candraw=!0;intervalRef=setInterval(reading,200)}function stopdraw(){candraw=!1;intervalRef&&clearInterval(intervalRef)}
function setButtons(){setWatch(function(){load()},BTN1,{repeat:!1,edge:"falling"});setWatch(Bangle.showLauncher,BTN2,{repeat:!1,edge:"falling"});setWatch(docalibrate,BTN3,{repeat:!1,edge:"falling"})}var SCREENACCESS={withApp:!0,request:function(){this.withApp=!1;stopdraw();clearWatch()},release:function(){this.withApp=!0;startdraw();setButtons()}};Bangle.on("lcdPower",function(b){SCREENACCESS.withApp&&(b?startdraw():stopdraw())});Bangle.on("kill",function(){Bangle.setCompassPower(0)});Bangle.loadWidgets();
Bangle.setCompassPower(1);CALIBDATA?(startdraw(),setButtons()):docalibrate({},!0);

BIN
apps/magnav/magnav.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/magnav/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4ATiwAGFdYzlFp4xeFyYwZD49kxGs2fX6+z1mIsgxcDQtAxArCAA+zxFAGDAYFxAsJAAuIGCxcF1omHgEABI+sGCouERRIvJSgKTEFzovLGAJgRCIiMIF5ySGF57qMF5nXsgvORoggLF5yRPLyAvO6+IF6LsKF6JgEF5lkD5gvPYIiOaF6CQMBYesD5oAP1gvPXxpfDAQIAFYCILDJ5wvP64veACCPeAB6PQd4p9EQQ4MLd6GIF7uIF5YwDsgiNAY4vHsguLYBJfXXxiQKL66ONF4lAL7dAF5pgIF6y9DFxYvEi2sF6+sDwgvLGAryEACLsEFxrCGGCmzXh5gJSQYAPRgovQGA1kMR7qEFyQwHi2IGJWzxCLEFygwIMYOI1gzC2esxBbGFywxKABotXGCwuaGKQtdGZorjAH4A/AF4="))

View File

@ -0,0 +1,113 @@
// Code based on the original Mixed Clock
/* jshint esversion: 6 */
var locale = require("locale");
const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 };
const Center = { "x": 120, "y": 96 };
const Widths = { hour: 2, minute: 2 };
var buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
function rotatePoint(x, y, d) {
rad = -1 * d / 180 * Math.PI;
var sin = Math.sin(rad);
var cos = Math.cos(rad);
xn = ((Center.x + x * cos - y * sin) + 0.5) | 0;
yn = ((Center.y + x * sin - y * cos) + 0.5) | 0;
p = [xn, yn];
return p;
}
// from https://github.com/espruino/Espruino/issues/1702
function setLineWidth(x1, y1, x2, y2, lw) {
var dx = x2 - x1;
var dy = y2 - y1;
var d = Math.sqrt(dx * dx + dy * dy);
dx = dx * lw / d;
dy = dy * lw / d;
return [
// rounding
x1 - (dx + dy) / 2, y1 - (dy - dx) / 2,
x1 - dx, y1 -dy,
x1 + (dy - dx) / 2, y1 - (dx + dy) / 2,
x1 + dy, y1 - dx,
x2 + dy, y2 - dx,
// rounding
x2 + (dx + dy) / 2, y2 + (dy - dx) / 2,
x2 + dx, y2 + dy,
x2 - (dy - dx) / 2, y2 + (dx + dy) / 2,
x2 - dy, y2 + dx,
x1 - dy, y1 + dx
];
}
function drawMixedClock(force) {
if ((force || Bangle.isLCDOn()) && buf.buffer) {
var date = new Date();
var dateArray = date.toString().split(" ");
var isEn = locale.name.startsWith("en");
var point = [];
var minute = date.getMinutes();
var hour = date.getHours();
var radius;
g.reset();
buf.clear();
// draw date
buf.setFont("6x8", 2);
buf.setFontAlign(-1, 0);
buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
buf.setFontAlign(1, 0);
buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
buf.drawString(dateArray[3], 237, 176, true);
// draw hour and minute dots
for (i = 0; i < 60; i++) {
radius = (i % 5) ? 2 : 4;
point = rotatePoint(0, Radius.dots, i * 6);
buf.fillCircle(point[0], point[1], radius);
}
// draw digital time
buf.setFont("6x8", 3);
buf.setFontAlign(0, 0);
buf.drawString(dateArray[4], 120, 120, true);
// draw new minute hand
point = rotatePoint(0, Radius.min, minute * 6);
buf.drawLine(Center.x, Center.y, point[0], point[1]);
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
// draw new hour hand
point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0);
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
// draw center
buf.fillCircle(Center.x, Center.y, Radius.center);
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
}
}
Bangle.on('lcdPower', function(on) {
if (on)
drawMixedClock(true);
Bangle.drawWidgets();
});
setInterval(() => drawMixedClock(true), 30000); // force an update every 30s even screen is off
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawMixedClock(); // immediately draw
setInterval(drawMixedClock, 500); // update twice a second
// Show launcher when middle button pressed after freeing memory first
setWatch(() => {delete buf.buffer; Bangle.showLauncher()}, BTN2, {repeat:false,edge:"falling"});

BIN
apps/miclock2/clock-mixed.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

View File

@ -3,3 +3,4 @@
0.03: maximize numerals, make menu button configurable, change icon to mac palette, add default settings file, respect 12hour setting
0.04: Don't overwrite existing settings on app update
0.05: Fix settings issue
0.06: Improve rendering of Numeral 1, fix issue with alarms not showing up

View File

@ -8,7 +8,7 @@
var numerals = {
0:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,9],[30,25,61,25,69,33,69,67,61,75,30,75,22,67,22,33]],
1:[[59,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,59,27,51,19,51,9]],
1:[[50,1,82,1,90,9,90,92,82,100,73,100,65,92,65,27,50,27,42,19,42,9]],
2:[[9,1,82,1,90,9,90,53,82,61,21,61,21,74,82,74,90,82,90,92,82,100,9,100,1,92,1,48,9,40,70,40,70,27,9,27,1,19,1,9]],
3:[[9,1,82,1,90,9,90,92,82,100,9,100,1,92,1,82,9,74,70,74,70,61,9,61,1,53,1,48,9,40,70,40,70,27,9,27,1,19,1,9]],
4:[[9,1,14,1,22,9,22,36,69,36,69,9,77,1,82,1,90,9,90,92,82,100,78,100,70,92,70,61,9,61,1,53,1,9]],
@ -70,12 +70,8 @@ function draw(drawMode){
}
Bangle.setLCDMode();
clearWatch();
g.reset().clear();
setWatch(Bangle.showLauncher, settings.menuButton, {repeat:false,edge:"falling"});
g.clear();
clearInterval();
if (settings.color>0) _rCol=settings.color-1;
interval=setInterval(draw, REFRESH_RATE, settings.drawMode);
draw(settings.drawMode);

View File

@ -1 +1,2 @@
0.02: Make minor adjustments to widget, and discard stale weather data after a configurable period.
0.03: Fix flickering last updated time.

View File

@ -3,7 +3,7 @@
function formatDuration(millis) {
let pluralize = (n, w) => n + " " + w + (n == 1 ? "" : "s");
if (millis < 60000) return pluralize(Math.floor(millis/1000), "second");
if (millis < 60000) return "< 1 minute";
if (millis < 3600000) return pluralize(Math.floor(millis/60000), "minute");
if (millis < 86400000) return pluralize(Math.floor(millis/3600000), "hour");
return pluralize(Math.floor(millis/86400000), "day");
@ -41,7 +41,7 @@
g.setFont("6x8", 1).setFontAlign(0, 0, 0);
g.drawString(w.txt.charAt(0).toUpperCase()+w.txt.slice(1), 120, 190);
drawUpdateTime(w);
drawUpdateTime();
g.flip();
}
@ -63,7 +63,7 @@
}
}
let interval = setInterval(drawUpdateTime, 1000);
let interval = setInterval(drawUpdateTime, 60000);
Bangle.on('lcdPower', (on) => {
if (interval) {
clearInterval(interval);
@ -71,7 +71,7 @@
}
if (on) {
drawUpdateTime();
interval = setInterval(drawUpdateTime, 1000);
interval = setInterval(drawUpdateTime, 60000);
}
});

View File

@ -6,8 +6,9 @@ function scheduleExpiry(json) {
clearTimeout(expiryTimeout);
expiryTimeout = undefined;
}
if (json.weather && json.weather.time && json.expiry) {
let t = json.weather.time + json.expiry - Date.now();
let expiry = "expiry" in json ? json.expiry : 2*3600000;
if (json.weather && json.weather.time && expiry) {
let t = json.weather.time + expiry - Date.now();
expiryTimeout = setTimeout(() => {
expiryTimeout = undefined;

View File

@ -78,8 +78,12 @@ function cmdListDevices() {
});
noble.startScanning([], true);
setTimeout(function() {
console.log("Stopping scan");
noble.stopScanning();
}, 2000);
setTimeout(function() {
process.exit(0);
}, 500);
}, 4000);
}
function cmdInstallApp(appId, deviceAddress) {
@ -88,7 +92,8 @@ function cmdInstallApp(appId, deviceAddress) {
if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`);
return AppInfo.getFiles(app, {
fileGetter:function(url) {
return Promise.resolve(require("fs").readFileSync(url).toString());
console.log(__dirname+"/"+url);
return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString());
}, settings : SETTINGS}).then(files => {
//console.log(files);
var command = files.map(f=>f.cmd).join("\n")+"\n";
@ -101,6 +106,7 @@ function bangleSend(command, deviceAddress) {
var args = [].slice.call(arguments);
console.log("UART: "+args.join(" "));
}
//console.log("Sending",JSON.stringify(command));
var RESET = true;
var DEVICEADDRESS = "";

View File

@ -1,8 +1,8 @@
if (typeof btoa==="undefined") {
// Don't define btoa as a function here because Apple's
// iOS browser defines the function even though it's in
// an IF statement that is never executed!
btoa = function(d) { return Buffer.from(d).toString('base64'); }
// an IF statement that is never executed
btoa = function(d) { return Buffer.from(d,'binary').toString('base64'); }
}
// Converts a string into most efficient way to send to Espruino (either json, base64, or compressed base64)

View File

@ -403,7 +403,7 @@ function checkDependencies(app, uploadOptions) {
if (found)
console.log(`Found dependency in installed app '${found.id}'`);
else {
var foundApps = appJSON.filter(app=>app.type==dependency);
let foundApps = appJSON.filter(app=>app.type==dependency);
if (!foundApps.length) throw new Error(`Dependency of '${dependency}' listed, but nothing satisfies it!`);
console.log(`Apps ${foundApps.map(f=>`'${f.id}'`).join("/")} implement '${dependency}'`);
found = foundApps[0]; // choose first app in list