Adding Ant+ BatteryStatus into Datafield

Hi All,
I've just started working on making my own watchfaces and now working on a DataField. I've almost zero programming knowledge so it has been a hard but fun slog.

So far I've got the DF working nicely including ActivityInfo (DerailleurIndex, Cadence, HR, Speed, Ascent, etc..),  SensorHistory (Temp), SystemStats (battery, Notifications, clock) but I can't figure out how to get the BatteryStatus from my ANT+ devices (eTAP and Bontrager lights).

I've spent a fair bit of time reading and testing examples from the Knowledge base, specifically looking at LightNetworkListener without much luck. What I'd like to do is get the BatteryStatus (I understand that if you have a "Pair" i..e eTAP or 2xlights, then the lowest battery will be reported rather than both) and then use the status to plot a battery bar.

My full code is below, (happy for other feedback as well ,it's a good learning experience) if some generous person could help me out.

using Toybox.WatchUi;
using Toybox.Activity as Act;
using Toybox.Graphics as Gfx;
using Toybox.System as Sys;
using Toybox.UserProfile;
using Toybox.Lang;
using Toybox.SensorHistory;
using Toybox.AntPlus;

//Gear Graphics Constants
const x = 90;
const y = 20;
const barheight = 20;
const barwidth = 7;
const gap = 8;

//Pedal Cadence 
const CADLow = 55;
const CADMid = 76;
const CADHigh = 105;

//HR Zones if not using UserProfile
//const HRZone1 = 143;
//const HRZone2 = 157; 
//const HRZone3 = 164;
//const HRZone4 = 174; 
//const HRZone5 = 205;



class RideView extends WatchUi.DataField 
{
	var activityInfo;
    var HR, CAD, SPEED, ASC, DISTANCE, TIMER,timerhr, timermin, timersec, GEARFRONT, GEARREAR, BATTERY; 
	var h, w, h2, w2;
	var Icons22;
	var HRZone, HRZone1, HRZone2, HRZone3, HRZone4, HRZone5;
	var LightBatteryStatus; 
	var eTapBatteryStatus;
	 
//Initialise Class Veriables	
    function initialize() {
        DataField.initialize();
        Icons22 = Toybox.WatchUi.loadResource(Rez.Fonts.typicons22);
 		LightBatteryStatus = 1;
 		eTapBatteryStatus = 1;
 		HRZone = UserProfile.getHeartRateZones(UserProfile.getCurrentSport());

    }


    // Set your layout here. Anytime the size of obscurity of
    // the draw context is changed this will be called.
    function onLayout(dc) {
     // Get Screen Dimensions
		w = dc.getWidth();
        h = dc.getHeight();
        h2 = h/2;
        w2 = w/2;
        
    // Set the background color
  		dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_BLACK);
        dc.fillRectangle(0,0,240,240);  	
	}

// Get the Metrics from the Activity. See Activity.Info in the documentation for available information.
    function compute(info) {
 
	
	}


    // Display the value you computed here. This will be called
    // once a second when the data field is visible.
    function onUpdate(dc) {
   		activityInfo = Act.getActivityInfo(); 	    

		dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
		

		if (activityInfo != null && activityInfo.frontDerailleurIndex != null) {
 			GEARFRONT = activityInfo.frontDerailleurIndex;}
 		else
 			{GEARFRONT = 0;}
 		
 		if (activityInfo != null && activityInfo.rearDerailleurIndex != null) {
 			GEARREAR = activityInfo.rearDerailleurIndex;}
 		else
 			{GEARREAR = 0;}

//		GEARS in X/Y format
//        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
//        dc.drawText(120, h-65, Gfx.FONT_TINY, GEARFRONT+ " /" +GEARREAR, Gfx.TEXT_JUSTIFY_LEFT);

//		Front Gear Image
		dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
		dc.drawRectangle(x, y, barwidth, barheight);
		dc.drawRectangle(x+(1*gap), y-15, barwidth, barheight+15);
		if (GEARFRONT == 1)
			{dc.fillRectangle(x,y,barwidth,barheight);}
			if (GEARFRONT == 2)
			{dc.fillRectangle(x+(1*gap), y-15, barwidth, barheight+15);}
					

//		Rear Gear Image
		dc.drawRectangle(x+13+(1*gap), y-15, barwidth, barheight+15);
		dc.drawRectangle(x+13+(2*gap), y-13, barwidth, barheight+13);
 		dc.drawRectangle(x+13+(3*gap), y-11, barwidth, barheight+11);
 		dc.drawRectangle(x+13+(4*gap), y-9, barwidth, barheight+9);
 		dc.drawRectangle(x+13+(5*gap), y-7, barwidth, barheight+7);
 		dc.drawRectangle(x+13+(6*gap), y-5, barwidth, barheight+5);
 		dc.drawRectangle(x+13+(7*gap), y-3, barwidth, barheight+3);
 		dc.drawRectangle(x+13+(8*gap), y-1, barwidth, barheight+1);
 		dc.drawRectangle(x+13+(9*gap), y+1, barwidth, barheight-1);
 		dc.drawRectangle(x+13+(10*gap), y+3, barwidth, barheight-3);
 		dc.drawRectangle(x+13+(11*gap), y+5, barwidth, barheight-5);
 		if (GEARREAR == 1)
			{dc.fillRectangle(x+13+(1*gap), y-15, barwidth, barheight+15);}
		else if  (GEARREAR == 2)
			{dc.fillRectangle(x+13+(2*gap), y-13, barwidth, barheight+13);}	
		else if  (GEARREAR == 3)
			{dc.fillRectangle(x+13+(3*gap), y-11, barwidth, barheight+11);}	
 		else if  (GEARREAR == 4)
			{dc.fillRectangle(x+13+(4*gap), y-9, barwidth, barheight+9);}
 		else if  (GEARREAR == 5)
			{dc.fillRectangle(x+13+(5*gap), y-7, barwidth, barheight+7);}
 		else if  (GEARREAR == 6)
			{dc.fillRectangle(x+13+(6*gap), y-5, barwidth, barheight+5);}
 		else if  (GEARREAR == 7)
			{dc.fillRectangle(x+13+(7*gap), y-3, barwidth, barheight+3);}
		else if  (GEARREAR == 8)
			{dc.fillRectangle(x+13+(8*gap), y-1, barwidth, barheight+1);}	
		else if  (GEARREAR == 9)
			{dc.fillRectangle(x+13+(9*gap), y+1, barwidth, barheight-1);}	
		else if  (GEARREAR == 10)
			{dc.fillRectangle(x+13+(10*gap), y+3, barwidth, barheight-3);}	
		else if (GEARREAR == 11)
			{dc.fillRectangle(x+13+(11*gap), y+5, barwidth, barheight-5);}	 
  
// Gear Battery Bar  
		dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
  		dc.drawRectangle(0, 43, 240, 8);
  		
//  	eTapBatteryStatus = Toybox.AntPlus.getBatteryStatus({});
		dc.setColor(Gfx.COLOR_GREEN, Gfx.COLOR_TRANSPARENT);
		if (eTapBatteryStatus == 1) {dc.fillRectangle(0,44, 240, 6);}
		else if (eTapBatteryStatus == 2) {dc.fillRectangle(0,44, 180, 6);}
		else if (eTapBatteryStatus == 3) {dc.fillRectangle(0,44, 120, 6);}
		else if (eTapBatteryStatus == 4) {dc.fillRectangle(0,44, 60, 6);}
		else if (eTapBatteryStatus == 5) {dc.fillRectangle(0,44, 1, 6);}		  
  
// Watch Battery Bar
		var myStats = Sys.getSystemStats();
		BATTERY = myStats.battery;
		dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
  		dc.drawRectangle(0, 120, 240, 9);
		dc.setColor(Gfx.COLOR_RED, Gfx.COLOR_TRANSPARENT);
		dc.fillRectangle(0,121, 240*BATTERY/100, 7);
//		dc.drawText(162, 200, Gfx.FONT_XTINY, BATTERY.format("%d") + "%", Gfx.TEXT_JUSTIFY_LEFT);
		
// Light Battery Bar 
  // BATT_STATUS_NEW = 1, BATT_STATUS_GOOD = 2, BATT_STATUS_OK = 3, BATT_STATUS_LOW = 4, BATT_STATUS_CRITICAL = 5 
 		dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
  		dc.drawRectangle(0, 190, 240, 8); 
//  	LightBatteryStatus = Toybox.AntPlus.getBatteryStatus({});
		dc.setColor(Gfx.COLOR_BLUE, Gfx.COLOR_TRANSPARENT);
		if (LightBatteryStatus == 1) {dc.fillRectangle(0,191, 240, 6);}
		else if (LightBatteryStatus == 2) {dc.fillRectangle(0,191, 180, 6);}
		else if (LightBatteryStatus == 3) {dc.fillRectangle(0,191, 120, 6);}
		else if (LightBatteryStatus == 4) {dc.fillRectangle(0,191, 60, 6);}
		else if (LightBatteryStatus == 5) {dc.fillRectangle(0,191, 1, 6);}
		

  // Heart Rate 

   		
   		if (activityInfo != null && activityInfo.currentHeartRate != null) {
 			HR = activityInfo.currentHeartRate;}
 		else
 			{HR = 0;}
    
        dc.drawRectangle(0, 52, 72, 67);

		if ( HR <= HRZone[1]) {dc.setColor(Gfx.COLOR_LT_GRAY,Gfx.COLOR_TRANSPARENT);}
       	else if ( HR > HRZone[1] && HR <= HRZone[2]) {dc.setColor(Gfx.COLOR_BLUE,Gfx.COLOR_TRANSPARENT);}
       	else if ( HR > HRZone[2] && HR <= HRZone[3]) {dc.setColor(Gfx.COLOR_GREEN,Gfx.COLOR_TRANSPARENT);}
       	else if ( HR > HRZone[3] && HR <= HRZone[4]) {dc.setColor(Gfx.COLOR_ORANGE,Gfx.COLOR_TRANSPARENT);}
       	else if ( HR > HRZone[4]) {dc.setColor(Gfx.COLOR_RED,Gfx.COLOR_TRANSPARENT);}
       	

       	dc.fillRectangle(0,52, 72, 67);
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        dc.drawText(42, 53, Gfx.FONT_XTINY, "HR", Gfx.TEXT_JUSTIFY_RIGHT);
 		dc.drawText(67, 70, Gfx.FONT_NUMBER_MEDIUM, HR, Gfx.TEXT_JUSTIFY_RIGHT);
 		
 
 
   // SPEED
  		if (activityInfo != null && activityInfo.currentSpeed != null) {
 			SPEED = activityInfo.currentSpeed;}
 		else
 			{SPEED = 0;}
    	SPEED = SPEED*3.6;
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        dc.drawText(120, 53, Gfx.FONT_XTINY, "km/h", Gfx.TEXT_JUSTIFY_CENTER);
		dc.drawText(120, 70, Gfx.FONT_NUMBER_MEDIUM, SPEED.format("%.1f"), Gfx.TEXT_JUSTIFY_CENTER); 
 	
 // Cadence 

 		if (activityInfo != null && activityInfo.currentCadence != null) {
 			CAD = activityInfo.currentCadence;}
 		else
 			{CAD = 0;}
    	dc.drawRectangle(168, 52, 240, 67);
    	
    	if ( CAD <= CADMid) {dc.setColor(Gfx.COLOR_YELLOW,Gfx.COLOR_TRANSPARENT);}
       	else if ( CAD > CADMid && CAD <= CADHigh) {dc.setColor(Gfx.COLOR_GREEN,Gfx.COLOR_TRANSPARENT);}
       	else if ( CAD > CADHigh) {dc.setColor(Gfx.COLOR_PURPLE,Gfx.COLOR_TRANSPARENT);}
       	
       	dc.fillRectangle(168, 52, 240, 67);
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        dc.drawText(190, 52, Gfx.FONT_XTINY, "CAD", Gfx.TEXT_JUSTIFY_LEFT);
 		dc.drawText(170, 70, Gfx.FONT_NUMBER_MEDIUM, CAD, Gfx.TEXT_JUSTIFY_LEFT); 	
 
	

//	ASCENT
 		dc.drawRectangle(80, 129, 2, 61);
 		if (activityInfo != null && activityInfo.totalAscent != null) {
 			ASC = activityInfo.totalAscent;}
 		else
 			{ASC = 0;}  
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        dc.drawText(78, 130, Gfx.FONT_NUMBER_MILD, ASC.format("%2d"), Gfx.TEXT_JUSTIFY_RIGHT);
        dc.drawText(78, 170, Gfx.FONT_XTINY, "Ascent", Gfx.TEXT_JUSTIFY_RIGHT);
 		
 
 // DISTANCE
 		if (activityInfo != null && activityInfo.elapsedDistance != null) {
 			DISTANCE = activityInfo.elapsedDistance / 1000;}
 		else
 			{DISTANCE = 0;}
   
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        if (DISTANCE < 100){
        dc.drawText(120, 129, Gfx.FONT_NUMBER_MEDIUM, DISTANCE.format("%.1f"), Gfx.TEXT_JUSTIFY_CENTER);}
        else {
        dc.drawText(120, 129, Gfx.FONT_NUMBER_MEDIUM, DISTANCE.format("%2d"), Gfx.TEXT_JUSTIFY_CENTER);}
        
        dc.drawText(120, 170, Gfx.FONT_XTINY, "km", Gfx.TEXT_JUSTIFY_CENTER);
 		

//	TIMER
		dc.drawRectangle(153, 129, 2, 61);
 		if (activityInfo != null && activityInfo.timerTime  != null) {
 			TIMER = activityInfo.timerTime/1000 ;
 			timerhr = TIMER/3600;
 			timermin = (TIMER-(timerhr*3600))/60;
 			timersec = TIMER%60;}
 		else
 		{TIMER = 0;}  
 		
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
        dc.drawText(160, 130, Gfx.FONT_NUMBER_MILD, timerhr.format("%01d")+":"+timermin.format("%02d"), Gfx.TEXT_JUSTIFY_LEFT);
        dc.drawText(160, 170, Gfx.FONT_XTINY, "Timer", Gfx.TEXT_JUSTIFY_LEFT);
 		 
 

			
 // Notifications
		var mySettings = Sys.getDeviceSettings();
		var flags = "";
		if (mySettings.notificationCount > 0) {flags = flags + "l";}
		dc.setColor(Gfx.COLOR_RED, Gfx.COLOR_TRANSPARENT);
		dc.drawText(76, 200, Icons22, flags,  Gfx.TEXT_JUSTIFY_RIGHT);			
 
 // Temperature
		var TempC = Toybox.SensorHistory.getTemperatureHistory({});

		dc.setColor(Gfx.COLOR_RED, Gfx.COLOR_TRANSPARENT);
 		dc.drawText(165, 200, Gfx.FONT_XTINY, TempC.next().data.format("%2d")+""+"c", Gfx.TEXT_JUSTIFY_LEFT);

				
 						
 // Get and show the current time
        var clock = Sys.getClockTime();  
        var timeDisplay = getClockTime  (clock);
        
        dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);
 		dc.drawText(120, 195, Gfx.FONT_NUMBER_MILD, timeDisplay, Gfx.TEXT_JUSTIFY_CENTER);	

    }
