How to enable an "always-on" mode for Venu/AMOLED

I am trying to do a basic Venu watch face where I'm just printing a large font digital time while in "high power" mode and then printing a very small font time when the device enters sleep. The goal is to get an "always-on" functionality working.

The CIQ3.1 release notes say this:

"Always On watch faces behave differently from MIP to AMOLED. With MIP screens, you can use View.onPartialUpdate to update a portion of the screen every second. With AMOLED screen, this is no longer allowed. Instead, when WatchUi.onEnterSleep is called, you are allowed to render a watch face that must obey the rules of the AMOLED burn-in
protector:

  • No more than 10% screen pixels can be on
  • No pixel can be on longer than 3 mins

Ways you can prevent burn in are by drawing the time with a thin font, shifting the time every minute as not to repeatedly leave the same pixels on, and not having static tick marks that leave the same pixels on.

Note that watch faces can detect whether a product has screen protection enforced by checking the value of DeviceSettings.requiresBurnInProtection."

I have used the OnEnterSleep method to set a var called lowPower (and the OnExitSleep to clear it) and then I do a forced update via WatchUi.requestUpdate(). The update method then paints the screen black and draws the text. The text is very small and clearly less than 10% of the pixels. I also implemented a basic rotational algorithm where the text is shifted around the screen automatically on every update, as per the requirements above. The text never overlaps with itself so I'm pretty sure I'm meeting all of the burn-in requirements.

In the simulator I see everything working correctly, and I can toggle Low Power Mode and everything appears as I want it to. I also don't get the heatmap warning indicator, so again I'm pretty sure I'm meeting all of the requirements. But when I put the .prg on my watch and try to run it, the watch always turns the display off. I see the high power mode, and then I see the small text when it goes to sleep, but then the display immediately turns off. I'm not sure what I am doing wrong.

  •  finally got it working thanks to you! I really appreciate the idea of the checkerboard, matte! Can't thank you enough, had been scratching my head for a week about this Sweat smile

  • I'm sure a moving 'font based' checkerboard would look more elegant, however the following vector analog hands do work on the Venu in Low Power.  (This layout occasionally triggers a burn in warning once per 24hr in the sim (which I believe is based on the hour hand starting point), but this is unlikely to be an issue in the real world.)

    The code has separate hands for full and low power.  And the dimensions for the arms are pre-determined for the the original Venu for simplicity.  I'm not saying this code is efficient or as tight as it could be, but just demonstrates the point. - J

    using Toybox.Graphics as Gfx;
    using Toybox.System as Sys;
    using Toybox.Math as Math;
    using Toybox.Time as Time;
    using Toybox.WatchUi;
    
    
    class VenuSimpleView extends WatchUi.WatchFace {
    	var background_color = 0x000000;
    	var width_screen, height_screen;
    	var pi30 = Math.PI / 30.0;
        var pi50 = Math.PI / 50.0;
        var pi6  = Math.PI / 6.0;
        var pi7  = Math.PI / 3.5;
        var cx;
        var cy;
        var outerHourHandL;
        var outerHourHandR;
        var lowouterHourHandL;
        var lowouterHourHandR;
        var outerMinuteHandL;
        var outerMinuteHandR;
        var lowouterMinuteHandL;
        var lowouterMinuteHandR;
        var inLowPower=false;
    	var canBurnIn=false;
    	var leftright=true;
        
    
        //! Constructor
        function initialize() {
    		outerHourHandL =   [ [0,-10], [-1.5,-11], [-2.5,-12.5],[-4,-14],[-4,-130], [0,-140], [0,-10]];
    		outerHourHandR =   [ [0,-10], [1.5,-11], [2.5,-12.5],[4,-14], [4,-130], [0,-140], [0,-10]];	
    		lowouterHourHandL =   [[-5,-55], [-4,-145], [0,-155],[-4,-145],[-5,-55]];
    		lowouterHourHandR =   [ [5,-55], [4,-145], [0,-155],[4,-145],[5,-55]];
    							
            outerMinuteHandL = [ [0,-10], [-2,-11], [-4.5,-13],[-5,-15], [-5,-167], [0,-180], [0,-10]];	
            outerMinuteHandR = [ [0,-10], [2,-11], [4.5,-13],[5,-15], [5,-167], [0,-180], [0,-10]];	
    		lowouterMinuteHandL = [ [-3,-30], [-6,-180], [0,-190],[0,-160],[-6,-160],[-3,-30], [0,-30]];	
    		lowouterMinuteHandR = [ [3,-30], [6,-180], [0,-190],[0,-160],[6,-160], [3,-30], [0,-30]];
    
         
            WatchFace.initialize();
            var sSettings=Sys.getDeviceSettings();
            //check Burn in Protect req
            if(sSettings has :requiresBurnInProtection) {
            	//yes - no      
            	canBurnIn=sSettings.requiresBurnInProtection;        	
            }        
        }
        
        //! Load your resources here
        function onLayout(dc) {
        	//screen dimensions
    		width_screen = dc.getWidth();
    		height_screen = dc.getHeight();
    		cx = width_screen / 2;
            cy = height_screen / 2;
        }
    
        //! Restore the state of the app and prepare the view to be shown
        function onShow() {
        }
    
        //! Update the view
        function onUpdate(dc) {
            var hour= Sys.getClockTime().hour;
            var min = Sys.getClockTime().min;
            var y=height_screen/2;
            		
            // Clear the screen
            dc.setColor(background_color, 0x000000);
            dc.fillRectangle(0,0, width_screen, height_screen);		
    		
            // Set time
    		dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_TRANSPARENT);
      		hour = hour + (min / 60.0);
            hour = hour * pi6;
            min = min * pi30;
    
    		if(inLowPower) {
    			if(canBurnIn) {
    				//move things on the venu
    				y=(leftright) ? 1 : 2;
    				leftright=!leftright;
    	}			
    		//actually display time
    		if(y == 1) {
    			drawHand(dc, hour, cx, cy+2, lowouterHourHandR, Gfx.COLOR_LT_GRAY);
    			drawHand(dc, hour, cx, cy+2, lowouterHourHandL, Gfx.COLOR_LT_GRAY);
    			drawHand(dc, min, cx, cy, lowouterMinuteHandR, Gfx.COLOR_WHITE);
    			drawHand(dc, min, cx, cy, lowouterMinuteHandL, Gfx.COLOR_WHITE);		
    			}
    			
    		else if(y == 2) {
    			drawHand(dc, hour, cx, cy-2, lowouterHourHandL, Gfx.COLOR_LT_GRAY);
    			drawHand(dc, hour, cx, cy-2, lowouterHourHandR, Gfx.COLOR_LT_GRAY);
            	drawHand(dc, min, cx, cy, lowouterMinuteHandL, Gfx.COLOR_WHITE);
            	drawHand(dc, min, cx, cy, lowouterMinuteHandR, Gfx.COLOR_WHITE);
            }
    			
    		} else {
    		// Draw analog time		
            drawHand(dc, hour, cx, cy, outerHourHandL, Gfx.COLOR_LT_GRAY);
            drawHand(dc, hour, cx, cy, outerHourHandR, Gfx.COLOR_WHITE);
                    
            // Draw the minute hand
            drawHand(dc, min, cx, cy, outerMinuteHandL, Gfx.COLOR_LT_GRAY);
            drawHand(dc, min, cx, cy, outerMinuteHandR, Gfx.COLOR_WHITE);
    	}			
    		}
    
        function drawHand(dc, angle, centerX, centerY, coords, color) {
            var result = new [coords.size()];
            var cos = Math.cos(angle);
            var sin = Math.sin(angle);
    
            // Transform the coordinates
            for (var i = 0; i < coords.size(); i += 1) {
                var x = (coords[i][0] * cos) - (coords[i][1] * sin) + centerX;
                var y = (coords[i][0] * sin) + (coords[i][1] * cos) + centerY;
                result[i] = [x, y];
            }
            dc.setColor(color, Gfx.COLOR_TRANSPARENT);
            dc.fillPolygon(result);
        }
    	
        function drawBackground(dc) {
            var width = dc.getWidth();
            var height = dc.getHeight();
        }
    
        function onHide() {
        }
        
        function onExitSleep() {
        	inLowPower=false;
        	WatchUi.requestUpdate();  	
        }
        
        function onEnterSleep() {
            inLowPower=true;
        	WatchUi.requestUpdate();        
        }
    }

  • Felipe - Great that you got this working.  It would be great if you shared the View.mc code related to low power mode. - J

  • OK Got it working too... the next question would be whats the minimum screen (checkerboard) needed to pass the burn-in test?  My example below is a 1x1 black & white checkerboard.  Its noticeably darker than the Garmin example posted above.  A 2x2 would seem too chunky... so something like 1 (black) to 3 (white) would look better.  I'm trying to think if there is some way to mathematically calculate what would meet Garmin's Low Power criteria?  Will have to experiment...

  • The key is no pixel can be on for more than 3 minutes, so you need to decide what will work for you,

  • The image above is done with 50% screen i.e. 1 back square for every 1 white square.  I have experimented with increasing the white area to 2/3, 3/4 etc.  All trigger the Burn In warning.  So not sure how Garmin does it on its Venu faces.  Open to suggestions! - J

  • Have you tried using the heatmap in the sim?  For native watch faces, Garmin doesn't use CIQ, and might have their own tricks.

  • , sorry for the delay in posting the code back, but I was double-checking it on another watch face just to make sure what needed to be posted here. So here it goes:

    class AnalogView extends WatchUi.WatchFace
    {
        var inLowPower=false;
        var canBurnIn=false;
        var upTop=true;  
    
        function initialize() {
            WatchFace.initialize();
            var sSettings=System.getDeviceSettings();
            //first check if the setting is available on the current device
            if(sSettings has :requiresBurnInProtection) {
                //get the state of the setting      
                canBurnIn=sSettings.requiresBurnInProtection;
            }
        }
    
        // Handle the update event
        function onUpdate(dc) {
            if(inLowPower and canBurnIn) {
                upTop=!upTop;
                dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_BLACK); // removing the background color and all the data points from the background, leaving just the hour hands
                dc.fillRectangle(0, 0, dc.getWidth(), dc.getHeight()); //width & height?
            } else {
            // Fill the entire background with whatever color you want.
            dc.setColor(Graphics.COLOR_WHITE, Graphics.COLOR_BLACK);
            dc.fillRectangle(0, 0, dc.getWidth(), dc.getHeight());
        
            // Draw everything that will be displayed only in high power mode
        		…
        	}
        
        	//Outside of the if clause you should draw everything that will always be displayed (high and low power)
        		…
        		
        	var borderColor, HandColor;
            if (inLowPower==true and canBurnIn==true) {
        		HandColor=Graphics.COLOR_WHITE;
                borderColor=Graphics.COLOR_DK_GRAY;
            } 
            borderColor= XXX; // Normal Color
            HandColor = XXX; // Normal Color
        
            drawHands(borderColor, HandColor);
        
            if (inLowPower==true and canBurnIn==true) {		
                var checkerboard = WatchUI.loadResource(Rez.Fonts.Checkerboard);
              	dc.setColor(Graphics.COLOR_BLACK, Graphics.COLOR_TRANSPARENT);
              	for (var row=(upTop) ? 1 : 0; row < height+49; row += 49) {
                	for (var col=0 ; col <= width; col += 49) {
                  	    dc.drawText( row, col , checkerboard, "X", Graphics.TEXT_JUSTIFY_CENTER);
                    }
                }
            }	
        }
        
        // This method is called when the device re-enters sleep mode.
        function onEnterSleep() {
            inLowPower=true;
            WatchUi.requestUpdate(); 
        }
        
        // This method is called when the device exits sleep mode.
        function onExitSleep() {
            inLowPower=false;
            WatchUi.requestUpdate(); 
        }
    }

    Please feel free to suggest any improvement to my code. I'm making sure that the hands are always white when the low power mode is on, for better visibility. My hands are quite thicker than yours as well, so I'm not sure if visibility will be a problem or not (don't have an amoled watch on hand to test). It definitely doesn't look to be an issue on the simulator:

  •   Yes... I'm just having to iterate through various checkerboard versions since there is no way to predict (or calculate yet) how the checkerboard patterns will perform.  I think something would have to be written in Java or Python to really dial it in.  A 50% shaded checkerboard, shifted 1 pixel each minute will obviously re-write each pixel each minute when the requirement is only every 3 mins.  So one might guess that a 33% shaded pattern might work, but according to the heat map - due to the width of the hour hand - some pixels are still triggering the burn in warning.  It might also be a case where the pattern just needs to shift more... but I have to do more work to visualize that.

    Agree that the Garmin faces are using non-CIQ Garmin only tricks!