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 "TemperatureControlPublicAccess.h"
23 #include "ModifyValuesScreen.h"
24 #include "PublicDataRequest.h"
25 #include "PublicData.h"
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"
35 #include "checksumm.h"
36 #include "ConfigValue.h"
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")
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")
56 #define hotend_temp_checksum CHECKSUM("hotend_temperature")
57 #define bed_temp_checksum CHECKSUM("bed_temperature")
59 Panel
* Panel::instance
= nullptr;
64 this->counter_changed
= false;
65 this->click_changed
= false;
66 this->refresh_flag
= false;
67 this->enter_menu_mode();
69 this->do_buttons
= false;
70 this->do_encoder
= false;
72 this->start_up
= true;
73 this->current_screen
= NULL
;
74 strcpy(this->playing_file
, "Playing file");
82 void Panel::on_module_loaded()
84 // Exit if this module is not enabled
85 if ( !THEKERNEL
->config
->value( panel_checksum
, enable_checksum
)->by_default(false)->as_bool() ) {
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());
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();
112 // no lcd type defined
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();
120 // some panels may need access to this global info
121 this->lcd
->setPanel(this);
123 // the number of screen lines the panel supports
124 this->screen_lines
= this->lcd
->get_screen_lines();
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();
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();
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();
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();
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
);
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);
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
);
165 THEKERNEL
->slow_ticker
->attach( 1000, this, &Panel::encoder_check
);
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
);
174 THEKERNEL
->slow_ticker
->attach( 20, this, &Panel::refresh_tick
);
177 // Enter a screen, we only care about it now
178 void Panel::enter_screen(PanelScreen
*screen
)
180 this->current_screen
= screen
;
181 this->reset_counter();
182 this->current_screen
->on_enter();
186 void Panel::reset_counter()
189 this->counter_changed
= false;
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
)
196 this->refresh_flag
= true;
201 // Encoder pins changed in interrupt or call from on_idle
202 uint32_t Panel::encoder_check(uint32_t dummy
)
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
;
218 this->counter_changed
= true;
219 (*this->counter
) += delta
;
225 // Read and update each button
226 uint32_t Panel::button_tick(uint32_t dummy
)
228 this->do_buttons
= true;
232 // Read and update encoder
233 uint32_t Panel::encoder_tick(uint32_t dummy
)
235 this->do_encoder
= true;
239 void Panel::on_gcode_received(void *argument
)
241 Gcode
*gcode
= static_cast<Gcode
*>(argument
);
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();
251 // on main loop, we can send gcodes or do anything that waits in this loop
252 void Panel::on_main_loop(void *argument
)
254 if (this->current_screen
!= NULL
) {
255 this->current_screen
->on_main_loop();
256 this->lcd
->on_main_loop();
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
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
)
280 if (this->start_up
) {
284 string
build(v
.get_build());
285 string
date(v
.get_build_date());
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....");
292 if (this->lcd
->hasGraphics()) {
293 this->lcd
->bltGlyph(24, 40, ohw_logo_antipixel_width
, ohw_logo_antipixel_height
, ohw_logo_antipixel_bits
);
296 this->lcd
->on_refresh(true); // tell lcd to display now
298 // Default top screen
299 this->top_screen
= new MainMenuScreen();
300 this->custom_screen
->set_parent(this->top_screen
);
301 this->start_up
= false;
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) {
309 if (mms
->watch_screen
!= this->current_screen
) {
310 this->enter_screen(mms
->watch_screen
);
311 // TODO do we need to reset any state?
317 if(current_screen
== NULL
&& this->idle_time
> 20*4) {
318 this->enter_screen(mms
->watch_screen
);
322 if(this->do_encoder
) {
323 this->do_encoder
= false;
327 if (this->do_buttons
) {
328 // we don't want to do SPI in interrupt mode
329 this->do_buttons
= false;
331 // read the actual buttons
332 int but
= lcd
->readButtons();
335 if(current_screen
== NULL
) {
336 // we were in startup screen so go to watch screen
337 this->enter_screen(mms
->watch_screen
);
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
);
350 // If we are in menu mode and the position has changed
351 if ( this->mode
== MENU_MODE
&& this->counter_change() ) {
355 // If we are in control mode
356 if ( this->mode
== CONTROL_MODE
&& this->counter_change() ) {
357 this->control_value_update();
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();
370 // Hooks for button clicks
371 uint32_t Panel::on_up(uint32_t dummy
)
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;
381 uint32_t Panel::on_down(uint32_t dummy
)
383 int inc
= (this->mode
== CONTROL_MODE
) ? -1 : (this->menu_offset
+1);
384 *this->counter
+= inc
;
385 this->counter_changed
= true;
389 // on most menu screens will go back to previous higher menu
390 uint32_t Panel::on_back(uint32_t dummy
)
392 if (this->mode
== MENU_MODE
&& this->current_screen
!= NULL
&& this->current_screen
->parent
!= NULL
) {
393 this->enter_screen(this->current_screen
->parent
);
398 uint32_t Panel::on_select(uint32_t dummy
)
400 // TODO make configurable, including turning off
401 // buzz is ignored on panels that do not support buzz
402 this->click_changed
= true;
404 lcd
->buzz(60, 300); // 50ms 300Hz
408 uint32_t Panel::on_pause(uint32_t dummy
)
411 THEKERNEL
->pauser
->take();
414 THEKERNEL
->pauser
->release();
420 bool Panel::counter_change()
422 if ( this->counter_changed
) {
423 this->counter_changed
= false;
431 if ( this->click_changed
) {
432 this->click_changed
= false;
441 void Panel::enter_menu_mode(bool force
)
443 this->mode
= MENU_MODE
;
444 this->counter
= &this->menu_selected_line
;
445 this->menu_changed
= force
;
448 void Panel::setup_menu(uint16_t rows
)
450 this->setup_menu(rows
, min(rows
, this->max_screen_lines()));
453 void Panel::setup_menu(uint16_t rows
, uint16_t lines
)
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
;
462 void Panel::menu_update()
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
469 // this allows it to wrap but with new method we dont; want to wrap
470 msl
= msl
% ( this->menu_rows
<< this->menu_offset
);
472 msl
+= this->menu_rows
<< this->menu_offset
;
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;
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
;
488 if ( this->menu_rows
> this->panel_lines
) {
490 // old way of scrolling not nice....
491 if ( this->menu_current_line
>= 2 ) {
492 this->menu_start_line
= this->menu_current_line
- 1;
494 if ( this->menu_current_line
> this->menu_rows
- this->panel_lines
) {
495 this->menu_start_line
= this->menu_rows
- this->panel_lines
;
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
504 }else if(sl
< 0 ) { // do we want to scroll down?
505 this->menu_start_line
+= sl
; // scroll down
510 this->menu_start_line
= 0;
513 this->menu_changed
= true;
516 bool Panel::menu_change()
518 if ( this->menu_changed
) {
519 this->menu_changed
= false;
526 bool Panel::control_value_change()
528 if ( this->control_value_changed
) {
529 this->control_value_changed
= false;
536 bool Panel::enter_control_mode(float passed_normal_increment
, float passed_pressed_increment
)
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;
546 void Panel::control_value_update()
548 // TODO what do we do here?
549 this->control_value_changed
= true;
552 void Panel::set_control_value(float value
)
554 this->control_base_value
= value
;
557 float Panel::get_control_value()
559 return this->control_base_value
+ (this->control_normal_counter
* this->normal_increment
);
562 bool Panel::is_playing() const
566 bool ok
= THEKERNEL
->public_data
->get_value( player_checksum
, is_playing_checksum
, &returned_data
);
568 bool b
= *static_cast<bool *>(returned_data
);
574 void Panel::set_playing_file(string f
)
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;
583 static float getTargetTemperature(uint16_t heater_cs
)
586 bool ok
= THEKERNEL
->public_data
->get_value( temperature_control_checksum
, heater_cs
, current_temperature_checksum
, &returned_data
);
589 struct pad_temperature temp
= *static_cast<struct pad_temperature
*>(returned_data
);
590 return temp
.target_temperature
;
596 void Panel::setup_temperature_screen()
598 // setup temperature screen
599 auto mvs
= new ModifyValuesScreen();
600 this->temperature_screen
= mvs
;
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
);
606 for(auto i
: modules
) {
607 if (!THEKERNEL
->config
->value(temperature_control_checksum
, i
, enable_checksum
)->as_bool()) continue;
609 bool ok
= THEKERNEL
->public_data
->get_value( temperature_control_checksum
, i
, current_temperature_checksum
, &returned_data
);
612 struct pad_temperature t
= *static_cast<struct pad_temperature
*>(returned_data
);
614 // rename if two of the known types
616 if(t
.designator
== "T") name
= "Hotend";
617 else if(t
.designator
== "B") name
= "Bed";
618 else name
= t
.designator
.c_str();
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