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