// // Created by MightyPork on 2022/08/20. // #include #include "fancontrol.h" #include "freertos/FreeRTOS.h" #include "freertos/timers.h" #include "settings.h" #include "actuators.h" #include "onewires.h" #include "tasks.h" const char *TAG = "fc"; #define UNIDIR_T_MEAS_PERIOD 15 /* s */ struct FanControlState gState = {}; static void timerCallback(); static void invalidate_temps(); void settings_blind_time_set(uint16_t blind_time) { // if the blind is surely at the end bool nadoraz = (gState.blind_position >= gSettings.blind_time) || (gState.blind_position >= blind_time); gSettings.blind_time = blind_time; if (nadoraz) { gState.blind_position = blind_time; } settings_persist(SETTINGS_blind_time); } static void fanctltask(void *dummy) { TickType_t last_wake_time = xTaskGetTickCount(); bool statusled = 0; while (1) { act_statusled_set(statusled); statusled ^= 1; // timerCallback(); vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(1000)); gState.uptime_secs += 1; if (gState.uptime_secs >= 3600) { gState.uptime_secs = 0; gState.uptime_hours += 1; } if ((gState.effective_vent_mode & 6) != 0) { gState.motor_secs += 1; if (gState.motor_secs >= 3600) { gState.motor_secs = 0; gState.motor_hours += 1; } } } } void fancontrol_init() { gState.set_vent_mode = gSettings.initial_mode; gState.set_power = gSettings.initial_power; // this doesnt work // TimerHandle_t hTimer = xTimerCreate("fanctl", // pdMS_TO_TICKS(1000), // pdTRUE, // NULL, // timerCallback); // // xTimerStart(hTimer, pdMS_TO_TICKS(2000)); xTaskCreate(fanctltask, "fc", FCTL_TASK_STACK, NULL, FCTL_TASK_PRIO, NULL); } static const char * vent_mode_labels[] = { [VENT_MODE_OFF] = "OFF", [VENT_MODE_FREE] = "FREE", [VENT_MODE_OUT] = "OUT", [VENT_MODE_IN] = "IN", [VENT_MODE_RECUP] = "RECUP", }; static const char * recup_mode_labels[] = { [RECUP_MODE_TIME] = "TIME", [RECUP_MODE_TEMP] = "TEMP", }; static void timerCallback() { // posun rolety if (gAct.blind) { if (gState.blind_position < gSettings.blind_time) { gState.blind_position++; } } else { if (gState.blind_position > 0) { gState.blind_position--; } } if (gAct.power > 0) { if (gState.real_direction != gAct.dir) { if (gState.ramp > 0) { gState.ramp--; } else { gState.run_time = 0; gState.real_direction = gAct.dir; } } else { if (gState.ramp < gSettings.ramp_time) { gState.ramp++; } } gState.run_time++; } else { if (gState.ramp > 0) { gState.ramp--; } else { gState.run_time = 0; } } bool end_temp_meas = false; switch (gState.effective_vent_mode) { case VENT_MODE_OFF: act_motor_power_set(0); act_blind_set(0); break; case VENT_MODE_FREE: act_motor_power_set(0); act_blind_set(1); break; case VENT_MODE_OUT: act_motor_direction_set(MOTOR_DIR_OUT); act_blind_set(1); if (gState.blind_position >= gSettings.blind_time) { act_motor_power_set(gState.set_power); } if (gState.ramp >= gSettings.ramp_time * 2) { end_temp_meas = true; } break; case VENT_MODE_IN: act_motor_direction_set(MOTOR_DIR_IN); act_blind_set(1); if (gState.blind_position >= gSettings.blind_time) { act_motor_power_set(gState.set_power); } if (gState.ramp >= gSettings.ramp_time * 2) { end_temp_meas = true; } break; case VENT_MODE_RECUP: act_blind_set(1); if (gState.blind_position >= gSettings.blind_time) { act_motor_power_set(gState.set_power); if (gState.real_direction == gAct.dir && gState.run_time >= gSettings.min_recup_time) { // Stop condition bool do_switch = false; // TODO questionable logic, verify if (gSettings.recup_mode == RECUP_MODE_TIME) { do_switch = gState.run_time >= gSettings.recup_time; } else if (gState.run_time >= gSettings.max_recup_time) { do_switch = true; } else { // temp-based switching - magic(tm) // Delta = IN - OUT const int16_t ideal_delta = gState.t_indoor - gState.t_outdoor; int16_t stop_delta = ((int32_t) ideal_delta * (int32_t) (100 - gSettings.recup_factor)) / 100; int16_t delta = 0; bool allow_temp_switch = false; if (gState.real_direction == MOTOR_DIR_OUT) { if (gState.valid_t_indoor && gState.valid_t_exhaust && gState.valid_t_outdoor) { delta = gState.t_indoor - gState.t_exhaust; allow_temp_switch = true; } } else { // IN if (gState.valid_t_inflow && gState.valid_t_indoor && gState.valid_t_outdoor) { delta = gState.t_inflow - gState.t_outdoor; allow_temp_switch = true; } } ESP_LOGI(TAG, "Delta now %d, max %d, stop %d (RF %d%%), allow_switch %d Cx10", delta, ideal_delta, stop_delta, gSettings.recup_factor, allow_temp_switch); if (allow_temp_switch) { if (gSettings.summer_mode) { if (ideal_delta < 0) { // warmer outside, trying to keep cool in if (delta >= stop_delta) { do_switch = true; } } // colder outside - no stopping (will run to max recup time) } else { if (ideal_delta > 0) { // colder outside, trying to keep warmth in if (delta <= stop_delta) { do_switch = true; } } // warmer outside - no stopping (will run to max recup time) } } } if (do_switch) { // zmena smeru if (gAct.dir == MOTOR_DIR_IN) { gState.real_recup_time_in = gState.run_time; } else { gState.real_recup_time_out = gState.run_time; } gState.run_time = 0; act_motor_direction_set(1 - gAct.dir); end_temp_meas = true; } } } break; } if (gState.effective_vent_mode == VENT_MODE_RECUP) { if (gState.real_direction == MOTOR_DIR_OUT) { gState.instantaneous_vent_mode = VENT_MODE_OUT; } else { gState.instantaneous_vent_mode = VENT_MODE_IN; } } else { gState.instantaneous_vent_mode = gState.effective_vent_mode; } // Measure temperatures int16_t t1 = act_temp1(); int16_t t2 = act_temp2(); gState.t_actual_in = t1; gState.t_actual_out = t2; gState.valid_t_actual_in = gState.valid_t_actual_out = tempSensorsOk; if (end_temp_meas) { switch (gState.real_direction) { case MOTOR_DIR_IN: gState.t_outdoor = gState.t_actual_out; gState.t_inflow = gState.t_actual_in; gState.valid_t_outdoor = gState.valid_t_inflow = true; if (gState.effective_vent_mode == VENT_MODE_IN) { gState.valid_t_indoor = gState.valid_t_exhaust = false; } break; case MOTOR_DIR_OUT: gState.t_indoor = gState.t_actual_in; gState.t_exhaust = gState.t_actual_out; gState.valid_t_indoor = gState.valid_t_exhaust = true; if (gState.effective_vent_mode == VENT_MODE_OUT) { gState.valid_t_outdoor = gState.valid_t_inflow = false; } break; } } ESP_LOGI(TAG, "%s (ef %s, inst %s), rt %ds %d%%m, Tid %d%s, Tod %d%s, Tit %d%s, Teh %d%s, T1 %d%s, T2 %d%s Cx10", vent_mode_labels[gState.set_vent_mode], vent_mode_labels[gState.effective_vent_mode], vent_mode_labels[gState.instantaneous_vent_mode], gState.run_time, gAct.power, gState.t_indoor, gState.valid_t_indoor ? "" : "!", gState.t_outdoor, gState.valid_t_outdoor ? "" : "!", gState.t_inflow, gState.valid_t_inflow ? "" : "!", gState.t_exhaust, gState.valid_t_exhaust ? "" : "!", gState.t_actual_in, gState.valid_t_actual_in ? "" : "!", gState.t_actual_out, gState.valid_t_actual_out ? "" : "!" ); } void fan_set_vent_mode(enum ventilation_mode mode) { ESP_LOGI(TAG, "Set vent mode = %d", mode); if (mode == gState.set_vent_mode) { return; } gState.set_vent_mode = mode; if (gState.set_power != 0 || mode == VENT_MODE_FREE) { gState.effective_vent_mode = mode; } else if (gState.set_power == 0) { gState.effective_vent_mode = VENT_MODE_OFF; } if (mode == VENT_MODE_OFF || mode == VENT_MODE_FREE) { invalidate_temps(); } } static void invalidate_temps() { gState.valid_t_indoor = false; gState.valid_t_outdoor = false; gState.valid_t_inflow = false; gState.valid_t_exhaust = false; gState.valid_t_actual_in = false; gState.valid_t_actual_out = false; } void fan_set_power(uint16_t power) { ESP_LOGI(TAG, "Set power = %d%%", power); gState.set_power = power; if (power == 0) { gState.effective_vent_mode = VENT_MODE_OFF; invalidate_temps(); } else { gState.effective_vent_mode = gState.set_vent_mode; } }