{"id":19892217,"url":"https://github.com/berg0162/airflow","last_synced_at":"2026-04-13T09:31:29.062Z","repository":{"id":190345941,"uuid":"383053312","full_name":"Berg0162/airflow","owner":"Berg0162","description":"Thermoregulated Airflow Cooling for Indoor Cycling","archived":false,"fork":false,"pushed_at":"2023-11-03T07:44:21.000Z","size":17723,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-11T19:43:51.343Z","etag":null,"topics":["airspeed-velocity","airstream","android","app","arduino","convection","cooling","cycling","evaporation","fan","feather","headwind","heat","heat-balance","heat-transfer","indoor","nrf52840","radiation","rt-critical-power"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Berg0162.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2021-07-05T07:31:40.000Z","updated_at":"2023-11-03T07:44:25.000Z","dependencies_parsed_at":"2023-12-20T13:25:37.910Z","dependency_job_id":"6b01c629-6e97-40ec-9c6c-7b824e824e4d","html_url":"https://github.com/Berg0162/airflow","commit_stats":{"total_commits":247,"total_committers":2,"mean_commits":123.5,"dds":0.04858299595141702,"last_synced_commit":"e05ace91ee06459d0e9fe8604125ac4bb3a8aa84"},"previous_names":["berg0162/airflow"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Berg0162%2Fairflow","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Berg0162%2Fairflow/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Berg0162%2Fairflow/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Berg0162%2Fairflow/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Berg0162","download_url":"https://codeload.github.com/Berg0162/airflow/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241319939,"owners_count":19943639,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["airspeed-velocity","airstream","android","app","arduino","convection","cooling","cycling","evaporation","fan","feather","headwind","heat","heat-balance","heat-transfer","indoor","nrf52840","radiation","rt-critical-power"],"created_at":"2024-11-12T18:22:32.121Z","updated_at":"2026-04-13T09:31:29.034Z","avatar_url":"https://github.com/Berg0162.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"../main/images/AF_logo.png\" width=\"64\" height=\"64\" alt=\"Airflow Icon\"\u003e \u0026nbsp; AIRFLOW\n\u003cimg src=\"../main/images/AIRFLOW_com.png\" width=\"900\" height=\"500\" align = \"middle\" alt=\"Airflow\"\u003e \u003cbr\u003e\n# Thermoregulated Airflow Cooling for Indoor Cycling \u003cbr\u003e\nThe \u003cb\u003eAIRFLOW\u003c/b\u003e is a smart device that accurately generates the ideal airflow velocity for an optimally thermoregulated human body when heat production balances heat loss. Every minute the \u003cb\u003eAIRFLOW\u003c/b\u003e smart device determines the very appropriate airflow velocity to reconcile the Heat Balance Equation.\n\u003cimg src=\"../main/images/HeatBalancePic_small.png\" width=\"400\" height=\"100\" align = \"right\" alt=\"Heat Balance\"\u003e For indoor cycling on a static trainer a stable Heat Balance is paramount. Thermal heat stress may seriously affect the performance, overall productivity, safety and health of an individual. Discomfort at least, even heat illness and at worst heat stroke are three phases of the reaction of the human body when exposed to an unstable Heat Balance (hot, humid environment and inadequate airflow). \u003cbr clear=\"left\"\u003e\n\u003cimg src=\"../main/images/AIRFLOW_equation.png\" width=\"500\" height=\"140\" align = \"right\" alt=\"Airflow equation\"\u003e\nThe Heat Balance Equation can be rewritten in such a way that the requested Airflow Velocity (\u003cb\u003eV\u003csub\u003ea\u003c/sub\u003e\u003c/b\u003e) is a function of the other terms and can be calculated at any time!\nThe \u003cb\u003eAIRFLOW\u003c/b\u003e smart device continuously tunes accordingly the requested airflow velocity of the fan(s) for a stable Heat Balance during all phases of an indoor cycling workout, warm-up, intensity intervals, intermittent recovery and at cooldown. The cyclist has no on-the-way interference and can fully concentrate on the demands of the stationary trainer workout, always facing the ideal airstream that will cool him/her appropriately. \u003cbr clear=\"left\"\u003e\nIn a separate document the Heat Balance Equation and the applied algorithms are explained and elaborated see: \u003cbr\u003e\n* [View Heat Balance And Algorithms Elaborated](../main/docs/Heat_Balance_And_Algorithms_Elaborated.pdf) \u003cbr\u003e\n* [View Calculation Example in Appendix](../main/docs/APPENDIX.pdf) \u003cbr\u003e\n\n# What is needed to determine the critical variables of the Heat Balance Equation \u003cbr\u003e\n\n* \u003cb\u003eHeart Rate Monitor (Dual ANT+ and) Bluetooth LE transmitter\u003c/b\u003e \n\u003cimg src=\"../main/images/heart_rate_monitor.jpg\" width=\"200\" height=\"150\" ALIGN=\"right\" alt=\"Heart rate monitor\" \u003e \u003cbr\u003e\nThe \u003cb\u003eAIRFLOW\u003c/b\u003e device needs continuous measurement of the heart rate to determine the critical mean body temperature which is proportional to the netto heat that is stored in the body. Most cyclists are used to wear a heart rate band during a workout and when that band is transmitting data over BLE it will be suitable for the AIRFLOW device. Notice that many of the (older) devices allow only \u003cb\u003eone\u003c/b\u003e BLE connection at the time, which means that you cannot concurrently connect your cycling computer and Zwift computer and/or \u003cb\u003eAIRFLOW\u003c/b\u003e device over BLE with the heart rate band... If possible: Use ANT+ for regular connections and only BLE for the AIRFLOW device!\n\u003cbr clear=\"left\"\u003e\n\n* \u003cb\u003eA Power meter with (Dual ANT+ and) Bluetooth LE transmitter\u003c/b\u003e\n\u003cimg src=\"../main/images/power_meters.png\" width=\"350\" height=\"200\" ALIGN=\"right\" alt=\"Power meters\" \u003e \u003cbr\u003e\nThe \u003cb\u003eAIRFLOW\u003c/b\u003e device needs continuous measurement of the critical cycling power, produced during a workout, to determine how much energy the body is generating.\n\u003cins\u003eThe Power meter of your smart indoor trainer can supply the power measurements that you push during a workout.\u003c/ins\u003e Or a dedicated power meter mounted on the bike at the crank, pedals or rear hub as long as it is capable of transmitting power data over Bluetooth Low Energy (BLE). Notice that many of the (older) devices allow only \u003cb\u003eone\u003c/b\u003e BLE connection at the time, which means that you cannot concurrently connect your cyling computer and Zwift computer and/or AIRFLOW device over BLE with the power meter... If possible: Use ANT+ for regular connections and only BLE for the \u003cb\u003eAIRFLOW\u003c/b\u003e device!\n\u003cbr clear=\"left\"\u003e\n\n* \u003cb\u003eTemperature \u0026 Humidity Sensor\u003c/b\u003e \u003cbr\u003e\nAmbient air temperature and relative humidity are critical variables during any serious workout. These are measured with a Adafruit sensor continuously and is part of the electronic circuitry that processes all measurements! \u003cbr\u003e\n\n# Electronic Components and Circuitry of AIRFLOW device\u003cbr\u003e\n* Adafruit Sensirion SHT31-D - Temperature \u0026 Humidity Sensor\n* Adafruit OLED 128x64 I2C blue display\n* Robotdyn AC Light Dimmer Module, 1 Channel, 3.3V/5V logic, AC 50/60hz, 220V/110V\n* Adafruit Feather nRF52840 Express\n* Table Fan 220/110 Volt AC 50/60 hz\n\u003cbr\u003e\nIn the following image the wiring of the electronic components is shown.\u003cbr\u003e\n\u003cimg src=\"../main/images/AF_circuitry.png\" width=\"900\" height=\"500\" ALIGN=\"middle\" alt=\"Circuitry\" \u003e \u003cbr\u003e\n\n# Controlling Air Velocity of Fans with PWM \u003cbr\u003e\n\nThe AC Dimmer Module by Robotdyn is designed to control Alternating Current/voltage (110/220V). It can control AC levels up to 400V/8А. In most cases, the AC Dimmer Module is used to turn the power ON/OFF or \u003cb\u003edim\u003c/b\u003e lamps or heating elements. It can be used as well with fans, pumps, air cleaners, etcetera. A major benefit of the Robotdyn board is that the 110/220V part is (optically) isolated from the 5V logical control part, to minimize the possibility of high voltage damage of the attached low voltage microcontroller. The logical level of the Dimmer Module is tolerant to 5V and 3.3V, therefore it can be connected to a microcontroller with 5V and 3.3V level logic. \u003cbr\u003e\n\u003cimg src=\"../main/images/Duty_Cycle_animation.gif\" width=\"160\" height=\"110\" ALIGN=\"right\" alt=\"Duty Cycle\" \u003e\nFor our Arduino Feather nRF52 Express a dedicated Timer/Interrupt handling library has been applied, which allows external interrupts and process internal time interrupts to switch AC power ON/OFF to enable PWM on the fans. The AC Dimmer is connected to the Arduino Feather nRF52 board via two digital pins. One pin (interrupt) is to detect the Zero Crossing of the 50 Hz AC Phase, which is applied to initiate an interrupt signal to start the cut-off of the AC cycle. A second pin (PWM) is to control the width (or time) of the cut-off duration of the cycle, which determines how long the current is switched ON/OFF. This results in a software controlled duty cycle of the fan that will force the fan to proportionally blow the cooling air harder or softer. This way the airflow velocity of the fan(s) can be controlled precisely and set to the appropriate level! \u003cbr clear=\"left\"\u003e\n\n# Timer library \u003cbr\u003e\n[View the code of the Timer library](../main/arduino/libraries/Timer/Timer.cpp) \u003cbr\u003e\n[View the header of the Timer library](../main/arduino/libraries/Timer/Timer.h) \u003cbr\u003e\n\nI am much obliged to Cedric Honnet who designed the Timer library initially as a part of the Hivetracker project, ref: [Original Hivetracker Timer library](https://github.com/HiveTracker/Timer). The library code has been updated to conform the more recent nRF52-timer implementations on nRF52 boards.\u003cbr\u003e\n\n# All Relevant Code Snippets in Main program for handling duty cycles of both fans\u003cbr\u003e\n\nThe global variables \u003cb\u003eActualUpperFanPerc\u003c/b\u003e and \u003cb\u003eActualLowerFanPerc\u003c/b\u003e are determined by the outcome of the Heat-Balance-Equation and set to the appropriate percentage of fan capacity (0-100%) and this is translated to precise time intervals (in microseconds) for the duty cycle by \u003cb\u003eSetBothFanPeriods(void)\u003c/b\u003e. Every 10 milliseconds a Zero Cross (external) interrupt is detected and the duty cycles for both fans are set separately using 2 of the accurate internal nRF52 microsecond-timers.\u003cbr\u003e\n\n```C++\n#include \u003cnrf_timer.h\u003e // Native nRF52 timers library\n#include \u003cTimer.h\u003e     // Heavy duty micro(!)seconds timer library based on nRF52 timers, needs to reside in the libraries folder\n```\n```C++\n// Feather nRF52840 I/O Pin declarations for connection to the ROBOTDYN AC DIMMER boards\n#define PWM_PIN_U_FAN  (12U) // Upper Fan \n#define ISR_PIN        (11U) // AC Cycle Zero Cross detection pin (for both FANS)\n#define PWM_PIN_L_FAN  (10U) // Lower FAN\n\n// Values are in microseconds and a half 50Hz cycle is 10.000us = 10 ms !\n#define MAX_TIME_OFF_UPPER (6950U) // Maximal time of half an AC Cycle (10.000us) to be CUT-OFF in microseconds!\n#define MAX_TIME_OFF_LOWER (6920U) // Maximal time of half an AC Cycle (10.000us) to be CUT-OFF in microseconds!\n#define MIN_TIME_OFF (2000U)       // Minimal time to process for full power --\u003e NO (!) CUT-OFF situation in microseconds!\n```\n```C++\n...\n  // ISR Function is called 2(!) times in one full 50Hz cycle --\u003e 100 times in a second\n  attachInterrupt(digitalPinToInterrupt(ISR_PIN), zero_Cross_ISR, RISING);  // Set Zero-Crossing Interrupt Service Routine\n...\n```\n```C++\nvoid zero_Cross_ISR(void) {\n  // Normal mode\n  digitalWrite(PWM_PIN_L_FAN, LOW); // Set AC cycle OFF --\u003e LOW\n  digitalWrite(PWM_PIN_U_FAN, LOW); // Set AC cycle OFF --\u003e LOW\n\n  NrfTimer2.attachInterrupt(\u0026LFanDutyCycleEnds, ActualLowerFanPeriod); // microseconds !\n  NrfTimer1.attachInterrupt(\u0026UFanDutyCycleEnds, ActualUpperFanPeriod); // microseconds !\n}\n\nvoid UFanDutyCycleEnds(void) {\n  digitalWrite(PWM_PIN_U_FAN, HIGH); // Set AC cycle ON --\u003e HIGH\n}\n\nvoid LFanDutyCycleEnds(void) {\n  digitalWrite(PWM_PIN_L_FAN, HIGH); // Set AC cycle ON --\u003e HIGH\n}\n\n```\n\n```C++\nvoid SetBothFanPeriods(void) {\n  // ------------------------------------------------------------------------\n  ActualUpperFanPeriod = int(map(ActualUpperFanPerc, 0, 100, MAX_TIME_OFF_UPPER, MIN_TIME_OFF));\n  ActualLowerFanPeriod = int(map(ActualLowerFanPerc, 0, 100, MAX_TIME_OFF_LOWER, MIN_TIME_OFF));\n}\n```\n# Code snippet of Heat Balance Equation routine\nNotice that Heart Rate (HRM) and Cycling Power (CPS) is measured at their respective device dependent frequencies, usually once to 4 times per second! The incoming HRM and CPS data are processed continously. Once per minute the \u003cb\u003eHeatBalanceAlgorithm\u003c/b\u003e routine is called to calculate all terms of the Heat Balance Equation and to update the appropriate airflow velocity of the fans.\n\n```C++\nvoid HeatBalanceAlgorithm(void) {\n  double F2, F3;\n  float DeltaPska, DeltaTska, Psa;\n  float Psk = 0.0; // Partial vapor pressure at skin temperature\n  float Pair = 0.0; // Partial vapor pressure at ambient air temperature\n  // -----------------------------------------------------------------------------------------------\n  // NOTICE: Dry-Bulb Temperature EQUALS, in indoors situation, the Ambient Air Temperature --\u003e Tdb = Tair\n  //------------------------------------------------------------------------------------------------\n  // Algorithm that estimates Core Temp. based on HeartRateMeasurement \u003e\u003e\u003e\u003e\u003e AT ONE MINUTE INTERVALS !!\n  if (IsConnectedToHRM) { // Only calculate and update Core Temperature when connected to HRM-strap\n    EstimateTcore(Tcore_prev, v_prev, (double)HBM_average); // Estimate the Core Body Temperature from Heart Beat sequence\n  }\n  v_prev = v_cur;\n\n  // Calculate environmental variables\n  Psa = (float)Pantoine((double)Tair); // in units kPa\n  Pair = RH * Psa; // saturated water vapour pressure at ambient temperature corrected for Relative Humidity (default = 70%)\n\n  // Calculate Metabolic Energy\n  MetEnergy = (double)( CPS_average * ((float)(100 / GE)) ) / Ad; // in Joules/m2 --\u003e individualized (div Ad)\n\n  // Estimate now the mean body skin temperature from\n  Tskin = EstimateSkinTemp(Tair, Pair, Vair, MetEnergy, (float)Tcore_cur);\n\n  // Continued: Calculate environmental variables and determine the appropriate Delta's\n  Psk = (float)Pantoine((double)Tskin); // saturated water vapour pressure at the wetted skin surface in units kPa\n  DeltaPska = (Psk - Pair); // Calculate the DeltaPska pKa\n  DeltaTska = (Tskin - Tair); // Calculate DeltaTska in degrees Celsius\n\n  // Determine now the Mean Body Temperature from estimated Core Temperature and mean estimated Skin temperature\n  Tbody_cur = (float)0.67 * Tcore_cur + 0.33 * Tskin; // equation according to Kerslake, 1972 (confirmed in other papers)\n\n  // Calculate how much heat has been produced since previous round\n  if (IsConnectedToHRM) { // Only calculate and update HeatChange when connected to HRM-strap\n    if (Tcore_cur \u003e Tcore_start) { // Skip first measurement(s) until a steady effort is delivered --\u003e Tcore_start temperature has reached!\n      DeltaStoredHeat = HeatChange(Tbody_cur, Tbody_prev, HC_Interval); // Heat Change calculated in J/m2\n    } else {\n      DeltaStoredHeat = 0.0;\n    }\n  }\n  // ...... some tweaking ....\n  if ( (DeltaStoredHeat \u003c 0) \u0026\u0026 TWEAK ) { // TWEAK Heat loss  !\n    StoredHeat += (1.50*DeltaStoredHeat); // Tcore-algorithm underestimates Heat Loss (Cool down) --\u003e Increase artificially Heat Loss contribution !!!\n  } else {\n    StoredHeat += DeltaStoredHeat;   // equation  S == internally stored body heat NO Tweaking\n  }\n\n  // Calculate Core Temp change\n  DeltaTc = (Tcore_cur - Tcore_prev);\n\n  // Store the actual T-results now for comparison in the next round to determine the T-Delta's\n  Tcore_prev = Tcore_cur;\n  Tbody_prev = Tbody_cur;\n\n  // Calculate Terms from Heat balance equation\n  F2 = (double)(Hc * DeltaTska + He * DeltaPska); // equation  1 (C+E) without Va(exp 0.84)\n  Radiation = (double)Hr * DeltaTska;        // equation  Radiation heat exchange\n\n  // equation component H  use CPS_average for \"External Work\"\n  HeatProduced = (double)( CPS_average * ((float)(100/GE) - 1) ) / Ad; // in J/m2 equation 1 H = (M-W)/Ad \u0026 GE = W/M -\u003e M = W/GE\n  SumHeatProduced += HeatProduced * HC_Interval / 1000; // sum of total heat produced during the workout, units in kJ/m2\n  SumEnergyProduced += MetEnergy * HC_Interval / 1000; // in kJ/m2 --\u003e individualized\n\n  // Solve Full Heat Balance equation with HeatProduced (H) AND HeatStored (S) to find air velocity !\n  // Heat Balance equation:\n  // StoredHeat represents the stored internal heat (S) that is to be removed together with the produced heat (H),\n  // the sum of both (!) is the heat we want to exchange with the environment...\n  F3 = (HeatProduced + StoredHeat - Radiation) / F2; // right side of the FULL heat balance equation S = StoredHeat\n  // POW with decimal exponent (1.1905) can't process a negative base (F3), --\u003e POW equation gives \"nan\" result!\n  if ( F3 \u003e 0 ) {\n    Vair = (float)pow(F3, 1.1905);         // in equation exp == 0.84 --\u003e 84/100 -\u003e 100/84 = 1.1905\n  } else Vair = 0.0;\n\n  // Now that we know Va, C and E can be re-calculated !! Notice H and R are independent of airflow speed so they hold the same values!!\n  Convection = (float)pow((double)Vair, 0.84) * Hc * DeltaTska; // 0.84 to conform with previous calculations !!\n  Ereq = (float)(HeatProduced + StoredHeat - Radiation - Convection); // Requested Evaporative Heat exchange with the environment\n  if (Ereq \u003c 0) {\n    Ereq = 0;  // in startup phase value can be negative force to zero !\n  }\n  //Sweat Rate = (147 + 1.527*Ereq - 0.87*Emax) --\u003e The Original equation of Conzalez et al. 2009\n  // Adapted since in our heat balance equation Ereq = Emax (by definition!) -\u003e rewritten to: SwRate = 147 +(0.657*Ereq)\n  if (Ereq \u003e 0) {\n    SwRate = (147 + (0.657 * Ereq)); // per milli liter fluid is equal to weight in grams -\u003e Notice SwRate is in gr/m2/hour\n  } else SwRate = 0;\n\n  HeatState = HeatBalanceState((int)Ereq);\n\n  if (OpMode == HEATBALANCE) { // Values have changed -\u003e translate to Airflow intensity\n    ActualUpperFanPerc = (uint16_t)(AirFlowToFanPercFactor[BikePos]*Vair + 0.5); // conversion from outcome Heat-Balance-Equation to percentage Fan intensity\n    ActualUpperFanPerc = constrain(ActualUpperFanPerc, 0, 100);\n    AdjustForFanBalance();\n    SetFanThresholds();\n    SetBothFanPeriods(); // Translate Perc to Microseconds Duty cycle Fan operation\n  }\n}\n```\n# Bike Position\n```C++\n// Variables that account for BIKE POSITION\n#define UP   (0U) // Upright Bike Position\n#define DP   (1U) // Dropped Bike Position (straight arms!)\n#define TTP  (2U) // Time-Trial Bike Position\nconst char* BikePos_str[] = { \"UP\", \"DP\", \"TTP\" };\nuint16_t BikePos = UP; // Default set to UP\n```\n\u003cimg src=\"../main/images/Bike_Positions.png\" width=\"668\" height=\"214\" ALIGN=\"right\" alt=\"Bike position\" \u003e\nThe frontal area of a cyclist meets most of the airflow that the fan(s) generate and that area is dependent on the bike position. In the algorithm the bike position is taken into account when calculating the ideal airflow for exchanging heat with the environment. The user can set/change the preferred position at any time with the help of the Airflow Companion App (\u003cb\u003eAlgorithmic Settings\u003c/b\u003e).\u003cbr clear=\"right\"\u003e\n\n# Matching V\u003csub\u003eair\u003c/sub\u003e and Fan Capacity\u003cbr\u003e\nAirflow speed of the impellor of the fan is linear proportional to the rotation frequency. According to the First Fan Affinity Law: Volumetric air flow is proportional to RPM (Impellor Rotations Per Minute). \u003cbr\u003e\n* Higher impellor frequency results in proportional higher air flow speed. Just \u003cb\u003eONE\u003c/b\u003e multiplying factor (FUP) is therefore appropriate for the whole range of fan operation!\n* Measurement with a handheld anemometer showed that the applied fan generates, at max capacity, an airflow velocity of about 30 km/hour! NOTICE: This is fully dependent of the Fan's mechanical properties, size, power, manufacturer, etcetera and most likely shall be different in your case! \u003cbr\u003e\n\nThe fans are set (by the software) from 0% - 100% duty cycle (no flow - max flow). To map the fan capacity percentage to the requested (Heat Balanced) Airflow (\u003cb\u003eV\u003csub\u003eair\u003c/sub\u003e\u003c/b\u003e) follow this rule: At a certain set of situational values (RH, air temperature, Gross Efficiency, cyling power induced, etcetera) the heat balance equation results in a requested Airflow of 8.32 m/s (3.6 * 8.32 = 30 km/h) and this should equal the very same \u003cb\u003eFan Airflow Velocity\u003c/b\u003e, in our case: max fan capacity (100%).\n* When a requested airflow speed of 8.32 m/s is reached the fan(s) should operate close to 100% capacity --\u003e therefore the multiplying factor (\u003cb\u003eFUP\u003c/b\u003e) should be 100/8.32 = 12! In other words: (FUP * Requested Airflow) == (12 (No Dimension) * 8.32 (m/s)) = 100% of the max fan capacity (resulting in 30 km/h air velocity)\n```C++\nfloat Vair = 0.0; // Calculated requested AirFlow as a result of the Heat Balance equation\n\nconst float FUP = 12.0;  // The (constant) multiplying factor\n// The AirFlowToFanPercFactor Conversion Factor can be derived taking into account the compensation for smaller sized frontal areas of the rider in\n// the different bike positions with their appropriately increased factors!\nconst float AirFlowToFanPercFactor[] = {FUP,        // Upright Bike Position\n                                       (FUP + 0.4), // Dropped Bike Position (straight arms!)\n                                       (FUP + 0.8)};// Time-Trial Bike Position\n#define MPS (1U) // Show Vair in meters per second m/s\n#define KPH (2U) // Show Vair in kilometers per hour km/h\n#define MPH (3U) // Show Vair in Miles per hour mph\nconst uint8_t AirSpeedUnit = KPH;\n```\n# OLED 128x64 Presentation Sequence\n\u003cimg src=\"../main/images/OLED_Scr_1.jpg\" width=\"400\" height=\"400\" ALIGN=\"right\" alt=\"Oled scr collage 1\" \u003e\nAt start-up (power on) the user is informed about the BLE connection process of the \u003cb\u003eAIRFLOW\u003c/b\u003e device with Heart Rate Monitor, Power Meter and Smart Phone.\nOn the top bar, icons are shown that indicate the (BLE or I2C) connection status of: Heart Rate Monitor, Power Meter, Smart Phone, Humidity and Temperature. During operation a sequence of (\u003cb\u003e11\u003c/b\u003e) informative screens are shown on the Blue Oled display that detail the measured or calculated critical values that determine the Heat Balance Equation.\n\n```C++\n// Create a SoftwareTimer that will drive our OLED display sequence.\nSoftwareTimer RoundRobin; // Timer for OLED display show time\n#define SCHEDULED_TIME 10000 // Time span to show an OLED Display screen in millis\nint Scheduled = -1; // Counter for current OLED display screen, start to show SETTINGS Oled Display first !!\n#define MAX_SCHEDULED 11 // Number of Oled Display screens to show sequentially -\u003e Settings Screen (-1) does not count!\n// Display Settings Sequence Show (\"1\") NoShow (\"0\")\nchar DisplaySettings[MAX_SCHEDULED + 1] = \"00000000000\"; // MAX_SCHEDULED display screen sequence --\u003e all OFF\n// -------------------------------------------------------\n```\n\n\u003cbr\u003e\u003cbr clear=\"left\"\u003e\n\n\u003cimg src=\"../main/images/OLED_Scr_2.jpg\" width=\"400\" height=\"400\" ALIGN=\"right\" alt=\"Oled scr collage 2\" \u003e\n\n```C++\n...\n  // Set up a repeating softwaretimer that fires every SCHEDULED_TIME seconds to invoke the OLED Time Sharing schedular\n  RoundRobin.begin(SCHEDULED_TIME, DisplaySchedular_callback);\n  RoundRobin.start();\n...\n```\n\n```C++\n// Function to count up after SCHEDULED_TIME for Oled display sequence\nvoid DisplaySchedular_callback(TimerHandle_t _handle) {\n  // 0 to MAX_SCHEDULED are assigned\n  do {\n    if (++Scheduled \u003c MAX_SCHEDULED) {\n    } else { // MAX is reached\n      Scheduled = 0; // start sequence all over again\n    }\n  } while (DisplaySettings[Scheduled] == '0'); // Check for No Show screens\n  TimeCaptureMillis = millis(); // Capture start time of SCHEDULED_TIME interval\n}\n```\n\u003cbr\u003e\nOn the top bar, the double chevron to the left always indicates whether you \u003cb\u003enetto\u003c/b\u003e gain (up) or loose (down) internal heat during the workout! Every 10 seconds the content of a new screen in the sequence (of \u003cb\u003e11\u003c/b\u003e) is shown. 2 Screens in the sequence (#1 and #9) change of data set during display. The user can switch the \u003cb\u003e11\u003c/b\u003e screens to be shown in the sequence \u003cb\u003eon/off\u003c/b\u003e, in accordance with his/her preference and at any time during operation with the help of the Airflow Companion App (\u003cb\u003eDisplay Settings\u003c/b\u003e).\n\u003cbr clear=\"left\"\u003e\n\n# Airflow Companion App \u003cbr\u003e\n\u003cimg src=\"../main/images/AIRFLOW_app.jpg\" width=\"800\" height=\"800\" ALIGN=\"middle\" alt=\"Companion app\" \u003e \u003cbr\u003e\nFrom the start of the project it was decided to develop an AIRFLOW Companion App that would allow changing settings and if all went wrong to allow a strict manual control of the fans.\nI have little experience with App development and decided to build one (for Android) in the accessible environment of [MIT App Inventor 2](https://appinventor.mit.edu).\u003cbr\u003e\n+ Download the \u003cb\u003eMIT App Inventor\u003c/b\u003e AIRFLOW Companion App code with extension *file*\u003cb\u003e.aia\u003c/b\u003e\n+ [Visit at AppInventor](https://appinventor.mit.edu), You can get started by clicking the orange \"Create Apps!\" button from any page on the website.\n+ Get started and upload the AIRFLOW Companion App code. Since some time \u003cb\u003eMIT App Inventor\u003c/b\u003e also supports the Apple platform, I have not tested the code for IOS use!\n+ Or upload the AIRFLOW Companion App \u003cb\u003eAPK\u003c/b\u003e to your Android device directly and install the APK. Android will call this a security vulnerability!\u003cbr\u003e\n# Operation Flow in Setup\u003cbr\u003e\n+ At start-up (power-on) the AIRFLOW device starts scanning for appropriate BLE devices in the neighbourhood, first it tries to establish a HRM \u003cb\u003eand\u003c/b\u003e Power meter connection! The OLED display shows the different steps, progress and details of the running process. Notice that the scanning time for devices is limited (to 30 seconds) and have HRM and Power activated before you start the AIRFLOW device!\n+ Now start the Companion app and have it scanning for the AIRFLOW device!\n+ For a limited time period the AIRFLOW device advertises for a smartphone! Have the Airflow Companion App running and actively looking for the Airflow device. \n+ If detected, the Airflow Companion App establishes a connection over BLE (with the AIRFLOW device). From that moment on the Nordic UART service (a.k.a. BLEUART) for exchange of information is applied. A simple dedicated protocol was implemented that allows for bidirectional exchange of short strings containing diagnostic messages and/or operation settings between smartphone and AIRFLOW device.\n+ After connection is established, the Airflow device sends the latest (persistent) settings data to allow the Companion App user to assess the current, previously user set or default, values.\n+ If something went wrong, simply reset (power off/on) the AIRFLOW device and the above procedure is repeated!\n+ \u003cb\u003eRegular operation is now started. The AIRFLOW device controls the fans (within settings) using current HRM and Power data.\u003c/b\u003e\n+ Whenever the user changes the current settings or control data during operation, the Airflow Companion App sends these to the Airflow device to immediately apply them. \u003cbr\u003e\n# Critical Power on the fly\n\u003cimg src=\"../main/images/CP_W_Prime_Oled.png\" width=\"147\" height=\"147\" ALIGN=\"left\" alt=\"Companion app\" \u003e\nIn addition to an ideal airflow velocity for an optimally thermoregulated human body, the \u003cb\u003eAIRFLOW\u003c/b\u003e user can get (as a bonus) insight in the development of his/her \u003cb\u003eCritical Power\u003c/b\u003e when training intensity is intense and long enough! The applied \u003cb\u003eArduino nRF52480 Express\u003c/b\u003e CPU is so powerfull that Real Time calculation of \u003cb\u003eCP\u003c/b\u003e and \u003cb\u003eW Prime\u003c/b\u003e can be accomplished aside all the calculations for determining the Heat Balance terms and setting the fans to the appropriate blowing capacity. Setting on/off this functionality and/or changing your starting \u003cb\u003eCP\u003c/b\u003e and \u003cb\u003eW Prime\u003c/b\u003e values are also an integral part of the AIRFLOW Companion app! \nIf you want to know in more detail about the science, math and implementation...\u003cbr clear=\"right\"\u003e\n\n[See RT-Critical-Power](https://github.com/Berg0162/RT-Critical-Power)\n\n# AIRFLOW Mechanic Construction\n\u003cimg src=\"https://www.instructables.com/assets/img/instructables-logo-v2.png\" width=\"32\" height=\"48\" align=\"left\" alt=\"Instructables\"\u003e On the \"Instructables\" site an instructable is published that focusses most on a low cost construction of a 2-Fans-Holder and integrated mount of the applied circuitry.\nSee [AIRFLOW on INSTRUCTABLES](https://www.instructables.com/id/AIRFLOW-Thermoregulated-Cooling-for-Indoor-Cycling/)\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fberg0162%2Fairflow","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fberg0162%2Fairflow","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fberg0162%2Fairflow/lists"}