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