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"
15 #include "libs/USBDevice/USBMSD/SDCard.h"
16 #include "libs/SDFAT.h"
18 #include "modules/utils/player/PlayerPublicAccess.h"
19 #include "screens/CustomScreen.h"
20 #include "screens/MainMenuScreen.h"
21 #include "SlowTicker.h"
24 #include "TemperatureControlPublicAccess.h"
25 #include "ModifyValuesScreen.h"
26 #include "PublicDataRequest.h"
27 #include "PublicData.h"
28 #include "StreamOutputPool.h"
29 #include "platform_memory.h"
31 #include "panels/ReprapDiscountGLCD.h"
32 #include "panels/ST7565.h"
33 #include "panels/UniversalAdapter.h"
36 #include "checksumm.h"
37 #include "ConfigValue.h"
39 #include "TemperatureControlPool.h"
41 // for parse_pins in mbed
44 #define panel_checksum CHECKSUM("panel")
45 #define enable_checksum CHECKSUM("enable")
46 #define lcd_checksum CHECKSUM("lcd")
47 #define rrd_glcd_checksum CHECKSUM("reprap_discount_glcd")
48 #define st7565_glcd_checksum CHECKSUM("st7565_glcd")
49 #define viki2_checksum CHECKSUM("viki2")
50 #define mini_viki2_checksum CHECKSUM("mini_viki2")
51 #define universal_adapter_checksum CHECKSUM("universal_adapter")
53 #define menu_offset_checksum CHECKSUM("menu_offset")
54 #define encoder_resolution_checksum CHECKSUM("encoder_resolution")
55 #define jog_x_feedrate_checksum CHECKSUM("alpha_jog_feedrate")
56 #define jog_y_feedrate_checksum CHECKSUM("beta_jog_feedrate")
57 #define jog_z_feedrate_checksum CHECKSUM("gamma_jog_feedrate")
58 #define longpress_delay_checksum CHECKSUM("longpress_delay")
60 #define ext_sd_checksum CHECKSUM("external_sd")
61 #define sdcd_pin_checksum CHECKSUM("sdcd_pin")
62 #define spi_channel_checksum CHECKSUM("spi_channel")
63 #define spi_cs_pin_checksum CHECKSUM("spi_cs_pin")
65 #define hotend_temp_checksum CHECKSUM("hotend_temperature")
66 #define bed_temp_checksum CHECKSUM("bed_temperature")
68 Panel
* Panel::instance
= nullptr;
73 this->counter_changed
= false;
74 this->click_changed
= false;
75 this->refresh_flag
= false;
76 this->enter_menu_mode();
78 this->do_buttons
= false;
79 this->do_encoder
= false;
81 this->start_up
= true;
82 this->current_screen
= NULL
;
84 this->extmounter
= nullptr;
85 this->external_sd_enable
= false;
87 strcpy(this->playing_file
, "Playing file");
93 delete this->extmounter
;
97 void Panel::on_module_loaded()
99 // Exit if this module is not enabled
100 if ( !THEKERNEL
->config
->value( panel_checksum
, enable_checksum
)->by_default(false)->as_bool() ) {
105 // Initialise the LCD, see which LCD to use
106 if (this->lcd
!= NULL
) delete this->lcd
;
107 int lcd_cksm
= get_checksum(THEKERNEL
->config
->value(panel_checksum
, lcd_checksum
)->by_default("reprap_discount_glcd")->as_string());
109 // Note checksums are not const expressions when in debug mode, so don't use switch
110 if (lcd_cksm
== rrd_glcd_checksum
) {
111 this->lcd
= new ReprapDiscountGLCD();
112 } else if (lcd_cksm
== st7565_glcd_checksum
) {
113 this->lcd
= new ST7565();
114 } else if (lcd_cksm
== viki2_checksum
) {
115 this->lcd
= new ST7565(1); // variant 1
116 } else if (lcd_cksm
== mini_viki2_checksum
) {
117 this->lcd
= new ST7565(2); // variant 2
118 } else if (lcd_cksm
== universal_adapter_checksum
) {
119 this->lcd
= new UniversalAdapter();
121 // no known lcd type defined
127 if(THEKERNEL
->config
->value( panel_checksum
, ext_sd_checksum
)->by_default(false)->as_bool()) {
128 this->external_sd_enable
= true;
129 // external sdcard detect
130 this->sdcd_pin
.from_string(THEKERNEL
->config
->value( panel_checksum
, ext_sd_checksum
, sdcd_pin_checksum
)->by_default("nc")->as_string())->as_input();
131 this->extsd_spi_channel
= THEKERNEL
->config
->value(panel_checksum
, ext_sd_checksum
, spi_channel_checksum
)->by_default(0)->as_number();
132 string s
= THEKERNEL
->config
->value( panel_checksum
, ext_sd_checksum
, spi_cs_pin_checksum
)->by_default("2.8")->as_string();
133 s
= "P" + s
; // Pinnames need to be Px_x
134 this->extsd_spi_cs
= parse_pins(s
.c_str());
135 this->register_for_event(ON_SECOND_TICK
);
138 // these need to be called here as they need the config cache loaded as they enumerate modules
139 this->custom_screen
= new CustomScreen();
141 // some panels may need access to this global info
142 this->lcd
->setPanel(this);
144 // the number of screen lines the panel supports
145 this->screen_lines
= this->lcd
->get_screen_lines();
147 // some encoders may need more clicks to move menu, this is a divisor and is in config as it is
148 // an end user usability issue
149 this->menu_offset
= THEKERNEL
->config
->value( panel_checksum
, menu_offset_checksum
)->by_default(0)->as_number();
151 // override default encoder resolution if needed
152 this->encoder_click_resolution
= THEKERNEL
->config
->value( panel_checksum
, encoder_resolution_checksum
)->by_default(this->lcd
->getEncoderResolution())->as_number();
154 // load jogging feedrates in mm/min
155 jogging_speed_mm_min
[0] = THEKERNEL
->config
->value( panel_checksum
, jog_x_feedrate_checksum
)->by_default(3000.0f
)->as_number();
156 jogging_speed_mm_min
[1] = THEKERNEL
->config
->value( panel_checksum
, jog_y_feedrate_checksum
)->by_default(3000.0f
)->as_number();
157 jogging_speed_mm_min
[2] = THEKERNEL
->config
->value( panel_checksum
, jog_z_feedrate_checksum
)->by_default(300.0f
)->as_number();
159 // load the default preset temeratures
160 default_hotend_temperature
= THEKERNEL
->config
->value( panel_checksum
, hotend_temp_checksum
)->by_default(185.0f
)->as_number();
161 default_bed_temperature
= THEKERNEL
->config
->value( panel_checksum
, bed_temp_checksum
)->by_default(60.0f
)->as_number();
164 this->up_button
.up_attach( this, &Panel::on_up
);
165 this->down_button
.up_attach( this, &Panel::on_down
);
166 this->click_button
.up_attach( this, &Panel::on_select
);
167 this->back_button
.up_attach( this, &Panel::on_back
);
168 this->pause_button
.up_attach( this, &Panel::on_pause
);
171 //setting longpress_delay
172 int longpress_delay
= THEKERNEL
->config
->value( panel_checksum
, longpress_delay_checksum
)->by_default(0)->as_number();
173 this->up_button
.set_longpress_delay(longpress_delay
);
174 this->down_button
.set_longpress_delay(longpress_delay
);
175 // this->click_button.set_longpress_delay(longpress_delay);
176 // this->back_button.set_longpress_delay(longpress_delay);
177 // this->pause_button.set_longpress_delay(longpress_delay);
180 THEKERNEL
->slow_ticker
->attach( 50, this, &Panel::button_tick
);
181 if(lcd
->encoderReturnsDelta()) {
182 // panel handles encoder pins and returns a delta
183 THEKERNEL
->slow_ticker
->attach( 10, this, &Panel::encoder_tick
);
186 THEKERNEL
->slow_ticker
->attach( 1000, this, &Panel::encoder_check
);
189 // Register for events
190 this->register_for_event(ON_IDLE
);
191 this->register_for_event(ON_MAIN_LOOP
);
192 this->register_for_event(ON_GCODE_RECEIVED
);
193 this->register_for_event(ON_HALT
);
196 THEKERNEL
->slow_ticker
->attach( 20, this, &Panel::refresh_tick
);
199 // Enter a screen, we only care about it now
200 void Panel::enter_screen(PanelScreen
*screen
)
202 if(this->current_screen
!= nullptr)
203 this->current_screen
->on_exit();
205 this->current_screen
= screen
;
206 this->reset_counter();
207 this->current_screen
->on_enter();
211 void Panel::reset_counter()
214 this->counter_changed
= false;
217 // Indicate the idle loop we want to call the refresh hook in the current screen
218 // called 20 times a second
219 uint32_t Panel::refresh_tick(uint32_t dummy
)
221 this->refresh_flag
= true;
226 // Encoder pins changed in interrupt or call from on_idle
227 uint32_t Panel::encoder_check(uint32_t dummy
)
229 // if encoder reads go through SPI like on universal panel adapter this needs to be
230 // done in idle loop, this is indicated by lcd->encoderReturnsDelta()
231 // however when reading encoder directly it needs to be done
232 // frequently, universal panel adapter will return an actual delta count so won't miss any if polled slowly
233 // 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
234 static int encoder_counter
= 0; // keeps track of absolute encoder position
235 static int last_encoder_click
= 0; // last readfing of divided encoder count
236 int change
= lcd
->readEncoderDelta();
237 encoder_counter
+= change
;
238 int clicks
= encoder_counter
/this->encoder_click_resolution
;
239 int delta
= clicks
- last_encoder_click
; // the number of clicks this time
240 last_encoder_click
= clicks
;
243 this->counter_changed
= true;
244 (*this->counter
) += delta
;
250 // Read and update each button
251 uint32_t Panel::button_tick(uint32_t dummy
)
253 this->do_buttons
= true;
257 // Read and update encoder
258 uint32_t Panel::encoder_tick(uint32_t dummy
)
260 this->do_encoder
= true;
264 void Panel::on_gcode_received(void *argument
)
266 Gcode
*gcode
= static_cast<Gcode
*>(argument
);
268 if ( gcode
->m
== 117 ) { // set LCD message
269 this->message
= get_arguments(gcode
->get_command());
270 if (this->message
.size() > 20) this->message
= this->message
.substr(0, 20);
271 gcode
->mark_as_taken();
276 // on main loop, we can send gcodes or do anything that waits in this loop
277 void Panel::on_main_loop(void *argument
)
279 if (this->current_screen
!= NULL
) {
280 this->current_screen
->on_main_loop();
281 this->lcd
->on_main_loop();
286 #define ohw_logo_antipixel_width 80
287 #define ohw_logo_antipixel_height 15
288 static const uint8_t ohw_logo_antipixel_bits
[] = {
289 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
290 0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x33, 0x18, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x5E,
291 0x80, 0x2D, 0x6B, 0x9B, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x18, 0xAB, 0xFF, 0xFF,
292 0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x7B, 0xB3, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x7F, 0x80, 0x33,
293 0x78, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x81, 0xF3, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD,
294 0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xB3, 0x18, 0xDD, 0x98, 0xC5, 0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xAD,
295 0x6B, 0x5D, 0x6B, 0x5D, 0x80, 0x73, 0x80, 0x3F, 0xFC, 0x21, 0x1B, 0x55, 0x08, 0xC5, 0x80, 0xF3,
296 0xC0, 0x3F, 0xFD, 0xAD, 0x5B, 0x49, 0x6A, 0xDD, 0x80, 0xE1, 0xC0, 0x3F, 0xFD, 0xAD, 0x68, 0xDD,
297 0x6B, 0x45, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
298 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
301 // On idle things, we don't want to do shit in interrupts
302 // don't queue gcodes in this
303 void Panel::on_idle(void *argument
)
305 if (this->start_up
) {
309 string
build(v
.get_build());
310 string
date(v
.get_build_date());
312 this->lcd
->setCursor(0, 0); this->lcd
->printf("Welcome to Smoothie");
313 this->lcd
->setCursor(0, 1); this->lcd
->printf("%s", build
.substr(0, 20).c_str());
314 this->lcd
->setCursor(0, 2); this->lcd
->printf("%s", date
.substr(0, 20).c_str());
315 this->lcd
->setCursor(0, 3); this->lcd
->printf("Please wait....");
317 if (this->lcd
->hasGraphics()) {
318 this->lcd
->bltGlyph(24, 40, ohw_logo_antipixel_width
, ohw_logo_antipixel_height
, ohw_logo_antipixel_bits
);
321 this->lcd
->on_refresh(true); // tell lcd to display now
323 // Default top screen
324 this->top_screen
= new MainMenuScreen();
325 this->custom_screen
->set_parent(this->top_screen
);
326 this->start_up
= false;
330 MainMenuScreen
*mms
= static_cast<MainMenuScreen
*>(this->top_screen
);
331 // after being idle for a while switch to Watch screen
332 if (this->current_screen
!= NULL
&& this->idle_time
> this->current_screen
->idle_timeout_secs()*20) {
334 if (mms
->watch_screen
!= this->current_screen
) {
335 this->enter_screen(mms
->watch_screen
);
336 // TODO do we need to reset any state?
342 if(current_screen
== NULL
&& this->idle_time
> 20*4) {
343 this->enter_screen(mms
->watch_screen
);
347 if(this->do_encoder
) {
348 this->do_encoder
= false;
352 if (this->do_buttons
) {
353 // we don't want to do SPI in interrupt mode
354 this->do_buttons
= false;
356 // read the actual buttons
357 int but
= lcd
->readButtons();
360 if(current_screen
== NULL
) {
361 // we were in startup screen so go to watch screen
362 this->enter_screen(mms
->watch_screen
);
367 // fire events if the buttons are active and debounce is satisfied
368 this->up_button
.check_signal(but
& BUTTON_UP
);
369 this->down_button
.check_signal(but
& BUTTON_DOWN
);
370 this->back_button
.check_signal(but
& BUTTON_LEFT
);
371 this->click_button
.check_signal(but
& BUTTON_SELECT
);
372 this->pause_button
.check_signal(but
& BUTTON_PAUSE
);
375 // If we are in menu mode and the position has changed
376 if ( this->mode
== MENU_MODE
&& this->counter_change() ) {
380 // If we are in control mode
381 if ( this->mode
== CONTROL_MODE
&& this->counter_change() ) {
382 this->control_value_update();
385 // If we must refresh
386 if ( this->refresh_flag
) {
387 this->refresh_flag
= false;
388 if (this->current_screen
!= NULL
) {
389 this->current_screen
->on_refresh();
390 this->lcd
->on_refresh();
395 // Hooks for button clicks
396 uint32_t Panel::on_up(uint32_t dummy
)
398 // this is simulating encoder clicks, but needs to be inverted to
399 // increment values on up,increment by
400 int inc
= (this->mode
== CONTROL_MODE
) ? 1 : -(this->menu_offset
+1);
401 *this->counter
+= inc
;
402 this->counter_changed
= true;
406 uint32_t Panel::on_down(uint32_t dummy
)
408 int inc
= (this->mode
== CONTROL_MODE
) ? -1 : (this->menu_offset
+1);
409 *this->counter
+= inc
;
410 this->counter_changed
= true;
414 // on most menu screens will go back to previous higher menu
415 uint32_t Panel::on_back(uint32_t dummy
)
417 if (this->mode
== MENU_MODE
&& this->current_screen
!= NULL
&& this->current_screen
->parent
!= NULL
) {
418 this->enter_screen(this->current_screen
->parent
);
423 uint32_t Panel::on_select(uint32_t dummy
)
425 // TODO make configurable, including turning off
426 // buzz is ignored on panels that do not support buzz
427 this->click_changed
= true;
429 lcd
->buzz(60, 300); // 50ms 300Hz
433 uint32_t Panel::on_pause(uint32_t dummy
)
435 if (!THEKERNEL
->pauser
->paused()) {
436 THEKERNEL
->pauser
->take();
438 THEKERNEL
->pauser
->release();
443 bool Panel::counter_change()
445 if ( this->counter_changed
) {
446 this->counter_changed
= false;
454 if ( this->click_changed
) {
455 this->click_changed
= false;
464 void Panel::enter_menu_mode(bool force
)
466 this->mode
= MENU_MODE
;
467 this->counter
= &this->menu_selected_line
;
468 this->menu_changed
= force
;
471 void Panel::setup_menu(uint16_t rows
)
473 this->setup_menu(rows
, min(rows
, this->max_screen_lines()));
476 void Panel::setup_menu(uint16_t rows
, uint16_t lines
)
478 this->menu_selected_line
= 0;
479 this->menu_current_line
= 0;
480 this->menu_start_line
= 0;
481 this->menu_rows
= rows
;
482 this->panel_lines
= lines
;
485 void Panel::menu_update()
487 // Limits, up and down
488 // NOTE menu_selected_line is changed in an interrupt and can change at any time
489 int msl
= this->menu_selected_line
; // hopefully this is atomic
492 // this allows it to wrap but with new method we dont; want to wrap
493 msl
= msl
% ( this->menu_rows
<< this->menu_offset
);
495 msl
+= this->menu_rows
<< this->menu_offset
;
498 // limit selected line to screen lines available
499 if(msl
>= this->menu_rows
<<this->menu_offset
){
500 msl
= (this->menu_rows
-1)<<this->menu_offset
;
501 }else if(msl
< 0) msl
= 0;
504 this->menu_selected_line
= msl
; // update atomically we hope
505 // figure out which actual line to select, if we have a menu offset it means we want to move one line per two clicks
506 if(msl
% (this->menu_offset
+1) == 0) { // only if divisible by offset
507 this->menu_current_line
= msl
>> this->menu_offset
;
511 if ( this->menu_rows
> this->panel_lines
) {
513 // old way of scrolling not nice....
514 if ( this->menu_current_line
>= 2 ) {
515 this->menu_start_line
= this->menu_current_line
- 1;
517 if ( this->menu_current_line
> this->menu_rows
- this->panel_lines
) {
518 this->menu_start_line
= this->menu_rows
- this->panel_lines
;
521 // new way we only scroll the lines when the cursor hits the bottom of the screen or the top of the screen
522 // do we want to scroll up?
523 int sl
= this->menu_current_line
- this->menu_start_line
; // screen line we are on
524 if(sl
>= this->panel_lines
) {
525 this->menu_start_line
+= ((sl
+1)-this->panel_lines
); // scroll up to keep it on the screen
527 }else if(sl
< 0 ) { // do we want to scroll down?
528 this->menu_start_line
+= sl
; // scroll down
533 this->menu_start_line
= 0;
536 this->menu_changed
= true;
539 bool Panel::menu_change()
541 if ( this->menu_changed
) {
542 this->menu_changed
= false;
549 bool Panel::control_value_change()
551 if ( this->control_value_changed
) {
552 this->control_value_changed
= false;
559 bool Panel::enter_control_mode(float passed_normal_increment
, float passed_pressed_increment
)
561 this->mode
= CONTROL_MODE
;
562 this->normal_increment
= passed_normal_increment
;
563 this->counter
= &this->control_normal_counter
;
564 this->control_normal_counter
= 0;
565 this->control_base_value
= 0;
569 void Panel::control_value_update()
571 // TODO what do we do here?
572 this->control_value_changed
= true;
575 void Panel::set_control_value(float value
)
577 this->control_base_value
= value
;
580 float Panel::get_control_value()
582 return this->control_base_value
+ (this->control_normal_counter
* this->normal_increment
);
585 bool Panel::is_playing() const
589 bool ok
= PublicData::get_value( player_checksum
, is_playing_checksum
, &returned_data
);
591 bool b
= *static_cast<bool *>(returned_data
);
597 bool Panel::is_suspended() const
601 bool ok
= PublicData::get_value( player_checksum
, is_suspended_checksum
, &returned_data
);
603 bool b
= *static_cast<bool *>(returned_data
);
609 void Panel::set_playing_file(string f
)
611 // just copy the first 20 characters after the first / if there
612 size_t n
= f
.find_last_of('/');
613 if (n
== string::npos
) n
= 0;
614 strncpy(playing_file
, f
.substr(n
+ 1, 19).c_str(), sizeof(playing_file
));
615 playing_file
[sizeof(playing_file
) - 1] = 0;
618 bool Panel::mount_external_sd(bool on
)
620 // now setup the external sdcard if we have one and mount it
622 if(this->sd
== nullptr) {
623 PinName mosi
, miso
, sclk
, cs
= this->extsd_spi_cs
;
624 if(extsd_spi_channel
== 0) {
625 mosi
= P0_18
; miso
= P0_17
; sclk
= P0_15
;
626 } else if(extsd_spi_channel
== 1) {
627 mosi
= P0_9
; miso
= P0_8
; sclk
= P0_7
;
629 this->external_sd_enable
= false;
630 THEKERNEL
->streams
->printf("Bad SPI channel for external SDCard\n");
633 size_t n
= sizeof(SDCard
);
634 void *v
= AHB0
.alloc(n
);
635 memset(v
, 0, n
); // clear the allocated memory
636 this->sd
= new(v
) SDCard(mosi
, miso
, sclk
, cs
); // allocate object using zeroed memory
638 delete this->extmounter
; // if it was not unmounted before
639 size_t n
= sizeof(SDFAT
);
640 void *v
= AHB0
.alloc(n
);
641 memset(v
, 0, n
); // clear the allocated memory
642 this->extmounter
= new(v
) SDFAT("ext", this->sd
); // use cleared allocated memory
643 this->sd
->disk_initialize(); // first one seems to fail, but works next time
644 THEKERNEL
->streams
->printf("External SDcard mounted as /ext\n");
646 delete this->extmounter
;
647 this->extmounter
= nullptr;
648 THEKERNEL
->streams
->printf("External SDcard unmounted\n");
653 void Panel::on_second_tick(void *arg
)
655 if(!this->external_sd_enable
|| this->start_up
) return;
657 // sd insert detect, mount sdcard if inserted, unmount if removed
658 if(this->sdcd_pin
.connected()) {
659 if(this->extmounter
== nullptr && this->sdcd_pin
.get()) {
660 mount_external_sd(true);
661 // go to the play screen and the /ext directory
662 // TODO we don't want to do this if we just booted and card was already in
663 THEKERNEL
->current_path
= "/ext";
664 MainMenuScreen
*mms
= static_cast<MainMenuScreen
*>(this->top_screen
);
665 THEPANEL
->enter_screen(mms
->file_screen
);
667 }else if(this->extmounter
!= nullptr && !this->sdcd_pin
.get()){
668 mount_external_sd(false);
671 // TODO for panels with no sd card detect we need to poll to see if card is inserted - or not
675 void Panel::on_halt(void *arg
)
677 halted
= (arg
== nullptr);