Use fewer predefined element types and instead rely on datasources

pull/1916/head
Martin Boonk 2022-02-19 19:22:44 +01:00
parent 80e5eff3bd
commit 299a66b0c7
2 changed files with 333 additions and 168 deletions

View File

@ -3,13 +3,13 @@
This app is a highly customizable watchface. To use it, you need to select This app is a highly customizable watchface. To use it, you need to select
a watchface from another source. a watchface from another source.
## Usage # Usage
Choose the folder which contains the watchface, then clock "Upload to watch". Choose the folder which contains the watchface, then clock "Upload to watch".
## Design watch faces # Design watch faces
### Folder structure ## Folder structure
* watchfacename * watchfacename
@ -18,21 +18,169 @@ Choose the folder which contains the watchface, then clock "Upload to watch".
* info.json * info.json
#### resources ### resources
This folder contains image files. It can have subfolders. These files will This folder contains image files. It can have subfolders. These files will
be read and converted into a resource bundle used by the clock be read and converted into a resource bundle used by the clock
#### face.json Folder types:
This file contains the description of the watch face elements. * Number
* Contains files named 0.ext to 9.ext and minus.ext
* WeatherIcon
* Contains files named with 3 digits for openweathermap weather codes, i.e. 721.ext
* Icon
* Notifications: sound.ext, silent.ext, vibrate.ext
* other status icons: on.ext, off.ext
* Scale
* Contains the components of the scale, named 0.ext to y.ext, y beeing the last element of the scale
#### info.json ### face.json
This file contains the description of the watch face elements.
#### Object types:
##### Properties
```
Properties: {
"Redraw": {
"Unlocked": 5000,
"Locked": 6000
}"
}
```
##### Images
```
Image: {
"X": 0,
"Y": 0,
ImagePath: [ "path", "in", "resources", "file" ]
}
```
##### Coded Images
```
CodedImage: {
"X": 0,
"Y": 0,
"Value": "WeatherCode",
ImagePath: [ "path", "in", "resources", "file" ]
}
```
The `Value` field is one of the implemented numerical values.
##### Number
Can bottom right, or top left aligned. Will currently force all numbers to
be integer.
```
"Number": {
"X": 123,
"Y": 123,
"Alignment": "BottomRight",
"Value": "Temperature",
"Spacing": 1,
"ImagePath": [ "path", "to", "numbers", "folder" ]
}
```
The `Value` field is one of the implemented numerical values.
`Alignment` is either `BottomRight` or `TopLeft`
##### Scale
```
"Scale": {
"X": 123,
"Y": 123,
"Value": "Temperature",
"MinValue": "-20",
"MaxValue": "50",
"ImagePath": [ "path", "to", "scale", "folder" ]
}
```
The `Value` field is one of the implemented numerical values.
`MaxValue` and `MinValue` set the start and endpoints of the scale.
##### MultiState
```
"MultiState": {
"X": 0,
"Y": 0,
"Value": "Lock",
"ImagePath": ["icons", "status", "lock"]
}
```
The `Value` field is one of the implemented multi state values.
##### Nesting
```
Container: {
"X": 10,
"Y": 10,
OtherContainer: {
"X": 5,
"Y": 5,
SomeElement: {
"X": 2,
"Y": 2,
<Content>
}
}
}
```
`SomeElement` will be drawn at X- and Y-position 2+5+10=17, because positions are relative to parent element.
Container names can be everything but other object names.
#### Implemented data sources
##### For Number objects
* Hour
* HourTens
* HourOnes
* Minute
* MinuteTens
* MinuteOnes
* Day
* DayTens
* DayOnes
* Month
* MonthTens
* MonthOnes
* Pulse
* Steps
* Temperature
* Pressure
* Altitude
* BatteryPercentage
* BatteryVoltage
* WeatherCode
* WeatherTemperature
##### For MultiState
* on/off
* Lock
* Charge
* Alarm
* Bluetooth
* BluetoothPeripheral
* HRM
* Barometer
* Compass
* GPS
* on/off/vibrate
* Notifications
### info.json
This file contains information for the conversion process, it will not be This file contains information for the conversion process, it will not be
stored on the watch stored on the watch
## TODO # TODO
* Performance improvements * Performance improvements
* Mark elements with how often they need to be redrawn * Mark elements with how often they need to be redrawn
@ -44,6 +192,6 @@ stored on the watch
* Description of the file format * Description of the file format
* Allow additional files for upload declared in info.json * Allow additional files for upload declared in info.json
## Creator # Creator
[halemmerich](https://github.com/halemmerich) [halemmerich](https://github.com/halemmerich)

View File

@ -40,8 +40,15 @@ function splitNumberToDigits(num){
return String(num).split('').map(item => Number(item)); return String(num).split('').map(item => Number(item));
} }
function drawNumber(element, offset, number){ function drawNumber(element, offset){
//print("drawNumber: ", element, number); var number = numbers[element.Value]();
//print("drawNumber: ", number, element, offset);
if (number) number = number.toFixed(0);
//var numberOffset = updateOffset(element, offset);
var numberOffset = offset;
var isNegative; var isNegative;
var digits; var digits;
if (number == undefined){ if (number == undefined){
@ -56,8 +63,8 @@ function drawNumber(element, offset, number){
//print("digits: ", digits); //print("digits: ", digits);
var numberOfDigits = element.Digits; var numberOfDigits = element.Digits;
if (!numberOfDigits) numberOfDigits = digits.length; if (!numberOfDigits) numberOfDigits = digits.length;
var firstDigitX = element.TopLeftX; var firstDigitX = element.X;
var firstDigitY = element.TopLeftY; var firstDigitY = element.Y;
var firstImage = getByPath(resources, element.ImagePath, 0); var firstImage = getByPath(resources, element.ImagePath, 0);
if (element.Alignment == "BottomRight"){ if (element.Alignment == "BottomRight"){
@ -67,14 +74,14 @@ function drawNumber(element, offset, number){
numberWidth += firstImage.width + element.Spacing; numberWidth += firstImage.width + element.Spacing;
} }
//print("Number width: ", numberWidth, firstImage.width, element.Spacing); //print("Number width: ", numberWidth, firstImage.width, element.Spacing);
firstDigitX = element.BottomRightX - numberWidth + 1; firstDigitX = element.X - numberWidth + 1;
firstDigitY = element.BottomRightY - firstImage.height + 1; firstDigitY = element.Y - firstImage.height + 1;
//print("Calculated start " + firstDigitX + "," + firstDigitY + " From:" + element.BottomRightX + " " + firstImage.width + " " + element.Spacing); //print("Calculated start " + firstDigitX + "," + firstDigitY + " From:" + element.BottomRightX + " " + firstImage.width + " " + element.Spacing);
} }
var currentX = firstDigitX; var currentX = firstDigitX;
if (isNegative){ if (isNegative){
drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, "minus"); drawElement({X:currentX,Y:firstDigitY}, numberOffset, element.ImagePath, "minus");
currentX += firstImage.width + element.Spacing; currentX += firstImage.width + element.Spacing;
} }
@ -87,7 +94,7 @@ function drawNumber(element, offset, number){
currentDigit = 0; currentDigit = 0;
} }
//print("Digit " + currentDigit + " " + currentX); //print("Digit " + currentDigit + " " + currentX);
drawElement({X:currentX,Y:firstDigitY}, offset, element.ImagePath, currentDigit); drawElement({X:currentX,Y:firstDigitY}, numberOffset, element.ImagePath, currentDigit);
currentX += firstImage.width + element.Spacing; currentX += firstImage.width + element.Spacing;
} }
} }
@ -99,19 +106,37 @@ function setColors(properties){
function drawElement(pos, offset, path, lastElem){ function drawElement(pos, offset, path, lastElem){
//print("drawElement ",pos, offset, path, lastElem); //print("drawElement ",pos, offset, path, lastElem);
var image = getByPath(resources, path, lastElem); var resource = getByPath(resources, path, lastElem);
if (image){ if (resource){
setColors(offset); var image = getImg(resource);
g.drawImage(getImg(image),offset.X + pos.X,offset.Y + pos.Y); if (image){
setColors(offset);
//print("drawImage from drawElement", image, pos, offset);
g.drawImage(image ,offset.X + pos.X,offset.Y + pos.Y);
} else {
//print("Could not create image from", resource);
}
} else { } else {
print("Could not create image from", path, lastElem); //print("Could not get resource from", path, lastElem);
} }
} }
function drawScale(scale, offset, value){ function drawScale(scale, offset){
//print("drawScale", scale, offset);
var segments = scale.Segments; var segments = scale.Segments;
var value = numbers[scale.Value]();
var maxValue = scale.MaxValue ? scale.MaxValue : 1;
var minValue = scale.MinValue ? scale.MinValue : 0;
value = value/maxValue;
value -= minValue;
//print("Value is ", value, "(", maxValue, ",", minValue, ")");
var scaleOffset = updateOffset(scale, offset);
for (var i = 0; i < value * segments.length; i++){ for (var i = 0; i < value * segments.length; i++){
drawElement(segments[i], offset, scale.ImagePath, i); drawElement(segments[i], scaleOffset, scale.ImagePath, i);
} }
} }
@ -119,18 +144,6 @@ function drawDigit(element, offset, digit){
drawElement(element, offset, element.ImagePath, digit); drawElement(element, offset, element.ImagePath, digit);
} }
function drawMonthAndDay(element, offset){
var date = new Date();
var dateOffset = updateOffset(element, offset);
if (element.Separate){
var separateOffset = updateOffset(element.Separate, dateOffset);
drawNumber(element.Separate.Month, separateOffset, date.getMonth() + 1);
drawNumber(element.Separate.Day, separateOffset, date.getDate());
}
}
function drawImage(image, offset, name){ function drawImage(image, offset, name){
if (image.ImagePath) { if (image.ImagePath) {
//print("drawImage", image, offset, name); //print("drawImage", image, offset, name);
@ -142,42 +155,49 @@ function drawImage(image, offset, name){
} }
} }
function drawWeather(element, offset){ function drawCodedImage(image, offset){
var jsonWeather = require("Storage").readJSON('weather.json'); var code = numbers[image.Value]();
var weather = jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; //print("drawCodedImage", image, offset, code);
if (image.ImagePath) {
var weatherOffset = updateOffset(element, offset); var factor = 1;
var currentCode = code;
var iconOffset = updateOffset(element.Icon, weatherOffset); while (code / factor > 1){
if (weather && weather.code && element.Icon){ currentCode = Math.floor(currentCode/factor)*factor;
var weathercode = weather.code; //print("currentCode", currentCode);
//print(getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)); if (getByPath(resources, image.ImagePath, currentCode)){
if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){ break;
weathercode = Math.floor(weathercode/10)*10; }
//print("Weathercode ", weathercode); factor *= 10;
} }
if (!getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){ if (code / factor > 1){
weathercode = Math.floor(weathercode/100)*100; //print("found match");
//print("Weathercode ", weathercode); drawImage(image, offset, currentCode);
}
if (getByPath(resources, element.Icon.CustomIcon.ImagePath, weathercode)){
//print("Weathercode ", weathercode);
drawImage(element.Icon.CustomIcon, offset, weathercode);
}
} else if (getByPath(resources, element.Icon.CustomIcon.ImagePath, "000")) {
drawImage(element.Icon.CustomIcon, iconOffset, "000");
}
if (element.Temperature){
var tempOffset = updateOffset(element.Temperature, weatherOffset);
if (weather && weather.temp && element.Temperature){
drawNumber(element.Temperature.Current.Number, tempOffset, (weather.temp - 273.15).toFixed(0));
} else { } else {
drawNumber(element.Temperature.Current.Number, tempOffset); //print("fallback");
drawImage(image, offset, "fallback");
} }
drawImage(element.Temperature.Current, tempOffset, "centigrade");
} }
}
function getWeatherCode(){
var jsonWeather = require("Storage").readJSON('weather.json');
var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
if (weather && weather.code){
return weather.code;
}
return undefined;
}
function getWeatherTemperature(){
var jsonWeather = require("Storage").readJSON('weather.json');
var weather = (jsonWeather && jsonWeather.weather) ? jsonWeather.weather : undefined;
if (weather && weather.temp){
//print("Weather temp is", weather.temp);
return weather.temp - 273.15;
}
return undefined;
} }
function updateOffset(element, offset){ function updateOffset(element, offset){
@ -190,58 +210,60 @@ function updateOffset(element, offset){
return newOffset; return newOffset;
} }
function drawTime(element, offset){ var numbers = {};
var date = new Date(); numbers.Hour = () => { return new Date().getHours(); };
var hours = date.getHours(); numbers.HourTens = () => { return Math.floor(new Date().getHours()/10); };
var minutes = date.getMinutes(); numbers.HourOnes = () => { return Math.floor(new Date().getHours()%10); };
numbers.Hour12 = () => { return new Date().getHours()%12; };
var offsetTime = updateOffset(element, offset); numbers.Hour12Tens = () => { return Math.floor((new Date().getHours()%12)/10); };
numbers.Hour12Ones = () => { return Math.floor((new Date().getHours()%12)%10); };
numbers.Minute = () => { return new Date().getMinutes(); };
numbers.MinuteTens = () => { return Math.floor(new Date().getMinutes()/10); };
numbers.MinuteOnes = () => { return Math.floor(new Date().getMinutes()%10); };
numbers.Second = () => { return new Date().getSeconds(); };
numbers.SecondTens = () => { return Math.floor(new Date().getSeconds()/10); };
numbers.SecondOnes = () => { return Math.floor(new Date().getSeconds()%10); };
numbers.Day = () => { return new Date().getDate(); };
numbers.DayTens = () => { return Math.floor(new Date().getDate()/10); };
numbers.DayOnes = () => { return Math.floor(new Date().getDate()%10); };
numbers.Month = () => { return new Date().getMonth() + 1; };
numbers.MonthTens = () => { return Math.floor((new Date().getMonth() + 1)/10); };
numbers.MonthOnes = () => { return Math.floor((new Date().getMonth() + 1)%10); };
numbers.Pulse = () => { return pulse; };
numbers.Steps = () => { return Bangle.getHealthStatus ? Bangle.getHealthStatus("day").steps : undefined; };
numbers.Temperature = () => { return temp; };
numbers.Pressure = () => { return press; };
numbers.Altitude = () => { return alt; };
numbers.BatteryPercentage = () => { return E.getBattery(); };
numbers.BatteryVoltage = () => { return NRF.getBattery(); };
numbers.WeatherCode = () => getWeatherCode();
numbers.WeatherTemperature = () => getWeatherTemperature();
var offsetHours = updateOffset(element.Hours, offsetTime); var multistates = {};
if (element.Hours.Tens) { multistates.Lock = () => { return Bangle.isLocked() ? "on" : "off"; };
drawDigit(element.Hours.Tens, offsetHours, Math.floor(hours/10)); multistates.Charge = () => { return Bangle.isCharging() ? "on" : "off"; };
} multistates.Notifications = () => { return ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "off" : "vibrate"; };
multistates.Alarm = () => { return (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off"; };
multistates.Bluetooth = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; };
//TODO: Implement peripheral connection status
multistates.BluetoothPeripheral = () => { return NRF.getSecurityStatus().connected ? "on" : "off"; };
multistates.HRM = () => { return Bangle.isHRMOn ? "on" : "off"; };
multistates.Barometer = () => { return Bangle.isBarometerOn() ? "on" : "off"; };
multistates.Compass = () => { return Bangle.isCompassOn() ? "on" : "off"; };
multistates.GPS = () => { return Bangle.isGPSOn() ? "on" : "off"; };
if (element.Hours.Ones) { function drawMultiState(element, offset){
drawDigit(element.Hours.Ones, offsetHours, hours % 10); //print("drawMultiState", element, offset);
} drawImage(element, offset, multistates[element.Value]());
var offsetMinutes = updateOffset(element.Minutes, offsetTime);
if (element.Minutes.Tens) {
drawDigit(element.Minutes.Tens, offsetMinutes, Math.floor(minutes/10));
}
if (element.Minutes.Ones) {
drawDigit(element.Minutes.Ones, offsetMinutes, minutes % 10);
}
} }
function drawSteps(element, offset){ var drawing = false;
//print("drawSteps", element, offset);
if (Bangle.getHealthStatus) {
drawNumber(element.Number, offset, Bangle.getHealthStatus("day").steps);
} else {
drawNumber(element.Number, offset);
}
}
function drawBattery(element, offset){
if (element.Scale){
drawScale(element.Scale, offset, E.getBattery()/100);
}
}
function drawStatus(element, offset){
var statusOffset = updateOffset(element, offset);
if (element.Lock) drawImage(element.Lock, statusOffset, Bangle.isLocked() ? "on" : "off");
if (element.Charge) drawImage(element.Charge, statusOffset, Bangle.isCharging() ? "on" : "off");
if (element.Bluetooth) drawImage(element.Bluetooth, statusOffset, NRF.getSecurityStatus().connected ? "on" : "off");
if (element.Alarm) drawImage(element.Alarm, statusOffset, (require('Storage').readJSON('alarm.json',1)||[]).some(alarm=>alarm.on) ? "on" : "off");
if (element.Notifications) drawImage(element.Notifications, statusOffset, ((require("Storage").readJSON("setting.json", 1) || {}).quiet|0) ? "soundoff" : "vibrate");
}
function draw(element, offset){ function draw(element, offset){
if (!element){ var initial = !element;
if (initial){
if (drawing) return;
drawing = true;
element = face; element = face;
g.clear(); g.clear();
} }
@ -252,57 +274,46 @@ function draw(element, offset){
setColors(elementOffset); setColors(elementOffset);
//print("Using offset", elementOffset); //print("Using offset", elementOffset);
//print("Starting drawing loop", element);
for (var current in element){ for (var current in element){
//print("Handling ", current, " with offset ", elementOffset); //print("Handling ", current, " with offset ", elementOffset);
var currentElement = element[current]; var currentElement = element[current];
switch(current){ try {
case "X": switch(current){
case "Y": case "X":
case "Properties": case "Y":
case "ForegroundColor": case "Properties":
case "BackgroundColor": case "ForegroundColor":
//Nothing to draw for these case "BackgroundColor":
break; //Nothing to draw for these
case "Background": break;
drawImage(currentElement, elementOffset); case "MultiState":
break; drawMultiState(currentElement, elementOffset);
case "Time": break;
drawTime(currentElement, elementOffset); case "Image":
break; drawImage(currentElement, elementOffset);
case "Battery": break;
drawBattery(currentElement, elementOffset); case "CodedImage":
break; drawCodedImage(currentElement, elementOffset);
case "Steps": break;
drawSteps(currentElement, elementOffset); case "Number":
break; drawNumber(currentElement, elementOffset);
case "Pulse": break;
if (pulse) drawNumber(currentElement.Number, elementOffset, pulse); case "Scale":
break; drawScale(currentElement, elementOffset);
case "Pressure": break;
if (press) drawNumber(currentElement.Number, elementOffset, press.toFixed(0)); default:
break; //print("Enter next level", elementOffset);
case "Altitude": draw(currentElement, elementOffset);
if (alt) drawNumber(currentElement.Number, elementOffset, alt.toFixed(0)); //print("Done next level");
break; }
case "Temperature": } catch (e){
if (temp) drawNumber(currentElement.Number, elementOffset, temp.toFixed(0)); print("Error during drawing of", current, "in", element, e);
break;
case "MonthAndDay":
drawMonthAndDay(currentElement, elementOffset);
break;
case "Weather":
drawWeather(currentElement, elementOffset);
break;
case "Status":
drawStatus(currentElement, elementOffset);
break;
default:
//print("Enter next level", currentElement, elementOffset);
draw(currentElement, elementOffset);
} }
} }
//print("Finished drawing loop"); //print("Finished drawing loop");
if (initial){
drawing = false;
}
} }
var pulse,alt,temp,press; var pulse,alt,temp,press;
@ -311,9 +322,12 @@ var pulse,alt,temp,press;
var zeroOffset={X:0,Y:0}; var zeroOffset={X:0,Y:0};
function initialDraw(){ draw(undefined, zeroOffset); }
function handleHrm(e){ function handleHrm(e){
if (e.confidence > 70){ if (e.confidence > 70){
pulse = e.bpm; pulse = e.bpm;
initialDraw();
} }
} }
@ -321,6 +335,7 @@ function handlePressure(e){
alt = e.altitude; alt = e.altitude;
temp = e.temperature; temp = e.temperature;
press = e.pressure; press = e.pressure;
initialDraw();
} }
@ -334,28 +349,30 @@ function handleLock(isLocked){
Bangle.setHRMPower(1, "imageclock"); Bangle.setHRMPower(1, "imageclock");
Bangle.setBarometerPower(1, 'imageclock'); Bangle.setBarometerPower(1, 'imageclock');
unlockedDrawInterval = setInterval(()=>{ unlockedDrawInterval = setInterval(()=>{
draw(face, zeroOffset); initialDraw();
},unlockedRedraw?unlockedRedraw:1000); },unlockedRedraw?unlockedRedraw:1000);
draw(face, zeroOffset); draw(face, zeroOffset);
} else { } else {
Bangle.setHRMPower(0, "imageclock"); Bangle.setHRMPower(0, "imageclock");
Bangle.setBarometerPower(0, 'imageclock'); Bangle.setBarometerPower(0, 'imageclock');
clearInterval(unlockedDrawInterval); if (unlockedDrawInterval) clearInterval(unlockedDrawInterval);
} }
} }
Bangle.setUI("clock"); Bangle.setUI("clock");
Bangle.on('GPS', initialDraw);
Bangle.on('charging', initialDraw);
Bangle.on('mag', initialDraw);
Bangle.on('health', initialDraw);
Bangle.on('pressure', handlePressure); Bangle.on('pressure', handlePressure);
Bangle.on('HRM', handleHrm); Bangle.on('HRM', handleHrm);
Bangle.on('lock', handleLock); Bangle.on('lock', handleLock);
draw(face, zeroOffset);
setInterval(()=>{ setInterval(()=>{
draw(face, zeroOffset); initialDraw();
}, lockedRedraw ? lockedRedraw : 6000); }, lockedRedraw ? lockedRedraw : 60000);
initialDraw();