From 33fa62e36bcd1a0cf67d06bda9604b605c1a387a Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 13 Jul 2020 17:23:42 -0300 Subject: [PATCH] Update to Python 3 --- AbstractPowerCalculator.py | 8 +++---- BtAtsPowerCalculator.py | 24 ++++++++++---------- KurtKineticPowerCalculator.py | 2 +- PowerMeterTx.py | 41 ++++++++++++++++++----------------- README.md | 7 +++--- SpeedCadenceSensorRx.py | 32 +++++++++++++-------------- bot.py | 33 +++++++++++++++++----------- config.py | 24 +++++++------------- constants.py | 2 +- vpower.cfg | 25 +++++++++++---------- vpower.py | 37 +++++++++++++++++++------------ 11 files changed, 123 insertions(+), 112 deletions(-) diff --git a/AbstractPowerCalculator.py b/AbstractPowerCalculator.py index adcf11a..00e3f13 100644 --- a/AbstractPowerCalculator.py +++ b/AbstractPowerCalculator.py @@ -34,7 +34,7 @@ def update(self, revs_per_sec): # We just keep track of energy and time for now self.energy += delta_energy self.last_time = current_time - if self._DEBUG: print "cumulative_time(): " + repr(self.cumulative_time()) + if self._DEBUG: print("cumulative_time(): " + repr(self.cumulative_time())) # We only update the observer with a power reading up to twice a second, which is roughly # as often as a crank-based power meter @@ -45,7 +45,7 @@ def cumulative_time(self): return self.last_time - self.init_time def send_power(self): - if self._DEBUG: print "send_power" + if self._DEBUG: print("send_power") timeGap = self.cumulative_time() if timeGap == 0.0: return @@ -59,6 +59,6 @@ def send_power(self): # Tell whoever is listening if self.observer: self.observer.update(avePower) - if self._DEBUG: print "Power: ", repr(avePower) + if self._DEBUG: print("Power: ", repr(avePower)) else: - print "Power: ", repr(avePower) + print("Power: ", repr(avePower)) diff --git a/BtAtsPowerCalculator.py b/BtAtsPowerCalculator.py index 8dfb959..522c2ef 100644 --- a/BtAtsPowerCalculator.py +++ b/BtAtsPowerCalculator.py @@ -17,7 +17,7 @@ def __init__(self): self.dynamic_air_density = None def check_for_bme280_sensor(self): - print "Check for temperature/pressure/humidity sensor" + print("Check for temperature/pressure/humidity sensor") try: import os, sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -25,14 +25,14 @@ def check_for_bme280_sensor(self): import bme280 bme280.readBME280All() # The first reading after boot-up can be off, so throw it away temperature, pressure, humidity = bme280.readBME280All() - print "Temp (C): " + repr(temperature) - print "Pressure: " + repr(pressure) - print "Humidity: " + repr(humidity) - print "Air density: " + repr(self.calc_air_density(temperature, pressure, humidity)) + print("Temp (C): " + repr(temperature)) + print("Pressure: " + repr(pressure)) + print("Humidity: " + repr(humidity)) + print("Air density: " + repr(self.calc_air_density(temperature, pressure, humidity))) self.dynamic_air_density = True except (ImportError, IOError) as e: self.dynamic_air_density = False - print "Not found" + print("Not found") A = 0.290390167 B = -0.0461311774 @@ -44,7 +44,7 @@ def check_for_bme280_sensor(self): # Power = A * v ^ 3 + B * v ^ 2 + C * v + d # where v is speed in revs / sec and constants A, B, C & D are as defined above. def power_from_speed(self, revs_per_sec): - if self._DEBUG: print "power_from_speed" + if self._DEBUG: print("power_from_speed") if self.dynamic_air_density is None: self.check_for_bme280_sensor() @@ -54,12 +54,12 @@ def power_from_speed(self, revs_per_sec): import bme280 temperature, pressure, humidity = bme280.readBME280All() if self._DEBUG: - print "Temp (C): " + repr(temperature) - print "Pressure: " + repr(pressure) - print "Humidity: " + repr(humidity) + print("Temp (C): " + repr(temperature)) + print("Pressure: " + repr(pressure)) + print("Humidity: " + repr(humidity)) self.update_air_density(temperature, pressure, humidity) - if self._DEBUG: print "air_density_correction: " + repr(self.air_density_correction) + if self._DEBUG: print("air_density_correction: " + repr(self.air_density_correction)) rs = revs_per_sec power = self.correction_factor * (self.A * rs * rs * rs * self.air_density_correction + self.B * rs * rs + @@ -73,7 +73,7 @@ def update_air_density_correction(self): @staticmethod def calc_air_density(t, p, h): _DEBUG = BtAtsPowerCalculator._DEBUG - if _DEBUG: print "set_air_density(temp=" + repr(t) + ", press=" + repr(p) + ", humi=" + repr(h) + ")" + if _DEBUG: print("set_air_density(temp=" + repr(t) + ", press=" + repr(p) + ", humi=" + repr(h) + ")") Rd = 287.05 # Specific gas constant for dry air J / (KgK) Rv = 461.495 # Specific gas constant for water vapour J / (KgK) water_vapour_pressure = BtAtsPowerCalculator.saturation_pressure(t) * h / 100.0 diff --git a/KurtKineticPowerCalculator.py b/KurtKineticPowerCalculator.py index e0775f6..b601562 100644 --- a/KurtKineticPowerCalculator.py +++ b/KurtKineticPowerCalculator.py @@ -16,7 +16,7 @@ def __init__(self): # Power = A * v ^ 3 + B * v ^ 2 + C * v + d # where v is speed in miles/hour and constants A, B, C & D are as defined above. def power_from_speed(self, revs_per_sec): - if self._DEBUG: print "power_from_speed" + if self._DEBUG: print("power_from_speed") miles_per_rev = self.wheel_circumference / 1609.34 mph = revs_per_sec * 3600 * miles_per_rev diff --git a/PowerMeterTx.py b/PowerMeterTx.py index 6669f85..a017d6e 100644 --- a/PowerMeterTx.py +++ b/PowerMeterTx.py @@ -1,10 +1,10 @@ import sys -from ant.core import message +from ant.core import message, node from ant.core.constants import * from ant.core.exceptions import ChannelError from constants import * -from config import VPOWER_DEBUG +from config import NETKEY, VPOWER_DEBUG CHANNEL_PERIOD = 8182 @@ -25,12 +25,13 @@ def __init__(self, antnode, sensor_id): self.channel = antnode.getFreeChannel() try: self.channel.name = 'C:POWER' - self.channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_TRANSMIT) + network = node.Network(NETKEY, 'N:ANT+') + self.channel.assign(network, CHANNEL_TYPE_TWOWAY_TRANSMIT) self.channel.setID(POWER_DEVICE_TYPE, sensor_id, 0) - self.channel.setPeriod(8182) - self.channel.setFrequency(57) + self.channel.period = CHANNEL_PERIOD + self.channel.frequency = 57 except ChannelError as e: - print "Channel config error: "+e.message + print("Channel config error: " + repr(e)) self.powerData = PowerMeterTx.PowerData() def open(self): @@ -44,25 +45,25 @@ def unassign(self): # Power was updated, so send out an ANT+ message def update(self, power): - if VPOWER_DEBUG: print 'PowerMeterTx: update called with power ', power + if VPOWER_DEBUG: print('PowerMeterTx: update called with power ', power) self.powerData.eventCount = (self.powerData.eventCount + 1) & 0xff - if VPOWER_DEBUG: print 'eventCount ', self.powerData.eventCount + if VPOWER_DEBUG: print('eventCount ', self.powerData.eventCount) self.powerData.cumulativePower = (self.powerData.cumulativePower + int(power)) & 0xffff - if VPOWER_DEBUG: print 'cumulativePower ', self.powerData.cumulativePower + if VPOWER_DEBUG: print('cumulativePower ', self.powerData.cumulativePower) self.powerData.instantaneousPower = int(power) - if VPOWER_DEBUG: print 'instantaneousPower ', self.powerData.instantaneousPower + if VPOWER_DEBUG: print('instantaneousPower ', self.powerData.instantaneousPower) - payload = chr(0x10) # standard power-only message - payload += chr(self.powerData.eventCount) - payload += chr(0xFF) # Pedal power not used - payload += chr(0xFF) # Cadence not used - payload += chr(self.powerData.cumulativePower & 0xff) - payload += chr(self.powerData.cumulativePower >> 8) - payload += chr(self.powerData.instantaneousPower & 0xff) - payload += chr(self.powerData.instantaneousPower >> 8) + payload = bytearray(b'\x10') # standard power-only message + payload.append(self.powerData.eventCount) + payload.append(0xFF) # Pedal power not used + payload.append(0xFF) # Cadence not used + payload.append(self.powerData.cumulativePower & 0xff) + payload.append(self.powerData.cumulativePower >> 8) + payload.append(self.powerData.instantaneousPower & 0xff) + payload.append(self.powerData.instantaneousPower >> 8) ant_msg = message.ChannelBroadcastDataMessage(self.channel.number, data=payload) sys.stdout.write('+') sys.stdout.flush() - if VPOWER_DEBUG: print 'Write message to ANT stick on channel ' + repr(self.channel.number) - self.antnode.driver.write(ant_msg.encode()) \ No newline at end of file + if VPOWER_DEBUG: print('Write message to ANT stick on channel ' + repr(self.channel.number)) + self.antnode.send(ant_msg) diff --git a/README.md b/README.md index 697c99d..e3d0b24 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,12 @@ Supported devices: ### Running from source code -* Install [Python 2](https://www.python.org/downloads/) if not already installed +* Install [Python 3](https://www.python.org/downloads/) if not already installed * Clone or download [python-ant](https://github.com/oldnapalm/python-ant) repo -* Open Command Prompt, CD to the python-ant repo directory and run ``C:\Python27\python.exe setup.py install`` +* Open Command Prompt, CD to the python-ant repo directory and run ``python setup.py install`` +* Run ``pip install configparser pywin32`` * Clone or download this repo -* CD to the repo directory and run ``C:\Python27\python.exe vpower.py`` +* CD to the repo directory and run ``python vpower.py`` ## Troubleshooting diff --git a/SpeedCadenceSensorRx.py b/SpeedCadenceSensorRx.py index 477ad00..7996a9f 100644 --- a/SpeedCadenceSensorRx.py +++ b/SpeedCadenceSensorRx.py @@ -1,9 +1,8 @@ -from ant.core import event -from ant.core import message +from ant.core import event, message, node from ant.core.constants import * from constants import * -from config import VPOWER_DEBUG +from config import NETKEY, VPOWER_DEBUG # Receiver for Speed and/or Cadence ANT+ sensor @@ -19,17 +18,18 @@ def __init__(self, antnode, sensor_type, sensor_id): # Get the channel self.channel = antnode.getFreeChannel() self.channel.name = 'C:SPEED' - self.channel.assign('N:ANT+', CHANNEL_TYPE_TWOWAY_RECEIVE) + network = node.Network(NETKEY, 'N:ANT+') + self.channel.assign(network, CHANNEL_TYPE_TWOWAY_RECEIVE) self.channel.setID(sensor_type, sensor_id, 0) - self.channel.setSearchTimeout(TIMEOUT_NEVER) + self.channel.searchTimeout = TIMEOUT_NEVER if sensor_type == SPEED_DEVICE_TYPE: period = 8118 elif sensor_type == CADENCE_DEVICE_TYPE: period = 8102 elif sensor_type == SPEED_CADENCE_DEVICE_TYPE: period = 8086 - self.channel.setPeriod(period) - self.channel.setFrequency(57) + self.channel.period = period + self.channel.frequency = 57 def set_revs_per_sec(self, rps): self.revsPerSec = rps @@ -56,7 +56,7 @@ def stopped(self): # TODO return False - def process(self, msg): + def process(self, msg, channel): if isinstance(msg, message.ChannelBroadcastDataMessage): dp = None # Get the datapage according to the configured device type @@ -71,7 +71,7 @@ def process(self, msg): # Parse the incoming message into a SpeedCadenceData object message_data = SpeedCadenceData() - dp.parse(msg.getPayload(), message_data) + dp.parse(msg.data, message_data) if VPOWER_DEBUG: message_data.print_speed() @@ -96,7 +96,7 @@ def process(self, msg): self.set_revs_per_sec(revs_diff / time_diff) elif isinstance(msg, message.ChannelStatusMessage): - if msg.getStatus() == EVENT_CHANNEL_CLOSED: + if msg.status == EVENT_CHANNEL_CLOSED: # Channel closed, re-open open() @@ -109,22 +109,22 @@ def __init__(self): self.cadenceEventTime = None def print_speed(self): - print 'speedRevCount: ', self.speedRevCount - print 'speedEventTime: ', self.speedEventTime + print('speedRevCount: ', self.speedRevCount) + print('speedEventTime: ', self.speedEventTime) def print_cadence(self): - print 'cadenceRevCount: ', self.cadenceRevCount - print 'cadenceEventTime: ', self.cadenceEventTime + print('cadenceRevCount: ', self.cadenceRevCount) + print('cadenceEventTime: ', self.cadenceEventTime) class DataPage(object): @staticmethod def parse_event_time(payload, offset): - return (ord(payload[offset+1]) | (ord(payload[offset + 2]) << 8)) / 1024.0 + return (payload[offset] | (payload[offset + 1] << 8)) / 1024.0 @staticmethod def parse_rev_count(payload, offset): - return ord(payload[offset+1]) | (ord(payload[offset + 2]) << 8) + return payload[offset] | (payload[offset + 1] << 8) class SpeedDataPage(DataPage): diff --git a/bot.py b/bot.py index 8d73f4a..122f6ab 100644 --- a/bot.py +++ b/bot.py @@ -1,11 +1,14 @@ #!/usr/bin/env python +import sys import time import win32api -import Tkinter as tk +import tkinter as tk from ant.core import driver from ant.core import node +from usb.core import find + from PowerMeterTx import PowerMeterTx from config import DEBUG, LOG, NETKEY, POWER_SENSOR_ID @@ -14,11 +17,11 @@ def on_exit(sig, func=None): if power_meter: - print "Closing power meter" + print("Closing power meter") power_meter.close() power_meter.unassign() if antnode: - print "Stopping ANT node" + print("Stopping ANT node") antnode.stop() win32api.SetConsoleCtrlHandler(on_exit, True) @@ -27,20 +30,25 @@ def disable_event(): pass try: - stick = driver.USB2Driver(None, log=LOG, debug=DEBUG) + devs = find(find_all=True) + for dev in devs: + if dev.idVendor == 0x0fcf and dev.idProduct in [0x1008, 0x1009]: + break + + stick = driver.USB2Driver(log=LOG, debug=DEBUG, idProduct=dev.idProduct) antnode = node.Node(stick) - print "Starting ANT node" + print("Starting ANT node") antnode.start() - key = node.NetworkKey('N:ANT+', NETKEY) + key = node.Network(NETKEY, 'N:ANT+') antnode.setNetworkKey(0, key) - print "Starting power meter with ANT+ ID " + repr(POWER_SENSOR_ID) + print("Starting power meter with ANT+ ID " + repr(POWER_SENSOR_ID)) try: # Create the power meter object and open it power_meter = PowerMeterTx(antnode, POWER_SENSOR_ID) power_meter.open() except Exception as e: - print "power_meter error: " + e.message + print("power_meter error: " + repr(e)) power_meter = None master = tk.Tk() @@ -52,11 +60,9 @@ def disable_event(): w = tk.Scale(master, from_=0, to=1000, length=200, orient=tk.HORIZONTAL) w.pack() - t = 0 last = 0 - power = 0 - print "Main wait loop" + print("Main wait loop") while True: try: power = w.get() @@ -70,5 +76,6 @@ def disable_event(): break except Exception as e: - print "Exception: "+repr(e) - raw_input() + print("Exception: " + repr(e)) + if getattr(sys, 'frozen', False): + input() diff --git a/config.py b/config.py index 120a53e..07e1576 100644 --- a/config.py +++ b/config.py @@ -1,4 +1,4 @@ -from ConfigParser import ConfigParser +from configparser import ConfigParser import os, sys from ant.core import log @@ -21,21 +21,13 @@ CONFIG = ConfigParser() _CONFIG_FILENAME = os.path.join(SCRIPT_DIR, 'vpower.cfg') -# If there's a command-line argument, it's the location of the config file -if len(sys.argv) > 1: - _CONFIG_FILENAME = sys.argv[1] SECTION = 'vpower' -try: - if VPOWER_DEBUG: print 'Open file ' + _CONFIG_FILENAME - file = open(_CONFIG_FILENAME, 'rb') - if VPOWER_DEBUG: print 'Parse config' - CONFIG.readfp(file) -except Exception as e: - print "Error: "+repr(e.__class__) +if VPOWER_DEBUG: print('Read config file') +CONFIG.read(_CONFIG_FILENAME) -if VPOWER_DEBUG: print 'Get config items' +if VPOWER_DEBUG: print('Get config items') # Type of sensor connected to the trainer SENSOR_TYPE = CONFIG.getint(SECTION, 'speed_sensor_type') @@ -61,7 +53,7 @@ # ANT+ ID of the virtual power sensor # The expression below will choose a fixed ID based on the CPU's serial number -POWER_SENSOR_ID = int(int(hashlib.md5(getserial()).hexdigest(), 16) & 0xfffe) + 1 +POWER_SENSOR_ID = int(int(hashlib.md5(getserial().encode()).hexdigest(), 16) & 0xfffe) + 1 # If set to True, the stick's driver will dump everything it reads/writes from/to the stick. DEBUG = CONFIG.getboolean(SECTION, 'debug') @@ -73,8 +65,8 @@ # LOG = log.LogWriter(filename="vpower.log") # ANT+ network key -NETKEY = '\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' +NETKEY = b'\xB9\xA5\x21\xFB\xBD\x72\xC3\x45' if LOG: - print "Using log file:", LOG.filename - print "" + print("Using log file:", LOG.filename) + print("") diff --git a/constants.py b/constants.py index 3b4d8ce..7e5efe6 100644 --- a/constants.py +++ b/constants.py @@ -11,7 +11,7 @@ def getserial(): # Extract serial from wmic command cpuserial = "0000000000000000" try: - cpuserial = subprocess.check_output('wmic cpu get ProcessorId').split('\n')[1].strip() + cpuserial = subprocess.check_output('wmic cpu get ProcessorId').decode().split('\n')[1].strip() except: cpuserial = "ERROR000000000" diff --git a/vpower.cfg b/vpower.cfg index c373d4b..2931287 100644 --- a/vpower.cfg +++ b/vpower.cfg @@ -1,20 +1,22 @@ [vpower] # Type of ANT+ speed sensor connected to the trainer -speed_sensor_type = 123 ; 0x7B Speed-only sensor -#speed_sensor_type = 121 ; 0x79 Speed & Cadence sensor +# 121 (0x79) = Speed & Cadence sensor +# 123 (0x7B) = Speed-only sensor +speed_sensor_type = 123 # ANT+ ID of the above sensor -speed_sensor_id: 0 +speed_sensor_id = 0 -# Calculator class for the model of turbo (uncomment accordingly) -#power_calculator = BtAtsPowerCalculator ; Bike Technologies ATS -#power_calculator = CycleOpsFluid2PowerCalculator ; CycleOps Fluid 2 -#power_calculator = GenericFluidPowerCalculator ; Generic Fluid -power_calculator = GenericMagneticPowerCalculator ; Generic Magnetic (medium resistance) -#power_calculator = KurtKineticPowerCalculator ; Kurt Kinetic Fluid -#power_calculator = TacxBlueMotionPowerCalculator ; Tacx Blue Motion 4/10 +# Calculator class for the trainer +# BtAtsPowerCalculator = Bike Technologies ATS +# CycleOpsFluid2PowerCalculator = CycleOps Fluid 2 +# GenericFluidPowerCalculator = Generic Fluid Trainer +# GenericMagneticPowerCalculator = Generic Magnetic Trainer (medium resistance) +# KurtKineticPowerCalculator = Kurt Kinetic Fluid +# TacxBlueMotionPowerCalculator = Tacx Blue Motion 4/10 +power_calculator = GenericMagneticPowerCalculator -# For tyre-driven trainers, the wheel circumference in meters (2.122 for Continental Home trainer tyre) +# For tyre-driven trainers, the wheel circumference in meters wheel_circumference = 2.105 # For wind/air trainers, the current air density in kg/m3 (if not using a BME280 weather sensor) @@ -26,7 +28,6 @@ air_density = 1.191 air_density_update_secs = 10 # Overall correction factor, e.g. to match a user's power meter on another bike -#correction_factor = 0.945 correction_factor = 1.0 # Set to True to enable copious amounts of debug output diff --git a/vpower.py b/vpower.py index a84156d..7b9e5c0 100644 --- a/vpower.py +++ b/vpower.py @@ -1,10 +1,13 @@ #!/usr/bin/env python +import sys import time import win32api from ant.core import driver from ant.core import node +from usb.core import find + from PowerMeterTx import PowerMeterTx from SpeedCadenceSensorRx import SpeedCadenceSensorRx from config import DEBUG, LOG, NETKEY, POWER_CALCULATOR, POWER_SENSOR_ID, SENSOR_TYPE, SPEED_SENSOR_ID @@ -15,30 +18,35 @@ def on_exit(sig, func=None): if speed_sensor: - print "Closing speed sensor" + print("Closing speed sensor") speed_sensor.close() speed_sensor.unassign() if power_meter: - print "Closing power meter" + print("Closing power meter") power_meter.close() power_meter.unassign() if antnode: - print "Stopping ANT node" + print("Stopping ANT node") antnode.stop() win32api.SetConsoleCtrlHandler(on_exit, True) try: - print "Using " + POWER_CALCULATOR.__class__.__name__ + print("Using " + POWER_CALCULATOR.__class__.__name__) + + devs = find(find_all=True) + for dev in devs: + if dev.idVendor == 0x0fcf and dev.idProduct in [0x1008, 0x1009]: + break - stick = driver.USB2Driver(None, log=LOG, debug=DEBUG) + stick = driver.USB2Driver(log=LOG, debug=DEBUG, idProduct=dev.idProduct) antnode = node.Node(stick) - print "Starting ANT node" + print("Starting ANT node") antnode.start() - key = node.NetworkKey('N:ANT+', NETKEY) + key = node.Network(NETKEY, 'N:ANT+') antnode.setNetworkKey(0, key) - print "Starting speed sensor" + print("Starting speed sensor") try: # Create the speed sensor object and open it speed_sensor = SpeedCadenceSensorRx(antnode, SENSOR_TYPE, SPEED_SENSOR_ID & 0xffff) @@ -46,22 +54,22 @@ def on_exit(sig, func=None): # Notify the power calculator every time we get a speed event speed_sensor.notify_change(POWER_CALCULATOR) except Exception as e: - print"speed_sensor error: " + e.message + print("speed_sensor error: " + repr(e)) speed_sensor = None - print "Starting power meter with ANT+ ID " + repr(POWER_SENSOR_ID) + print("Starting power meter with ANT+ ID " + repr(POWER_SENSOR_ID)) try: # Create the power meter object and open it power_meter = PowerMeterTx(antnode, POWER_SENSOR_ID) power_meter.open() except Exception as e: - print "power_meter error: " + e.message + print("power_meter error: " + repr(e)) power_meter = None # Notify the power meter every time we get a calculated power value POWER_CALCULATOR.notify_change(power_meter) - print "Main wait loop" + print("Main wait loop") while True: try: time.sleep(1) @@ -69,5 +77,6 @@ def on_exit(sig, func=None): break except Exception as e: - print "Exception: "+repr(e) - raw_input() + print("Exception: " + repr(e)) + if getattr(sys, 'frozen', False): + input()