/* This file is part of Wattzap Community Edition. * * Wattzap Community Edtion is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Wattzap Community Edition is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Wattzap. If not, see <http://www.gnu.org/licenses/>. */ package com.wattzap.model.ant; import org.cowboycoders.ant.messages.data.BroadcastDataMessage; import com.wattzap.controller.MessageBus; import com.wattzap.controller.MessageCallback; import com.wattzap.controller.Messages; import com.wattzap.model.RouteReader; import com.wattzap.model.UserPreferences; import com.wattzap.model.dto.Point; import com.wattzap.model.dto.Telemetry; import com.wattzap.model.power.Power; import com.wattzap.utils.Rolling; /** * Power Meter. Converts power to speed based on rider's mass. * * @author David George * @date 14th November 2014 * * (c) 2014 David George / Wattzap.com */ public class PowerListener extends AntListener implements MessageCallback { public static String name = "C:POW"; private static final byte DEVICE_TYPE = (byte) 0x0B; private static final short MESSAGE_PERIOD = 8182; // 4hz 8182/32768 private boolean simulSpeed; private long lastTime; private double distance = 0.0; private boolean cadenceSensor; private int count = -1; RouteReader routeData; private double mass; private Rolling averagePower; private final UserPreferences userPrefs = UserPreferences.INSTANCE; // initialize for pairing private double wheelSize = userPrefs.getWheelSizeCM(); private Power power = userPrefs.getPowerProfile(); public PowerListener() { MessageBus.INSTANCE.register(Messages.START, this); MessageBus.INSTANCE.register(Messages.STARTPOS, this); MessageBus.INSTANCE.register(Messages.GPXLOAD, this); int id = userPrefs.getCadenceId(); if (id > 0) { cadenceSensor = true; // cadence done by cadence sensor } } @Override public void receiveMessage(BroadcastDataMessage message) { if (message.getUnsignedData()[0] == 0x10) { // simple power message int powerWatts = 0; if (message.getUnsignedData()[1] != count) { powerWatts = (message.getUnsignedData()[7] << 8) | message.getUnsignedData()[6]; powerWatts = (int) averagePower.add(powerWatts); count = message.getUnsignedData()[1] & 0xFF; } int rpm = -1; if (message.getUnsignedData()[3] != 255) { rpm = (byte) (message.getUnsignedData()[3] & 0xFF); } if (lastTime == -1) { lastTime = System.currentTimeMillis(); return; } double speed = 0; double distanceKM = 0; double timeS = 0; long currentTime = System.currentTimeMillis(); long tDiff = currentTime - lastTime; lastTime = currentTime; Telemetry t = new Telemetry(); t.setPower(powerWatts); // if we have GPX Data and Simulspeed is enabled calculate speed // based on power and gradient using magic sauce if (simulSpeed && routeData != null) { Point p = routeData.getPoint(distance); if (routeData.routeType() == RouteReader.SLOPE) { if (p == null) { // end of the road distance = 0.0; return; } if (powerWatts > 0) { // only works when power is positive, this is most of // the time on a turbo speed = (power.getRealSpeed(mass, p.getGradient() / 100, powerWatts)) * 3.6; // d = s * t distanceKM = (speed * tDiff) / 360000; // System.out.println("speed " + speed + " distanceKM " // + distanceKM + " watts " + powerWatts + " mass " + // mass + " tDiff " + tDiff); } } else { // power profile, speed is the ratio of our trainer power to // the expected power double ratio = (powerWatts / p.getPower()); // speed is video speed * power ratio speed = p.getSpeed() * ratio; distanceKM = (speed / 3600) * timeS; } } else { speed = power.getRealSpeed(mass, 0, powerWatts); distanceKM = (speed * tDiff) / 360000; } t.setDistanceMeters(distance * 1000); if (routeData != null) { Point p = routeData.getPoint(distance); if (p == null) { // end of the road distance = 0.0; return; } t.setElevation(p.getElevation()); t.setGradient(p.getGradient()); t.setLatitude(p.getLatitude()); t.setLongitude(p.getLongitude()); } t.setSpeed(speed); t.setTime(currentTime); distance += distanceKM; MessageBus.INSTANCE.send(Messages.SPEED, t); if (cadenceSensor == false && rpm != -1) { MessageBus.INSTANCE.send(Messages.CADENCE, rpm); } } } @Override public void callback(Messages message, Object o) { switch (message) { case START: // get up to date values mass = userPrefs.getTotalWeight(); wheelSize = userPrefs.getWheelSizeCM(); power = userPrefs.getPowerProfile(); simulSpeed = userPrefs.isVirtualPower(); lastTime = -1; averagePower = new Rolling(userPrefs.getPowerSmoothing()); break; case STARTPOS: distance = (Double) o; break; case GPXLOAD: this.routeData = (RouteReader) o; distance = 0.0; break; } } @Override public int getChannelId() { return 0; } @Override public int getChannelPeriod() { return MESSAGE_PERIOD; } @Override public int getDeviceType() { return DEVICE_TYPE; } @Override public String getName() { return name; } }