Enumerate hotends for display in panel temperature settings
[clinton/Smoothieware.git] / src / modules / utils / panel / Panel.cpp
1 /*
2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl).
3 Smoothie is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Smoothie is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5 You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
6 */
7
8 #include "libs/Kernel.h"
9 #include "Panel.h"
10 #include "PanelScreen.h"
11
12 #include "libs/nuts_bolts.h"
13 #include "libs/utils.h"
14 #include "Button.h"
15
16 #include "modules/utils/player/PlayerPublicAccess.h"
17 #include "screens/CustomScreen.h"
18 #include "screens/MainMenuScreen.h"
19 #include "SlowTicker.h"
20 #include "Gcode.h"
21 #include "Pauser.h"
22 #include "TemperatureControlPublicAccess.h"
23 #include "ModifyValuesScreen.h"
24 #include "PublicDataRequest.h"
25 #include "PublicData.h"
26
27 #include "panels/I2CLCD.h"
28 #include "panels/VikiLCD.h"
29 #include "panels/Smoothiepanel.h"
30 #include "panels/ReprapDiscountGLCD.h"
31 #include "panels/ST7565.h"
32 #include "panels/UniversalAdapter.h"
33
34 #include "version.h"
35 #include "checksumm.h"
36 #include "ConfigValue.h"
37
38 #define panel_checksum CHECKSUM("panel")
39 #define enable_checksum CHECKSUM("enable")
40 #define lcd_checksum CHECKSUM("lcd")
41 #define i2c_lcd_checksum CHECKSUM("i2c_lcd")
42 #define viki_lcd_checksum CHECKSUM("viki_lcd")
43 #define smoothiepanel_checksum CHECKSUM("smoothiepanel")
44 #define panelolu2_checksum CHECKSUM("panelolu2")
45 #define rrd_glcd_checksum CHECKSUM("reprap_discount_glcd")
46 #define st7565_glcd_checksum CHECKSUM("st7565_glcd")
47 #define universal_adapter_checksum CHECKSUM("universal_adapter")
48
49 #define menu_offset_checksum CHECKSUM("menu_offset")
50 #define encoder_resolution_checksum CHECKSUM("encoder_resolution")
51 #define jog_x_feedrate_checksum CHECKSUM("alpha_jog_feedrate")
52 #define jog_y_feedrate_checksum CHECKSUM("beta_jog_feedrate")
53 #define jog_z_feedrate_checksum CHECKSUM("gamma_jog_feedrate")
54 #define longpress_delay_checksum CHECKSUM("longpress_delay")
55
56 #define hotend_temp_checksum CHECKSUM("hotend_temperature")
57 #define bed_temp_checksum CHECKSUM("bed_temperature")
58
59 Panel* Panel::instance= nullptr;
60
61 Panel::Panel()
62 {
63 instance= this;
64 this->counter_changed = false;
65 this->click_changed = false;
66 this->refresh_flag = false;
67 this->enter_menu_mode();
68 this->lcd = NULL;
69 this->do_buttons = false;
70 this->do_encoder = false;
71 this->idle_time = 0;
72 this->start_up = true;
73 this->current_screen = NULL;
74 strcpy(this->playing_file, "Playing file");
75 }
76
77 Panel::~Panel()
78 {
79 delete this->lcd;
80 }
81
82 void Panel::on_module_loaded()
83 {
84 // Exit if this module is not enabled
85 if ( !THEKERNEL->config->value( panel_checksum, enable_checksum )->by_default(false)->as_bool() ) {
86 delete this;
87 return;
88 }
89
90 // Initialise the LCD, see which LCD to use
91 if (this->lcd != NULL) delete this->lcd;
92 int lcd_cksm = get_checksum(THEKERNEL->config->value(panel_checksum, lcd_checksum)->by_default("i2c")->as_string());
93
94 // Note checksums are not const expressions when in debug mode, so don't use switch
95 if (lcd_cksm == i2c_lcd_checksum) {
96 this->lcd = new I2CLCD();
97 } else if (lcd_cksm == viki_lcd_checksum) {
98 this->lcd = new VikiLCD();
99 this->lcd->set_variant(0);
100 } else if (lcd_cksm == panelolu2_checksum) {
101 this->lcd = new VikiLCD();
102 this->lcd->set_variant(1);
103 } else if (lcd_cksm == smoothiepanel_checksum) {
104 this->lcd = new Smoothiepanel();
105 } else if (lcd_cksm == rrd_glcd_checksum) {
106 this->lcd = new ReprapDiscountGLCD();
107 } else if (lcd_cksm == st7565_glcd_checksum) {
108 this->lcd = new ST7565();
109 } else if (lcd_cksm == universal_adapter_checksum) {
110 this->lcd = new UniversalAdapter();
111 } else {
112 // no lcd type defined
113 return;
114 }
115
116 // these need to be called here as they need the config cache loaded as they enumerate modules
117 this->custom_screen= new CustomScreen();
118 setup_temperature_screen();
119
120 // some panels may need access to this global info
121 this->lcd->setPanel(this);
122
123 // the number of screen lines the panel supports
124 this->screen_lines = this->lcd->get_screen_lines();
125
126 // some encoders may need more clicks to move menu, this is a divisor and is in config as it is
127 // an end user usability issue
128 this->menu_offset = THEKERNEL->config->value( panel_checksum, menu_offset_checksum )->by_default(0)->as_number();
129
130 // override default encoder resolution if needed
131 this->encoder_click_resolution = THEKERNEL->config->value( panel_checksum, encoder_resolution_checksum )->by_default(this->lcd->getEncoderResolution())->as_number();
132
133 // load jogging feedrates in mm/min
134 jogging_speed_mm_min[0] = THEKERNEL->config->value( panel_checksum, jog_x_feedrate_checksum )->by_default(3000.0f)->as_number();
135 jogging_speed_mm_min[1] = THEKERNEL->config->value( panel_checksum, jog_y_feedrate_checksum )->by_default(3000.0f)->as_number();
136 jogging_speed_mm_min[2] = THEKERNEL->config->value( panel_checksum, jog_z_feedrate_checksum )->by_default(300.0f )->as_number();
137
138 // load the default preset temeratures
139 default_hotend_temperature = THEKERNEL->config->value( panel_checksum, hotend_temp_checksum )->by_default(185.0f )->as_number();
140 default_bed_temperature = THEKERNEL->config->value( panel_checksum, bed_temp_checksum )->by_default(60.0f )->as_number();
141
142
143 this->up_button.up_attach( this, &Panel::on_up );
144 this->down_button.up_attach( this, &Panel::on_down );
145 this->click_button.up_attach( this, &Panel::on_select );
146 this->back_button.up_attach( this, &Panel::on_back );
147 this->pause_button.up_attach( this, &Panel::on_pause );
148
149
150 //setting longpress_delay
151 int longpress_delay = THEKERNEL->config->value( panel_checksum, longpress_delay_checksum )->by_default(0)->as_number();
152 this->up_button.set_longpress_delay(longpress_delay);
153 this->down_button.set_longpress_delay(longpress_delay);
154 // this->click_button.set_longpress_delay(longpress_delay);
155 // this->back_button.set_longpress_delay(longpress_delay);
156 // this->pause_button.set_longpress_delay(longpress_delay);
157
158
159 THEKERNEL->slow_ticker->attach( 50, this, &Panel::button_tick );
160 if(lcd->encoderReturnsDelta()) {
161 // panel handles encoder pins and returns a delta
162 THEKERNEL->slow_ticker->attach( 10, this, &Panel::encoder_tick );
163 }else{
164 // read encoder pins
165 THEKERNEL->slow_ticker->attach( 1000, this, &Panel::encoder_check );
166 }
167
168 // Register for events
169 this->register_for_event(ON_IDLE);
170 this->register_for_event(ON_MAIN_LOOP);
171 this->register_for_event(ON_GCODE_RECEIVED);
172
173 // Refresh timer
174 THEKERNEL->slow_ticker->attach( 20, this, &Panel::refresh_tick );
175 }
176
177 // Enter a screen, we only care about it now
178 void Panel::enter_screen(PanelScreen *screen)
179 {
180 this->current_screen = screen;
181 this->reset_counter();
182 this->current_screen->on_enter();
183 }
184
185 // Reset the counter
186 void Panel::reset_counter()
187 {
188 *this->counter = 0;
189 this->counter_changed = false;
190 }
191
192 // Indicate the idle loop we want to call the refresh hook in the current screen
193 // called 20 times a second
194 uint32_t Panel::refresh_tick(uint32_t dummy)
195 {
196 this->refresh_flag = true;
197 this->idle_time++;
198 return 0;
199 }
200
201 // Encoder pins changed in interrupt or call from on_idle
202 uint32_t Panel::encoder_check(uint32_t dummy)
203 {
204 // if encoder reads go through SPI like on universal panel adapter this needs to be
205 // done in idle loop, this is indicated by lcd->encoderReturnsDelta()
206 // however when reading encoder directly it needs to be done
207 // frequently, universal panel adapter will return an actual delta count so won't miss any if polled slowly
208 // NOTE FIXME (WHY is it in menu only?) this code will not work if change is not 1,0,-1 anything greater (as in above case) will not work properly
209 static int encoder_counter = 0; // keeps track of absolute encoder position
210 static int last_encoder_click= 0; // last readfing of divided encoder count
211 int change = lcd->readEncoderDelta();
212 encoder_counter += change;
213 int clicks= encoder_counter/this->encoder_click_resolution;
214 int delta= clicks - last_encoder_click; // the number of clicks this time
215 last_encoder_click= clicks;
216
217 if ( delta != 0 ) {
218 this->counter_changed = true;
219 (*this->counter) += delta;
220 this->idle_time = 0;
221 }
222 return 0;
223 }
224
225 // Read and update each button
226 uint32_t Panel::button_tick(uint32_t dummy)
227 {
228 this->do_buttons = true;
229 return 0;
230 }
231
232 // Read and update encoder
233 uint32_t Panel::encoder_tick(uint32_t dummy)
234 {
235 this->do_encoder = true;
236 return 0;
237 }
238
239 void Panel::on_gcode_received(void *argument)
240 {
241 Gcode *gcode = static_cast<Gcode *>(argument);
242 if ( gcode->has_m) {
243 if ( gcode->m == 117 ) { // set LCD message
244 this->message = get_arguments(gcode->command);
245 if (this->message.size() > 20) this->message = this->message.substr(0, 20);
246 gcode->mark_as_taken();
247 }
248 }
249 }
250
251 // on main loop, we can send gcodes or do anything that waits in this loop
252 void Panel::on_main_loop(void *argument)
253 {
254 if (this->current_screen != NULL) {
255 this->current_screen->on_main_loop();
256 this->lcd->on_main_loop();
257 }
258 }
259
260
261 #define ohw_logo_antipixel_width 80
262 #define ohw_logo_antipixel_height 15
263 static const uint8_t ohw_logo_antipixel_bits[] = {
264 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
265 0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x33, 0x18, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x5E,
266 0x80, 0x2D, 0x6B, 0x9B, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x18, 0xAB, 0xFF, 0xFF,
267 0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x7B, 0xB3, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x7F, 0x80, 0x33,
268 0x78, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x81, 0xF3, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD,
269 0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xB3, 0x18, 0xDD, 0x98, 0xC5, 0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xAD,
270 0x6B, 0x5D, 0x6B, 0x5D, 0x80, 0x73, 0x80, 0x3F, 0xFC, 0x21, 0x1B, 0x55, 0x08, 0xC5, 0x80, 0xF3,
271 0xC0, 0x3F, 0xFD, 0xAD, 0x5B, 0x49, 0x6A, 0xDD, 0x80, 0xE1, 0xC0, 0x3F, 0xFD, 0xAD, 0x68, 0xDD,
272 0x6B, 0x45, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
273 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
274 };
275
276 // On idle things, we don't want to do shit in interrupts
277 // don't queue gcodes in this
278 void Panel::on_idle(void *argument)
279 {
280 if (this->start_up) {
281 this->lcd->init();
282
283 Version v;
284 string build(v.get_build());
285 string date(v.get_build_date());
286 this->lcd->clear();
287 this->lcd->setCursor(0, 0); this->lcd->printf("Welcome to Smoothie");
288 this->lcd->setCursor(0, 1); this->lcd->printf("%s", build.substr(0, 20).c_str());
289 this->lcd->setCursor(0, 2); this->lcd->printf("%s", date.substr(0, 20).c_str());
290 this->lcd->setCursor(0, 3); this->lcd->printf("Please wait....");
291
292 if (this->lcd->hasGraphics()) {
293 this->lcd->bltGlyph(24, 40, ohw_logo_antipixel_width, ohw_logo_antipixel_height, ohw_logo_antipixel_bits);
294 }
295
296 this->lcd->on_refresh(true); // tell lcd to display now
297
298 // Default top screen
299 this->top_screen= new MainMenuScreen();
300 this->custom_screen->set_parent(this->top_screen);
301 this->start_up = false;
302 return;
303 }
304
305 MainMenuScreen *mms= static_cast<MainMenuScreen*>(this->top_screen);
306 // after being idle for a while switch to Watch screen
307 if (this->current_screen != NULL && this->idle_time > this->current_screen->idle_timeout_secs()*20) {
308 this->idle_time = 0;
309 if (mms->watch_screen != this->current_screen) {
310 this->enter_screen(mms->watch_screen);
311 // TODO do we need to reset any state?
312 }
313
314 return;
315 }
316
317 if(current_screen == NULL && this->idle_time > 20*4) {
318 this->enter_screen(mms->watch_screen);
319 return;
320 }
321
322 if(this->do_encoder) {
323 this->do_encoder= false;
324 encoder_check(0);
325 }
326
327 if (this->do_buttons) {
328 // we don't want to do SPI in interrupt mode
329 this->do_buttons = false;
330
331 // read the actual buttons
332 int but = lcd->readButtons();
333 if (but != 0) {
334 this->idle_time = 0;
335 if(current_screen == NULL) {
336 // we were in startup screen so go to watch screen
337 this->enter_screen(mms->watch_screen);
338 return;
339 }
340 }
341
342 // fire events if the buttons are active and debounce is satisfied
343 this->up_button.check_signal(but & BUTTON_UP);
344 this->down_button.check_signal(but & BUTTON_DOWN);
345 this->back_button.check_signal(but & BUTTON_LEFT);
346 this->click_button.check_signal(but & BUTTON_SELECT);
347 this->pause_button.check_signal(but & BUTTON_PAUSE);
348 }
349
350 // If we are in menu mode and the position has changed
351 if ( this->mode == MENU_MODE && this->counter_change() ) {
352 this->menu_update();
353 }
354
355 // If we are in control mode
356 if ( this->mode == CONTROL_MODE && this->counter_change() ) {
357 this->control_value_update();
358 }
359
360 // If we must refresh
361 if ( this->refresh_flag ) {
362 this->refresh_flag = false;
363 if (this->current_screen != NULL) {
364 this->current_screen->on_refresh();
365 this->lcd->on_refresh();
366 }
367 }
368 }
369
370 // Hooks for button clicks
371 uint32_t Panel::on_up(uint32_t dummy)
372 {
373 // this is simulating encoder clicks, but needs to be inverted to
374 // increment values on up,increment by
375 int inc = (this->mode == CONTROL_MODE) ? 1 : -(this->menu_offset+1);
376 *this->counter += inc;
377 this->counter_changed = true;
378 return 0;
379 }
380
381 uint32_t Panel::on_down(uint32_t dummy)
382 {
383 int inc = (this->mode == CONTROL_MODE) ? -1 : (this->menu_offset+1);
384 *this->counter += inc;
385 this->counter_changed = true;
386 return 0;
387 }
388
389 // on most menu screens will go back to previous higher menu
390 uint32_t Panel::on_back(uint32_t dummy)
391 {
392 if (this->mode == MENU_MODE && this->current_screen != NULL && this->current_screen->parent != NULL) {
393 this->enter_screen(this->current_screen->parent);
394 }
395 return 0;
396 }
397
398 uint32_t Panel::on_select(uint32_t dummy)
399 {
400 // TODO make configurable, including turning off
401 // buzz is ignored on panels that do not support buzz
402 this->click_changed = true;
403 this->idle_time = 0;
404 lcd->buzz(60, 300); // 50ms 300Hz
405 return 0;
406 }
407
408 uint32_t Panel::on_pause(uint32_t dummy)
409 {
410 if (!paused) {
411 THEKERNEL->pauser->take();
412 paused = true;
413 } else {
414 THEKERNEL->pauser->release();
415 paused = false;
416 }
417 return 0;
418 }
419
420 bool Panel::counter_change()
421 {
422 if ( this->counter_changed ) {
423 this->counter_changed = false;
424 return true;
425 } else {
426 return false;
427 }
428 }
429 bool Panel::click()
430 {
431 if ( this->click_changed ) {
432 this->click_changed = false;
433 return true;
434 } else {
435 return false;
436 }
437 }
438
439
440 // Enter menu mode
441 void Panel::enter_menu_mode(bool force)
442 {
443 this->mode = MENU_MODE;
444 this->counter = &this->menu_selected_line;
445 this->menu_changed = force;
446 }
447
448 void Panel::setup_menu(uint16_t rows)
449 {
450 this->setup_menu(rows, min(rows, this->max_screen_lines()));
451 }
452
453 void Panel::setup_menu(uint16_t rows, uint16_t lines)
454 {
455 this->menu_selected_line = 0;
456 this->menu_current_line = 0;
457 this->menu_start_line = 0;
458 this->menu_rows = rows;
459 this->panel_lines = lines;
460 }
461
462 void Panel::menu_update()
463 {
464 // Limits, up and down
465 // NOTE menu_selected_line is changed in an interrupt and can change at any time
466 int msl = this->menu_selected_line; // hopefully this is atomic
467
468 #if 0
469 // this allows it to wrap but with new method we dont; want to wrap
470 msl = msl % ( this->menu_rows << this->menu_offset );
471 while ( msl < 0 ) {
472 msl += this->menu_rows << this->menu_offset;
473 }
474 #else
475 // limit selected line to screen lines available
476 if(msl >= this->menu_rows<<this->menu_offset){
477 msl= (this->menu_rows-1)<<this->menu_offset;
478 }else if(msl < 0) msl= 0;
479 #endif
480
481 this->menu_selected_line = msl; // update atomically we hope
482 // figure out which actual line to select, if we have a menu offset it means we want to move one line per two clicks
483 if(msl % (this->menu_offset+1) == 0) { // only if divisible by offset
484 this->menu_current_line = msl >> this->menu_offset;
485 }
486
487 // What to display
488 if ( this->menu_rows > this->panel_lines ) {
489 #if 0
490 // old way of scrolling not nice....
491 if ( this->menu_current_line >= 2 ) {
492 this->menu_start_line = this->menu_current_line - 1;
493 }
494 if ( this->menu_current_line > this->menu_rows - this->panel_lines ) {
495 this->menu_start_line = this->menu_rows - this->panel_lines;
496 }
497 #else
498 // new way we only scroll the lines when the cursor hits the bottom of the screen or the top of the screen
499 // do we want to scroll up?
500 int sl= this->menu_current_line - this->menu_start_line; // screen line we are on
501 if(sl >= this->panel_lines) {
502 this->menu_start_line += ((sl+1)-this->panel_lines); // scroll up to keep it on the screen
503
504 }else if(sl < 0 ) { // do we want to scroll down?
505 this->menu_start_line += sl; // scroll down
506 }
507 #endif
508
509 }else{
510 this->menu_start_line = 0;
511 }
512
513 this->menu_changed = true;
514 }
515
516 bool Panel::menu_change()
517 {
518 if ( this->menu_changed ) {
519 this->menu_changed = false;
520 return true;
521 } else {
522 return false;
523 }
524 }
525
526 bool Panel::control_value_change()
527 {
528 if ( this->control_value_changed ) {
529 this->control_value_changed = false;
530 return true;
531 } else {
532 return false;
533 }
534 }
535
536 bool Panel::enter_control_mode(float passed_normal_increment, float passed_pressed_increment)
537 {
538 this->mode = CONTROL_MODE;
539 this->normal_increment = passed_normal_increment;
540 this->counter = &this->control_normal_counter;
541 this->control_normal_counter = 0;
542 this->control_base_value = 0;
543 return true;
544 }
545
546 void Panel::control_value_update()
547 {
548 // TODO what do we do here?
549 this->control_value_changed = true;
550 }
551
552 void Panel::set_control_value(float value)
553 {
554 this->control_base_value = value;
555 }
556
557 float Panel::get_control_value()
558 {
559 return this->control_base_value + (this->control_normal_counter * this->normal_increment);
560 }
561
562 bool Panel::is_playing() const
563 {
564 void *returned_data;
565
566 bool ok = THEKERNEL->public_data->get_value( player_checksum, is_playing_checksum, &returned_data );
567 if (ok) {
568 bool b = *static_cast<bool *>(returned_data);
569 return b;
570 }
571 return false;
572 }
573
574 void Panel::set_playing_file(string f)
575 {
576 // just copy the first 20 characters after the first / if there
577 size_t n = f.find_last_of('/');
578 if (n == string::npos) n = 0;
579 strncpy(playing_file, f.substr(n + 1, 19).c_str(), sizeof(playing_file));
580 playing_file[sizeof(playing_file) - 1] = 0;
581 }
582
583 static float getTargetTemperature(uint16_t heater_cs)
584 {
585 void *returned_data;
586 bool ok = THEKERNEL->public_data->get_value( temperature_control_checksum, heater_cs, current_temperature_checksum, &returned_data );
587
588 if (ok) {
589 struct pad_temperature temp = *static_cast<struct pad_temperature *>(returned_data);
590 return temp.target_temperature;
591 }
592
593 return 0.0F;
594 }
595
596 void Panel::setup_temperature_screen()
597 {
598 // setup temperature screen
599 auto mvs= new ModifyValuesScreen();
600 this->temperature_screen= mvs;
601
602 // enumerate heaters and add a menu item for each one
603 vector<uint16_t> modules;
604 THEKERNEL->config->get_module_list( &modules, temperature_control_checksum );
605
606 for(auto i : modules) {
607 if (!THEKERNEL->config->value(temperature_control_checksum, i, enable_checksum )->as_bool()) continue;
608 void *returned_data;
609 bool ok = THEKERNEL->public_data->get_value( temperature_control_checksum, i, current_temperature_checksum, &returned_data );
610 if (!ok) continue;
611
612 struct pad_temperature t = *static_cast<struct pad_temperature *>(returned_data);
613
614 // rename if two of the known types
615 const char *name;
616 if(t.designator == "T") name= "Hotend";
617 else if(t.designator == "B") name= "Bed";
618 else name= t.designator.c_str();
619
620 mvs->addMenuItem(name, // menu name
621 [i]() -> float { return getTargetTemperature(i); }, // getter
622 [i](float t) { THEKERNEL->public_data->set_value( temperature_control_checksum, i, &t ); }, // setter
623 1.0F, // increment
624 0.0F, // Min
625 500.0F // Max
626 );
627 }
628 }