//	Convert to 12 hour time
	hidden function getClockTime(clockTime) {
		var hour, min,ampm, result;

		hour = clockTime.hour;
		min = clockTime.min.format("%02d");
		
		ampm = (hour > 11) ? "PM" : "AM";
		hour = hour % 12;
		hour = (hour == 0) ? 12 : hour;
		hour = hour.format("%2d");
		
		result = Lang.format("$1$:$2$", [hour, min]);
		return result;
	}
	

}

  • Did you managed to get the Battery Life from ANT+?

    I noticed in your code you commented out quite a few of those lines. 

    I'm trying to get Battery Life from an ANT+ power Meter.. can't seem to see it.. The IOS App (connect via BLE)  tho can get the amount of battery life left tho..

  • kind off. The ANT+ device gives off a battery status update which I then listen to and update a bar graph. That being said it  only updates it's status when there is a change. So if you are wanting to know your powermeter battery it will only tell you if the state changes.  If you start a ride and the status hasnt changed then the datafield wont show you the battery. If the powermeter battery state drops from 1(new) -> 2(good) then it will tell the datafield.

    I got distracted by work so havent circled back to see how to fix it.

    Some of my code is below, it is a bit of a mess but hopefully it helps.

    var mBikePowerListener, mBikePower;
    var mPowerBatteryStatus = 0; 
    var mPowerState=0;
    
    <Snip>        
    //Initialise Class Veriables	
        function initialize() {
            mBikePowerListener = new MyBikePowerListener();
            mBikePower = new AntPlus.BikePower(mBikePowerListener);
            
    <snip<        
            //Power Battery
     		//Power State, Dead=0, Closed=1, Searching=2, Tracking=3, CNT(?)=4
     		// BATT_STATUS_NEW = 1, BATT_STATUS_GOOD = 2, BATT_STATUS_OK = 3, BATT_STATUS_LOW = 4, BATT_STATUS_CRITICAL = 5 	
     		dc.setColor(Gfx.COLOR_LT_GRAY, Gfx.COLOR_TRANSPARENT);
            if (null != mBikePowerListener.mPowerBatteryStatus && ShowInfo==true) {
                dc.drawText(66, 194,Gfx.FONT_XTINY,"P"+ mBikePowerListener.mPowerState+mBikePowerListener.mPowerBatteryStatus,Gfx.TEXT_JUSTIFY_LEFT);}
            dc.setPenWidth(1);
    		dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
      		dc.drawRectangle(0, 43, 240, 8);
      		if (mBikePowerListener.mPowerState == 3){dc.setColor(Gfx.COLOR_GREEN, Gfx.COLOR_TRANSPARENT);}
    		else {dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_TRANSPARENT);}
    		
    		if (mBikePowerListener.mPowerBatteryStatus == 1) {dc.fillRectangle(0,44, 240, 6);}
    		else if (mBikePowerListener.mPowerBatteryStatus == 2) {dc.fillRectangle(0,44, 180, 6);}
    		else if (mBikePowerListener.mPowerBatteryStatus == 3) {dc.fillRectangle(0,44, 120, 6);}
    		else if (mBikePowerListener.mPowerBatteryStatus == 4) {dc.fillRectangle(0,44, 60, 6);}
    		else if (mBikePowerListener.mPowerBatteryStatus == 5) {dc.fillRectangle(0,44, 30, 6);}
    
    <snip>
    
    class MyBikePowerListener extends AntPlus.BikePowerListener {
        var mPower = 0;
    	
    	function onDeviceStateUpdate(data){
    		mPowerState = data.state;
    		Sys.println("PowerState= "+mPowerState);
    	}
    	
       function onBatteryStatusUpdate(BatteryStatus){	
    		mPowerBatteryStatus = BatteryStatus.batteryStatus;
        	Sys.println("Power Battery:"+mPowerBatteryStatus);
    
    	}
    }

  • Hi - Looks like you're using ANT+ calls instead of the generic ANT.

    The ANT+ calls are not available for some of the lower end watches hence it is not really usable to me. 

    I did however managed to use generic ANT and get the 0x52 page but the ouput values - well, I need to know how to decipher them. I tried to get the Common Data Page ANT+ documentation, but despite registering, seems like I can't get it unless I pay USD1500 to join.,

  • https://developer.garmin.com/downloads/connect-iq/monkey-c/doc/Toybox/AntPlus/BatteryStatus.html

    the battery status also exposes the battery voltage, I could easily query that from the running dynamics for instance. The big problem here is that there's no easy way (unless you're prepared to do a lot of testing and reverse engineering) to convert a voltage into a battery percentage , which is what you really want to see.

  • Thanks that's ANT+ calls and only available on select watches and not the Forerunner series which is why I want to get to it.

    Anyways, you are right about the battery voltage. The thing is - I can't figure out how to decipher them even as battery voltage.'

    On a Bike Power Meter that does have some output - the below is the page from 0x52 and the payload #6 and #7 is what I'm supposed to be looking at I believe. Unfortunately, I do not know how to decipher 252 or 1 into battery voltage. 

    [82, 255, 255, 255, 255, 17, 252, 51] 

    [82, 255, 255, 255, 255, 17, 1, 52]

  • According to the Common Data Pages docs, for data page 82, byte 6 is the Fractional Battery Voltage in units of 1/256, and 7 is the Descriptive Bit Field that describes the status (bits 4-6) and Coarse Battery Voltage (bits 0-3, with 0x0F reserved to indicate the Fractional Battery Voltage is invalid).

    var payload = [
      82,    // page number 0x52
      255,   // reserved 0xFF
      255,   // battery identifier (0xFF if not used)
      255,   // cumulative operating time lsb
      255,   // cumulative operating time
      17,    // cumulative operating time msb
      252,   // fractional battery voltage, value 0-255 in units of 1/256 (V)
      51     // descriptive bit field
    ] 
    
    //const BATTERY_STATUS_STRINGS = [
    //    "Reserved",
    //    "New",
    //    "Good",
    //    "Ok",
    //    "Low",
    //    "Critical",
    //    "Reserved",
    //    "Invalid"
    //];
    
    
    // bits 0-3 of byte 7 
    var battery_voltage_valid = (payload[7] & 0x0F) != 0x0F;
    if (battery_voltage_valid) {
        // this is Volts 0 - 14
        var coarse_battery_voltage = payload[7] & 0x0F;
        
        // this is Volts 0 to 1
        var fractional_battery_voltage = 100.0f * payload[6] / 256;
        
        // this is the total Volts
        var total_battery_voltage = coarse_battery_voltage + fractional_battery_voltage;
    }
    
    var battery_status_indx = (payload[7] & 0x70) >> 4;
    var battery_status_desc = BATTERY_STATUS_STRINGS[ battery_status_indx ];

  • after 4 months, I finally (somewhat) get this code. Thanks  tho I'm still googling a lot about bit masking and bit shift and still not as clear as I need to be. 

  • I've been also working on getting battery status of my connected devices, your code was very helpful to get started.

    I found that there is a way to get battery status at any time (or at least when the info is available), based on your example:

    var status = mBikePower.getBatteryStatus(null);

    if (status != null) {

    mPowerBatteryStatus = status.batteryStatus;

    }

  • Hi Travis

    Could you post a link to the common data pages documentation, from which you have this info. I cannot find it.

    thanks

    regards

    Erich

  • Here is link to Ant+ device profiles document, at thisisant.com:

    https://www.thisisant.com/developer/resources/downloads/#documents_tab

    To be able to access them, you first need to register to the page. It'll take day or two to get approval. After that, you have to go to your profile, and become "adopter" from "upgrade your account" section. All this is free, after it's done you can access the documents, including the common data page doc.