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/>.
8 #include "libs/Kernel.h"
10 #include "PanelScreen.h"
12 #include "libs/nuts_bolts.h"
13 #include "libs/utils.h"
16 #include "modules/utils/player/PlayerPublicAccess.h"
17 #include "screens/CustomScreen.h"
18 #include "screens/MainMenuScreen.h"
19 #include "SlowTicker.h"
22 #include "PublicData.h"
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"
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")
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")
48 #define hotend_temp_checksum CHECKSUM("hotend_temperature")
49 #define bed_temp_checksum CHECKSUM("bed_temperature")
53 this->counter_changed
= false;
54 this->click_changed
= false;
55 this->refresh_flag
= false;
56 this->enter_menu_mode();
58 this->do_buttons
= false;
60 this->start_up
= true;
61 this->current_screen
= NULL
;
62 strcpy(this->playing_file
, "Playing file");
70 void Panel::on_module_loaded()
72 // Exit if this module is not enabled
73 if ( !THEKERNEL
->config
->value( panel_checksum
, enable_checksum
)->by_default(false)->as_bool() ) {
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());
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();
98 // no lcd type defined
102 this->custom_screen
= new CustomScreen(); // this needs to be called here as it needs the config cache loaded
104 // some panels may need access to this global info
105 this->lcd
->setPanel(this);
107 // the number of screen lines the panel supports
108 this->screen_lines
= this->lcd
->get_screen_lines();
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();
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();
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();
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();
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
);
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);
143 THEKERNEL
->slow_ticker
->attach( 100, this, &Panel::button_tick
);
144 THEKERNEL
->slow_ticker
->attach( 1000, this, &Panel::encoder_check
);
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
);
152 THEKERNEL
->slow_ticker
->attach( 20, this, &Panel::refresh_tick
);
155 // Enter a screen, we only care about it now
156 void Panel::enter_screen(PanelScreen
*screen
)
158 screen
->panel
= this;
159 this->current_screen
= screen
;
160 this->reset_counter();
161 this->current_screen
->on_enter();
165 void Panel::reset_counter()
168 this->counter_changed
= false;
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
)
175 this->refresh_flag
= true;
180 // Encoder pins changed in interrupt
181 uint32_t Panel::encoder_check(uint32_t dummy
)
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
;
191 if ( change
!= 0 && encoder_counter
% this->encoder_click_resolution
== 0 ) {
192 this->counter_changed
= true;
193 (*this->counter
) += change
;
199 // Read and update each button
200 uint32_t Panel::button_tick(uint32_t dummy
)
202 this->do_buttons
= true;
206 void Panel::on_gcode_received(void *argument
)
208 Gcode
*gcode
= static_cast<Gcode
*>(argument
);
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();
218 // on main loop, we can send gcodes or do anything that waits in this loop
219 void Panel::on_main_loop(void *argument
)
221 if (this->current_screen
!= NULL
) {
222 this->current_screen
->on_main_loop();
223 this->lcd
->on_main_loop();
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
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
)
247 if (this->start_up
) {
251 string
build(v
.get_build());
252 string
date(v
.get_build_date());
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....");
259 if (this->lcd
->hasGraphics()) {
260 this->lcd
->bltGlyph(24, 40, ohw_logo_antipixel_width
, ohw_logo_antipixel_height
, ohw_logo_antipixel_bits
);
263 this->lcd
->on_refresh(true); // tell lcd to display now
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;
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) {
277 if (mms
->watch_screen
!= this->current_screen
) {
278 this->enter_screen(mms
->watch_screen
);
279 // TODO do we need to reset any state?
285 if(current_screen
== NULL
&& this->idle_time
> 20*4) {
286 this->enter_screen(mms
->watch_screen
);
290 if (this->do_buttons
) {
291 // we don't want to do I2C in interrupt mode
292 this->do_buttons
= false;
294 // read the actual buttons
295 int but
= lcd
->readButtons();
298 if(current_screen
== NULL
) {
299 // we were in startup screen so go to watch screen
300 this->enter_screen(mms
->watch_screen
);
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
);
313 // If we are in menu mode and the position has changed
314 if ( this->mode
== MENU_MODE
&& this->counter_change() ) {
318 // If we are in control mode
319 if ( this->mode
== CONTROL_MODE
&& this->counter_change() ) {
320 this->control_value_update();
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();
333 // Hooks for button clicks
334 uint32_t Panel::on_up(uint32_t dummy
)
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;
344 uint32_t Panel::on_down(uint32_t dummy
)
346 int inc
= (this->mode
== CONTROL_MODE
) ? -1 : (this->menu_offset
+1);
347 *this->counter
+= inc
;
348 this->counter_changed
= true;
352 // on most menu screens will go back to previous higher menu
353 uint32_t Panel::on_back(uint32_t dummy
)
355 if (this->mode
== MENU_MODE
&& this->current_screen
!= NULL
&& this->current_screen
->parent
!= NULL
) {
356 this->enter_screen(this->current_screen
->parent
);
361 uint32_t Panel::on_select(uint32_t dummy
)
363 // TODO make configurable, including turning off
364 // buzz is ignored on panels that do not support buzz
365 this->click_changed
= true;
367 lcd
->buzz(60, 300); // 50ms 300Hz
371 uint32_t Panel::on_pause(uint32_t dummy
)
374 THEKERNEL
->pauser
->take();
377 THEKERNEL
->pauser
->release();
383 bool Panel::counter_change()
385 if ( this->counter_changed
) {
386 this->counter_changed
= false;
394 if ( this->click_changed
) {
395 this->click_changed
= false;
404 void Panel::enter_menu_mode()
406 this->mode
= MENU_MODE
;
407 this->counter
= &this->menu_selected_line
;
408 this->menu_changed
= false;
411 void Panel::setup_menu(uint16_t rows
)
413 this->setup_menu(rows
, min(rows
, this->max_screen_lines()));
416 void Panel::setup_menu(uint16_t rows
, uint16_t lines
)
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
;
425 void Panel::menu_update()
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
432 // this allows it to wrap but with new method we dont; want to wrap
433 msl
= msl
% ( this->menu_rows
<< this->menu_offset
);
435 msl
+= this->menu_rows
<< this->menu_offset
;
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;
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
;
451 if ( this->menu_rows
> this->panel_lines
) {
453 // old way of scrolling not nice....
454 if ( this->menu_current_line
>= 2 ) {
455 this->menu_start_line
= this->menu_current_line
- 1;
457 if ( this->menu_current_line
> this->menu_rows
- this->panel_lines
) {
458 this->menu_start_line
= this->menu_rows
- this->panel_lines
;
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
467 }else if(sl
< 0 ) { // do we want to scroll down?
468 this->menu_start_line
+= sl
; // scroll down
473 this->menu_start_line
= 0;
476 this->menu_changed
= true;
479 bool Panel::menu_change()
481 if ( this->menu_changed
) {
482 this->menu_changed
= false;
489 bool Panel::control_value_change()
491 if ( this->control_value_changed
) {
492 this->control_value_changed
= false;
499 bool Panel::enter_control_mode(float passed_normal_increment
, float passed_pressed_increment
)
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;
509 void Panel::control_value_update()
511 // TODO what do we do here?
512 this->control_value_changed
= true;
515 void Panel::set_control_value(float value
)
517 this->control_base_value
= value
;
520 float Panel::get_control_value()
522 return this->control_base_value
+ (this->control_normal_counter
* this->normal_increment
);
525 bool Panel::is_playing() const
529 bool ok
= THEKERNEL
->public_data
->get_value( player_checksum
, is_playing_checksum
, &returned_data
);
531 bool b
= *static_cast<bool *>(returned_data
);
537 void Panel::set_playing_file(string f
)
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;