diff --git a/StartUp/ExplorAnt.bat b/StartUp/ExplorAnt.bat new file mode 100644 index 00000000..c479c910 --- /dev/null +++ b/StartUp/ExplorAnt.bat @@ -0,0 +1,3 @@ +del ExplorAnt*.log +..\pythoncode\ExplorAnt.py -d127 +pause \ No newline at end of file diff --git a/StartUp/FortiusAnt (gui, autostart, simulate)==test ANT===.bat b/StartUp/FortiusAnt (gui, autostart, simulate)==test ANT===.bat index 7a0f8e11..fe7ecedf 100644 --- a/StartUp/FortiusAnt (gui, autostart, simulate)==test ANT===.bat +++ b/StartUp/FortiusAnt (gui, autostart, simulate)==test ANT===.bat @@ -1,10 +1,9 @@ -del *.log +del FortiusANT*.log rem No = 0x00 # 0 rem Application = 0x01 # 1 rem Function = 0x02 # 2 rem Data1 = 0x04 # 4 antDongle rem Data2 = 0x08 # 8 usbTrainer rem Multiprocessing = 0x10 # 16 -rem ..\pythoncode\FortiusAnt.py -g -a -A -H0 -s -P -d7 -..\pythoncode\FortiusAnt.py -g -a -s -d127 +..\pythoncode\FortiusAnt.py -g -a -A -H0 -s -P -d0 pause \ No newline at end of file diff --git a/WindowsExecutable/FortiusANT.exe b/WindowsExecutable/FortiusANT.exe index 10db1d14..a4bbbfbc 100644 Binary files a/WindowsExecutable/FortiusANT.exe and b/WindowsExecutable/FortiusANT.exe differ diff --git a/pythoncode/ExplorAnt.py b/pythoncode/ExplorAnt.py index d2cfc367..3b4a787f 100644 --- a/pythoncode/ExplorAnt.py +++ b/pythoncode/ExplorAnt.py @@ -349,6 +349,10 @@ def __init__(self, Channel, DeviceType, DeviceNumber, DeviceTypeID, Transmission AntDongle.SlaveTrainer_ChannelConfig(clv.fe) logfile.Console ('FE slave channel %s opened; listening to master device %s' % (ant.channel_FE_s, clv.fe)) + if clv.scs > 0: + AntDongle.SlaveSCS_ChannelConfig(clv.scs) + logfile.Console ('SCS slave channel %s opened; listening to master device %s' % (ant.channel_SCS_s, clv.scs)) + if clv.vtx > 0: AntDongle.SlaveVTX_ChannelConfig(clv.vtx) logfile.Console ('VTX slave channel %s opened; listening to master device %s' % (ant.channel_VTX_s, 0)) @@ -378,6 +382,8 @@ def __init__(self, Channel, DeviceType, DeviceNumber, DeviceTypeID, Transmission FE_page80_done = False FE_page81_done = False + SCS_s_count = 0 + VTX_UsingVirtualSpeed, VTX_Power, VTX_Speed, VTX_CalibrationState, VTX_Cadence = 0,0,0,0,0 VTX_S1, VTX_S2, VTX_Serial, VTX_Alarm = 0,0,0,0 VTX_Major, VTX_Minor, VTX_Build = 0,0,0 @@ -564,6 +570,41 @@ def __init__(self, Channel, DeviceType, DeviceNumber, DeviceTypeID, Transmission logfile.Console ("FE Page=%s SWrevision=%s.%s Serial#=%s" % \ (DataPageNumber, FE_SWrevisionMain, FE_SWrevisionSupp, FE_SerialNumber)) + #------------------------------------------------------- + # SCS_s = Heart rate Monitor Display + # We are slave, listening to a master (Speed Cadence Sensor) + #------------------------------------------------------- + if Channel == ant.channel_SCS_s: + SCS_s_count += 1 + if SCS_s_count > 99: SCS_s_count= 0 + + #--------------------------------------------------- + # Only one Data page for SCS! msgUnpage_SCS + #--------------------------------------------------- + Unknown = False + EventTime, CadenceRevolutionCount, _EventTime, \ + SpeedRevolutionCount = ant.msgUnpage_SCS(info) + + try: + _ = pEventTime + except: + pass + else: + if EventTime != pEventTime: + cadence = 60 * (CadenceRevolutionCount - pCadenceRevolutionCount) * 1024 / \ + (EventTime - pEventTime) + cadence = int(cadence) + + speed = (SpeedRevolutionCount - pSpeedRevolutionCount) * 2.096 * 3.600 / \ + (EventTime - pEventTime) + print ('EventTime=%5s (%5s) CadenceRevolutionCount=%5s (%5s) Cadence=%3s EventTime=%5s SpeedRevolutionCount=%5s Speed=%4.1f' % \ + (EventTime, EventTime - pEventTime, \ + CadenceRevolutionCount, CadenceRevolutionCount - pCadenceRevolutionCount, \ + cadence, _EventTime, SpeedRevolutionCount, speed)) + pCadenceRevolutionCount = CadenceRevolutionCount + pSpeedRevolutionCount = SpeedRevolutionCount + pEventTime = EventTime + #------------------------------------------------------- # VTX_s = Tacx i-Vortex trainer # We are slave, listening to a master (the real trainer) @@ -707,9 +748,10 @@ def __init__(self, Channel, DeviceType, DeviceNumber, DeviceTypeID, Transmission ) else: - logfile.Console ("HRM#=%2s hr=%3s FE-C#=%2s Speed=%4s Cadence=%3s Power=%3s hr=%3s VTX ID=%s Speed=%4s Cadence=%3s Target=%s" % \ + logfile.Console ("HRM#=%2s hr=%3s FE-C#=%2s Speed=%4s Cadence=%3s Power=%3s hr=%3s SCS#=%2s VTX ID=%s Speed=%4s Cadence=%3s Target=%s" % \ (HRM_s_count, HRM_HeartRate, \ FE_s_count, FE_Speed, FE_Cadence, FE_Power, FE_HeartRate, \ + SCS_s_count, \ VTX_VortexID, VTX_Speed, VTX_Cadence, Power )\ ) diff --git a/pythoncode/FortiusAntBody.py b/pythoncode/FortiusAntBody.py index 3d842828..36a5f085 100644 --- a/pythoncode/FortiusAntBody.py +++ b/pythoncode/FortiusAntBody.py @@ -1,7 +1,10 @@ #------------------------------------------------------------------------------- # Version info #------------------------------------------------------------------------------- -__version__ = "2020-05-27" +__version__ = "2020-06-12" +# 2020-06-12 Added: BikePowerProfile and SpeedAndCadenceSensor final +# 2020-06-11 Added: BikePowerProfile (master) +# 2020-06-09 Added: SpeedAndCadenceSensor (master) # 2020-05-27 Added: msgPage71_CommandStatus handled -- and tested # 2020-05-24 i-Vortex adjustments # - in manual mode, ANTdongle must be present as well, so that @@ -167,8 +170,10 @@ from datetime import datetime import antDongle as ant -import antHRM as hrm import antFE as fe +import antHRM as hrm +import antPWR as pwr +import antSCS as scs import debug from FortiusAntGui import mode_Power, mode_Grade import logfile @@ -544,9 +549,23 @@ def Tacx2DongleSub(self, Restart): #------------------------------------------------------------------- AntDongle.SlaveVHU_ChannelConfig(0) - if clv.scs != None: - AntDongle.SlaveSCS_ChannelConfig(clv.scs) # Create ANT+ slave channel for SCS - # 0: auto pair, nnn: defined SCS + if True: + #------------------------------------------------------------------- + # Create ANT+ master channel for PWR + #------------------------------------------------------------------- + AntDongle.PWR_ChannelConfig(ant.channel_PWR) + + if clv.scs == None: + #------------------------------------------------------------------- + # Create ANT+ master channel for SCS + #------------------------------------------------------------------- + AntDongle.SCS_ChannelConfig(ant.channel_SCS) + else: + #------------------------------------------------------------------- + # Create ANT+ slave channel for SCS + # 0: auto pair, nnn: defined SCS + #------------------------------------------------------------------- + AntDongle.SlaveSCS_ChannelConfig(clv.scs) pass if not clv.gui: logfile.Console ("Ctrl-C to exit") @@ -668,8 +687,10 @@ def Tacx2DongleSub(self, Restart): # Initialize antHRM and antFE module #--------------------------------------------------------------------------- if debug.on(debug.Function): logfile.Write('Tacx2Dongle; initialize ANT') - hrm.Initialize() fe.Initialize() + hrm.Initialize() + pwr.Initialize() + scs.Initialize() #--------------------------------------------------------------------------- # Initialize CycleTime: fast for PedalStrokeAnalysis @@ -713,8 +734,8 @@ def Tacx2DongleSub(self, Restart): # Hook for future development #------------------------------------------------------------------- # if clv.scs == None: - # SpeedKmh = SpeedKmhT - # Cadence = CadenceT + # SpeedKmh = SpeedKmhSCS + # Cadence = CadenceSCS #------------------------------------------------------------------- # If NO HRM defined, use the HeartRate from the trainer @@ -794,6 +815,21 @@ def Tacx2DongleSub(self, Restart): if clv.hrm == None and TacxTrainer.HeartRate > 0: messages.append(hrm.BroadcastHeartrateMessage(HeartRate)) + #--------------------------------------------------------------- + # Broadcast Bike Power message + #--------------------------------------------------------------- + if True: + messages.append(pwr.BroadcastMessage( \ + TacxTrainer.CurrentPower, TacxTrainer.Cadence)) + + #--------------------------------------------------------------- + # Broadcast Speed and Cadence Sensor message + #--------------------------------------------------------------- + if clv.scs == None: + messages.append(scs.BroadcastMessage( \ + TacxTrainer.PedalEchoTime, TacxTrainer.PedalEchoCount, \ + TacxTrainer.VirtualSpeedKmh, TacxTrainer.Cadence)) + #--------------------------------------------------------------- # Broadcast TrainerData message to the CTP (Trainer Road, ...) #--------------------------------------------------------------- @@ -1105,7 +1141,7 @@ def Tacx2DongleSub(self, Restart): if debug.on(debug.Data2): logfile.Write ("Sleep(%4.2f) to fill %s seconds done." % (SleepTime, CycleTime) ) else: if ElapsedTime > CycleTime * 2 and debug.on(debug.Any): - logfile.Console ("Tacx2Dongle; Processing time %5.3f is %5.3f longer than planned %5.3f (seconds)" % (ElapsedTime, SleepTime * -1, CycleTime) ) + logfile.Write ("Tacx2Dongle; Processing time %5.3f is %5.3f longer than planned %5.3f (seconds)" % (ElapsedTime, SleepTime * -1, CycleTime) ) pass EventCounter += 1 # Increment and ... diff --git a/pythoncode/FortiusAntGui.py b/pythoncode/FortiusAntGui.py index 7f5dc509..43f2a1b8 100644 --- a/pythoncode/FortiusAntGui.py +++ b/pythoncode/FortiusAntGui.py @@ -1,8 +1,9 @@ #------------------------------------------------------------------------------- # Version info #------------------------------------------------------------------------------- -WindowTitle = "Fortius Antifier v3.1" -__version__ = "2020-05-24" +WindowTitle = "Fortius Antifier v3.2" +__version__ = "2020-06-12" +# 2020-05-12 Version 3.2 with SCS and PWR profile # 2020-05-24 Initial GUI messages made more general # TargetResistance not displayed when zero (for i-Vortex) # 2020-05-15 Window title adjusted to version 3.0, comment on teeth. diff --git a/pythoncode/antDongle.py b/pythoncode/antDongle.py index a0259dc8..ee33713f 100644 --- a/pythoncode/antDongle.py +++ b/pythoncode/antDongle.py @@ -1,8 +1,11 @@ #--------------------------------------------------------------------------- # Version info #--------------------------------------------------------------------------- -__version__ = "2020-05-26" -# 2020-05-26 Added: msgPage71_CommandStatus +__version__ = "2020-06-12" +# 2020-06-12 Added: BikePowerProfile and SpeedAndCadenceSensor final +# 2020-06-10 Changed: ChannelPeriods defined decimal, like ANT+ specification +# 2020-06-09 Added: SpeedAndCadenceSensor +# 2020-05-26 Added: msgPage71_CommandStatus # 2020-05-25 Changed: DongleDebugMessage() adjusted with some more info # 2020-05-20 Changed: DecomposeMessage() made foolproof against wrong data # msgPage172_TacxVortexHU_ChangeHeadunitMode wrong page @@ -91,25 +94,27 @@ #--------------------------------------------------------------------------- # Our own choice what channels are used # -# A different channel is used for HRM as HRM_d, even though usually a device -# will be either one or the other. ExplorANT can be both. +# Note that a running program cannot be slave AND master for same device +# since the channels are statically assigned! #--------------------------------------------------------------------------- # M A X # c h a n n e l s m a y b e 8 s o b e w a r e h e r e ! #--------------------------------------------------------------------------- -channel_FE = 0 # ANT+ channel for Fitness Equipment -channel_HRM = 1 # ANT+ channel for Heart Rate Monitor -channel_VTX = 2 # ANT+ Channel for Tacx i-Vortex +channel_FE = 0 # ANT+ channel for Fitness Equipment +channel_FE_s = channel_FE # slave=Cycle Training Program -channel_FE_s = 4 # ANT+ channel for Fitness Equipment (slave=Cycle Training Program) -channel_HRM_s = 5 # ANT+ channel for Heart Rate Monitor (slave=display) -channel_VTX_s = 6 # ANT+ Channel for Tacx i-Vortex (slave=Cycle Training Program) -channel_VHU_s = 7 # ANT+ Channel for Tacx i-Vortex Headunit +channel_HRM = 1 # ANT+ channel for Heart Rate Monitor +channel_HRM_s = channel_HRM # slave=display or Cycle Training Program -# There are only 8 channels available and Speed Cadence Sensor is not used -# Fixed channel assignment is not handy therefore, but since we do not use -# SCS, park them for later -channel_SCS = -1 # ANT+ Channel for Speed Cadence Sensor -channel_SCS_s = -1 # ANT+ Channel for Speed Cadence Sensor (slave=display) +channel_PWR = 2 # ANT+ Channel for Power Profile + +channel_SCS = 3 # ANT+ Channel for Speed Cadence Sensor +channel_SCS_s = channel_SCS # slave=display or Cycle Training Program + +channel_VTX = 4 # ANT+ Channel for Tacx i-Vortex +channel_VTX_s = channel_VTX # slave=Cycle Training Program + +channel_VHU_s = 5 # ANT+ Channel for Tacx i-Vortex Headunit + # slave=Cycle Training Program #--------------------------------------------------------------------------- # i-Vortex Headunit modes @@ -125,6 +130,8 @@ DeviceNumber_HRM = 57592 # slaves (TrainerRoad, Zwift, ExplorANT) will find. DeviceNumber_VTX = 57593 # DeviceNumber_VHU = 57594 # +DeviceNumber_SCS = 57595 # +DeviceNumber_PWR = 57596 # ModelNumber_FE = 2875 # short antifier-value=0x8385, Tacx Neo=2875 SerialNumber_FE = 19590705 # int 1959-7-5 @@ -137,6 +144,13 @@ HWrevision_HRM = 1 # char SWversion_HRM = 1 # char +ModelNumber_PWR = 2161 # Garmin Vector 2 (profile.xlsx, garmin_product) +SerialNumber_PWR = 19590702 # int 1959-7-2 +HWrevision_PWR = 1 # char +SWrevisionMain_PWR = 1 # char +SWrevisionSupp_PWR = 1 # char + + if False: # Tacx Neo Erik OT; perhaps relevant for Tacx Desktop App # because TDA does not want to pair with FortiusAnt... DeviceNumber_FE = 48365 @@ -213,9 +227,9 @@ Manufacturer_tacx = 89 Manufacturer_trainer_road =281 - DeviceTypeID_FE = DeviceTypeID_fitness_equipment DeviceTypeID_HRM = DeviceTypeID_heart_rate +DeviceTypeID_PWR = DeviceTypeID_bike_power DeviceTypeID_SCS = DeviceTypeID_bike_speed_cadence DeviceTypeID_VTX = 61 # Tacx i-Vortex # 0x3d according TotalReverse @@ -614,6 +628,7 @@ def ResetDongle(self): def SlavePair_ChannelConfig(self, channel_pair, \ DeviceNumber=0, DeviceTypeID=0, TransmissionType=0): # Slave, by default full wildcards ChannelID, see msg51 comment + logfile.Console ('FortiusANT tries to pair with any ANT+ device') if debug.on(debug.Data1): logfile.Write ("SlavePair_ChannelConfig()") messages=[ msg42_AssignChannel (channel_pair, ChannelType_BidirectionalReceive, NetworkNumber=0x00), @@ -626,24 +641,26 @@ def SlavePair_ChannelConfig(self, channel_pair, \ self.Write(messages, True, False) def Trainer_ChannelConfig(self): + logfile.Console ('FortiusANT broadcasts data as an ANT+ Controlled Fitness Equipent device (FE-C), id=%s' % DeviceNumber_FE) if debug.on(debug.Data1): logfile.Write ("Trainer_ChannelConfig()") messages=[ msg42_AssignChannel (channel_FE, ChannelType_BidirectionalTransmit, NetworkNumber=0x00), msg51_ChannelID (channel_FE, DeviceNumber_FE, DeviceTypeID_FE, TransmissionType_IC_GDP), msg45_ChannelRfFrequency (channel_FE, RfFrequency_2457Mhz), - msg43_ChannelPeriod (channel_FE, ChannelPeriod=0x2000), # 4 Hz + msg43_ChannelPeriod (channel_FE, ChannelPeriod=8192), # 4 Hz msg60_ChannelTransmitPower (channel_FE, TransmitPower_0dBm), msg4B_OpenChannel (channel_FE) ] self.Write(messages) def SlaveTrainer_ChannelConfig(self, DeviceNumber): + logfile.Console ('FortiusANT receives data from an ANT+ Controlled Fitness Equipent device (FE-C)') if debug.on(debug.Data1): logfile.Write ("SlaveTrainer_ChannelConfig()") messages=[ msg42_AssignChannel (channel_FE_s, ChannelType_BidirectionalReceive, NetworkNumber=0x00), msg51_ChannelID (channel_FE_s, DeviceNumber, DeviceTypeID_FE, TransmissionType_IC_GDP), msg45_ChannelRfFrequency (channel_FE_s, RfFrequency_2457Mhz), - msg43_ChannelPeriod (channel_FE_s, ChannelPeriod=0x2000), # 4 Hz + msg43_ChannelPeriod (channel_FE_s, ChannelPeriod=8192), # 4 Hz msg60_ChannelTransmitPower (channel_FE_s, TransmitPower_0dBm), msg4B_OpenChannel (channel_FE_s), msg4D_RequestMessage (channel_FE_s, msgID_ChannelID) @@ -651,37 +668,66 @@ def SlaveTrainer_ChannelConfig(self, DeviceNumber): self.Write(messages) def HRM_ChannelConfig(self): + logfile.Console ('FortiusANT broadcasts data as an ANT+ Heart Rate Monitor (HRM), id=%s' % DeviceNumber_HRM) if debug.on(debug.Data1): logfile.Write ("HRM_ChannelConfig()") messages=[ msg42_AssignChannel (channel_HRM, ChannelType_BidirectionalTransmit, NetworkNumber=0x00), msg51_ChannelID (channel_HRM, DeviceNumber_HRM, DeviceTypeID_HRM, TransmissionType_IC), msg45_ChannelRfFrequency (channel_HRM, RfFrequency_2457Mhz), - msg43_ChannelPeriod (channel_HRM, ChannelPeriod=0x1f86), + msg43_ChannelPeriod (channel_HRM, ChannelPeriod=8070), # 4,06 Hz msg60_ChannelTransmitPower (channel_HRM, TransmitPower_0dBm), msg4B_OpenChannel (channel_HRM) ] self.Write(messages) def SlaveHRM_ChannelConfig(self, DeviceNumber): + logfile.Console ('FortiusANT receives data from an ANT+ Heart Rate Monitor (HRM display)') if debug.on(debug.Data1): logfile.Write ("SlaveHRM_ChannelConfig()") messages=[ msg42_AssignChannel (channel_HRM_s, ChannelType_BidirectionalReceive, NetworkNumber=0x00), msg51_ChannelID (channel_HRM_s, DeviceNumber, DeviceTypeID_HRM, TransmissionType_IC), msg45_ChannelRfFrequency (channel_HRM_s, RfFrequency_2457Mhz), - msg43_ChannelPeriod (channel_HRM_s, ChannelPeriod=0x1f86), + msg43_ChannelPeriod (channel_HRM_s, ChannelPeriod=8070), # 4,06 Hz msg60_ChannelTransmitPower (channel_HRM_s, TransmitPower_0dBm), msg4B_OpenChannel (channel_HRM_s), msg4D_RequestMessage (channel_HRM_s, msgID_ChannelID) ] self.Write(messages) + def PWR_ChannelConfig(self, DeviceNumber): + logfile.Console ('FortiusANT broadcasts data as an ANT+ Bicycle Power Sensor (PWR), id=%s' % DeviceNumber_PWR) + if debug.on(debug.Data1): logfile.Write ("PWR_ChannelConfig()") + messages=[ + msg42_AssignChannel (channel_PWR, ChannelType_BidirectionalTransmit, NetworkNumber=0x00), + msg51_ChannelID (channel_PWR, DeviceNumber_PWR, DeviceTypeID_PWR, TransmissionType_IC), + msg45_ChannelRfFrequency (channel_PWR, RfFrequency_2457Mhz), + msg43_ChannelPeriod (channel_PWR, ChannelPeriod=8182), # 4,0059 Hz + msg60_ChannelTransmitPower (channel_PWR, TransmitPower_0dBm), + msg4B_OpenChannel (channel_PWR), + ] + self.Write(messages) + + def SCS_ChannelConfig(self, DeviceNumber): + logfile.Console ('FortiusANT broadcasts data as an ANT+ Speed and Cadence Sensor (SCS), id=%s' % DeviceNumber_SCS) + if debug.on(debug.Data1): logfile.Write ("SCS_ChannelConfig()") + messages=[ + msg42_AssignChannel (channel_SCS, ChannelType_BidirectionalTransmit, NetworkNumber=0x00), + msg51_ChannelID (channel_SCS, DeviceNumber_SCS, DeviceTypeID_SCS, TransmissionType_IC), + msg45_ChannelRfFrequency (channel_SCS, RfFrequency_2457Mhz), + msg43_ChannelPeriod (channel_SCS, ChannelPeriod=8086), # 4,05 Hz + msg60_ChannelTransmitPower (channel_SCS, TransmitPower_0dBm), + msg4B_OpenChannel (channel_SCS), + ] + self.Write(messages) + def SlaveSCS_ChannelConfig(self, DeviceNumber): + logfile.Console ('FortiusANT receives data from an ANT+ Speed and Cadence Sensor (SCS Display)') if debug.on(debug.Data1): logfile.Write ("SlaveSCS_ChannelConfig()") messages=[ msg42_AssignChannel (channel_SCS_s, ChannelType_BidirectionalReceive, NetworkNumber=0x00), msg51_ChannelID (channel_SCS_s, DeviceNumber, DeviceTypeID_SCS, TransmissionType_IC), msg45_ChannelRfFrequency (channel_SCS_s, RfFrequency_2457Mhz), - msg43_ChannelPeriod (channel_SCS_s, ChannelPeriod=0x1f86), + msg43_ChannelPeriod (channel_SCS_s, ChannelPeriod=8086), # 4,05 Hz msg60_ChannelTransmitPower (channel_SCS_s, TransmitPower_0dBm), msg4B_OpenChannel (channel_SCS_s), msg4D_RequestMessage (channel_SCS_s, msgID_ChannelID) @@ -689,6 +735,7 @@ def SlaveSCS_ChannelConfig(self, DeviceNumber): self.Write(messages) def VTX_ChannelConfig(self): # Pretend to be a Tacx i-Vortex + logfile.Console ('FortiusANT broadcasts data as an ANT+ Tacx i-Vortex (VTX), id=%s' % DeviceNumber_VTX) if debug.on(debug.Data1): logfile.Write ("VTX_ChannelConfig()") messages=[ msg42_AssignChannel (channel_VTX, ChannelType_BidirectionalTransmit, NetworkNumber=0x01), @@ -702,6 +749,7 @@ def VTX_ChannelConfig(self): # Pretend to be a Tacx i-Vo self.Write(messages) def SlaveVTX_ChannelConfig(self, DeviceNumber): # Listen to a Tacx i-Vortex + logfile.Console ('FortiusANT receives data from an ANT+ Tacx i-Vortex (VTX Controller)') if debug.on(debug.Data1): logfile.Write ("SlaveVTX_ChannelConfig()") messages=[ msg42_AssignChannel (channel_VTX_s, ChannelType_BidirectionalReceive, NetworkNumber=0x01), @@ -716,6 +764,7 @@ def SlaveVTX_ChannelConfig(self, DeviceNumber): # Listen to a Tacx i-Vortex def SlaveVHU_ChannelConfig(self, DeviceNumber): # Listen to a Tacx i-Vortex Headunit # See comment above msgPage000_TacxVortexHU_StayAlive + logfile.Console ('FortiusANT receives data from an ANT+ Tacx i-Vortex Headunit (VHU Controller)') if debug.on(debug.Data1): logfile.Write ("SlaveVHU_ChannelConfig()") messages=[ msg42_AssignChannel (channel_VHU_s, ChannelType_BidirectionalReceive, NetworkNumber=0x01), @@ -915,7 +964,10 @@ def DongleDebugMessage(text, d): elif id == msgID_BroadcastData or id == msgID_AcknowledgedData: # Pagenumber in Payload if p < 0: pass - elif p & 0x7f == 0: p_ = 'Default data page' # D00000693_-_ANT+_Device_Profile_-_Heart_Rate_Rev_2.1 + elif Channel in (channel_SCS, channel_SCS_s): + p_ = ' Speed and Cadence Sensor datapage' + p = None + elif p & 0x7f == 0: p_ = 'Default data page' # D00000693_-_ANT+_Device_Profile_-_Heart_Rate_Rev_2.1 # Also called "Unknown data page" # 'HRM' but other devices have other meanings # Left for future improvements. @@ -926,7 +978,7 @@ def DongleDebugMessage(text, d): elif p & 0x7f == 4: p_ = 'HRM Previous Heart beat' elif p & 0x7f == 5: p_ = 'HRM Swim interval summary' elif p & 0x7f == 6: p_ = 'HRM Capabilities' - elif p == 16: p_ = 'General FE data' + elif p == 16: p_ = 'Main data page' elif p == 25: p_ = 'Trainer Data' elif p == 48: p_ = 'Basic Resistance' elif p == 49: p_ = 'Target Power' @@ -945,7 +997,7 @@ def DongleDebugMessage(text, d): elif p ==221: p_ = 'VHU Button pressed' else : p_ = '??' - p_ = " p=%s(%s)" % (p, p_) # Page, show number and name + if p != None: p_ = " p=%s(%s)" % (p, p_) # Page, show number and name elif id == msgID_RF_EVENT: pass # We could fill info with error code @@ -1097,6 +1149,34 @@ def unmsg64_ChannelResponse(info): return tuple[nChannel], tuple[nInitiatingMessageID], tuple[nResponseCode] +# ------------------------------------------------------------------------------ +# P a g e 1 6 P o w e r p r o f i l e +# ------------------------------------------------------------------------------ +# Refer: https://www.thisisant.com/developer/resources/downloads#documents_tab +# trainer: D00001086_ANT+_Device_Profile_-_Bicycle_Power_Rev_5.1.pdf +# Data page 16 (0x10) Power-only Main Data Page +# ------------------------------------------------------------------------------ +def msgPage16_PowerOnly (Channel, EventCount, Cadence, AccumulatedPower, CurrentPower): + DataPageNumber = 16 + + EventCount = int(min(0xff, EventCount )) + Cadence = int(min(0xff, Cadence )) + AccumulatedPower = int(min(0xffff, AccumulatedPower)) + CurrentPower = int(min(0xffff, CurrentPower )) + + fChannel = sc.unsigned_char # First byte of the ANT+ message content + fDataPageNumber = sc.unsigned_char # First byte of the ANT+ datapage (payload) + fEventCount = sc.unsigned_char + fPedalPower = sc.unsigned_char + fInstantaneousCadence = sc.unsigned_char + fAccumulatedPower = sc.unsigned_short + fInstantaneousPower = sc.unsigned_short + + format = sc.no_alignment + fChannel + fDataPageNumber + fEventCount + fPedalPower + fInstantaneousCadence + fAccumulatedPower + fInstantaneousPower + info = struct.pack(format, Channel, DataPageNumber, EventCount, 0xff, Cadence, AccumulatedPower, CurrentPower) + + return info + # ------------------------------------------------------------------------------ # P a g e 0 0 T a c x V o r t e x D a t a S p e e d # ------------------------------------------------------------------------------ @@ -2058,3 +2138,45 @@ def msgUnpage_Hrm (info): tuple = struct.unpack (format, info) return tuple[0], tuple[1], tuple[2], tuple[3], tuple[4], tuple[5], tuple[6], tuple[7] + +# ------------------------------------------------------------------------------ +# P a g e 0 S p e e d C a d e n c e S e n s o r +# ------------------------------------------------------------------------------ +# https://www.thisisant.com/developer/resources/downloads#documents_tab +# D00001163_-_ANT+_Device_Profile_-_Bicycle_Speed_and_Cadence_2.1.pdf +# ------------------------------------------------------------------------------ +def msgPage_SCS (Channel, CadenceEventTime, CadenceRevolutionCount, SpeedEventTime, SpeedRevolutionCount): + _DataPageNumber = 0 + CadenceEventTime = int(min(0xffff, CadenceEventTime )) + CadenceRevolutionCount = int(min(0xffff, CadenceRevolutionCount)) + SpeedEventTime = int(min(0xffff, SpeedEventTime )) + SpeedRevolutionCount = int(min(0xffff, SpeedRevolutionCount )) + + fChannel = sc.unsigned_char # First byte of the ANT+ message content + _fDataPageNumber = sc.unsigned_char # First byte of the ANT+ datapage (payload) + fCadenceEventTime = sc.unsigned_short + fCadenceRevolutionCount = sc.unsigned_short + fSpeedEventTime = sc.unsigned_short + fSpeedRevolutionCount = sc.unsigned_short + + format = sc.no_alignment + fChannel + fCadenceEventTime + \ + fCadenceRevolutionCount + fSpeedEventTime + fSpeedRevolutionCount + info = struct.pack (format, Channel, CadenceEventTime, \ + CadenceRevolutionCount, SpeedEventTime, SpeedRevolutionCount) + + return info + +def msgUnpage_SCS (info): + fChannel = sc.unsigned_char # First byte of the ANT+ message content + _fDataPageNumber = sc.unsigned_char # First byte of the ANT+ datapage (payload) + fCadenceEventTime = sc.unsigned_short + fCadenceRevolutionCount = sc.unsigned_short + fSpeedEventTime = sc.unsigned_short + fSpeedRevolutionCount = sc.unsigned_short + + format = sc.no_alignment + fChannel + fCadenceEventTime + \ + fCadenceRevolutionCount + fSpeedEventTime + fSpeedRevolutionCount + tuple = struct.unpack (format, info) + + # EventTime, CadenceRevolutionCount, EventTime, SpeedRevolutionCount + return tuple[1], tuple[2], tuple[3], tuple[4] diff --git a/pythoncode/antPWR.py b/pythoncode/antPWR.py new file mode 100644 index 00000000..397f7008 --- /dev/null +++ b/pythoncode/antPWR.py @@ -0,0 +1,53 @@ +#------------------------------------------------------------------------------- +# Version info +#------------------------------------------------------------------------------- +__version__ = "2020-06-11" +# 2020-06-11 First version, based upon antHRM.py +#------------------------------------------------------------------------------- +import time +import antDongle as ant + +def Initialize(): + global EventCount, AccumulatedPower + EventCount = 0 + AccumulatedPower = 0 + +def BroadcastMessage (CurrentPower, Cadence): + global EventCount, AccumulatedPower + + if EventCount % 120 == 0: # Transmit page 0x50 = 80 + info = ant.msgPage80_ManufacturerInfo(ant.channel_PWR, 0xff, 0xff, \ + ant.HWrevision_PWR, ant.Manufacturer_garmin, ant.ModelNumber_PWR) + + elif EventCount % 121 == 0: # Transmit page 0x51 = 81 + info = ant.msgPage81_ProductInformation(ant.channel_PWR, 0xff, \ + ant.SWrevisionSupp_PWR, ant.SWrevisionMain_PWR, ant.SerialNumber_PWR) + + elif EventCount % 60 == 0: # Transmit page 0x52 = 82 + info = ant.msgPage82_BatteryStatus(ant.channel_PWR) + + else: + AccumulatedPower += CurrentPower + info= ant.msgPage16_PowerOnly (ant.channel_PWR, EventCount, Cadence, AccumulatedPower, CurrentPower) + + pwrdata = ant.ComposeMessage (ant.msgID_BroadcastData, info) + + #------------------------------------------------------------------------- + # Prepare for next event + #------------------------------------------------------------------------- + EventCount += 1 + if EventCount > 0xff or AccumulatedPower > 0xffff: + Initialize() + + #------------------------------------------------------------------------- + # Return message to be sent + #------------------------------------------------------------------------- + return pwrdata + +#------------------------------------------------------------------------------- +# Main program for module test +#------------------------------------------------------------------------------- +if __name__ == "__main__": + Initialize() + pwrdata = BroadcastMessage (456.7, 123) + print (pwrdata) \ No newline at end of file diff --git a/pythoncode/antSCS.py b/pythoncode/antSCS.py new file mode 100644 index 00000000..f32a38a5 --- /dev/null +++ b/pythoncode/antSCS.py @@ -0,0 +1,82 @@ +#------------------------------------------------------------------------------- +# Version info +#------------------------------------------------------------------------------- +__version__ = "2020-06-09" +# 2020-06-09 First version, based upon antHRM.py +#------------------------------------------------------------------------------- +import time +import antDongle as ant +import logfile + +def Initialize(): + global PedalEchoPreviousCount, CadenceEventTime, CadenceEventCount, SpeedEventTime, SpeedEventCount + PedalEchoPreviousCount = 0 # There is no previous + CadenceEventTime = 0 # Initiate the even variables + CadenceEventCount = 0 + SpeedEventTime = 0 + SpeedEventCount = 0 + +def BroadcastMessage (_PedalEchoTime, PedalEchoCount, SpeedKmh, Cadence): + global PedalEchoPreviousCount, CadenceEventTime, CadenceEventCount, SpeedEventTime, SpeedEventCount + + #------------------------------------------------------------------------- + # If pedal passed the magnet, calculate new values + # Otherwise repeat previous message + #------------------------------------------------------------------------- + if PedalEchoCount != PedalEchoPreviousCount: + #--------------------------------------------------------------------- + # Cadence variables + # Based upon the number of pedal-cycles that are done and the given + # cadence, calculate the elapsed time. + # _PedalEchoTime is not used, because that give rounding errors and + # an instable reading. + #--------------------------------------------------------------------- + PedalCycles = PedalEchoCount - PedalEchoPreviousCount + ElapsedTime = PedalCycles / Cadence * 60 # count / count/min * seconds/min = seconds + CadenceEventTime += ElapsedTime * 1024 # 1/1024 seconds + CadenceEventCount += PedalCycles + + #--------------------------------------------------------------------- + # Speed variables + # First calculate how many wheel-cycles can be done + # Then (based upon rounded #of cycles) calculate the elapsed time + #--------------------------------------------------------------------- + Circumference = 2.096 # Note: SimulANT has 2.070 as default + WheelCadence = SpeedKmh / 3.6 / Circumference # km/hr / kseconds/hr / meters = cycles/s + WheelCycles = round(ElapsedTime * WheelCadence, 0) # seconds * /s = cycles + + ElapsedTime = WheelCycles / SpeedKmh * 3.6 * Circumference + SpeedEventTime += ElapsedTime * 1024 + SpeedEventCount += WheelCycles + + #------------------------------------------------------------------------- + # Rollover after 0xffff + #------------------------------------------------------------------------- + if CadenceEventTime > 0xffff or CadenceEventCount > 0xffff or \ + SpeedEventTime > 0xffff or SpeedEventCount > 0xffff: + Initialize() + + #------------------------------------------------------------------------- + # Prepare for next event + #------------------------------------------------------------------------- + PedalEchoPreviousCount = PedalEchoCount + + #------------------------------------------------------------------------- + # Compose message + #------------------------------------------------------------------------- + info = ant.msgPage_SCS (ant.channel_SCS, CadenceEventTime, CadenceEventCount, SpeedEventTime, SpeedEventCount) + scsdata = ant.ComposeMessage (ant.msgID_BroadcastData, info) + + #------------------------------------------------------------------------- + # Return message to be sent + #------------------------------------------------------------------------- + return scsdata + +#------------------------------------------------------------------------------- +# Main program for module test +#------------------------------------------------------------------------------- +if __name__ == "__main__": + Initialize() + time.sleep(1) + scsdata = BroadcastMessage (0, 1, 45.6, 123) + print (logfile.HexSpace(scsdata)) \ No newline at end of file diff --git a/pythoncode/usbTrainer.py b/pythoncode/usbTrainer.py index 7764b6c7..fc5e7ab2 100644 --- a/pythoncode/usbTrainer.py +++ b/pythoncode/usbTrainer.py @@ -1,7 +1,11 @@ #------------------------------------------------------------------------------- # Version info #------------------------------------------------------------------------------- -__version__ = "2020-06-08" +__version__ = "2020-06-12" +# 2020-06-12 Added: BikePowerProfile and SpeedAndCadenceSensor final +# 2020-06-11 Changed: if clsTacxNewUsbTrainer less than 40 bytes received, retry +# 2020-06-10 Changed: Speed and Cadence Sensor metrics (PedalEchoCount) +# 2020-06-09 Changed: VirtualSpeed in clsSimulatedTrainer corrected # 2020-06-08 Changed: clsTacxAntVortexTrainer uses TargetResistance, even # when expressed in Watts, so that also PowerFactor applies. # Previously TargetPower was used directly, short-cutting @@ -232,6 +236,9 @@ class clsTacxTrainer(): CurrentResistance = 0 # int HeartRate = 0 # int PedalEcho = 0 + PreviousPedalEcho = 0 # detection for PedalEcho=1 + PedalEchoCount = 0 # count of PedalEcho=1 events + PedalEchoTime = time.time() # the time of the last PedalEcho event SpeedKmh = 0 # round(,1) VirtualSpeedKmh = 0 # see Grade_mode TargetResistanceFT = 0 # int Returned from trainer @@ -491,6 +498,14 @@ def Refresh(self, QuarterSecond, TacxMode): # Make all variables consistent #----------------------------------------------------------------------- + #----------------------------------------------------------------------- + # PedalEcho for Speed and Cadence Sensor + #----------------------------------------------------------------------- + if self.PedalEcho == 1 and self.PreviousPedalEcho == 0: + self.PedalEchoCount += 1 + self.PedalEchoTime = time.time() + self.PreviousPedalEcho = self.PedalEcho + #----------------------------------------------------------------------- # Calculate Virtual speed applying the digital gearbox # if DOWN has been pressed, we pretend to be riding slower than the @@ -714,18 +729,15 @@ def __init__(self, clv): def Refresh (self, _QuarterSecond=None, _TacxMode=None): if debug.on(debug.Function):logfile.Write ("clsSimulatedTrainer.Refresh()") # ---------------------------------------------------------------------- - # Trigger for pedalstroke analysis + # Trigger for pedalstroke analysis (PedalEcho) + # Data for Speed and Cadence Sensor (-Time and -Count) # ---------------------------------------------------------------------- - try: - _ = self.__LastPedalEcho - except: - self.__LastPedalEcho = time.time() - - if self.Cadence and (time.time() - self.__LastPedalEcho) > 60 / self.Cadence: - self.__LastPedalEcho= time.time() - self.PedalEcho = 1 + if self.Cadence and (time.time() - self.PedalEchoTime) > 60 / self.Cadence: + self.PedalEchoTime = time.time() + self.PedalEcho = 1 + self.PedalEchoCount += 1 else: - self.PedalEcho = 0 + self.PedalEcho = 0 # ---------------------------------------------------------------------- # Randomize figures @@ -760,7 +772,6 @@ def Refresh (self, _QuarterSecond=None, _TacxMode=None): self.Cadence *= (1 + random.randint(-2,2) / 100) # Variation of 2% self.SpeedKmh = 35 * self.Cadence / 100 # Speed is 35 kmh at cadence 100 (My highest gear) - self.VirtualSpeedKmh= self.SpeedKmh self.HeartRate = HRmax * (0.5 + ((self.CurrentPower - 100) / (ftp - 100) ) * 0.3) # As if power is linear with power @@ -771,6 +782,8 @@ def Refresh (self, _QuarterSecond=None, _TacxMode=None): if self.HeartRate > HRmax: self.HeartRate = HRmax # maximize HR self.HeartRate += random.randint(-5,5) # Variation of heartrate by 5 beats + self.VirtualSpeedKmh= self.SpeedKmh + #------------------------------------------------------------------------------- # c l s T a c x A n t V o r t e x T r a i n e r #------------------------------------------------------------------------------- @@ -1533,9 +1546,19 @@ def SendToTrainerUSBData(self, TacxMode, Calibrate, PedalEcho, Target, Weight): def _ReceiveFromTrainer(self): if debug.on(debug.Function):logfile.Write ("clsTacxNewUsbTrainer._ReceiveFromTrainer()") #----------------------------------------------------------------------- - # Read from trainer + # Read from trainer + # 64 bytes are expected + # 48 bytes are returned by some trainers + # 24 bytes are sometimes returned (T1932, Gui Leite 2020-06-11) and + # seem to be incomplete buffers and are ignored. #----------------------------------------------------------------------- - data = self.USB_Read() + retry = 4 + data = [] + while retry and len(data) < 40: + data = self.USB_Read() + retry -= 1 + + if len(data) < 40: logfile.Console('Tacx returns insufficient data, len=%s' % len(data)) #----------------------------------------------------------------------- # Define buffer format