-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
V12 won't connect Kickr Core on bluetooth #11
Comments
/*********************************************************************
This is programming code for ESP32 Espressif Wroom boards
Tested with Adafruit Feather ESP32 V2 a.k.a. Huzzah
The code uses heavily the supplied ESP32 NimBLE libraries !!
see: https://github.com/h2zero/NimBLE-Arduino
Many have invested time and resources providing open source code!
MIT license, check LICENSE for more information
All text must be included in any redistribution
*********************************************************************/
/* -----------------------------------------------------------------------------------------------------
* This code should work with all indoor cycling trainers that fully support,
* Wahoo Kickr, Cycling Power Service and Cycling Speed & Cadence Service
* ------------------------------------------------------------------------------------------------------
*
* The code links a BLE Server (a peripheral to Zwift) and a BLE Client (a central to the Trainer) with a bridge
* in between, the ESP32 being man-in-the-middle (MITM). The ESP32 is an integral part of the Simcline,
* that interprets the exchanged road grade and moves the front wheel up and down with the change in inclination.
* The ESP32-bridge can control, filter and alter the bi-directional interchanged data!
* The client-side (central) scans and connects with the trainer relevant services: CPS + Wahoo and CSC (optional).
* It collects all cyling data of the services and passes these on to the server-side....
* The client-side supplies the indoor trainer with target and resistance control data.
* The server-side (peripheral) advertises and enables connection with cycling apps like Zwift and collects the app's
* control commands, target and resistance data. It passes these on to the client-side....
* The server-side supplies the app with the generated cycling data in return.
*
* The client plus server (MITM) are transparent to the Wahoo trainer as well as to the training app Zwift or alike!
*
* Requirements: Zwift app or alike, operational Simcline and a Wahoo Kickr trainer
* 0) Upload and Run this code on the Simcline (i.c. ESP32 board)
* 1) Start the Serial Monitor to catch debugging info and check the Oled display
* 2) The code will do basic testing of mechanical parts and sensors
* 3) Start/Power On the indoor trainer
* 4) Simcline and trainer (with <name>) will pair as reported in the output
* 5) Start Zwift on your computer or tablet and wait....
* 6) Search on the Zwift pairing screens for the Simcline a.k.a. <SIM32>
* 7) Pair: Power, Cadence and Controllable one after another with <SIM32>
* 8) Optionally one can pair as well devices for heartrate and/or steering (Sterzo)
* 9) Start the default Zwift ride or any ride you wish
* 10) Make Serial Monitor output window visible on top of the Zwift window
* 11) Hop on the bike: do the work and feel resistance change with the road
* 12) Inspect the info presented by Serial Monitor.....
*
* This device is identified with the name <SIM32>. You will see this only when connecting to Zwift on the
* pairing screens! Notice: Zwift extends device names with additional numbers for identification!
*
*/
/*
Version 1.0
Changed Stack Depth values from 2048 to 4096 for Server Control Point Indicate (modified) and Write w Response
Version 1.1
Inserted check (boolean) on Control Point Write-Response out of synch...
Version 1.2
Changed device identification naming to a simpler scheme: SIM32 or SIM52 instead of <SIM DevName>
Server Characteristic values (read only) are now updated when a new client connection is established
NimBLE registerForNotify() has been deprecated and is replaced with subscribe() / unsubscribe()
*/
// -------------------------------------------------------------------------------------------
// COMPILER DIRECTIVE to allow/suppress DEBUG messages that help debugging...
// Uncomment general "#define DEBUG" to activate
#define DEBUG
// Include these debug utility macros in all cases!
#include "DebugUtils.h"
#ifdef DEBUG
// Restrict activating one or more of the following DEBUG directives --> process intensive
// The overhead can lead to spurious side effects and a loss of quality of service handling!!
//#define DEBUG_CP_MEASUREMENT // If defined allows for parsing and decoding the Cycling Power Data
//#define DEBUG_CSC_MEASUREMENT // If defined allows for parsing and decoding the Cycling Speed and Cadence Data
//#define DEBUG_WAHOO_CONTROLPOINT_RESPONSE // If defined allows for parsing the Data
//#define DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA // If defined allows for parsing and decoding Data
//#define MOVEMENTDEBUG
#endif
// --------------------------------------------------------------------------------------------
#ifndef ADAFRUIT_FEATHER_ESP32_V2
#define ADAFRUIT_FEATHER_ESP32_V2
#endif
// Exponential EMA ALPHA filter definition
// Used to filter sequence of actuator target positions --> minimize consecutive small up/down movements
// Should be between low (10-40) is maximal and high (50-90) is minimal filtering
// Uncomment "#define EMA_ALPHA" to activate
//#define EMA_ALPHA 60 // Value is in percentage 0-99.
#define BLE_APPEARANCE_GENERIC_CYCLING 1152
#include <NimBLEDevice.h>
// We need this for setting the Server-side Generic Access Char's --> Appearance and DeviceName
#include <nimble/nimble/host/services/gap/include/services/gap/ble_svc_gap.h>
#define THISDEVICENAME "SIM32" // Shortname
const uint8_t MAX_PAYLOAD = 20; // Max 20 byte data size for single packet BLE transfer
// Struct containing Device info to administer dis/connected devices
typedef struct
{
uint8_t PeerAddress[6];
std::string PeerName;
uint16_t conn_handle;
bool IsConnected;
} Device_info_t;
// -----------------------------------------------------------------
// Your hardware MAC/DEVICE ADDRESSES
// Laptop/Desktop Device Address that runs Zwift, in printed format: [00:01:02:03:04:05]
// NimBLE demands you to enter addresses here in Little Endian format (reversed order)
#define LAPTOPADDRESS {0xbc,0xa0,0xec,0x7b,0x57,0xd0} // Little Endian format!!
// Trainer Wahoo Device Address, in printed format: [00:01:02:03:04:05]
#define TRAINERADDRESS {0xcf,0xc4,0x8c,0x1a,0x39,0xe5} // Little Endian format!!
// -----------------------------------------------------------------
// Initialize connectable device registration
Device_info_t Trainer = {TRAINERADDRESS, "MyTrainer", BLE_HS_CONN_HANDLE_NONE, false};
Device_info_t Laptop = { LAPTOPADDRESS, "MyLaptop" , BLE_HS_CONN_HANDLE_NONE, false};
Device_info_t Smartphone = { {0x00}, "MyPhone" , BLE_HS_CONN_HANDLE_NONE, false};
// ----------------------------------------------------------------------------------
// Client Generic Access --------------------------------------------------------------
#define UUID16_SVC_GENERIC_ACCESS BLEUUID((uint16_t)0x1800)
#define UUID16_CHR_DEVICE_NAME BLEUUID((uint16_t)0x2A00)
#define UUID16_CHR_APPEARANCE BLEUUID((uint16_t)0x2A01)
#define UUID16_CHR_PERIPHERAL_PREFERRED_CONNECTION_PARAMETERS BLEUUID((uint16_t)0x2A04)
#define UUID16_CHR_CENTRAL_ADDRESS_RESOLUTION BLEUUID((uint16_t)0x2AA6)
BLERemoteService* pRemote_GenericAccess_Service;
BLERemoteCharacteristic* pRemote_GA_Appearance_Chr; // Read
uint16_t client_GA_Appearance_Value = BLE_APPEARANCE_GENERIC_CYCLING; // Default decimal: 1152 -> Generic Cycling
BLERemoteCharacteristic* pRemote_GA_DeviceName_Chr;// Read, Write
std::string client_GA_DeviceName_Str = THISDEVICENAME;
// Client Service Device Information --------------------------------------------------
#define UUID16_SVC_DEVICE_INFORMATION BLEUUID((uint16_t)0x180A)
#define UUID16_CHR_MODEL_NUMBER_STRING BLEUUID((uint16_t)0x2A24)
#define UUID16_CHR_SERIAL_NUMBER_STRING BLEUUID((uint16_t)0x2A25)
#define UUID16_CHR_MANUFACTURER_NAME_STRING BLEUUID((uint16_t)0x2A29)
/*
#define UUID16_CHR_FIRMWARE_REVISION_STRING BLEUUID((uint16_t)0x2A26)
#define UUID16_CHR_HARDWARE_REVISION_STRING BLEUUID((uint16_t)0x2A27)
#define UUID16_CHR_SOFTWARE_REVISION_STRING BLEUUID((uint16_t)0x2A28)
*/
BLERemoteService* pRemote_DeviceInformation_Service;
BLERemoteCharacteristic* pRemote_DIS_ManufacturerName_Chr; // Read
std::string client_DIS_Manufacturer_Str;
BLERemoteCharacteristic* pRemote_DIS_ModelNumber_Chr; // Read
std::string client_DIS_ModelNumber_Str;
BLERemoteCharacteristic* pRemote_DIS_SerialNumber_Chr; // Read
std::string client_DIS_SerialNumber_Str;
// -------------------------------------------------------------------------------------
BLEService *server_DeviceInformation_Service;
BLECharacteristic *server_DIS_ModelNumber_Chr; // Read
BLECharacteristic *server_DIS_SerialNumber_Chr; // Read
BLECharacteristic *server_DIS_ManufacturerName_Chr; // Read
/*
BLECharacteristic *server_DIS_Firmware_Chr; // Read
std::string client_DIS_Firmware_Str = "12345";
BLECharacteristic *server_DIS_Hardware_Chr; // Read
std::string client_DIS_Hardware_Str = "12345";
BLECharacteristic *server_DIS_Software_Chr; // Read
std::string client_DIS_Software_Str = "12345";
*/
//--------------------------------------------------------------------------------------
/* NORDIC UART SERVICE a.k.a. NUS
* NUS Service: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E
* NUS RXD : 6E400002-B5A3-F393-E0A9-E50E24DCCA9E
* NUS TXD : 6E400003-B5A3-F393-E0A9-E50E24DCCA9E
*/
BLEUUID UUID_NUS_SERVICE("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
BLEUUID UUID_NUS_CHR_RXD("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
BLEUUID UUID_NUS_CHR_TXD("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
BLEService *server_NordicUart_Service;
BLECharacteristic *server_NUS_Rxd_Chr; // Write No Response (Receiving Data)
BLECharacteristic *server_NUS_Txd_Chr; // Read Notify (Sending Data)
/* Cycling Power Service ---------------------------------------------------------------
* CP Service: 0x1818
* CP Characteristic: 0x2A63 (Measurement) Mandatory
* CP Characteristic: 0x2A65 (Feature) Mandatory
* CP Characteristic: 0x2A5D (Location) Optional
* CP Characteristic: 0x2A66 (Control Point) Optional
*/
#define UUID16_SVC_CYCLING_POWER BLEUUID((uint16_t)0x1818)
#define UUID16_CHR_CYCLING_POWER_MEASUREMENT BLEUUID((uint16_t)0x2A63)
//#define UUID16_CHR_CYCLING_POWER_VECTOR BLEUUID((uint16_t)0x2A64)
#define UUID16_CHR_CYCLING_POWER_FEATURE BLEUUID((uint16_t)0x2A65)
//#define UUID16_CHR_CYCLING_POWER_CONTROL_POINT BLEUUID((uint16_t)0x2A66)
#define UUID16_CHR_SENSOR_LOCATION BLEUUID((uint16_t)0x2A5D) // shared with CSC
BLERemoteService* pRemote_CyclingPower_Service;
BLERemoteCharacteristic* pRemote_CP_Measurement_Chr; // Notify, Read
BLERemoteCharacteristic* pRemote_CP_Feature_Chr; // Read
uint32_t client_CP_Feature_Flags = 0;
BLERemoteCharacteristic* pRemote_CP_Location_Chr; // Read
uint8_t client_CP_Location_Value = {0x0C}; // --> rear wheel !
BLERemoteCharacteristic* pRemote_CP_ControlPoint_Chr; // Indicate, Write
/* ---------------------------------------------------------------------------------------------------------------
* Wahoo Proprietary Control Point Characteristic
* ---------------------------------------------------------------------------------------------------------------*/
// Proprietary Wahoo Trainer Control Point Characteristic is part of the Cycling Power Service !
static BLEUUID UUID16_CHR_WAHOO_CONTROL_POINT("A026E005-0A7D-4AB3-97FA-F1500F9FEB8B");
static BLERemoteCharacteristic* pRemote_Wahoo_ControlPoint_Chr; // Wahoo Control Point, optional, write & indicate
bool client_Wahoo_ControlPoint_Response = true; // CP Write-Response synch test
// -------------------------------------- SERVER -------------------------------------------------
BLEService *server_CyclingPower_Service;
BLECharacteristic *server_CP_Measurement_Chr; // Notify, Read
BLECharacteristic *server_CP_Feature_Chr; // Read
BLECharacteristic *server_CP_Location_Chr; // Read
//BLECharacteristic *server_CP_ControlPoint_Chr; // Indicate, Write
// Proprietary Wahoo Trainer Control Point Characteristic is part of the Cycling Power Service !
BLECharacteristic *server_Wahoo_ControlPoint_Chr; // Fitness Machine Control Point, optional, write & indicate
// ---------------------------------------------------------------------------------------
const uint8_t client_CP_Feature_Len = 20; // Num. of Feature elements
const char* client_CP_Feature_Str[client_CP_Feature_Len] = {
"Pedal power balance supported",
"Accumulated torque supported",
"Wheel revolution data supported",
"Crank revolution data supported",
"Extreme magnitudes supported",
"Extreme angles supported",
"Top/bottom dead angle supported",
"Accumulated energy supported",
"Offset compensation indicator supported",
"Offset compensation supported",
"Cycling power measurement characteristic content masking supported",
"Multiple sensor locations supported",
"Crank length adj. supported",
"Chain length adj. supported",
"Chain weight adj. supported",
"Span length adj. supported",
"Sensor measurement context",
"Instantaineous measurement direction supported",
"Factory calibrated date supported",
"Enhanced offset compensation supported" };
const char* client_Sensor_Location_Str[] = { "Other", "Top of shoe", "In shoe", "Hip",
"Front wheel", "Left crank", "Right crank", "Left pedal", "Right pedal", "Front hub",
"Rear dropout", "Chainstay", "Rear wheel", "Rear hub", "Chest", "Spider", "Chain ring"};
/*---------------------------------------------------------------------------------------
* Cycling Speed and Cadence Service
* CSC Service: 0x1816
* CSC Measurement Characteristic: 0x2A5B
* CSC Feature Characteristic: 0x2A5C
*/
#define UUID16_SVC_CYCLING_SPEED_AND_CADENCE BLEUUID((uint16_t)0x1816)
//#define UUID16_CHR_CSC_CONTROL_POINT BLEUUID((uint16_t)0x2A55)
#define UUID16_CHR_CSC_MEASUREMENT BLEUUID((uint16_t)0x2A5B)
#define UUID16_CHR_CSC_FEATURE BLEUUID((uint16_t)0x2A5C)
//#define UUID16_CHR_SENSOR_LOCATION BLEUUID((uint16_t)0x2A5D) // shared with CP
BLERemoteService* pRemote_CyclingSpeedCadence_Service;
BLERemoteCharacteristic* pRemote_CSC_Measurement_Chr; // Notify, Read
BLERemoteCharacteristic* pRemote_CSC_Feature_Chr; // Read
const uint8_t CSC_FEATURE_FIXED_DATALEN = 2; // UINT16
uint16_t client_CSC_Feature_Flags = 0;
const uint8_t client_CSC_Feature_Len = 3;
const char* client_CSC_Feature_Str[client_CSC_Feature_Len] = {"Wheel rev supported", "Crank rev supported", "Multiple locations supported"};
BLERemoteCharacteristic* pRemote_CSC_Location_Chr; // Read
uint8_t client_CSC_Location_Value = {0x0C}; // Default --> rear wheel !
// ---------------------------------------------------------------------------------------
BLEService *server_CyclingSpeedCadence_Service;
BLECharacteristic *server_CSC_Measurement_Chr; // Notify, Read
BLECharacteristic *server_CSC_Feature_Chr; // Read
BLECharacteristic *server_CSC_Location_Chr; // Read
// ---------------------------------------------------------------------------------------
/**
* Wahoo Trainer Proprietary Control Point opcodes in decimal
*
* LSO: uint8 Op Code
* MSO: 0..18 octets Parameters
*/
const uint8_t unlock = 32;
const uint8_t setResistanceMode = 64;
const uint8_t setStandardMode = 65;
const uint8_t setErgMode = 66;
const uint8_t setSimMode = 67;
const uint8_t setSimCRR = 68;
const uint8_t setSimWindResistance = 69;
const uint8_t setSimGrade = 70;
const uint8_t setSimWindSpeed = 71;
const uint8_t setWheelCircumference = 72;
const uint8_t unlockCommand[3] = {unlock, 0xEE, 0xFC}; // Unlock codes
uint8_t client_HR_Location_Value= { 0x01 }; // Chest
// --------------------------------------------------------------------------------------
BLEService *server_HeartRate_Service;
BLECharacteristic *server_HR_Measurement_Chr; // Notify Write
BLECharacteristic *server_HR_Location_Chr; // Read
// --------------------------------------------------------------------------------------
/**
* The Wahoo Proprietary Control Point data type structure
*
*/
const uint8_t WAHOO_CONTROL_POINT_DATALEN = 19; // Control point consists of 1 opcode (byte) and maximum 18 bytes as parameters
// This wahoocp_data_t structure represents the control point data. The first octet represents the opcode of the request
// followed by a parameter array of maximum 18 octects
typedef struct __attribute__( ( packed ) )
{
uint8_t OPCODE;
uint8_t OCTETS[(WAHOO_CONTROL_POINT_DATALEN-1)];
} wahoocp_data_t;
typedef union // The union type automatically maps the bytes member array to the wahoocp_data_t structure member values
{
wahoocp_data_t values;
uint8_t bytes[WAHOO_CONTROL_POINT_DATALEN];
} wahoocp_data_ut;
// Fitness Machine Control Point Data variable
wahoocp_data_ut server_Wahoo_Control_Point_Data;
// Wahoo Control Point: Response Buffers
const uint8_t WahooRespConfirm = 0x01; // Ok!
const uint8_t WahooRespUnknown = 0x02; // Unknown OpCode
const uint8_t WahooResponseCode= 0x00; // To be set later to the correct value
uint8_t WahooRespConfirmBuffer[2] = {WahooRespConfirm, WahooResponseCode};
uint8_t WahooRespUnknownBuffer[2] = {WahooRespUnknown, WahooResponseCode};
// Global variables for decoding of Control Point: INDOOR RESISTANCE PARAMETERS
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
float weight = 0; // Rider weight in kg
float rrc = 0; // Rolling Resistance Coefficient
float wrc = 0; // Wind Resistance Coefficient
#endif
float grade = 0; // percentage, resolution 0.01
// ----------------------------------------------------------------------------------------------
// ----------------------------------------------------------------------------------------------
BLEClient* pClient_Wahoo = nullptr;
BLEAdvertisedDevice* myDevice = nullptr;
BLEScan* pBLEScan = nullptr;
BLEServer* pServer = nullptr;
NimBLEAdvertising *pAdvertising = nullptr;
TaskHandle_t TaskWriteWithResponseHandle = NULL;
// These variables are handled in loop() to start sort of Callback functions
boolean doClientConnectCall = false;
boolean RestartScanningOnDisconnect = false;
boolean DoCallClientEnable = false;
boolean DoCallClientDisable = false;
// Values used to enable or disable notifications/indications
const uint8_t notificationOff[] = {0x0, 0x0};
const uint8_t notificationOn[] = {0x1, 0x0};
const uint8_t indicationOff[] = {0x0, 0x0};
const uint8_t indicationOn[] = {0x2, 0x0};
const bool indications = false; //false as first argument to subscribe to indications instead of notifications
const bool notifications = true; //true as first argument to subscribe to notifications
// ---------------------------------------------------------------------------------------
// Libraries for use of I2C devices (Oled and VL6180X distance sensor)
#include <SPI.h>
#include <Wire.h>
// Necessary libraries for use of Oled display(s)
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Additional splash screen bitmap and icon(s) for Oled
#include "Adafruit_SSD1306_Icons.h" // needs to be in the SAME (!) directory
// Declarations for an SSD1306 128x64 display connected to I2C (SDA, SCL pins)
#define SCREEN_WIDTH 128 // SSD1306-OLED display width, in pixels
#define SCREEN_HEIGHT 64 // SSD1306-OLED display height, in pixels
#define OLED_RESET -1 // No reset pin on this OLED display
#define OLED_I2C_ADDRESS 0x3C // I2C Address of OLED display
// Declare the display
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Declare Global var for OLED Display selection 1 (Cycling data) or 2 (Road Grade)
uint8_t OledDisplaySelection = 2; // default Road Grade to show
// LittleFS for internal storage of persistent data on the ESP32
#include <LittleFS.h>
using namespace fs;
// Managing persistence of some data with LittleFS system
// PeRSistent Data is written to a file with the name: PRS_FILENAME
#define PRS_FILENAME "/prsdata.txt"
// Minimum(!) Read and Write block sizes
#define LITTLEFS_BLOCK_SIZE 128
#define FORMAT_LITTLEFS_IF_FAILED true
// LittleFS--------------------------------------------------------------
//----------- Global variable definitions for high level movement control -----------------------------------------------
// In theory the RawgradeValue varies between 0 (equals -200% grade) and 40000 (equals +200% grade)
// SIMCLINE is mechanically working between -10% and +20% --> 19000 and 22000
//------------------------------------------------- WARNING --------------------------------------------------------------
//------------ SET THESE TWO VALUES IN ACCORDANCE WITH THE MECHANICAL RANGE LIMITATIONS OF YOUR SIMCLINE !!! -------------
// Raw Grade Value Minimally (Mechanically: the lowest position of wheel axis) 19000 is equiv. of 10% downhill road grade
#define RGVMIN 19500 // -5% // Always is RGVMIN < 20000 (flat road level)
// Raw Grade Value Maximally (Mechanically: the highest position of wheel axis) 22000 is equiv. of 20% uphill road grade
#define RGVMAX 22000 // 20% // +20% // Always is RGVMAX > 20000 (flat road level)
//------------------------------------------------- WARNING --------------------------------------------------------------
// Correction for measuring plane difference and midth wheel axis position (1 cm offset is an MEASUREOFFSET of about 40)
#define MEASUREOFFSET 50 // about 1.25 cm
// Raw Grade Value Minimally (Mechanically: the lowest position of wheel axis) 19000 is equiv. of 10% downhill road grade
// These values are derived from the above RGVMIN and RGVMAX settings
#define RGVMIN_GRADE (20000-RGVMIN)/100 // Notice: positive value of the Minimal downhill grade!
#define RGVMAX_GRADE (RGVMAX-20000)/100 // Notice: positive value of the Maximal uphill grade!
// Besides what is mechanically possible there are also limits in what is physically pleasant
// Keep the following aRGVMin and aRGVMax values within the limits of the mechanically feasible values of above !!!
// DEFAULT Minimally Allowed Raw Grade Value that should not be exceeded: -5%! -> Descent grade Limit
int aRGVmin = 19500;
// DEFAULT Maximally Allowed Raw Grade Value that should not be exceeded: 15%! -> Ascent grade limit
int aRGVmax = 21500;
// Value for a flat road equals 0% grade or a RGV of 20000; result needs to be corrected for the measure offset
long RawgradeValue = (20000 - MEASUREOFFSET);
int GradeChangeFactor = 100; // 100% means no effect, 50% means only halved up/down steps--> Road Grade Change Factor
// The Grade Percentage of a road is defined as a measure of the road's steepness as it rises and falls along its route
float gradePercentValue = 0;
//-----------------------------------------------------------------
/* ------------------------------------------------------------------------------------------------
* Warning I/O Pins can have identical board position but different I/O Pin declarations for
* connection with the pins of the Motor driver board
* ADAFRUIT_FEATHER_ESP32_V2 is nearly pin compatible with ARDUINO_NRF52840_FEATHER
*/
#ifdef ADAFRUIT_FEATHER_ESP32_V2
#define actuatorOutPin1 A0 // --> A0/P0.02 connected to pin IN2 of the DRV8871 Motor Driver board
#define actuatorOutPin2 A1 // --> A1/P0.03 connected to pin IN1 of the DRV8871 Motor Driver board
#endif
// -------------------------- WARNING ------------------------------------------------------------
// The following VL6180X sensor values are a 100% construction specific and
// should be experimentally determined, when the Actuator AND the VL6180X sensor are mounted!
// ------>>>> Test manually and use example/test sketches that go with the VL6180X sensor! <<<<---
// Microswitches should limit physically/mechanically the upper and lower position of the Actuator!
// The microswitches are mechanically controlled, and NOT by the software --> should be fail safe!
// Notice that unrestricted movement at the boundaries can damage the Actuator and/or construction!
// The following values are respected by the software and will (in normal cases!) never be exceeded!
#define MINPOSITION 35 // 265 // VL6180X highest value top microswitch activated to mechanically stop operation
#define MAXPOSITION 315 // 535 // VL6180X lowest value bottom microswitch activated to mechanically stop operation
// -------------------------- WARNING ------------------------------------------------------------
// Operational boundaries of the VL6180X sensor are used/set in class Lifter after calling its "init".
// A safe measuring range of at least 30 cm of total movement is recommended for the VL6180X sensor setting!
//
// Bandwidth is used in the code to take measuring errors and a safe margin into account when reaching
// the above defined max or min positions of the construction! The software does painstakingly respect
// these and is independent of the appropriate working of the microswitches when reaching the boundaries!
// These microswitches are a SECOND line of defence against out of range and potentially damaging movement!
#define BANDWIDTH 4
// Library code for low level measuring (VL6180X) and controlling UP and down movement
#include <Lifter.h>
// Decalaration of Lifter Class for control of the low level up/down movement
Lifter lift;
// Global variables for Lifter position control --> RawGradeValue has been defined/set previously to flat road level!!
int16_t TargetPosition = map(RawgradeValue, RGVMIN, RGVMAX, MAXPOSITION, MINPOSITION);
bool IsBasicMotorFunctions = false; // Mechanical motor functions
// -------------- xControlUpDownMovement task definitions --------------------------
SemaphoreHandle_t xSemaphore = NULL;
TaskHandle_t ControlTaskHandle = NULL;
// Set Arduino IDE Tools Menu --> Events Run On: "Core 1"
// Set Arduino IDE Tools Menu --> Arduino Runs On: "Core 1"
// Run xControlUpDownMovement on "Core 0"
const BaseType_t xControlCoreID = 0;
void xControlUpDownMovement(void *arg);
// ---------------------------------------------------------------------------------
// Client Connect and Disconnect callbacks defined
class client_Connection_Callbacks:public BLEClientCallbacks {
void onConnect(BLEClient* pClient);
void onDisconnect(BLEClient* pClient);
bool onConnParamsUpdateRequest(BLEClient* pClient, ble_gap_upd_params *params);
};
// Server Connect and Disconnect callbacks defined
class server_Connection_Callbacks:public BLEServerCallbacks {
void onConnect(BLEServer* pServer, ble_gap_conn_desc* desc);
void onDisconnect(BLEServer* pServer, ble_gap_conn_desc* desc);
void onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc);
};
// Handler class for Server Multi Characteristic action(s) defined
class CharacteristicCallbacks:public NimBLECharacteristicCallbacks {
/* We only define onSubscribe !!!
void onRead(NimBLECharacteristic* pCharacteristic);
void onWrite(NimBLECharacteristic* pCharacteristic);
void onNotify(NimBLECharacteristic* pCharacteristic);
void onStatus(NimBLECharacteristic* pCharacteristic, Status status, int code);
*/
void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue);
};
// Define CharacteristicCallbacks instance(s) globally to use for multiple Server Characteristics
static CharacteristicCallbacks server_Multi_Chr_Callbacks;
bool getPRSdata(void);
void setPRSdata(void);
void ConvertMacAddress(char *fullAddress, uint8_t addr[6], bool NativeFormat);
#ifdef EMA_ALPHA
int16_t EMA_TargetPositionFilter(int16_t current_value);
#endif
void ShowIconsOnTopBar(void);
void ShowOnOledLarge(const char* Line1, const char* Line2, const char* Line3, uint16_t Pause);
void ShowSlopeTriangleOnOled(void);
void SetManualGradePercentValue(void);
void SetNewRawGradeValue(float RoadGrade);
void SetNewActuatorPosition(void);
void server_setupGA(void);
void server_setupDIS(void);
void server_setupNUS(void);
void server_setupCSC(void);
void server_setupCPS(void);
void server_startADV(void);
void client_Start_Scanning(void);
bool client_Connect_Callback(void);
// ---------------------------------------------------------------------------------
void setup() {
#ifdef DEBUG
Serial.begin(115200);
while (!Serial) delay(10);
Serial.flush();
delay(1000); // Give Serial I/O time to settle
#endif
/* The Feather ESP32 V2 has a NEOPIXEL_I2C_POWER pin that must be pulled HIGH
* to enable power to the STEMMA QT port. Without it, the QT port will not work!
*/
#ifdef ADAFRUIT_FEATHER_ESP32_V2
// Turn on the I2C power on Stemma connector by pulling pin HIGH.
pinMode(NEOPIXEL_I2C_POWER, OUTPUT);
digitalWrite(NEOPIXEL_I2C_POWER, HIGH);
#endif
DEBUG_PRINTLN("");
DEBUG_PRINTLN("ESP32 NimBLE SIMCLINE supporting: CPS + Wahoo and CSC (optional)");
DEBUG_PRINTLN("------------------------ Version 01.2 --------------------------");
DEBUG_PRINTLN(THISDEVICENAME);
delay(200);
// LittleFS start the Littlefilesystem lib and see if we have persistent data ----
// This opens LittleFS with a root subdirectory /littlefs/
LittleFS.begin();
// WARNING --------- Uncomment only when strictly necessary!!! ---------
// Uncomment only the very first time the Simcline code is executed!!!
/* This creates LittleFS with a root subdirectory /littlefs/
LittleFS.format();
DEBUG_PRINTLN("Wipe out all persistent data, including any file(s)....");
*/
// Get or set (first time only) the values of relevant and crucial variables
// to persistence, whith the Companion App the user can set these on the fly!
// Get or set the values of aRGVmax, aRGVmin, GradeChangeFactor in PRSdata.
if (!getPRSdata()) {
setPRSdata();
}
// LittleFS------------------------------------------------------------------------
// Start the show for the Oled display
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDRESS)) {
DEBUG_PRINTLN(F("SSD1306 OLED display allocation failed!"));
} else {
DEBUG_PRINTLN(F("SSD1306 OLED display is running..."));
// Load Oled with initial display buffer contents on the screen,
// the SSD1306 library initializes with a Adafruit splash screen,
// (respect or edit the splash.h in the library).
display.display(); // Acknowledge Adafruit rights, license and efforts
delay(500); // show some time
}
// Ready to show our own SIMCLINE splash screen
display.clearDisplay(); // clean the oled screen
display.setTextColor(SSD1306_WHITE);
display.drawBitmap(24, 0, Mountain_bw_79x64, 79, 64, 1);
display.display();
delay(2000); // Take somewhat more time.....
//Show Name and SW version on Oled
ShowOnOledLarge("SIMCLINE", "Wahoo", "v01.0", 500);
// Initialize Lifter Class data, variables, test and set to work !
lift.Init(actuatorOutPin1, actuatorOutPin2, MINPOSITION, MAXPOSITION, BANDWIDTH);
// Test Actuator and VL8106X for proper functioning
ShowOnOledLarge("Testing", "Up & Down", "Functions", 100);
if (!lift.TestBasicMotorFunctions()) {
ShowOnOledLarge("Testing", "Functions", "Failed!", 500);
IsBasicMotorFunctions = false; // Not working properly
DEBUG_PRINTLN("Simcline >> ERROR << Basic Motor Funtions are NOT working!!");
} else {
ShowOnOledLarge("Testing", "Functions", "Done!", 500);
// Is working properly --> Start Motor Control Task
xSemaphore = xSemaphoreCreateBinary();
xTaskCreatePinnedToCore(xControlUpDownMovement, "xControlUpDownMovement", 4096, NULL, 10, &ControlTaskHandle, xControlCoreID);
xSemaphoreGive(xSemaphore);
DEBUG_PRINTLN("Motor Control Task Created and Active!");
IsBasicMotorFunctions = true;
DEBUG_PRINTLN("Simcline Basic Motor Funtions are working!!");
// Put Simcline in neutral: flat road position
#ifdef EMA_ALPHA
// Init EMA filter at first call with flat road position as reference
TargetPosition = EMA_TargetPositionFilter(TargetPosition);
#endif
SetNewActuatorPosition();
}
// Initialize NimBLE with maximum connections as Peripheral = 1, Central = 1
BLEDevice::init(THISDEVICENAME); // Shortname
// Start the Server-side now!
pServer = BLEDevice::createServer();
//Setup callbacks onConnect and onDisconnect
pServer->setCallbacks(new server_Connection_Callbacks());
// Set server auto-restart advertise on
pServer->advertiseOnDisconnect(true);
// Server setup
DEBUG_PRINTLN("Configuring the default Generic Access Service");
server_setupGA();
DEBUG_PRINTLN("Configuring the Server Nordic Uart Service");
server_setupNUS();
DEBUG_PRINTLN("Configuring the Server Device Information Service");
server_setupDIS();
DEBUG_PRINTLN("Configuring the Server Cycle Power Service");
server_setupCPS();
DEBUG_PRINTLN("Configuring the Server Cadence and Speed Service");
server_setupCSC();
DEBUG_PRINTLN("Setting up the Server advertising payload(s)");
server_startADV();
//BLEDevice::stopAdvertising();
DEBUG_PRINTLN("Server is advertising: CPS + Wahoo");
// Start the Client-side!
client_Start_Scanning();
while(pBLEScan->isScanning()) ; // Wait until scanning is finished
if(doClientConnectCall) {
doClientConnectCall = false;
bool dummy = client_Connect_Callback();
}
if(!Trainer.IsConnected) {
DEBUG_PRINTLN(">>> Failed to connect Trainer! Reset ESP32 and try again!");
while(1) {delay(100);}
}
//BLEDevice::startAdvertising();
//DEBUG_PRINTLN("Server is advertising: CPS + Wahoo");
} // End of setup.
// LittleFS --------------------------------------------------
bool getPRSdata(void) { // aRGVmax, aRGVmin, GradeChangeFactor -> PRSdata
if (LittleFS.exists(PRS_FILENAME)) {
File file = LittleFS.open(PRS_FILENAME, FILE_READ);
if (file) {
uint32_t readLen;
uint8_t buffer[LITTLEFS_BLOCK_SIZE+1] = { 0 };
readLen = file.read(buffer, LITTLEFS_BLOCK_SIZE);
buffer[readLen] = 0; // set the values to the pointed by variables
sscanf((char*)buffer, "%d %d %d %d", &aRGVmax, &aRGVmin, &GradeChangeFactor, &OledDisplaySelection);
DEBUG_PRINT(F("ESP32 internally Got persistent storage from: /littlefs/PRSdata -> "));
DEBUG_PRINTF("Max: %d Min: %d Perc.: %d Displ.: %d\n", aRGVmax, aRGVmin, GradeChangeFactor, OledDisplaySelection);
file.close();
return true;
}
}
return false;
}
void setPRSdata(void) { // aRGVmax, aRGVmin, GradeChangeFactor -> PRSdata
uint8_t buffer[LITTLEFS_BLOCK_SIZE+1] = { 0 };
File file = LittleFS.open(PRS_FILENAME, FILE_WRITE);
if (file) {
sprintf((char*)buffer, "%d %d %d %d", aRGVmax, aRGVmin, GradeChangeFactor, OledDisplaySelection);
file.write(buffer, LITTLEFS_BLOCK_SIZE);
file.close();
DEBUG_PRINT(F("ESP32 internally Set new values in persistent storage to: /littlefs/PRSdata -> "));
DEBUG_PRINTLN((char*)buffer);
}
}
// LittleFS --------------------------------------------------
void ConvertMacAddress(char *fullAddress, uint8_t addr[6], bool NativeFormat)
{ // Display byte by byte in HEX
if(NativeFormat) { // Unaltered: in Little Endian machine-representation
sprintf(fullAddress, "%02X:%02X:%02X:%02X:%02X:%02X", addr[0], addr[1], addr[2], \
addr[3], addr[4], addr[5], HEX);
} else { // Altered: In reversed order
sprintf(fullAddress, "%02X:%02X:%02X:%02X:%02X:%02X", addr[5], addr[4], addr[3], \
addr[2], addr[1], addr[0], HEX);
}
};
void ShowIconsOnTopBar(void) {
// Show Icons on Top Bar
if (Trainer.IsConnected) { // show icon
display.drawBitmap(112, 0, power_icon16x16, 16, 16, 1);
}
if (Laptop.IsConnected) { // show icon
display.drawBitmap(0, 0, zwift_icon16x16, 16, 16, 1);
}
if (Smartphone.IsConnected) { // show icon Phone
display.drawBitmap(0, 0, mobile_icon16x16, 16, 16, 1);
}
}
void ShowOnOledLarge(const char* Line1, const char* Line2, const char* Line3, uint16_t Pause) {
// Clear and set Oled to display 3 line info -> centered
int pos = 1;
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
// ShowIconsOnTopBar();
display.setTextSize(2); // Large characters 11 pixels wide
if (Line1) {
pos = round( (127 - (12 * strlen(Line1))) / 2 );
display.setCursor(pos, 2); // 16
display.print(Line1);
}
if (Line2) {
pos = round( (127 - (12 * strlen(Line2))) / 2 );
display.setCursor(pos, 22); // 16
display.print(Line2);
}
if (Line3) {
pos = round( (127 - (12 * strlen(Line3))) / 2 );
display.setCursor(pos, 44); // 16
display.print(Line3);
}
display.display();
delay(Pause); // Pause indicated time in ms
}
void ShowSlopeTriangleOnOled(void) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
ShowIconsOnTopBar();
display.setCursor(102, 10); //26
display.setTextSize(2);
display.print(F("%"));
char tmp[7];
dtostrf(gradePercentValue, 5, 1, tmp); // show sign only if negative
display.setCursor(10, 6); // 22
display.setTextSize(3);
display.print(tmp);
// The following calculations give more "weight" to lower grade values
// (like: 1.2% or 0.5%), these will occur more often in practice and are not well
// displayable at 128*64! --> 64 * 64 = 4096 and this value should not be
// exceeded (4096/20) = 204.8
int pos = 64 - int(sqrt(abs(204 * gradePercentValue))); // cast to int to get rid of decimals only now!
if (gradePercentValue > 0) {
display.fillTriangle( 1, 63, 127, 63, 127, pos, SSD1306_INVERSE);
} else {
display.fillTriangle( 127, 63, 1, 63, 0, pos, SSD1306_INVERSE);
}
// Draw the baseline to smooth small decimal values and show flat road case
display.drawFastHLine(1, 63, 127, SSD1306_WHITE);
display.display();
}
// ---------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------
#ifdef EMA_ALPHA
int16_t EMA_TargetPositionFilter(int16_t current_value) {
static int16_t exponential_average = current_value;
exponential_average = int16_t( (EMA_ALPHA * (uint32_t)current_value + (100 - EMA_ALPHA) * (uint32_t)exponential_average) / 100 );
return exponential_average;
}
#endif
void SetManualGradePercentValue(void)
{
gradePercentValue = float( (RawgradeValue - 20000 + MEASUREOFFSET) )/100;
SetNewActuatorPosition();
}
void SetNewRawGradeValue(float RoadGrade)
{
// ----- Recalculate to relevant values for this project ------
// Take into account the allowed Increase Percentage of the inclination
// 100% has no effect, 50% means every increase or decrease is divided by 2
// --> the increase or decrease of inclination is in 50% smaller steps...
gradePercentValue = RoadGrade;
RawgradeValue = (long)(RoadGrade*100) + 20000;
RawgradeValue = 20000 + long((RawgradeValue - 20000) * GradeChangeFactor / 100);
// in steps of 0.01% and with an offset of -200%
// gradeValue gradePercentValue
// 0 -200%
// 19000 -10%
// 20000 0%
// 22000 +20%
// 40000 +200%
// -------------------------------------
// Take into account the measuring offset
RawgradeValue = RawgradeValue - MEASUREOFFSET;
// Test for Maximally en Minimally Allowed Raw Grade Values ----------------------------------------
if (RawgradeValue < aRGVmin) {
RawgradeValue = aRGVmin; // Do not allow lower values than aRGVmin !!
}
if (RawgradeValue > aRGVmax) {
RawgradeValue = aRGVmax; // Do not allow values to exceed aRGVmax !!
}
// --------------------------------------------------------------------------------------------------
DEBUG_PRINTF("Set Simcline to Percentage: %02.1f %% RawgradeValue: %05d \n", gradePercentValue, RawgradeValue);
}
void SetNewActuatorPosition(void) {
// Handle mechanical movement i.e. wheel position in accordance with Road Inclination
// Map RawgradeValue ranging from 0 to 40.000 on the
// TargetPosition (between MINPOSITION and MAXPOSITION) of the Lifter
// Notice 22000 is equivalent to +20% incline and 19000 to -10% incline
RawgradeValue = constrain(RawgradeValue, RGVMIN, RGVMAX); // Keep values within the safe range
TargetPosition = map(RawgradeValue, RGVMIN, RGVMAX, MAXPOSITION, MINPOSITION);
// EMA filter for smoothing quickly fluctuating Target Position values see: Zwift Titan Grove
#ifdef EMA_ALPHA
TargetPosition = EMA_TargetPositionFilter(TargetPosition);
#endif
if(IsBasicMotorFunctions) {
xSemaphoreTake(xSemaphore, portMAX_DELAY);
lift.SetTargetPosition(TargetPosition);
xSemaphoreGive(xSemaphore);
#ifdef MOVEMENTDEBUG
DEBUG_PRINTF("RawgradeValue: %05d Grade percent: %03.1f%% ", RawgradeValue, gradePercentValue);
DEBUG_PRINTF("TargetPosition: %03d\n", TargetPosition, DEC);
#endif
}
}
void xControlUpDownMovement(void *arg) {
// Check "continuously" the Actuator Position and move Motor Up/Down until target position is reached
int OnOffsetAction = 0;
const TickType_t xDelay = 110 / portTICK_PERIOD_MS; // Block for 110ms < 10Hz sample rate of VL6180X
while(1) {
if(xSemaphoreTake(xSemaphore, portMAX_DELAY)) {
// BLE channels can interrupt and consequently target position changes on-the-fly !!
// We do not want changes in TargetPosition during one of the following actions!!!
OnOffsetAction = lift.GetOffsetPosition(); // calculate offset to target and determine action
switch (OnOffsetAction)
{
case 0 :
lift.brakeActuator();
#ifdef MOVEMENTDEBUG
DEBUG_PRINTLN(F(" -> Brake"));
#endif
break;
case 1 :
lift.moveActuatorUp();
#ifdef MOVEMENTDEBUG
DEBUG_PRINTLN(F(" -> Upward"));
#endif
break;
case 2 :
lift.moveActuatorDown();
#ifdef MOVEMENTDEBUG
DEBUG_PRINTLN(F(" -> Downward"));
#endif
break;
case 3 :
// Timeout --> OffsetPosition is undetermined --> do nothing and brake
lift.brakeActuator();
#ifdef MOVEMENTDEBUG
DEBUG_PRINTLN(F(" -> Timeout"));
#endif
break;
} // switch
xSemaphoreGive(xSemaphore);
}
vTaskDelay(xDelay);
} // while
} // end
// ----------------------------------------------------------------------------------
bool client_DeviceInformation_Connect(void)
{
// If Device Information is not found then go on.... NOT FATAL !
pRemote_DeviceInformation_Service = pClient_Wahoo->getService(UUID16_SVC_DEVICE_INFORMATION);
if ( pRemote_DeviceInformation_Service == nullptr ) {
DEBUG_PRINT(F("Device Information Service: NOT Found!\n"));
return true;
}
DEBUG_PRINT(F("Client Device Information Service: Found!\n"));
pRemote_DIS_ManufacturerName_Chr = pRemote_DeviceInformation_Service->getCharacteristic(UUID16_CHR_MANUFACTURER_NAME_STRING);
if ( pRemote_DIS_ManufacturerName_Chr != nullptr ) {
if(pRemote_DIS_ManufacturerName_Chr->canRead()) {
client_DIS_Manufacturer_Str = pRemote_DIS_ManufacturerName_Chr->readValue();
server_DIS_ManufacturerName_Chr->setValue(client_DIS_Manufacturer_Str); // Transfer/Update the value to the server side
DEBUG_PRINTF(" -> Client Reads Manufacturer Name: [%s]\n", client_DIS_Manufacturer_Str.c_str());
}
}
pRemote_DIS_ModelNumber_Chr = pRemote_DeviceInformation_Service->getCharacteristic(UUID16_CHR_MODEL_NUMBER_STRING);
if ( pRemote_DIS_ModelNumber_Chr != nullptr ) {
if(pRemote_DIS_ModelNumber_Chr->canRead()) {
client_DIS_ModelNumber_Str = pRemote_DIS_ModelNumber_Chr->readValue();
server_DIS_ModelNumber_Chr->setValue(client_DIS_ModelNumber_Str); // Transfer/Update the value to the server side
DEBUG_PRINTF(" -> Client Reads Model Number: [%s]\n", client_DIS_ModelNumber_Str.c_str());
}
}
pRemote_DIS_SerialNumber_Chr = pRemote_DeviceInformation_Service->getCharacteristic(UUID16_CHR_SERIAL_NUMBER_STRING);
if ( pRemote_DIS_SerialNumber_Chr != nullptr ) {
if(pRemote_DIS_SerialNumber_Chr->canRead()) {
client_DIS_SerialNumber_Str = pRemote_DIS_SerialNumber_Chr->readValue();
server_DIS_SerialNumber_Chr->setValue(client_DIS_SerialNumber_Str); // Transfer/Update the value to the server side
DEBUG_PRINTF(" -> Client Reads Serial Number: [%s]\n", client_DIS_SerialNumber_Str.c_str());
}
}
return true;
}
bool client_GenericAccess_Connect(void)
{
// If Generic Access is not found then go on.... NOT FATAL !
pRemote_GenericAccess_Service = pClient_Wahoo->getService(UUID16_SVC_GENERIC_ACCESS);
if ( pRemote_GenericAccess_Service == nullptr ) {
DEBUG_PRINTLN(F("Client Generic Access: NOT Found!"));
return true;
}
DEBUG_PRINTLN("Client Generic Access: Found!");
pRemote_GA_DeviceName_Chr = pRemote_GenericAccess_Service->getCharacteristic(UUID16_CHR_DEVICE_NAME);
if ( pRemote_GA_DeviceName_Chr != nullptr ) {
if(pRemote_GA_DeviceName_Chr->canRead()) {
client_GA_DeviceName_Str = pRemote_GA_DeviceName_Chr->readValue();
int RespErr = ble_svc_gap_device_name_set((const char*)client_GA_DeviceName_Str.c_str()); // Transfer/Update the value to the server side
DEBUG_PRINTF(" -> Client Reads Device Name: [%s]\n", client_GA_DeviceName_Str.c_str());
}
}
pRemote_GA_Appearance_Chr = pRemote_GenericAccess_Service->getCharacteristic(UUID16_CHR_APPEARANCE);
if ( pRemote_GA_Appearance_Chr != nullptr ) {
if(pRemote_GA_Appearance_Chr->canRead()) {
client_GA_Appearance_Value = pRemote_GA_Appearance_Chr->readUInt16();
int RespErr = ble_svc_gap_device_appearance_set(client_GA_Appearance_Value); // Transfer/Update the value to the server side
DEBUG_PRINTF(" -> Client Reads Appearance: [%d]\n", client_GA_Appearance_Value);
}
}
return true;
}
void client_CP_Measurement_Notify_Callback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
// Client CP Measurement data is tranferred to the Server
// NO TREATMENT OF COMMAND !!!
if((Laptop.IsConnected)) {
server_CP_Measurement_Chr->setValue(pData, length);
server_CP_Measurement_Chr->notify(); // Just pass on and process later!
}
#ifdef DEBUG_CP_MEASUREMENT
uint8_t buffer[length]= {};
// Transfer first the contents of data to buffer (array of chars)
DEBUG_PRINTF(" -> Client Rec'd Raw CP Data: [%d] [%d] [ ", isNotify, length);
for (int i = 0; i < sizeof(buffer); i++) {
if ( i <= sizeof(buffer)) {
buffer[i] = *pData++;
DEBUG_PRINTF("%02X ", buffer[i], HEX);
}
}
DEBUG_PRINT("] ");
uint8_t offset = 0;
// Get flags field
uint16_t flags = 0;
memcpy(&flags, &buffer[offset], 2); // Transfer buffer fields to variable
offset += 2; // UINT16
// Get Instantaneous Power values UINT16
uint16_t PowerValue = 0;
memcpy(&PowerValue, &buffer[offset], 2); // Transfer buffer fields to variable
offset += 2; // UINT16
DEBUG_PRINTF("Instantaneous Power: %4d\n", PowerValue);
// Get the other CP measurement values
if ((flags & 1) != 0) {
// Power Balance Present
DEBUG_PRINT(" --> Pedal Power Balance!");
}
if ((flags & 2) != 0) {
// Accumulated Torque
DEBUG_PRINTLN(" --> Accumulated Torque!");
}
// etcetera...
#endif
} // End cpmc_notify_callback
void client_Wahoo_ControlPoint_Indicate_Callback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
// The receipt of Control Point settings is acknowledged by the trainer: handle it
// Send Client's Response message to the Server
// NO TREATMENT OF COMMAND !!!
if((Laptop.IsConnected)) {
server_Wahoo_ControlPoint_Chr->setValue(pData, length);
server_Wahoo_ControlPoint_Chr->indicate(); // Just pass on and process later!
client_Wahoo_ControlPoint_Response = true; // Should be set now!
}
#ifdef DEBUG_WAHOO_CONTROLPOINT_RESPONSE
uint8_t RespBufferLen = (uint8_t)length;
uint8_t RespBuffer[RespBufferLen] = {}; // It is max 6 bytes long
// Transfer first the contents of data to buffer (array of chars)
DEBUG_PRINT("-> Client Rec'd Raw Wahoo Control Point Response Data: [ ");
for (int i = 0; i < sizeof(RespBuffer); i++) {
RespBuffer[i] = *pData++;
DEBUG_PRINTF("%02X ", RespBuffer[i], HEX);
}
DEBUG_PRINTLN("]");
#endif
}
bool client_CyclingPower_Connect(void)
{
// Obtain a reference to the remote CP service.
pRemote_CyclingPower_Service = pClient_Wahoo->getService(UUID16_SVC_CYCLING_POWER);
if (pRemote_CyclingPower_Service == nullptr) {
DEBUG_PRINTLN("Mandatory Cycling Power Service: Not Found!");
return false;
}
// ----------------------------- WAHOO --------------------------------------------------------------------------
DEBUG_PRINTLN("Client_CyclingPower_Service: Found!");
pRemote_Wahoo_ControlPoint_Chr = pRemote_CyclingPower_Service->getCharacteristic(UUID16_CHR_WAHOO_CONTROL_POINT);
if (pRemote_Wahoo_ControlPoint_Chr == nullptr) {
DEBUG_PRINTLN("Mandatory Client_Wahoo_ControlPoint_Chr: Not Found!");
return false; // Mandatory when service is present
}
DEBUG_PRINTLN("Client_Wahoo_ControlPoint_Chr: Found!");
if(pRemote_Wahoo_ControlPoint_Chr->canIndicate()) {
pRemote_Wahoo_ControlPoint_Chr->subscribe(indications, client_Wahoo_ControlPoint_Indicate_Callback);
} else {
DEBUG_PRINTLN("Mandatory Client_Wahoo_ControlPoint_Chr: Cannot Indicate!");
return false; // Mandatory when service is present
}
// ---------------------------------------------------------------------------------------------------------------
pRemote_CP_Measurement_Chr = pRemote_CyclingPower_Service->getCharacteristic(UUID16_CHR_CYCLING_POWER_MEASUREMENT);
if (pRemote_CP_Measurement_Chr == nullptr) {
DEBUG_PRINTLN("Mandatory client_CP_Measurement_Chr: Not Found!");
return false;
}
DEBUG_PRINTLN("Client_CP_Measurement_Chr: Found!");
if(pRemote_CP_Measurement_Chr->canNotify()) {
pRemote_CP_Measurement_Chr->subscribe(notifications, client_CP_Measurement_Notify_Callback);
} else {
DEBUG_PRINTLN("Mandatory Client_CP_Measurement_Chr: Cannot Notify!");
return false;
}
pRemote_CP_Feature_Chr = pRemote_CyclingPower_Service->getCharacteristic(UUID16_CHR_CYCLING_POWER_FEATURE);
if (pRemote_CP_Feature_Chr == nullptr) {
DEBUG_PRINTLN("Mandatory Client_CP_Feature_Chr: Not Found!");
return false;
}
DEBUG_PRINTLN("Client_CP_Feature_Chr: Found!");
// Read the value of the characteristic.
if(pRemote_CP_Feature_Chr->canRead())
{
// Read 32-bit client_CP_Feature_Chr value
client_CP_Feature_Flags = pRemote_CP_Feature_Chr->readUInt32();
server_CP_Feature_Chr->setValue(client_CP_Feature_Flags); // Transfer/Update the value to the server side
#ifdef DEBUG
const uint8_t CPFC_FIXED_DATALEN = 4;
uint8_t cpfcData[CPFC_FIXED_DATALEN] = {(uint8_t)(client_CP_Feature_Flags & 0xff), (uint8_t)(client_CP_Feature_Flags >> 8),
(uint8_t)(client_CP_Feature_Flags >> 16), (uint8_t)(client_CP_Feature_Flags >> 24)};
DEBUG_PRINT(" -> Client Reads Raw CP Feature bytes: [4] [ ");
for (int i = 0; i < CPFC_FIXED_DATALEN; i++) {
if ( i <= sizeof(cpfcData)) {
DEBUG_PRINTF("%02X ", cpfcData[i], HEX);
}
}
DEBUG_PRINTLN("] ");
for (int i = 0; i < client_CP_Feature_Len; i++) {
if ( client_CP_Feature_Flags & (1 << i) ) {
DEBUG_PRINTLN(client_CP_Feature_Str[i]);
}
}
#endif
} // canRead Feature
pRemote_CP_Location_Chr = pRemote_CyclingPower_Service->getCharacteristic(UUID16_CHR_SENSOR_LOCATION);
if (pRemote_CP_Location_Chr == nullptr) {
DEBUG_PRINTLN("Client_CP_Location_Chr: Not Found!");
} else {
DEBUG_PRINTLN("Client_CP_Location_Chr: Found!");
// Read the value of the characteristic.
if(pRemote_CP_Location_Chr->canRead()) {
client_CP_Location_Value = pRemote_CP_Location_Chr->readUInt8();
server_CP_Location_Chr->setValue(&client_CP_Location_Value, 1); // Transfer/Update the value to the server side
// CP sensor location value is 8 bit
DEBUG_PRINT(" -> Client Reads CP Location Sensor:");
DEBUG_PRINTF(" Loc#: %d %s\n", client_CP_Location_Value, client_Sensor_Location_Str[client_CP_Location_Value]);
}
}
return true;
}
void client_CSC_Measurement_Notify_Callback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify)
{
// Client CSC Measurement data is tranferred to the Server
// NO TREATMENT OF COMMAND !!!
if((Laptop.IsConnected)) {
server_CSC_Measurement_Chr->setValue(pData, length);
server_CSC_Measurement_Chr->notify(); // Just pass on and process later!
}
#ifdef DEBUG_CSC_MEASUREMENT
uint8_t buffer[length]= {};
// Transfer first the contents of data to buffer (array of chars)
DEBUG_PRINTF(" -> Client Rec'd Raw CSC Data: [%d] [%d] [ ", isNotify, length);
for (int i = 0; i < length; i++) {
if ( i <= sizeof(buffer)) {
buffer[i] = *pData++;
DEBUG_PRINTF("%02X ", buffer[i], HEX);
}
}
DEBUG_PRINT("] ");
uint8_t offset = 0;
// we define the offset that is to be used when reading the next field
// Size of variables (e.g. 2 (16) or 4 bytes (32)) are constants in BluetoothGattCharacteristic
// these represent the values you can find in the "Value Fields" table in the "Format" column
// Read the Flags field at buffer[0]
uint8_t flags = buffer[offset];
offset += 1; // UINT8
// we have to check the flags' nth bit to see if C1 field exists
if ((flags & 1) != 0) {
uint32_t cum_wheel_rev = 0;
memcpy(&cum_wheel_rev, &buffer[offset], 4);
offset += 4; // UINT32
uint16_t last_wheel_event = 0;
memcpy(&last_wheel_event, &buffer[offset], 2);
offset += 2; // UINT16
DEBUG_PRINTF(" Cum. wheel rev.: %d Last wheel event: %d ", cum_wheel_rev, last_wheel_event);
/* Calculation of speed at the Collector can be derived from the wheel circumference and
* data in two successive measurements. The Collector calculation can be performed as
* shown below:
* Speed = (Difference in two successive Cumulative Wheel Revolution values * Wheel Circumference)
* / (Difference in two successive Last Wheel Event Time values)
*/
}
// we have to check the flags' nth bit to see if C2 field exists
if ((flags & 2) != 0) {
uint16_t cum_cranks = 0;
memcpy(&cum_cranks, &buffer[offset], 2);
offset += 2; // UINT16
uint16_t last_crank_event = 0;
memcpy(&last_crank_event, &buffer[offset], 2);
offset += 2; // UINT16
DEBUG_PRINTF(" Cum cranks: %d Last crank event: %d", cum_cranks, last_crank_event);
/* Calculation of cadence at the Collector can be derived from data in two successive
* measurements. The Collector calculation can be performed as shown below:
* Cadence = (Difference in two successive Cumulative Crank Revolution values)
* / (Difference in two successive Last Crank Event Time values)
*/
}
// etcetera...
DEBUG_PRINTLN();
#endif
}
bool client_CyclingSpeedCadence_Connect(void)
{
// Obtain a reference to the remote HRM service.
pRemote_CyclingSpeedCadence_Service = pClient_Wahoo->getService(UUID16_SVC_CYCLING_SPEED_AND_CADENCE);
if (pRemote_CyclingSpeedCadence_Service == nullptr) {
DEBUG_PRINTLN("client_CyclingSpeedCadence_Service: Not Found! Not Mandatory");
return true;
}
DEBUG_PRINTLN("Client_CyclingSpeedCadence_Service: Found!");
pRemote_CSC_Measurement_Chr = pRemote_CyclingSpeedCadence_Service->getCharacteristic(UUID16_CHR_CSC_MEASUREMENT);
if (pRemote_CSC_Measurement_Chr == nullptr) {
DEBUG_PRINTLN("Mandatory client_CSC_Measurement_Chr: Not Found!");
return false;
}
DEBUG_PRINTLN("Client_CSC_Measurement_Chr: Found!");
if(pRemote_CSC_Measurement_Chr->canNotify()) {
pRemote_CSC_Measurement_Chr->subscribe(notifications, client_CSC_Measurement_Notify_Callback);
} else {
DEBUG_PRINTLN("Mandatory Client_CSC_Measurement_Chr: Cannot Notify!");
return false;
}
pRemote_CSC_Feature_Chr = pRemote_CyclingSpeedCadence_Service->getCharacteristic(UUID16_CHR_CSC_FEATURE);
if (pRemote_CSC_Feature_Chr == nullptr) {
DEBUG_PRINTLN("Mandatory Client_CSC_Feature_Chr: Not Found!");
return false;
}
DEBUG_PRINTLN("Client_CSC_Feature_Chr: Found!");
// Read the value of the characteristic.
if(pRemote_CSC_Feature_Chr->canRead())
{
// Read 16-bit client_CSC_Feature_Chr value
client_CSC_Feature_Flags = pRemote_CSC_Feature_Chr->readUInt16();
server_CSC_Feature_Chr->setValue(client_CSC_Feature_Flags); // Transfer/Update the value to the server side
#ifdef DEBUG
uint8_t cscfcData[CSC_FEATURE_FIXED_DATALEN] = { (uint8_t)(client_CSC_Feature_Flags & 0xff), (uint8_t)(client_CSC_Feature_Flags >> 8) }; // Little Endian Representation
DEBUG_PRINTF(" -> Client Reads Raw CSC Feature bytes: [2] [ ");
for (int i = 0; i < sizeof(cscfcData); i++) {
DEBUG_PRINTF("%02X ", cscfcData[i], HEX);
}
DEBUG_PRINTLN("] ");
for (int i = 0; i < client_CSC_Feature_Len; i++) {
if ( (client_CSC_Feature_Flags & (1 << i)) != 0 ) {
DEBUG_PRINTLN(client_CSC_Feature_Str[i]);
}
}
#endif
} // canRead Feature
pRemote_CSC_Location_Chr = pRemote_CyclingSpeedCadence_Service->getCharacteristic(UUID16_CHR_SENSOR_LOCATION);
if (pRemote_CSC_Location_Chr == nullptr) {
DEBUG_PRINTLN("Client_CSC_Location_Chr: Not Found!");
} else {
DEBUG_PRINTLN("Client_CSC_Location_Chr: Found!");
// Read the value of the characteristic.
if(pRemote_CSC_Location_Chr->canRead()) {
client_CSC_Location_Value = pRemote_CSC_Location_Chr->readUInt8();
server_CSC_Location_Chr->setValue(&client_CSC_Location_Value, 1); // Transfer/Update the value to the server side
// CSC sensor location value is 8 bit
DEBUG_PRINT(" -> Client Reads CSC Location Sensor:");
DEBUG_PRINTF(" Loc#: %d %s\n", client_CSC_Location_Value, client_Sensor_Location_Str[client_CSC_Location_Value]);
}
}
return true;
}
// This is NOT really a Callback --> It should have been implemented that way (see for instance Adafruit Bluefruit BLE library),
// however, now it is called from loop() ... a poor man's solution!
bool client_Connect_Callback(void) {
// Every time we want to connect to a Server a NEW Client is created !!
pClient_Wahoo = BLEDevice::createClient();
pClient_Wahoo->setClientCallbacks(new client_Connection_Callbacks());
// Connect to the Wahoo BLE Server.
pClient_Wahoo->connect(myDevice);
DEBUG_PRINTLN("Now checking all Client Services and Characteristics!");
DEBUG_PRINTLN("If Mandatory Services Fail --> the Client will disconnect!");
// Discover all relevant Services and Char's
if( !client_GenericAccess_Connect() ) {
pClient_Wahoo->disconnect();
return false;
}
if( !client_DeviceInformation_Connect() ) {
pClient_Wahoo->disconnect();
return false;
}
if( !client_CyclingPower_Connect() ) {
pClient_Wahoo->disconnect();
return false;
}
// --------------------------WAHOO UNLOCK --------------------------------------------------
delay(50);
// Unlock the client_Wahoo_ControlPoint Characteristic at the Wahoo trainer
DEBUG_PRINTLN("Client sends to Wahoo Control Point: [ 20 EE FC ] -> Unlock Command Key");
// write with response !!! writeValue(uint8_t* data, size_t length, bool response = false);
pRemote_Wahoo_ControlPoint_Chr->writeValue(unlockCommand, 3, true);
delay(100); // Give the trainer some time to wake up
// ------------------------------------------------------------------------------------------
if( !client_CyclingSpeedCadence_Connect() ) {
pClient_Wahoo->disconnect();
return false;
}
// When the client/trainer is RECONNECTING we need to enable/indicate all Remote Client Char's again!
if(Laptop.IsConnected) {
//Do NOT(!) allow for any possible delay (Regularly this is handled in loop() with DoCallClientEnable = true)
client_Set_All_NotificationIndication(true);
}
// ----------------------------------------------------------------------------------------------
Trainer.IsConnected = true;
client_Wahoo_ControlPoint_Response = true; // Should be set now!
return true;
};
/**
* Scan for BLE servers and find the first one that advertises the service we are looking for.
*/
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
/*
* Called for each advertising BLE server.
*/
void onResult(BLEAdvertisedDevice* advertisedDevice) {
//DEBUG_PRINT("Advertising Device-> ");
//DEBUG_PRINTLN(advertisedDevice.toString().c_str());
// We have found a server device, now see if it contains the service we are looking for.
if (advertisedDevice->haveServiceUUID() && advertisedDevice->isAdvertisingService(UUID16_SVC_CYCLING_POWER)) {
NimBLEAddress MyAddress = advertisedDevice->getAddress();
uint8_t RemoteAddress[6] = {};
memcpy(&RemoteAddress, MyAddress.getNative(), 6);
DEBUG_PRINTLN("Found advertising Peripheral with CPS enabled! See data:");
DEBUG_PRINTLN(advertisedDevice->toString().c_str());
// OK Server has CPS ervice exposed, now check for right mac adress
if ( !(memcmp((void*)RemoteAddress, Trainer.PeerAddress, 6) == 0) ) {
char fullMacAddress[18] = {}; //
ConvertMacAddress(fullMacAddress, RemoteAddress, false); // true -> Native representation!
DEBUG_PRINTF("Keep Scanning! Unknown Trainer Mac Address: [%s]\n", fullMacAddress);
return;
}
BLEDevice::getScan()->stop();
myDevice = advertisedDevice;
/* Connect to the Wahoo BLE Server -> Sorry you can't do that here!!! --------------------------------
** pClient_Wahoo->connect(myDevice); NOT ALLOWED TO CALL CONNECT --> CAUSES FATAL ERROR !!!! ???? */
doClientConnectCall = true; // Work around via loop()
} // Found our server
} // onResult
}; // MyAdvertisedDeviceCallbacks
void client_Connection_Callbacks::onConnect(BLEClient* pClient) {
Trainer.PeerName = myDevice->getName().c_str();
Trainer.conn_handle = pClient_Wahoo->getConnId();
#ifdef DEBUG
DEBUG_PRINT("Client Connection Parameters -> ");
uint16_t max_payload = pClient_Wahoo->getMTU()-3;
//DEBUG_PRINTF("Max Transmission Unit: [%d] ", max_payload);
uint16_t clientConnectionInterval = pClient_Wahoo->getConnInfo().getConnInterval();
DEBUG_PRINTF("Interval: [%d] ", clientConnectionInterval);
uint16_t clientConnectionLatency = pClient_Wahoo->getConnInfo().getConnLatency();
DEBUG_PRINTF("Latency: [%d] ", clientConnectionLatency);
uint16_t clientConnectionSupTimeout = pClient_Wahoo->getConnInfo().getConnTimeout();
DEBUG_PRINTF("Supervision Timeout: [%d]\n", clientConnectionSupTimeout);
char fullMacAddress[18] = {}; //
ConvertMacAddress(fullMacAddress, Trainer.PeerAddress, false); // true -> Native representation!
DEBUG_PRINTF("ESP32 Client connected to Server device with Name: [%s] MAC Address: [%s] Handle: [%d] MTU: [%d]\n", \
Trainer.PeerName.c_str(), fullMacAddress, Trainer.conn_handle, max_payload);
#endif
/** We can use the connection handle here to ask for different connection parameters.
* Args: connection handle, min connection interval, max connection interval, latency and supervision timeout.
* Units; Min/Max Intervals: 1.25 millisecond increments --> between: 24 (30 ms) resp. 48 (60 ms)
* Latency: number of intervals allowed to skip. --> keep zero
* Timeout: 10 millisecond increments. --> Try 400 (4000 ms)
*/
//pClient_Wahoo->updateConnParams(pClient_Wahoo->getConnId(), 24, 48, 0, 400);
//DEBUG_PRINTLN("Client Updates Connection Parameters -> Min Interval: [24] Max Interval: [48] Latency: [0] Supervision Timeout: [400]");
};
bool client_Connection_Callbacks::onConnParamsUpdateRequest(BLEClient* pClient, ble_gap_upd_params *params) {
DEBUG_PRINTLN("Client Connection Parameter Update Request!");
/** Minimum value for connection interval in 1.25ms units */
uint16_t clientConnectionMinInterval = params->itvl_min;
DEBUG_PRINTF("Min Interval: [%d]\n", clientConnectionMinInterval);
/** Maximum value for connection interval in 1.25ms units */
uint16_t clientConnectionMaxInterval = params->itvl_max;
DEBUG_PRINTF("Max Interval: [%d]\n", clientConnectionMaxInterval);
/** Connection latency */
uint16_t clientConnectionLatency = params->latency;
DEBUG_PRINTF("Latency: [%d]\n", clientConnectionLatency);
/** Supervision timeout in 10ms units */
uint16_t clientConnectionSupTimeout = params->supervision_timeout;
DEBUG_PRINTF("Sup. Timeout: [%d]\n", clientConnectionSupTimeout);
/** Minimum length of connection event in 0.625ms units */
uint16_t clientMinLenEvent = params->min_ce_len;
DEBUG_PRINTF("Min Length Event: [%d]\n", clientMinLenEvent);
/** Maximum length of connection event in 0.625ms units */
uint16_t clientMaxLenEvent = params->max_ce_len;
DEBUG_PRINTF("Max Length Event: [%d]\n", clientMaxLenEvent);
return true; // That is OK!
};
void client_Connection_Callbacks::onDisconnect(BLEClient* pClient) {
Trainer.IsConnected = false;
Trainer.conn_handle = BLE_HS_CONN_HANDLE_NONE;
char fullMacAddress[18] = {}; //
ConvertMacAddress(fullMacAddress, Trainer.PeerAddress, false); // true -> Native representation!
DEBUG_PRINTF("Client Disconnected from Server device with Name: [%s] Mac Address: [%s]!\n", Trainer.PeerName.c_str(), fullMacAddress);
RestartScanningOnDisconnect = true;
// It is an option to disconnect also the Server, however NOT necessary!! We choose not!
// if(Laptop.IsConnected) pServer->disconnect(Laptop.conn_handle);
};
void client_Start_Scanning(void)
{
// Retrieve a Scanner and set the callback we want to use to be informed when we
// have detected a new device. Specify that we want active scanning and start the
// scan to run for ## seconds.
pBLEScan = BLEDevice::getScan();
pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
pBLEScan->setInterval(1349);
pBLEScan->setWindow(449);
pBLEScan->setActiveScan(true);
DEBUG_PRINTLN("Client Starts Scanning for Server Device with CPS + Wahoo!");
//pBLEScan->start(5, false); // Scan for 5 seconds only
pBLEScan->start(0, false);
}
// Handler class for Server Multi Characteristic actions limited to onSubscribe
void CharacteristicCallbacks::onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
String str = "Central Updated CCCD -->";
if(subValue == 0) {
str += " Notify/Indicate Disabled for Char:";
}else if(subValue == 1) {
str += " Notify Enabled for Char:";
} else if(subValue == 2) {
str += " Indicate Enabled for Char:";
} else if(subValue == 3) {
str += " Notify & Indicate Enabled for Char:";
}
DEBUG_PRINTF("%s", str.c_str());
str = std::string(pCharacteristic->getUUID()).c_str();
DEBUG_PRINTF(" [%s]\n", str.c_str());
};
void server_startADV(void)
{
// Prepare for advertising
/** Optional: set the transmit power, default is 3db */
#ifdef ESP_PLATFORM
NimBLEDevice::setPower(ESP_PWR_LVL_P9); /** +9db */
#else
NimBLEDevice::setPower(9); /** +9db */
#endif
pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(UUID16_SVC_CYCLING_POWER);
pAdvertising->addServiceUUID(UUID16_SVC_CYCLING_SPEED_AND_CADENCE);
pAdvertising->setAppearance(client_GA_Appearance_Value);
DEBUG_PRINTF("Setting Appearance in Advertised data to [%d]\n", client_GA_Appearance_Value);
pAdvertising->setName(THISDEVICENAME);
DEBUG_PRINTF("Setting DeviceName in Advertised data to [%s]\n", THISDEVICENAME);
pAdvertising->setScanResponse(true);
pAdvertising->setMinInterval(144); // 32 in 0.625ms units, 0 = use default.
pAdvertising->setMaxInterval(244); // 244 in 0.625ms units, 0 = use default.
BLEDevice::startAdvertising();
// Start Advertising
}
void server_Connection_Callbacks::onConnect(BLEServer* pServer, ble_gap_conn_desc *desc) {
// Get some connection parameters of the peer device.
uint16_t serverConnectionHandle = desc->conn_handle;
uint16_t serverConnectionInterval = desc->conn_itvl; // Connection interval
uint16_t serverConnectionLatency = desc->conn_latency; // Connection latency
uint16_t serverConnectionSupTimeout = desc->supervision_timeout; // Connection supervision timeout
uint8_t RemoteAddress[6];
memcpy(&RemoteAddress, NimBLEAddress(desc->peer_id_addr).getNative(), 6);
char fullMacAddress[18] = {};
ConvertMacAddress(fullMacAddress, RemoteAddress, false); // true -> Native format!
BLEDevice::stopAdvertising();
DEBUG_PRINTF("Server Connection Parameters -> Interval: [%d] Latency: [%d] Supervision Timeout: [%d]\n",serverConnectionInterval, \
serverConnectionLatency, serverConnectionSupTimeout);
DEBUG_PRINTF("ESP32 Server connected to Client device with MAC Address: [%s] Conn Handle: [%d]\n", fullMacAddress, serverConnectionHandle);
/** We can use the connection handle here to ask for different connection parameters.
* Args: connection handle, min connection interval, max connection interval, latency and supervision timeout.
* Units; Min/Max Intervals: 1.25 millisecond increments --> between: 24 (30 ms) resp. 48 (60 ms)
* Latency: number of intervals allowed to skip. --> keep zero
* Timeout: 10 millisecond increments. --> Try 400 (4000 ms)
*/
//pServer->updateConnParams(serverConnectionHandle, 24, 48, 0, 400);
//DEBUG_PRINTLN("Server Updates Connection Parameters -> Min Interval: [24] Max Interval: [48] Latency: [0] Supervision Timeout: [400]");
// Who has been exactly connected?
// [1] Laptop is connecting
if (memcmp(RemoteAddress, Laptop.PeerAddress, 6) == 0 ) { // Check Laptop MAC address
// Laptop/PC is connecting !
memcpy(&Laptop.PeerAddress, RemoteAddress, 6);
Laptop.conn_handle = serverConnectionHandle;
Laptop.IsConnected = true;
DEBUG_PRINTF("Central (%s/Zwift) has to set CP/CSC/NUS CCCD Notify/Indicate (enable) and start....\n", Laptop.PeerName.c_str());
DoCallClientEnable = true;
return; // We are done here!
}
// [2] Smartphone is connecting
Smartphone.conn_handle = serverConnectionHandle;
Smartphone.IsConnected = true;
memcpy(Smartphone.PeerAddress, RemoteAddress, 6);
DEBUG_PRINTF("Central (%s/Simcline App) has to set NUS CCCD 'Notify' (enable) and start....\n", Smartphone.PeerName.c_str());
/* Alternative for exclusively connecting to Laptop!!
if ( !(memcmp(RemoteAddress, Laptop.PeerAddress, 6) == 0) ) {
DEBUG_PRINTLN("ERROR >>> Forced Server Disconnect: Unknown Laptop Mac Address!");
pServer->disconnect(desc->conn_handle);
return; // Failed
}
*/
};
void server_Connection_Callbacks::onMTUChange(uint16_t MTU, ble_gap_conn_desc* desc) {
DEBUG_PRINTF("Central (%s/Zwift) updated MTU to: [%u] for connection ID: %u\n", Laptop.PeerName.c_str(), MTU, desc->conn_handle);
};
void server_Connection_Callbacks::onDisconnect(BLEServer* pServer, ble_gap_conn_desc *desc) {
uint32_t count = pServer->getConnectedCount();
// Get some Disconnection parameters of the peer device.
uint16_t serverConnectionHandle = desc->conn_handle;
uint8_t RemoteAddress[6] = {};
memcpy(&RemoteAddress, NimBLEAddress(desc->peer_id_addr).getNative(), 6);
char fullMacAddress[18] = {}; //
ConvertMacAddress(fullMacAddress, RemoteAddress, false); // true -> Native format!
if (Laptop.conn_handle == serverConnectionHandle ) { // Laptop/Desktop is disconnected
Laptop.conn_handle = BLE_HS_CONN_HANDLE_NONE;
Laptop.IsConnected = false;
DEBUG_PRINTF("ESP32 Server disconnected from Central (%s) Conn handle: [%d] Mac Address: [%s]\n", Laptop.PeerName.c_str(), serverConnectionHandle, fullMacAddress);
if(Trainer.IsConnected) DoCallClientDisable = true; // Tell the client not to send data!
}
if (Smartphone.conn_handle == serverConnectionHandle ) { // Smartphone is disconnected
Smartphone.conn_handle = BLE_HS_CONN_HANDLE_NONE;
Smartphone.IsConnected = false;
DEBUG_PRINTF("ESP32 Server disconnected from Central (%s) Conn handle: [%d] Mac Address: [%s]\n", Smartphone.PeerName.c_str(), serverConnectionHandle, fullMacAddress);
}
DEBUG_PRINTLN(" --> ESP32 Server is advertising again!");
// NimBLe does auto advertise after disconnect
};
std::string WahooCPData; // Defined global to easely passing std::string
void TaskWriteWithResponse(void *parameter) {
// Just pass on and process later!
if( !pRemote_Wahoo_ControlPoint_Chr->writeValue(WahooCPData, true) ) { // true -> WithResponse (fatal if trainer is not responding: Guru paniced!!)
pClient_Wahoo->disconnect();
DEBUG_PRINTLN(">>> Error: NOT responding to Wahoo Control Point -> Write Value!");
}
vTaskDelete(TaskWriteWithResponseHandle);
};
class server_Wahoo_ControlPoint_Chr_callback: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
if(!client_Wahoo_ControlPoint_Response) { // skip the next Write action --> out of write-response-synch !!
DEBUG_PRINTLN("-> Server Rec'd Raw Wahoo Control Point Data --> SKIPPED: Out of Synch!");
return;
}
WahooCPData = server_Wahoo_ControlPoint_Chr->getValue();
uint8_t WahooCPDataLen = WahooCPData.length();
// Server Wahoo Control Point data is tranferred to the Client
// NO TREATMENT OF COMMAND !!!
// write with response !!! writeValue(string, bool response = false);
if(Trainer.IsConnected) {
xTaskCreate(&TaskWriteWithResponse, "Write w Response", 4096, (void *)NULL, 1, &TaskWriteWithResponseHandle); // 2048
client_Wahoo_ControlPoint_Response = false;
}
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
// Display the raw request packet
DEBUG_PRINTF("-> Server Rec'd Raw Wahoo Control Point Data [len: %d] ", WahooCPDataLen);
#endif
// Transfer the contents of data to server_Wahoo_Control_Point_Data.bytes
memset(server_Wahoo_Control_Point_Data.bytes, 0, sizeof(server_Wahoo_Control_Point_Data.bytes));
for (int i = 0; i < WahooCPDataLen; i++) {
server_Wahoo_Control_Point_Data.bytes[i] = WahooCPData[i];
}
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
/// Decodes an incoming Wahoo Control Point request
DEBUG_PRINTF("[OpCode: %02X] [Values: ", server_Wahoo_Control_Point_Data.values.OPCODE, HEX);
for (int i=0; i<WahooCPDataLen; i++) {
DEBUG_PRINTF("%02X ", server_Wahoo_Control_Point_Data.values.OCTETS[i], HEX);
}
DEBUG_PRINTLN("]");
// The documentation I found states that all write actions to this Wahoo CP characteristic are "Write with Response"
// So we have formally to acknowledge the receipt of the trainer setting
// Zwift does NOT care at all if one sets a response, it is still working !!!
#endif
switch(server_Wahoo_Control_Point_Data.values.OPCODE) {
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
case unlock: {
DEBUG_PRINTLN(" Request to Unlock Machine!");
break;
}
case setResistanceMode: {
DEBUG_PRINTLN(" Set Resistance Mode!");
break;
}
case setStandardMode: {
DEBUG_PRINTLN(" Set Standard Mode!");
break;
}
case setSimMode : {
DEBUG_PRINT(" Set Simulation Mode! ");
uint16_t tmp = (server_Wahoo_Control_Point_Data.values.OCTETS[0]) + (server_Wahoo_Control_Point_Data.values.OCTETS[1] << 8);
weight = (float(tmp) / 100); // Rider weight in Kg
tmp = ( (server_Wahoo_Control_Point_Data.values.OCTETS[2]) + (server_Wahoo_Control_Point_Data.values.OCTETS[3] << 8) );
rrc = (float(tmp) / 1000); // Rolling Resistance Coefficient
tmp = ( (server_Wahoo_Control_Point_Data.values.OCTETS[4]) + (server_Wahoo_Control_Point_Data.values.OCTETS[5] << 8) );
wrc = (float(tmp) / 1000); // Wind Resistance Coefficient
DEBUG_PRINTF(" --> Weight: %0.2f RRC: %f WRC: %f\n", weight, rrc, wrc);
break;
}
#endif
case setSimGrade: {
uint16_t gr = ( server_Wahoo_Control_Point_Data.values.OCTETS[0] + (server_Wahoo_Control_Point_Data.values.OCTETS[1] << 8) );
grade = 100 * float( ((gr * 2.0 / 65535) - 1.0) ); // Percentage of road grade --> range: between +1 and -1 (!)
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
DEBUG_PRINT(" Set Simulation Grade! ");
DEBUG_PRINTF(" --> Grade: %4.1f%%\n", grade); //
#endif
SetNewRawGradeValue(grade);
SetNewActuatorPosition();
ShowSlopeTriangleOnOled();
break;
}
#ifdef DEBUG_WAHOO_CONTROLPOINT_OPCODE_DATA
case setErgMode:
case setSimCRR:
case setSimWindResistance:
case setSimWindSpeed:
case setWheelCircumference:
{
DEBUG_PRINTLN(" Unresolved OpCode!");
break;
}
#endif
} // switch
}; // onWrite
void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
String str = "Central Updated CCCD -->";
if(subValue == 0) {
str += " Notify/Indicate Disabled for Char:";
}else if(subValue == 1) {
str += " Notify Enabled for Char:";
} else if(subValue == 2) {
str += " Indicate Enabled for Char:";
} else if(subValue == 3) {
str += " Notify & Indicate Enabled for Char:";
}
DEBUG_PRINTF("%s", str.c_str());
str = std::string(pCharacteristic->getUUID()).c_str();
DEBUG_PRINTF(" [%s]\n", str.c_str());
}; // onSubscribe
};
void server_setupCPS(void)
{
server_CyclingPower_Service = pServer->createService(UUID16_SVC_CYCLING_POWER);
server_CP_Measurement_Chr = server_CyclingPower_Service->createCharacteristic(UUID16_CHR_CYCLING_POWER_MEASUREMENT,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
server_CP_Measurement_Chr->setCallbacks(&server_Multi_Chr_Callbacks); //NIMBLE
server_CP_Feature_Chr = server_CyclingPower_Service->createCharacteristic(UUID16_CHR_CYCLING_POWER_FEATURE,
NIMBLE_PROPERTY::READ);
// Set server CP Feature Flags field
server_CP_Feature_Chr->setValue(client_CP_Feature_Flags);
server_CP_Location_Chr = server_CyclingPower_Service->createCharacteristic(UUID16_CHR_SENSOR_LOCATION,
NIMBLE_PROPERTY::READ);
// Set server_CP_Location for sensor
server_CP_Location_Chr->setValue(&client_CP_Location_Value, 1);
// Wahoo proprietary Control Point Characteristic --------------------------------------------------------------------------
server_Wahoo_ControlPoint_Chr = server_CyclingPower_Service->createCharacteristic(UUID16_CHR_WAHOO_CONTROL_POINT,
NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::INDICATE);
server_Wahoo_ControlPoint_Chr->setCallbacks(new server_Wahoo_ControlPoint_Chr_callback);
// -------------------------------------------------------------------------------------------------------------------------
server_CyclingPower_Service->start();
}
void server_setupCSC(void)
{
server_CyclingSpeedCadence_Service = pServer->createService(UUID16_SVC_CYCLING_SPEED_AND_CADENCE);
server_CSC_Measurement_Chr = server_CyclingSpeedCadence_Service->createCharacteristic(UUID16_CHR_CSC_MEASUREMENT,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
server_CSC_Measurement_Chr->setCallbacks(&server_Multi_Chr_Callbacks); //NIMBLE
server_CSC_Feature_Chr = server_CyclingSpeedCadence_Service->createCharacteristic(UUID16_CHR_CSC_FEATURE,
NIMBLE_PROPERTY::READ);
// Set server CSC Feature Flags field
server_CSC_Feature_Chr->setValue(client_CSC_Feature_Flags);
server_CSC_Location_Chr = server_CyclingSpeedCadence_Service->createCharacteristic(UUID16_CHR_SENSOR_LOCATION,
NIMBLE_PROPERTY::READ);
// Set server_CSC_Location for sensor
server_CSC_Location_Chr->setValue(&client_CSC_Location_Value, 1);
server_CyclingSpeedCadence_Service->start();
}
class server_NUS_Rxd_Chr_callback: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
// Read data received over NUS Rxd from Mobile Phone
std::string NusRxdData = server_NUS_Rxd_Chr->getValue();
uint8_t NusRxdDataLen = NusRxdData.length(); // Get the actual length of data bytes
// Display the raw packet data in actual length
DEBUG_PRINTF(" -> Server Rec'd NUS Rxd Data [%d][%s]\n", NusRxdDataLen, NusRxdData.c_str());
// The following routines parse and process the incoming commands
// Every NusRxdData packet starts with a '!' otherwise corrupt/invalid
if (NusRxdData[0] != '!') {
DEBUG_PRINTLN(F("-> Error: RXD-packet does not start with a '!'"));
return; // invalid NusRxdData packet: do not further parse and process
}
// RXpacket buffer has IdCode = "S"
if (NusRxdData[1] == 'S') { // Settings packet
// Besides what is mechanically possible there are also limits in what is physically pleasant/comfortable
// The allowed Raw Grade Value min and max values should be within the limits of the mechanically feasible values !!!
// Minimally allowed Raw Grade Value that should not be exceeded: -5%!
// default: aRGVmin is default set to 19500
// Maximally allowed Raw Grade Value that should not be exceeded: 15%!
// default: aRGVmax is default set to: 21500
// New Settings values have arrived --> parse, set values and store persistently
uint8_t iMax = 0, iMin = 0, iPerc = 0, iDispl = 0;
sscanf((char*)NusRxdData.c_str(), "!S%d;%d;%d;%d;", &iMax, &iMin, &iPerc, &iDispl);
// set Ascent Grade Limit to aRGVmax
iMax = constrain(iMax, 0, RGVMAX_GRADE);
aRGVmax = map(iMax, 0, RGVMAX_GRADE, 20000, RGVMAX);
// set Descent Grade Limit to aRGVmin
iMin = constrain(iMin, 0, RGVMIN_GRADE); // Notice: positive value!
aRGVmin = map(iMin, RGVMIN_GRADE, 0, RGVMIN, 20000);
// set Road Grade Change Factor
GradeChangeFactor = iPerc;
// set OledDisplaySelection
OledDisplaySelection = iDispl;
// LittleFS for persistent storage of these values
setPRSdata();
// LittleFS --------------------------------------
DEBUG_PRINT(F(" Settings: Max: ")); DEBUG_PRINT(iMax);
DEBUG_PRINT(F(" Min: ")); DEBUG_PRINT(iMin);
DEBUG_PRINT(F(" Perc: ")); DEBUG_PRINT(iPerc);
DEBUG_PRINT(F(" Displ: ")); DEBUG_PRINTLN(iDispl);
// Confirm to the PHONE: settings rcvd and set to persistent
DEBUG_PRINTF("Server Sends NUS TXD Confirm message: Done!\n");
server_NUS_Txd_Chr->setValue("!SDone!;");
server_NUS_Txd_Chr->notify();
return; // Settings rcvd and set to persistent
}
// Manual Control Buttons Up Down get parsed and processed!
// ONLY when the Actuator plus sensor are working well!
// i.e. low level up/down movement functions work !!
if (NusRxdData[1] == 'U' && IsBasicMotorFunctions) {
DEBUG_PRINTLN("-> Set motor UPward moving!");
RawgradeValue = RawgradeValue + 100;
SetManualGradePercentValue();
ShowSlopeTriangleOnOled();
return;
}
if (NusRxdData[1] == 'D' && IsBasicMotorFunctions) {
DEBUG_PRINTLN("-> Set motor DOWNward moving!");
RawgradeValue = RawgradeValue - 100;
SetManualGradePercentValue();
ShowSlopeTriangleOnOled();
return;
}
server_NUS_Txd_Chr->setValue("!UOut of Order!;");
server_NUS_Txd_Chr->notify();
DEBUG_PRINTF("\nServer Sends NUS TXD Error message: Out of Order!\n");
}; // onWrite
};
void server_NUS_Txd_Persistent_Settings(void) {
// Send persistent stored values to Mobile Phone for correct Settings!
// recalculate the values for use on the Phone
uint8_t TXpacketBuffer[16] = { 0 };
int iMax, iMin, iPerc, iDispl;
// set within limits
aRGVmax = constrain(aRGVmax, 0, RGVMAX);
aRGVmin = constrain(aRGVmin, 0, RGVMIN);
// set aRGVmax to Ascent Grade Limit in whole number
iMax = map(aRGVmax, 20000, RGVMAX, 0, RGVMAX_GRADE);
// set aRGVmin to Descent Grade Limit in whole number
iMin = map(aRGVmin, RGVMIN, 20000, RGVMIN_GRADE, 0);
// set GradeChangeFactor to Road Grade Change Factor
iPerc = GradeChangeFactor;
iDispl = OledDisplaySelection;
sprintf((char*)TXpacketBuffer, "!S%d;%d;%d;%d;", iMax, iMin, iPerc, iDispl);
// send these persistent data to the Settings page on the smartphone
server_NUS_Txd_Chr->notify(TXpacketBuffer, sizeof(TXpacketBuffer));
DEBUG_PRINTF("Server Sends NUS TXD Persistent settings to Phone: [%s]\n", (char*)TXpacketBuffer);
}
// Handler class for Server NUS Txd Characteristic actions limited to onSubscribe
class server_NUS_Txd_Callback: public NimBLECharacteristicCallbacks {
/* We only define onSubscribe !!!
void onRead(NimBLECharacteristic* pCharacteristic);
void onWrite(NimBLECharacteristic* pCharacteristic);
void onNotify(NimBLECharacteristic* pCharacteristic);
void onStatus(NimBLECharacteristic* pCharacteristic, Status status, int code);
*/
void onSubscribe(NimBLECharacteristic* pCharacteristic, ble_gap_conn_desc* desc, uint16_t subValue) {
String str = "Central Updated CCCD -->";
if(subValue == 0) {
str += " Notify/Indicate Disabled for Char:";
}else if(subValue == 1) {
str += " Notify Enabled for Char:";
} else if(subValue == 2) {
str += " Indicate Enabled for Char:";
} else if(subValue == 3) {
str += " Notify & Indicate Enabled for Char:";
}
DEBUG_PRINTF("%s", str.c_str());
str = std::string(pCharacteristic->getUUID()).c_str();
DEBUG_PRINTF(" [%s]\n", str.c_str());
if(subValue == 1) {
server_NUS_Txd_Persistent_Settings();
}
};
};
void server_setupNUS(void)
{
server_NordicUart_Service = pServer->createService(UUID_NUS_SERVICE);
server_NUS_Rxd_Chr = server_NordicUart_Service->createCharacteristic(UUID_NUS_CHR_RXD,
NIMBLE_PROPERTY::WRITE_NR); // Write with No response !!
server_NUS_Rxd_Chr->setCallbacks(new server_NUS_Rxd_Chr_callback());
server_NUS_Txd_Chr = server_NordicUart_Service->createCharacteristic(UUID_NUS_CHR_TXD,
NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY);
server_NUS_Txd_Chr->setCallbacks(new server_NUS_Txd_Callback()); //NIMBLE
server_NordicUart_Service->start();
}
void server_setupGA(void)
{
// Set the Generic Access Appearance value from default: [0] --> Unknown to [1152] --> Generic Cycling
int RespErr = ble_svc_gap_device_appearance_set(client_GA_Appearance_Value);
if(RespErr == 0) {
DEBUG_PRINTF("Successfully Set Generic Access Appearance Chr value to: [%d] Generic Cycling\n", client_GA_Appearance_Value);
} else {
DEBUG_PRINTLN("Unable to Set Generic Access Appearance Chr value!");
}
// Set Generic Access Device Name Chr to a value
RespErr = ble_svc_gap_device_name_set((const char*)client_GA_DeviceName_Str.c_str());
if(RespErr == 0) {
DEBUG_PRINTF("Successfully Set Generic Access Device Name Chr value to: [%s]\n", client_GA_DeviceName_Str.c_str());
} else {
DEBUG_PRINTLN("Unable to Set Generic Access Device Name Chr value!");
}
}
void server_setupDIS(void)
{
server_DeviceInformation_Service = pServer->createService(UUID16_SVC_DEVICE_INFORMATION);
server_DIS_ModelNumber_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_MODEL_NUMBER_STRING,
NIMBLE_PROPERTY::READ);
server_DIS_ModelNumber_Chr->setValue(client_DIS_ModelNumber_Str);
server_DIS_SerialNumber_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_SERIAL_NUMBER_STRING,
NIMBLE_PROPERTY::READ);
server_DIS_SerialNumber_Chr->setValue(client_DIS_SerialNumber_Str);
server_DIS_ManufacturerName_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_MANUFACTURER_NAME_STRING,
NIMBLE_PROPERTY::READ);
server_DIS_ManufacturerName_Chr->setValue(client_DIS_Manufacturer_Str);
/*
server_DIS_Firmware_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_FIRMWARE_REVISION_STRING,
NIMBLE_PROPERTY::READ);
server_DIS_Hardware_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_HARDWARE_REVISION_STRING,
NIMBLE_PROPERTY::READ);
server_DIS_Software_Chr = server_DeviceInformation_Service->createCharacteristic(UUID16_CHR_SOFTWARE_REVISION_STRING,
NIMBLE_PROPERTY::READ);
// Set Device Information varariables
server_DIS_Hardware_Chr->setValue(client_DIS_Hardware_Str);
server_DIS_Firmware_Chr->setValue(client_DIS_Firmware_Str);
server_DIS_Software_Chr->setValue(client_DIS_Software_Str);
*/
server_DeviceInformation_Service->start();
}
void client_Set_All_NotificationIndication(bool IsEnable)
{
if(IsEnable) { // Enable Client Char's
if( pRemote_Wahoo_ControlPoint_Chr != nullptr) { // Check: Is it exposed?
pRemote_Wahoo_ControlPoint_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOn, 2, true);
}
if ( pRemote_CSC_Measurement_Chr != nullptr ) {
pRemote_CSC_Measurement_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
}
if ( pRemote_CP_Measurement_Chr != nullptr ) {
pRemote_CP_Measurement_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOn, 2, true);
}
DEBUG_PRINTLN("All Client (Trainer) Characteristics are Notify/Indicate Enabled!");
} else { // Disable Client Char's
if( pRemote_Wahoo_ControlPoint_Chr != nullptr) { // Check: Is it exposed?
pRemote_Wahoo_ControlPoint_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)indicationOff, 2, true);
}
if ( pRemote_CSC_Measurement_Chr != nullptr ) {
pRemote_CSC_Measurement_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOff, 2, true);
}
if ( pRemote_CP_Measurement_Chr != nullptr ) {
pRemote_CP_Measurement_Chr->getDescriptor(BLEUUID((uint16_t)0x2902))->writeValue((uint8_t*)notificationOff, 2, true);
}
DEBUG_PRINTLN("All Client (Trainer) Characteristics are Notify/Indicate Disabled!");
}
} // end
void loop() { // loop() is used to start sort of Callback functions
// If the flag "DoCallClientEnable" is true, we enable Notify and Indicate on ALL Client Char's of BLE server!
if(Trainer.IsConnected && DoCallClientEnable) {
DoCallClientEnable = false;
client_Set_All_NotificationIndication(true);
}
// If the flag "DoCallClientDisable" is true, we disable Notify and Indicate on ALL Client Char's of BLE server!
if(Trainer.IsConnected && DoCallClientDisable) {
DoCallClientDisable = false;
client_Set_All_NotificationIndication(false);
}
// If the flag "doClientConnectCall" is true, we connect to the BLE server!
if (doClientConnectCall) {
doClientConnectCall = false;
bool dummy = client_Connect_Callback();
} // doClientConnectCall
// If the flag "RestartScanningOnDisconnect" is true, we start scanning for a (new) BLE Server!
if(RestartScanningOnDisconnect) {
//DEBUG_PRINTLN("Trying to set indicate off!");
//server_Wahoo_ControlPoint_Chr->getDescriptorByUUID(BLEUUID((uint16_t)0x2902))->setValue((uint8_t*)indicationOff, 2);
pBLEScan->clearResults(); // delete results from BLEScan buffer to release memory
DEBUG_PRINTLN("Client Restarts Scanning for Server Device (Wahoo) with CPS!");
RestartScanningOnDisconnect = false;
pBLEScan->start(0, false);
}
delay(200); // DO NOT REMOVE or Task watchdog will be triggered!!!
} // End of loop |
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
|
Did some testing with V11 (see bold) and everything went perfect. Motor Control Task Created and Active! |
Dear Jan-Willem,
change /* into // and */ into // Have fun! |
Hi Jörgen, At last succes.. I did what you advised and after a reset the Wahoo Kickr had a fix with the Sim32. I did a little testdrive in Zwift for testing the Simcline so I had a serial output. Hereby the serial monitor output from V12. Little other question. For rapid up-down climbs you build in some wait time for simcline to react on the Zwift commands. How can I activate this? Or is it better to have less trainer difficulty? Regards, groetjes JW rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) ESP32 NimBLE SIMCLINE supporting: CPS + Wahoo and CSC (optional)
|
Dear Jan-Willem,
I think it worth the try, to comment that line completely so change it into:
|
Did you install the Simcline App on an Android phone or are you an Iphone lover? I just saw that you found it yourself as well! Good work! |
I commented this line in the latest V12 script. Found the difference yesterday as i posted. 😉Groetjes,Jan-Willem Verzonden vanaf mijn Samsung Galaxy-smartphone.
-------- Oorspronkelijk bericht --------Van: Jörgen van den Berg ***@***.***> Datum: 30-11-2024 14:22 (GMT+01:00) Aan: Berg0162/simcline ***@***.***> Cc: Fukkie-S ***@***.***>, Author ***@***.***> Onderwerp: Re: [Berg0162/simcline] V12 won't connect Kickr Core on bluetooth
(Issue #11)
Dear Jan-Willem,
Finally I have found what is different in version v12 with respect to v11
If you look at the line 618 you will see the following:
while(pBLEScan->isScanning()) ; // Wait until scanning is finished
I think it worth the try, to comment that line completely so change it into:
//while(pBLEScan->isScanning()) ; // Wait until scanning is finished
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>
|
Android phone, JörgenGroetjes,Jan-Willem Verzonden vanaf mijn Samsung Galaxy-smartphone.
-------- Oorspronkelijk bericht --------Van: Jörgen van den Berg ***@***.***> Datum: 30-11-2024 14:23 (GMT+01:00) Aan: Berg0162/simcline ***@***.***> Cc: Fukkie-S ***@***.***>, Author ***@***.***> Onderwerp: Re: [Berg0162/simcline] V12 won't connect Kickr Core on bluetooth
(Issue #11)
Did you install the Simcline App on an Android phone or are you an Iphone lover?
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>
|
Ok, this morning I want to ride a Zwift ride and again difficulties with the Kickr connection. Tried to go back to the earlier version (v11) but it wasn't succesfull. I see in the monitor a successful fix and some time later a client disconnection. Than I remembered the earlier NimBLE library update and reversed it from 1.43 to 1.40. This give me immidiatly succes and now for several times i've a immidiate fix. (second output) 1__________________________
2_______________________ ESP32 NimBLE SIMCLINE supporting: CPS + Wahoo and CSC (optional) E (1223) esp_littlefs: mount failed, (-84) |
Another thing i found was when Zwift have a fix with Sim32 and Kickr it rizes Simcline to +2% - +3%. Anyone who knowes to fix this? Is it a setiing in Zwift? Or in Zwift preferences? On the other hand, when I connect to Simcline Compagnion stays it at 0%. ESP32 Server connected to Client device with MAC Address: [D0:57:7B:EC:A0:BC] Conn Handle: [1] |
Hi Jörgen, I found the section with code for a smoother ride when the road incline moves very quickly up and down. as in Titan Groves Gr JW void xControlUpDownMovement(void *arg) { |
I am not aware of critical differences between 1.43, 1.42 and 1.40. I do not see any error messages that point in this direction, so I think it is a coincidence....
Have you changed permanently the GradeChangeFactor to 200 ? For what reason? Are you sure it is not causing side-effects?
How come "format" is activated here? Do you have problems when switching between versions? Is LittleFS storage corrupted all the time?
Zwift has the strange habit to send after connection with the server a start value for the grade that will set the start resistance of the trainer. It will send new values when the ride starts!
Can you be more specific what value you mean on the App? The Simcline app does not ever get grade information at all!
YOU DO NOT WANT to interfere with vTaskDelay(xDelay); What problem do you want to solve? |
Best Jan-Willem, |
Hoi Jörgen, Goed idee. Inmiddels ben ik al weer een hoop wijzer geworden door het lezen over het Climb onderwerp. Ik ben van plan om de Arduino versie en bibliotheek te installeren op de laptop waar Zwift op geïnstalleerd staat om zo de ritten te kunnen loggen. Gisterenavond had ik onder het trainen 2x een Kickr die de verbinding verloor. Dat kon ik wel weer repareren door Simcline te resetten maar ik zou toch graag willen weten wat er nu precies gebeurd. Dan kunnen we wellicht verbindingsproblemen uit het log vissen. Het project is veel uitgebreider dan ik had verwacht maar ik pak de uitdaging met 2 handen aan. Het komt goed. 😉 Groetjes JW |
Wacht a.u.b. met weer een nieuwe kaart te trekken, first things first! Eerst even dit spel uitspelen! Stap voor stap, toch? |
Onderdeel van het speelveld is ook versie 1.2 Aub zie erop toe dat in v12 de volgende secties er precies zo uitzien als:
Tussen haakjes, heb je je gerealiseerd dat dankzij de 200 waarde in jouw code de (onverwachte) Zwift start waarde van 3.8 naar 2 * 3.8 = 7.6 ging toen je in ERG Mode ging trainen?
Ik hoor graag als er nog andere delen of waarden zijn gewijzigd. |
Als we een gelijk speelveld hebben is het eerste wat je gaat testen: het opstarten van v12 en zien of de procedure normaal verloopt.
Dus hou even bij (op papier ofzo) wat de resultaten van deze 3 stappen steeds zijn! Ik hoef geen serial output, tenzij er rare dingen gebeuren, maar wil wel weten hoevaak dit goed gaat en zo niet wat er steeds/soms misgaat! |
Ter informatie: ik heb zojuist mijn eigen setup getest en alles werkte 100% , onmiddellijk, volledig en zonder fouten of hickups. Mijn setup is een Wahoo Kickr trainer simulator running op een nRF52480 board. Een Adafruit Feather ESP32 V2 running Wahoo Kickr Simcline code v12 met een Simcline 2.0 + full working mechanics. Ik deed een rondje Zwift Sands en Sequoias, na de start een U-turn zodat je in een paar minuten op het kritieke stuk zit met de heuveltjes die elkaar snel opvolgen..... Nu jij nog, maar je zal wel zelf moeten trappen, ik reed in een bureau stoel! |
Ik gebruik Zwift Compagnion app soms wanneer ik in een workout zit of in een groepsrit. Simcline Compagnion app gebruik ik niet echt om iets in te stellen. Meer om even uit te testen en om te zien of de bluetooth verbindingen zichtbaar zijn. Ik ga ermee aan de slag, Jörgen. Kom later terug met met info. |
Nou, hier de resultaten: Eerst tools gecheckt en daarna het V12 script nagelopen en LittleFS instellingen gewijzigd omdat die anders waren. Daarna 10x getest en daarvan kreeg ik 5x een fix met de trainer. En als ik een fix met de trainer had dan kon ik ook doorstarten naar Zwift en een route fietsen. Wat opviel, wanneer de Sim32 de weerstand overnam bij het pairen in Zwift dan werd de voorkant in 3x van de gevallen naar 2,7 graden getild en 2x naar 4,9 graden. Trappen laat je dan wel uit je hoofd tot een Zwift route is geladen ;-) . 1 op 2 van de pogingen mislukt dus doordat na de bluetooth verbinding met de Kickr er geen services komen. Op zich geen probleem als ik weet dat met een herstart wel een succesvolle poging kan komen. Wanneer ik eerst Sim32 start en even wacht, daarna Kickr aanzet en het blauwe bluetooth lampje blijft knipperen dan kan ik stoppen, dan komt er geen fix. Als het blauwe lampje daarentegen meteen gaat branden dan weet ik dat ik kan doorgaan naar Zwift. Hieronder een output van een mislukte poging. Zo zagen de outputs van de mislukte pogingen er overigens allemaal uit . Persoonlijk denk ik dat het eerder een Wahoo Kickr probleem dan een probleem aan de kant van Sim32. Wat denk jij? ESP32 NimBLE SIMCLINE supporting: CPS + Wahoo and CSC (optional)
|
Beste Jan-Willem,
Kun je een screen shot maken en uploaden naar dit "issue" zoals ik gevraagd had? Sorry, maar ik wil het graag zelf zien! Dankzij de tests die je deed ben ik erachter dat je Kickr soms iets te traag reageert nadat er een connectie verzoek is gedaan. Er is dus sprake van een timing probleem tijdens het connectie proces... soms doet ie het niet en soms doet ie het wel! |
5 pogingen gedaan, 1x geen fix. Waarschijnlijk is het weinig wat er aan tijd bij moet. ;-) 1e poging en meteen een fix maar niet door gegaan naar Zwift 2e met fix en naar Zwift 3e poging, geen fix, SIM32 en Kickr reset 4e poging en koppeling met Zwift 5e poging ook meteen een fix, maar niet door gegaan naar Zwift |
Hoi Jörgen, We hadden vanavond een groepsrit op Zwift en na aanvankelijk een goede start viel ineens het vermogen weg. Dat was het begin van ruim een half uur testen en resetten. Hopelijk kun jij er wijs uit worden. Anyway, hier de output: Groetjes JW 1e start - foutmelding lineaire motor 2e start success en gestart met Zwift 3e start na wegvallen vermogen (Zwift aan gelaten - evenement bezig) 4e start omdat er geen fix kwam met Kickr (Zwift aan gelaten - evenement bezig) 5e start omdat de motor functie het niet deed (Zwift aan gelaten - evenement bezig) 6e start omdat verbinding met Kickr wegviel (Nu ook Zwift herstart) 7e start omdat er geen fix kwam met Kickr 8e start omdat Kickr verbinding wegviel en niet terug kwam 9e start omdat Kickr verbinding wegviel en niet meer terug kwam 10e start omdat er geen fix kwam met Kickr Gestopt, alles uit gezet, gedouched en met analyse begonnen. |
Beste Jan-Willem,
De bedoeling is dat jezelf dat stukje toevoegt (op de juiste plaats en in jouw actuele versie) en vervolgens weer compileert en upload naar de Feather (Simcline). Dat moet toch wel lukken? |
Hoi Jörgen, vanavond testavond. Over het algemeen kan ik positief berichten over de eerste fix met Kickr na de aanpassing. Echter is de verbinding nog verre van stabiel. Wellicht kun je wat met deze 7 pogingen ;-) 1e poging aangepaste versie V12 2e poging 3e poging 4e poging 5e poging 6e poging met V11 en delay voor Kickr om even uit te testen of dit enige verbetering gaf V12 weer geladen voor de laatste poging (7) van vandaag, Kickr Fix ging goed, Zwift fix ging ook goed |
Super bedankt! Connect gaat eigenlijk altijd goed, het is vooral reconnect (na een disconnect) dat problematisch is! Verder is dat spontaan afbreken (disconnect) natuurlijk opvallend. Dat was het al zichtbaar vanaf de allereerste uitdraai die je stuurde. Heel irritant! Vanavond heb ik wat anders te doen maar morgenochtend stuur ik een nieuwe versie (1.4) die beide problemen adresseert. Daar was ik al eerder aan begonnen! |
Toppie 👍Groetjes en bedankt vast,Jan-Willem Verzonden vanaf mijn Samsung Galaxy-smartphone.
..-------- Oorspronkelijk bericht --------Van: Jörgen van den Berg ***@***.***> Datum: 05-12-2024 16:26 (GMT+01:00) Aan: Berg0162/simcline ***@***.***> Cc: Fukkie-S ***@***.***>, Author ***@***.***> Onderwerp: Re: [Berg0162/simcline] V12 won't connect Kickr Core on bluetooth
(Issue #11)
Super bedankt! Connect gaat eigenlijk altijd goed, het is vooral reconnect (na een disconnect) dat problematisch is! Verder is dat spontaan afbreken (disconnect) natuurlijk opvallend. Dat was het al zichtbaar vanaf de allereerste uitdraai die je stuurde. Heel irritant! Vanavond heb ik wat anders te doen maar morgenochtend stuur ik een nieuwe versie (1.4) die beide problemen adresseert. Daar was ik al eerder aan begonnen!
Fijne avond!
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: ***@***.***>
|
Versie 1.4 staat klaar in Github. Deze versie heeft letterlijk uren op de pijnbank gelegen en is vele malen getest op Sands&Sequioas. Blijkt in mijn test omgeving heel stabiel (100% score), maar ja dat is niet hetzelfde als met jouw Kickr versie... Vergeet niet om jouw setup specifieke settings (Mac Addresses en Min en Max position) in deze v14 aan te brengen, voordat je die compileert en upload naar de Feather... Ik zou zeggen: Happy testing! |
Hoi Jórgen, Ik heb vanmiddag mijn best gedaan (20 starts) en wat opviel was het slechte pairen van de Kickr. Maar als er een fix was dan kon ik de hele rit uitfietsen. Ik heb uiteindelijk 3x Volcano circuit gefietst en de laatste keer zelfs 2x helemaal gestopt en daarna weer begonnen. En inderdaad wat je zei, de Kickr data word gewoon weer opgepakt en de verbinding blijft intact. Mijn complimenten daarvoor. Overigens zette ik de Kickr tussen elke gevraagde reset uit en wachtte minimaal 30 - 60 seconden alvorens de Feather te resetten. Anyway, je kijkt zelf maar hieronder. Wellicht kunnen we de resultaten van het pairen nog wat verbeteren. De verbinding valt in elk geval niet meer weg en dat is grote winst. Groetjes JW 1: Geen fix met Kickr 2: Geen fix met Kickr 3: Geen fix met Kickr 4: Geen fix met Kickr 5: Geen fix met Kickr 6: Geen fix met Kickr 7: Geen fix met Kickr 8: Eindelijk een fix met Kickr - Volcano Circuit helemaal kunnen fietsen (1e keer) 9: Motor check faalde 10: Geen fix met Kickr 11: Geen fix met Kickr 12: Fix en wederom Volcano Circuit kunnen fietsen (2e maal) 13: geen fix met Kickr 14: Fix met Kickr gelukt en Volcano Circuit uit gefietst(3e maal). Let op: nu met 2 volledige stops erin! 15: Geen fix met Kickr 16: Geen fix met Kickr 17: Motor check faalt 18: Geen fix met Kickr 19: Geen fix met Kickr 20: Geen fix mnet Kickr |
Beste Jan-Willem,
NB Er hoeft niet gefietst te worden, we zijn alleen geinteresseerd in connect'ie wel of connect'ie niet. Zet de trainer uit (Power OFF), reset de Simcline (ESP32 board) en daarna zet je de trainer weer aan (Power ON)... |
Hoi Jörgen, Een middagje testwerk achter de rug. Een hoop data om de verbinding van Sim Client 010 code te te analyseren. Uiteindelijk kreeg ik met eerst KickR aanzetten en dan pas Sim een betrouwbare methode voor succes. Dat gaf aanleiding voor het laden van V14 code en kijken of die methode daarin ook werkte (Eerst Kickr aan en wachten op een fix om daarna de laptop te starten en te controleren of de fix in Zwift ook zou gaan lukken. Daarom in het volgende comment een uitdraai van die pogingen. Succes met de analyse. Vg JW 1e connect poging met trappen en weerstand testen (gelukt) 2e poging - alles uit, Sim starten en daarna Kickr. Trappen en weerstand test (gelukt) 3e poging, alles uit eerst Sim starten en daarna Kickr. Trappen en weerstand getest (OK). 4e poging: Alles uit, eerst Sim starten en daarna Kickr. GEEN fix **5e poging: Alles uit, eerst Sim starten en daarna Kickr. Geen fix, aleen Kickr reset maar dat gaf geen resultaat, daarna alleen Sim reset en toen een fix 6e poging, Kickr aan laten staan, Alleen Sim reset en meteen een fix 7e poging, Alles uit, eerst Kickr aan en daarna Sim. Succes 8e poging. Alles uit, eerst Kickr aan en daarna Sim.Weer succes! 9e poging. Eerst Kickr aan en daarna Sim. Weer Succes! **10e poging. Eerst Kickr aan en daarna Sim. ** |
Hierbij het vervolg waarbij ik aanteken dat de resultaten nog steeds wisselend zijn. Succes met de analyse. 1e poging na reset laden van V14 - Eerst Kickr aan en daarna SIM - geen fix 2e poging. Eerst Kickr aan en daarna Sim. Uiteindelijk een fix (geduld!) 3e poging. Eerst Kickr en daarna Sim. Vrijwel meteen een fix 4e poging. Eerst Kickr en daarna Sim. Vrijwel meteen een fix 5e poging. Eerst Kickr en daarna Sim. En weer succes! 6e poging en nu met dezelfde volgorde naar Zwift. Helaas, geen fix met Kickr 7e poging: Kickr aan laten staan en Sim reset: Nu wel een fix met Kickr ** 8e poging. Alles uit en eerst Kickr aan en daarna Sim. Vrijwel meteen een fix.** poging 9, wanneer fix laptop aan zetten en door naar Zwift. Eerst Kickr aangezet en daarna Sim. Een fix dus door naar Poging 10. Kickr aangezet, Sim aangezet, met vertraging was de fix gelukt, laptop aangezet, Zwift gestart, Poging 11 (laatste vandaag). Een herhaling van de vorige echter stond de laptop nog aan. Geen fix met Kickr. Laptop uitgezet |
Beste Jan-Willem,
In de loop van de jaren heeft meneer Zwift de naamgeving (op het scherm) van de verschillende onderdelen gewijzigd, maar de bedoeling is toch behoorlijk duidelijk met de "oude" namen. Ik ben zo naif om te denken dat Simcline gebruikers de aangegeven volgorde ook steeds gebruiken. |
Hoi Jörgen, Dank weer voor je reply. Ik had uiteraard de volgorde wel gezien maar omdat dit niet consequent werkte ben ik zelf gaan experimenteren niet wetende dat bluetooth soms zo'n moeite heeft om te koppelen met Zwift (my bad). Wat je zegt over Zwift icm Wahoo klopt natuurlijk. Er was nooit concurrentie voor de Kickr dus altijd een fix. Bovendien werkte ik altijd met een ant+ dongel en niet rechtstreeks met bluetooth. Dat betekende een extra concurrent in het geheugen van Zwift. Ik heb nog wel een ideetje om te testen. Zwift houdt bij wie er allemaal koppelen en ik wil vanavond eens testen om dit xml bestand op alleen lezen te zetten om zo alleen Sim 32 als bekende cliënt te laten fungeren. Laat na mijn ritje vanavond nog wel even weten of dit werkt. Fijne dag nog.. Vg Jan-Willem |
Beste Jan-Willem, |
Hoi Jörgen, Ik heb je berichtje gemist door de drukte maar wel interessante data. Na aanvankelijk zonder log geprobeerd te hebben om Kickr gepaird te krijgen heb ik uiteindelijk de laptop er weer aangehangen om te zien wat er gebeurde. Weer vreselijk veel moeite om Kickr te pairen terwijl ik de volgorde aanhield die eerder zulke goede resultaten gaf. Nadat ik eindelijk succes had ben ik gestart met een Join workout. Dat liep niet goed af doordat de verbinding wegviel. Toen de workout afgemaakt met de Kickr rechtstreeks gekoppeld. Na de workout weer getest en ik kreeg op een gegeven moment wisselende resultaten. Wel verbinding met Kickr als ik trapte (dan kwam het verplichte signaal) en toch wegvallen als Zwift opstartte. Dan weer verbinding met Kickr zonder te trappen, maar weer geen signaal naar Zwift. Kortom ik kan er geen vinger achter krijgen. Morgen ga ik je nieuwe code testen. Groet JW Diverse pogingen om de Kickr te pairen op dezelfde manier als voorheen. Eerst Kickr starten en dan Sim 19:25:11.651 -> 19:31:54.022 -> 19:33:31.388 -> 19:35:08.291 -> Connect eindelijk gelukt en de laptop opgestart. Zwift gestart en een join workout gestart. Op een gegeven moment viel hetr vermogen weg en kwam niet meer terug. Daarna Kickr rechtstreeks verbonden en mijn workout afgemaakt. Na de workout de laptop uitgezet en opnieuw diverse pogingen om een connectie te maken 21:14:41.706 -> 21:16:07.106 -> 21:22:24.781 -> hieronder een interessante. Ik kreeg connectie en nadat ik de laptop had opgestart en Zwift zover was had ik wel verbinding met Sim maar niet meer met Kickr. |
Hoi Jörgen, ik zou graag een nieuwe feather willen bestellen maar er zijn zoveel smaken dat ik twijfel welke ik moet kopen. https://www.kiwi-electronics.com/nl/zoeken?search=feather&page=2 Alvast dank. Groetjes Jan-Willem |
Beste Jan-Willem, Het zou mooi zijn als de laatste versie v14 getest werd op die manier! Simcline en ERG Mode in Zwift, met welk doel? Heb je enig idee hoe ERG Mode werkt?
|
Goedemorgen Jörgen, Sorry dat ik de aangehouden procedure niet heb omschreven, het was laat en ik moest douchen / naar bed omdat mijn wekker 5:15u afloopt. Maar ik had me steeds aan de eerder omschreven opstartprocedure gehouden en op een gegeven moment gaan loggen met de Zwift laptop om te achterhalen wat er nu niet goed ging. Dit was van 19:23 tot 19:43u. Was toen al 10 minuten bezig met alleen de Sim en de Kickr om een goede verbinding te krijgen. Na de workout ben ik met de andere laptop gaan loggen en testen. Het was allemaal zeer wisselend en later zag ik waarschijnlijk de reden voor het moeilijke contact. De telefoon... Zwift stond daar nog open van de workout.🤦♂️🤦♂️🤦♂️ Ik ga vanavond de nieuwe code laden en testen. Mocht dit geen verbetering geven dan zou ik nog een nieuwe Arduino Feather V2 willen proberen met jou gewenste specificatie en test ik verder. Betreft je opmerking over ERG, ik had de workout natuurlijk meteen buiten Sim om moeten doen. Achteraf is mooi wonen, dus beschouw aub de output gegevens van gisteren maar als niet verzonden. Ik had beter moeten weten en opletten. Excuus daarvoor. Betreft je aanpassing voor Wahoo op de website: tiptop omschreven. Misschien een aanvulling van de Wahoo support website. Bluetooth apparaten (smartphone) en WiFi routers dicht in de buurt hebben een negatieve invloed op het tot stand komen van de verbinding (2,4 band). En draaiende ventilatoren die dicht in de buurt staan ook? Vast bedankt voor alles en je hoort van me. Groetjes Jan-Willem Ps. Dat Zwift open stond zag ik vanmorgen pas toen ik alle meldingen op mijn telefoon bekeek. Daar was gisterenavond geen tijd meer voor. |
Goedenavond Jörgen Hier zijn de testresultaten. Niet bemoedigend moet ik zeggen. Succes weer TEST 10-09-2024 V14_2 1e 2e 3e 4e 5e |
Beste Jan-Willem,
Kortom het uitgangspunt moet zijn: Oh, Simcline connect niet met de Wahoo en/of Zwift, wat doe IK verkeerd (procedure) of welke omstandigheden in MIJN ruimte kunnen dit veroorzaken? |
Hoi Jörgen, Kort door de bocht zal je ongetwijfeld gelijk hebben. Maar ik had gisteren tijdens testen van de laatste versie 14 geen Zwift laptop aan en ook geen telefoon in de buurt. Ik wil het probleem graag oplossen maar hoe kan ik controleren wat het pairingsproces tussen Kickr en Sim steeds verstoord? Ik weet vanuit de Sim Compagnion app dat er meer Mac adressen signaal uitzenden. Maar welke zit ons dan in de weg? Ik doe de opstart procedure consequent door eerst Kickr te starten en daarna Sim. En zonder laptop aan de Sim voor output kan ik eigenlijk aan het knipperende blauwe lanpje al zien dat er geen fix is en dat ik de procedure opnieuw kan doen. Als ik dan een gemiddelde score krijg van 5 uit tien en daarna ook nog eens soms de verbinding verlies.. Tja, dan lijkt er iets mis met de hardware of met de software. 🤷♂️ Maar goed. Als dit het is, dan is dit wat het is, Jörgen, dan waren het leerzame weken. Groetjes Jan-Willem |
Beste Jan-Willem,
In de volgende stap ga je een (1) component toevoegen namelijk de Laptop om Serial Output op het scherm te kunnen weergeven, als er een Zwift versie opstaat dan mag die absoluut NIET opgestart worden! |
Goedenavond Jörgen, Vast een positieve bericht. Ik heb met je advies nu steeds een snelle verbinding met de Kickr. Heel af en toe moet ik de Sim een keer resetten, maar omdat ik nu een drukknopje aan de buitenkant van de box heb zitten gaat dat ook makkelijker.
Morgenochtend zal ik na de workout 10x een repeterende starts uitvoeren in de bovenstaande procedure en daarna de resultaten delen. Ik heb inmiddels realtherm geïnstalleerd en geleerd hoe ik de output kan capturen in een tekst file. Dit scheelt een heleboel ge-emmer met resultaten uit IDE output te halen. Groetjes Jan-Willem |
13-12-2024 Test V14 De workout met de ANt+ dongle afgewerkt en vanmiddag weer verder getest met Sim32. Conclusie: bij verbindingsproblemen tussen Sim32 en Wahoo Kickr, zorg dat er geen device met bluetooth in de buurt ingeschakeld is waar de Wahoo Kickr eerder mee heeft gepaired. Dan protocol volgen door eerst Wahoo Kickr op te starten, even te wachten, dan Sim32 in te schakelen en door de zelfcheck te laten gaan. Dan checken op de Wahoo Kickr of het blauwe lampje aangeeft dat deze is gekoppeld met de Sim32. Daarna de device opstarten waar de Zwift app op staat en Zwift oplaten starten. In het pairmenu verbinding maken met Sim32 en evt checken of er Watt output is, daarna de weerstand koppelen aan Sim32. Deze reageert meteen door enkele graden te stijgen en de weerstand te verzwaren. Daarna een route uitzoeken en Zwiften maar. Heel erg bedankt voor je geduld, Jörgen. Het was voor mij als 62 jarige een hele leerzame oefening en ik ben enorm blij met het eindresultaat. Ik heb verder geen vragen meer. Groetjes en nog een fijn weekend, Jan-Willem |
Best Jan-Willem, |
Hi Jörgen,
As you mentioned to open an issue on Github, here is my loaded V12 program and the serial monitor output (after reset). Hope you can find why SIM32 won't connect Kickr anymore. I tried V11 and this version fix my Kickr immidiately.
And sorry for my English, my 4y education on the lower technical school is 45 year ago :-)
Best regards
Jan-Willem
The text was updated successfully, but these errors were encountered: