forked from FOSS/BangleApps
commit
4a643016fd
71
apps.json
71
apps.json
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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=="))
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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
|
|
@ -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) => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AEeGAAwttGMotLGMItPGLwuTGDQuVGDIfHq9RAAgvfDw+AFwtRq4weFZYAIwAvYJIguPSowvavl8F8qpFF6wwSF44AFvl9vo3GF8l80+t1unGAovkvovDvovrAAQvqR4IACR9TviGAovHABIuXF/4vgGAmAFx+AFzDxGACYvVGDAuWF+AwWFzAvwGCguaGCYucF+AwQFzwvwGBwugGBouiF+AwKF0gwJF0wwHF1AvwGAguqGAYusAH4A/AFI="))
|
|
@ -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();
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"BTN1": "timer.app.js",
|
||||
"BTN3": "calendar.app.js"
|
||||
"BTN1": "",
|
||||
"BTN3": ""
|
||||
}
|
||||
|
|
|
@ -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: "",
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
0.01: New App!
|
||||
0.02: Course marker
|
||||
0.03: Tilt compensation and calibration
|
||||
|
|
@ -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.*
|
||||
|
||||

|
||||
|
||||
## 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).
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/ACMBiIACiAWQCoYADCyEimf/mczkQYOBwMj/4ABC4MzmQYMLYMvCwQXDDARjKFogXFDAQuJiQWEC4szkIwIIooXHGBAuHC4wwIFwXcp4XKGA8Rn//7vMAYIXImYXFIwc97nNDAQXHJAsBj4RB/owBDAQXHmIXERofz7vcDAQXHMApeCAAPdGAIYBC45gFUok9GAXM4hgIXpBgBGAQYIPAcBibTEC4IwC4fzPBIXGJAk/C5amCJAnM74VBVQwXKVIPMbAQXRMAIXEJAoXLnpdDC5Z3FMAPTB4LhCC6ApEC5bXEDAwwBDwjvDgAXHCQgwFC4kRKoQAFGAgXDiIXEl4XKSIkyC4ioHC4qsCOwh4KRYoFCkIXEMBIXEn5eGMBQXGLwpIKC4hGIGBIWFFw4wJFxwwCkYXJFxIwCJIoWFFxIwCGIgWEFxQYEkTRDkQWODAYAFCxxjDAARbLAH4AFA=="))
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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);
|
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4ATiwAGFdYzlFp4xeFyYwZD49kxGs2fX6+z1mIsgxcDQtAxArCAA+zxFAGDAYFxAsJAAuIGCxcF1omHgEABI+sGCouERRIvJSgKTEFzovLGAJgRCIiMIF5ySGF57qMF5nXsgvORoggLF5yRPLyAvO6+IF6LsKF6JgEF5lkD5gvPYIiOaF6CQMBYesD5oAP1gvPXxpfDAQIAFYCILDJ5wvP64veACCPeAB6PQd4p9EQQ4MLd6GIF7uIF5YwDsgiNAY4vHsguLYBJfXXxiQKL66ONF4lAL7dAF5pgIF6y9DFxYvEi2sF6+sDwgvLGAryEACLsEFxrCGGCmzXh5gJSQYAPRgovQGA1kMR7qEFyQwHi2IGJWzxCLEFygwIMYOI1gzC2esxBbGFywxKABotXGCwuaGKQtdGZorjAH4A/AF4="))
|
|
@ -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"});
|
Binary file not shown.
After Width: | Height: | Size: 707 B |
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue