brewcontrol.cpp 14.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/* Copyright (c) 2017 Philippe Kalaf, MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or 
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
18 19


20 21
// brew worker period in ms 
#define BREW_WORKER_PERIOD 100
22

23 24
// Boiler PID worker in ms
#define PID_WORKER_PERIOD 500
25 26 27 28 29

// PWM period
// 0.8333 for 60Hz, 1 for 50Hz for 1% resolution
#define BOILER_PWM_PERIOD 0.8333

Philippe Kalaf's avatar
Philippe Kalaf committed
30 31 32
// Soft stop time
#define SOFT_STOP_TIME_S 7.0

33 34 35 36 37 38
// Steam timeout in seconds
#define STEAM_TIMEOUT 300.0

// Steam temperature
#define STEAM_TEMPERATURE 140.0

39 40 41 42 43 44 45 46 47
// Manage different brew modes and timings
#include "brewcontrol.h"

BrewControl::BrewControl(   PinName brew_pin, 
                            PinName flow_sensor_pin,
                            PinName zcd_input_pin,
                            PinName pump_control_pin,
                            PinName pressure_sensor_pin,
                            PinName temp_sensor_pin,
48
			    PinName temp2_sensor_pin,
49 50 51 52 53 54 55
                            PinName boiler_pwm_pin
                            ) : 
                            _brew_switch(brew_pin, 0), 
                            _flow_sensor(flow_sensor_pin), 
                            _pump_control(zcd_input_pin, pump_control_pin),
                            _pressure_sensor(pressure_sensor_pin),
                            _temp_sensor(temp_sensor_pin),
56
			    _temp2_sensor(temp2_sensor_pin),
57
                            _boiler_pwm(boiler_pwm_pin),
58
                            _brew_worker_thread(osPriorityNormal, 768),
59
                            _pid_worker_thread(osPriorityNormal, 256)
60 61 62
{
    _preinfuse_time = 0;
    _brew_switch = 0;
63

64 65 66 67 68 69 70
    // at 60Hz, we got 120 zero-crosses per sec, we want to capture 100 of 
    // those within each PWM period
    _boiler_pwm.period(BOILER_PWM_PERIOD);
    
    // let's start at 93 C
    _target_shot_temperature = 93;

71 72 73
    // 9 bars is default
    _target_shot_pressure = 9;

74 75 76
    // this is used for steam mode to return to prev target temp when done
    _prev_temp = 0;

77 78 79
    _boiler_pid.setPIDGains(0.075, 0.1, 0.9);
    _boiler_pid.setIntegratorLimits(0, 1);

80 81
    // Boiler is on by default
    enable_boiler();
82 83 84

    // Start main worker thread of brewing
    _brew_worker_thread.start(callback(this, &BrewControl::_brew_worker));
85 86 87

    // Start pid worker thread
    _pid_worker_thread.start(callback(this, &BrewControl::_boiler_pid_worker));
88 89

    _set_state(STOPPED);
90 91
}

92
float BrewControl::get_current_temperature_side()
93 94 95 96
{
    return _temp_sensor.read();
}

97 98
float BrewControl::get_current_temperature_top()
{
99
    return _temp2_sensor.read();
100 101 102 103
}

float BrewControl::get_current_temperature()
{
104 105
    // Let's return average of both sensors
    return (_temp_sensor.read() + _temp2_sensor.read())/2;
106 107
}

108 109
void BrewControl::set_shot_temperature(float shot_temp)
{
110 111
    _brew_worker_mutex.lock();

112 113
    if (shot_temp <= 20)
        _target_shot_temperature = 20;
Philippe Kalaf's avatar
Philippe Kalaf committed
114 115
    else if (shot_temp >= 150)
        _target_shot_temperature = 150;
116 117
    else
        _target_shot_temperature = shot_temp;
118 119

    _brew_worker_mutex.unlock();
120 121 122 123 124 125 126
}

float BrewControl::get_shot_temperature()
{
    return _target_shot_temperature;
}

127 128
void BrewControl::set_shot_pressure(float pressure)
{
129 130
    _brew_worker_mutex.lock();

131 132 133 134 135 136
    if (pressure <= 0)
        _target_shot_pressure = 0;
    else if (pressure >= 12)
        _target_shot_pressure = 12;
    else
        _target_shot_pressure = pressure;
137 138

    _brew_worker_mutex.unlock();
139
}
140 141 142 143

// Return pressure in bars
float BrewControl::get_current_pressure()
{
144
    return _pressure_sensor.read_bars();
145 146 147 148 149 150 151
}

int BrewControl::get_shot_volume()
{
    return _target_shot_volume;
}

152
void BrewControl::_boiler_pid_worker()
153
{
154 155 156 157 158 159
    while(true)
    {
        ThisThread::sleep_for(PID_WORKER_PERIOD);

        if(!_enable_boiler)
            continue;
160

161 162
        // take temperature measurement
        float latestTemp = get_current_temperature(); 
163

164
        float power = 0.0;
165

166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
        // if the temperature is near zero, we assume there's an error
        if ( latestTemp > 0.5 ) {
            // calculate PID update
            power = _boiler_pid.update(_target_shot_temperature - latestTemp,
                    latestTemp);
        }

        // Validate number
        if ( power > 1 )
            power = 1;
        else if ( power < 0 )
            power = 0;

        // let's set new power on boiler
        _boiler_pwm = power;

        // store the latest temperature reading
        _latest_temp = latestTemp;
    }
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
}

void BrewControl::enable_boiler()
{
    _enable_boiler = 1;
}

void BrewControl::disable_boiler()
{
    _enable_boiler = 0;
    _boiler_pwm = 0;
}

bool BrewControl::toggle_boiler()
{
    if (_enable_boiler)
        disable_boiler();
    else
        enable_boiler();
    
    return _enable_boiler;
}

208
void BrewControl::pressure_up(uint8_t value)
209
{
210 211 212
    // limit to 12 bars
    if (get_current_pressure() <= 11.5)
        _pump_control.level_up(value);
213 214
}

215
void BrewControl::pressure_down(uint8_t value)
216
{
217
    _pump_control.level_down(value);
218 219 220 221 222 223
}

// pre-infuse time in seconds
// set to 0 to disable
void BrewControl::set_preinfuse_time(int time)
{
224 225
    _brew_worker_mutex.lock();

226 227 228 229
    if (time > 0)
        _preinfuse_time = time;
    else
        _preinfuse_time = 0;
230 231

    _brew_worker_mutex.unlock();
232 233 234 235 236 237 238
}

int BrewControl::get_preinfuse_time()
{
    return _preinfuse_time;
}

Philippe Kalaf's avatar
Philippe Kalaf committed
239 240 241 242 243
void BrewControl::stop_preinfuse_now()
{
    _stop_preinfuse = 1;
}

244 245 246 247 248 249 250 251
uint8_t BrewControl::get_pump_level()
{
    return _pump_control.get_level();
}

// This is to set the wanted shot time
void BrewControl::set_shot_time(int time)
{
252 253
    _brew_worker_mutex.lock();

254
    _target_shot_time = time;
255 256

    _brew_worker_mutex.unlock();
257 258 259 260 261
}

// This is to set the wanted shot volume
void BrewControl::set_shot_volume(int volume)
{
262 263
    _brew_worker_mutex.lock();

264
    if (volume >= 0)
265
        _target_shot_volume = volume;
266 267

    _brew_worker_mutex.unlock();
268 269
}

270 271 272
// This is to set the wanted flow rate (ml/s)
void BrewControl::set_shot_flow_rate(float flow_rate)
{
273 274
    _brew_worker_mutex.lock();

275
    _target_flow_rate = flow_rate;
276 277

    _brew_worker_mutex.unlock();
278 279
}

280 281 282 283 284 285 286 287 288
// return current shot_clock in seconds
float BrewControl::get_current_time()
{
    return _shot_clock;
}

// return current volume in ml
float BrewControl::get_current_volume()
{
289
    return _flow_sensor.get_volume();
290 291 292 293 294 295 296 297 298
}

// return the current flow rate
float BrewControl::get_current_flow_rate()
{
    return _flow_sensor.get_flow_rate();
}

// read brew on/off state
299
uint8_t BrewControl::get_state()
300 301 302 303
{
    return _state;
}

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
#ifdef LOG
void BrewControl::_log_brew_params()
{
   if (_log_file)
    {
        fprintf(_log_file, "%f,%f,%f,%f,%f,%d\n",
                get_current_time(),
                get_current_volume(),
                get_current_temperature(),
                get_current_pressure(),
                get_current_flow_rate(),
                _state);
    }
}
#endif

320 321 322 323 324 325 326 327
// Internal helper function to set brew states properly
void BrewControl::_set_state(uint8_t state)
{
    _state = state;
    if (_state == BREWING || _state == STEAMING || _state == PRE_INFUSING)
        _brew_worker_thread.flags_set(1);
}

328
void BrewControl::_brew_worker()
329
{
330 331
    while(true)
    {
332 333 334 335 336 337 338
        // If we are not brewing, let's just wait until we start brewing
        if (_state != PRE_INFUSING && _state != BREWING && _state != STEAMING)
        {
            // _set_state() will set the flag when we start brewing
            ThisThread::flags_wait_any(1);
        }
        
339
        _brew_worker_mutex.lock();
340

341
#ifdef LOG
342
        _log_brew_params();
343 344
#endif

345
        if (_state == PRE_INFUSING)
346
        {
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
            // First let's fill up the portafilter
            if (_flow_sensor.get_volume() <= PORTAFILTER_VOLUME && !_stop_preinfuse)
            {
                _brew_worker_mutex.unlock();
                continue;
            }

            // It's full, let's stop the pump for set pre-infuse time
            if (_pump_control.get_level() != 0)
            {
                _pump_control.set_level(0);
                _shot_clock.reset();
            }

            // Once pre-infuse time runs out, set brew mode
            if (_shot_clock.read() >= _preinfuse_time)
            {
                _mode = _prev_mode;
                _pump_control.set_level(_prev_pressure);
                _flow_sensor.reset_count();
                _shot_clock.reset();
368
                _set_state(BREWING);
369 370 371 372 373
                _stop_preinfuse = 0;
            }

            _brew_worker_mutex.unlock();
            continue;
374
        }
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440

        if(_mode == MODE_TIME)
        {
            // Auto-adjust pressure to target
            float error = _target_shot_pressure - get_current_pressure();
            if(error < -0.25)
                pressure_down();
            else if(error > 0.25) 
                pressure_up();

            if(_shot_clock.read() >= _target_shot_time)
            {
                _brew_worker_mutex.unlock();
                soft_stop();
                _brew_worker_mutex.lock();
            }
        }
        else if(_mode == MODE_YIELD)
        {
            // Auto-adjust pressure to target
            float error = _target_shot_pressure - get_current_pressure();
            if(error < -0.25)
                pressure_down();
            else if(error > 0.25)
                pressure_up();

            if(_flow_sensor.get_volume() >= _target_shot_volume)
            {
                _brew_worker_mutex.unlock();
                soft_stop();
                _brew_worker_mutex.lock();
            }
        }
        else if(_mode == MODE_TIME_YIELD)
        {
            // Re-calculate target flowrate =
            // remaining volume / remaining time
            _target_flow_rate = (_target_shot_volume - _flow_sensor.get_volume())
                / (_target_shot_time - _shot_clock.read());
            // Auto-adjust flow-rate
            if(_target_flow_rate < 0)
                // oops! we have run out of time! go full power!
                _pump_control.set_level(100);
            else if(_target_flow_rate - _flow_sensor.get_flow_rate() < 0)
                pressure_down();
            else
                pressure_up();

            // Stop when target shot volume is reached
            if(_flow_sensor.get_volume() >= _target_shot_volume)
            {
                _brew_worker_mutex.unlock();
                soft_stop();
                _brew_worker_mutex.lock();
            }
        }
        else if(_mode == MODE_MANUAL)
        {
        }
        else if(_mode == MODE_STEAM)
        {
            // automatically stop steaming after STEAM_TIMEOUT
            if (_shot_clock.read() >= STEAM_TIMEOUT)
                _stop();
        }

441
        ThisThread::sleep_for(BREW_WORKER_PERIOD);
442
        _brew_worker_mutex.unlock();
443

444
    }
445 446
}

447
uint8_t BrewControl::start(uint8_t mode)
448
{
449 450
    _brew_worker_mutex.lock();

451
    // We are already brewing, return brewing mode
452 453 454 455 456
    if(_state == BREWING) 
    {
        _brew_worker_mutex.unlock();
        return _mode;
    }
457
    
458 459
    _mode = mode;

460 461
    // reset shot clock and flow sensor
    _flow_sensor.reset_count();
462 463
    _shot_clock.reset();
    _shot_clock.start();
464

465
    // Let's save settings before we set pre-infuse mode
466 467
    // only pre-infuse if set to > 0s and not in manual or steam mode
    if (_preinfuse_time && _mode != MODE_MANUAL && _mode != MODE_STEAM)
468 469
    {
        _prev_mode = _mode;
470
	_prev_pressure = get_pump_level();
471
	// set pre-infuse mode
472
	_set_state(PRE_INFUSING);
473 474

	// we pre-infuse at low pressure
475
        _pump_control.set_level(60);
476
    }
477
    else
478
        _set_state(BREWING);
479 480 481 482 483 484 485 486

    if(_mode == MODE_STEAM)
    {
 	// save currently set temperature to return to it after steaming
	_prev_temp = get_shot_temperature();
	set_shot_temperature(STEAM_TEMPERATURE);
    }
    else
487
	// Open solenoid
488
	_brew_switch = 1;
489
    
490 491
    _brew_worker_mutex.unlock();
    
492 493 494
    return _mode;
}

495 496
void BrewControl::toggle_solenoid()
{
497 498
    _brew_worker_mutex.lock();

499 500 501 502
    if(_brew_switch)
	_brew_switch = 0;
    else
	_brew_switch = 1;
503 504

    _brew_worker_mutex.unlock();
505 506
}

507
// This function is sometimes called from an ISR
508
void BrewControl::_stop()
509
{
510
    _set_state(STOPPED);
511 512
    
    // Close solenoid
513
    _brew_switch = 0;
514 515 516 517 518 519

    // Stop and reset all counters and brew params
    _shot_clock.stop();
    _shot_clock.reset();
    _target_shot_volume = 0;
    _target_shot_time = 0;
520
    _flow_sensor.reset_count();
521 522

    // if we were in steam mode, _prev_temp will have been set
523 524
    if(_prev_temp)
    {
525
        _target_shot_temperature = _prev_temp;
526 527
	_prev_temp = 0;
    }
528 529
}

Philippe Kalaf's avatar
Philippe Kalaf committed
530 531
void BrewControl::soft_stop()
{
532 533 534
    _brew_worker_mutex.lock();

    // Stop immediately if in steam mode
535 536 537
    if (_mode == MODE_STEAM)
    {
	_stop();
538
        _brew_worker_mutex.unlock();
539 540
	return;
    }
541 542

    // Turn off pump
Philippe Kalaf's avatar
Philippe Kalaf committed
543
    _pump_control.set_level(0);
544 545

    // Stop pressure counter and shot clock
546
    _average_pressure = _pressure_sensor.stop_count();
547 548
    _shot_clock.stop();

549
    _set_state(SOFT_STOPPING);
550 551 552 553 554 555

    // Call stop() after SOFT_STOP_TIME_S
    _soft_stop_timer.attach(callback(this, &BrewControl::_stop),
			SOFT_STOP_TIME_S);

    _brew_worker_mutex.unlock();
Philippe Kalaf's avatar
Philippe Kalaf committed
556 557
}

558 559 560 561 562
float BrewControl::get_average_pressure()
{
    return _average_pressure;
}

563
uint8_t BrewControl::toggle(uint8_t mode)
564
{
565
    if(_state == BREWING || _state == PRE_INFUSING)
566
	soft_stop();
567
    else if(_state == STOPPED)
568 569
	start(mode);

570 571 572 573 574 575 576
    return _state;
}

PhaseControl *BrewControl::get_pump_control_ptr()
{
    return &_pump_control;
}
577 578 579 580 581 582 583 584 585 586

uint16_t BrewControl::get_last_pulse_count_side()
{
    return _temp_sensor.get_last_pulse_count();
}

uint16_t BrewControl::get_last_pulse_count_top()
{
    return _temp2_sensor.get_last_pulse_count();
}