| 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 "libs/nuts_bolts.h" |
| 11 | #include "libs/utils.h" |
| 12 | #include <string> |
| 13 | using namespace std; |
| 14 | #include "Button.h" |
| 15 | #include "PanelScreen.h" |
| 16 | #include "screens/MainMenuScreen.h" |
| 17 | #include "modules/utils/player/PlayerPublicAccess.h" |
| 18 | |
| 19 | #include "panels/I2CLCD.h" |
| 20 | #include "panels/VikiLCD.h" |
| 21 | #include "panels/Smoothiepanel.h" |
| 22 | #include "panels/ReprapDiscountGLCD.h" |
| 23 | |
| 24 | Panel::Panel(){ |
| 25 | this->counter_changed = false; |
| 26 | this->click_changed = false; |
| 27 | this->refresh_flag = false; |
| 28 | this->enter_menu_mode(); |
| 29 | this->lcd= NULL; |
| 30 | this->do_buttons = false; |
| 31 | this->idle_time= 0; |
| 32 | strcpy(this->playing_file, "Playing file"); |
| 33 | } |
| 34 | |
| 35 | Panel::~Panel() { |
| 36 | delete this->lcd; |
| 37 | } |
| 38 | |
| 39 | void Panel::on_module_loaded(){ |
| 40 | // Exit if this module is not enabled |
| 41 | if( !this->kernel->config->value( panel_checksum, enable_checksum )->by_default(false)->as_bool() ){ |
| 42 | delete this; |
| 43 | return; |
| 44 | } |
| 45 | |
| 46 | |
| 47 | // Initialise the LCD, see which LCD to use |
| 48 | if (this->lcd != NULL) delete this->lcd; |
| 49 | int lcd_cksm = get_checksum(this->kernel->config->value(panel_checksum, lcd_checksum)->by_default("i2c")->as_string()); |
| 50 | |
| 51 | // Note checksums are not const expressions when in debug mode, so don't use switch |
| 52 | if(lcd_cksm == i2c_lcd_checksum) { |
| 53 | this->lcd = new I2CLCD(); |
| 54 | }else if(lcd_cksm == viki_lcd_checksum) { |
| 55 | this->lcd = new VikiLCD(); |
| 56 | this->lcd->set_variant(0); |
| 57 | }else if(lcd_cksm == panelolu2_checksum) { |
| 58 | this->lcd = new VikiLCD(); |
| 59 | this->lcd->set_variant(1); |
| 60 | }else if(lcd_cksm == smoothiepanel_checksum) { |
| 61 | this->lcd = new Smoothiepanel(); |
| 62 | }else if(lcd_cksm == rrd_glcd_checksum) { |
| 63 | this->lcd = new ReprapDiscountGLCD(); |
| 64 | }else{ |
| 65 | // no lcd type defined |
| 66 | return; |
| 67 | } |
| 68 | |
| 69 | // some panels may need access to this global info |
| 70 | this->lcd->setPanel(this); |
| 71 | |
| 72 | // the number of screen lines the panel supports |
| 73 | this->screen_lines= this->lcd->get_screen_lines(); |
| 74 | |
| 75 | // some encoders may need more clicks to move menu, this is a divisor and is in config as it is |
| 76 | // an end user usability issue |
| 77 | this->menu_offset = this->kernel->config->value( panel_checksum, menu_offset_checksum )->by_default(0)->as_number(); |
| 78 | |
| 79 | // load jogging feedrates in mm/min |
| 80 | jogging_speed_mm_min[0]= this->kernel->config->value( panel_checksum, jog_x_feedrate_checksum )->by_default(3000.0)->as_number(); |
| 81 | jogging_speed_mm_min[1]= this->kernel->config->value( panel_checksum, jog_y_feedrate_checksum )->by_default(3000.0)->as_number(); |
| 82 | jogging_speed_mm_min[2]= this->kernel->config->value( panel_checksum, jog_z_feedrate_checksum )->by_default(300.0)->as_number(); |
| 83 | |
| 84 | // load the default preset temeratures |
| 85 | default_hotend_temperature= this->kernel->config->value( panel_checksum, hotend_temp_checksum )->by_default(185.0)->as_number(); |
| 86 | default_bed_temperature= this->kernel->config->value( panel_checksum, bed_temp_checksum )->by_default(60.0)->as_number(); |
| 87 | |
| 88 | this->encoder_click_resolution= this->lcd->getEncoderResolution(); |
| 89 | this->lcd->init(); |
| 90 | this->lcd->printf("Starting..."); |
| 91 | |
| 92 | this->up_button.up_attach( this, &Panel::on_up ); |
| 93 | this->down_button.up_attach( this, &Panel::on_down ); |
| 94 | this->click_button.up_attach( this, &Panel::on_select ); |
| 95 | this->back_button.up_attach( this, &Panel::on_back ); |
| 96 | this->pause_button.up_attach( this, &Panel::on_pause ); |
| 97 | |
| 98 | this->kernel->slow_ticker->attach( 100, this, &Panel::button_tick ); |
| 99 | this->kernel->slow_ticker->attach( 1000, this, &Panel::encoder_check ); |
| 100 | |
| 101 | // Default top screen |
| 102 | this->top_screen = new MainMenuScreen(); |
| 103 | this->top_screen->set_panel(this); |
| 104 | this->enter_screen(this->top_screen->watch_screen); // default first screen is watch screen even though its parent is Mainmenu |
| 105 | |
| 106 | // Register for events |
| 107 | this->register_for_event(ON_IDLE); |
| 108 | this->register_for_event(ON_MAIN_LOOP); |
| 109 | |
| 110 | // Refresh timer |
| 111 | this->kernel->slow_ticker->attach( 20, this, &Panel::refresh_tick ); |
| 112 | } |
| 113 | |
| 114 | // Enter a screen, we only care about it now |
| 115 | void Panel::enter_screen(PanelScreen* screen){ |
| 116 | screen->panel = this; |
| 117 | this->current_screen = screen; |
| 118 | this->reset_counter(); |
| 119 | this->current_screen->on_enter(); |
| 120 | } |
| 121 | |
| 122 | // Reset the counter |
| 123 | void Panel::reset_counter(){ |
| 124 | *this->counter = 0; |
| 125 | this->counter_changed = false; |
| 126 | } |
| 127 | |
| 128 | // Indicate the idle loop we want to call the refresh hook in the current screen |
| 129 | uint32_t Panel::refresh_tick(uint32_t dummy){ |
| 130 | this->refresh_flag = true; |
| 131 | this->idle_time++; |
| 132 | return 0; |
| 133 | } |
| 134 | |
| 135 | // Encoder pins changed |
| 136 | uint32_t Panel::encoder_check(uint32_t dummy){ |
| 137 | // TODO if encoder reads go through i2c like on smoothie panel this needs to be |
| 138 | // optionally done in idle loop, however when reading encoder directly it needs to be done |
| 139 | // frequently, smoothie panel will return an actual delta count so won't miss any if polled slowly |
| 140 | static int encoder_counter = 0; |
| 141 | int change = lcd->readEncoderDelta(); |
| 142 | encoder_counter += change; |
| 143 | // TODO divisor needs to be configurable |
| 144 | if( change != 0 && encoder_counter % this->encoder_click_resolution == 0 ){ |
| 145 | this->counter_changed = true; |
| 146 | (*this->counter) += change; |
| 147 | this->idle_time= 0; |
| 148 | } |
| 149 | return 0; |
| 150 | } |
| 151 | |
| 152 | // Read and update each button |
| 153 | uint32_t Panel::button_tick(uint32_t dummy){ |
| 154 | this->do_buttons = true; |
| 155 | return 0; |
| 156 | } |
| 157 | |
| 158 | // on main loop, we can send gcodes or do anything that waits in this loop |
| 159 | void Panel::on_main_loop(void* argument){ |
| 160 | this->current_screen->on_main_loop(); |
| 161 | this->lcd->on_main_loop(); |
| 162 | } |
| 163 | |
| 164 | // On idle things, we don't want to do shit in interrupts |
| 165 | // don't queue gcodes in this |
| 166 | void Panel::on_idle(void* argument){ |
| 167 | |
| 168 | // after being idle for a while switch to Watch screen |
| 169 | if(this->idle_time > 20*5) { // 5 seconds |
| 170 | this->idle_time= 0; |
| 171 | if(this->top_screen->watch_screen != this->current_screen) { |
| 172 | this->enter_screen(this->top_screen->watch_screen); |
| 173 | // TODO do we need to reset any state? |
| 174 | } |
| 175 | |
| 176 | return; |
| 177 | } |
| 178 | |
| 179 | if(this->do_buttons) { |
| 180 | // we don't want to do I2C in interrupt mode |
| 181 | this->do_buttons = false; |
| 182 | |
| 183 | // read the actual buttons |
| 184 | int but= lcd->readButtons(); |
| 185 | if(but != 0) this->idle_time= 0; |
| 186 | |
| 187 | // fire events if the buttons are active and debounce is satisfied |
| 188 | this->up_button.check_signal(but&BUTTON_UP); |
| 189 | this->down_button.check_signal(but&BUTTON_DOWN); |
| 190 | this->back_button.check_signal(but&BUTTON_LEFT); |
| 191 | this->click_button.check_signal(but&BUTTON_SELECT); |
| 192 | this->pause_button.check_signal(but&BUTTON_PAUSE); |
| 193 | |
| 194 | // for debugging |
| 195 | if(but&BUTTON_RIGHT) { |
| 196 | lcd->init(); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | // If we are in menu mode and the position has changed |
| 201 | if( this->mode == MENU_MODE && this->counter_change() ){ |
| 202 | this->menu_update(); |
| 203 | } |
| 204 | |
| 205 | // If we are in control mode |
| 206 | if( this->mode == CONTROL_MODE && this->counter_change() ){ |
| 207 | this->control_value_update(); |
| 208 | } |
| 209 | |
| 210 | // If we must refresh |
| 211 | if( this->refresh_flag ){ |
| 212 | this->refresh_flag = false; |
| 213 | this->current_screen->on_refresh(); |
| 214 | this->lcd->on_refresh(); |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // Hooks for button clicks |
| 219 | uint32_t Panel::on_up(uint32_t dummy){ |
| 220 | // this is simulating encoder clicks, but needs to be inverted to |
| 221 | // increment values on up |
| 222 | int inc= (this->mode == CONTROL_MODE) ? 1 : -1; |
| 223 | *this->counter += inc; |
| 224 | this->counter_changed = true; |
| 225 | return 0; |
| 226 | } |
| 227 | uint32_t Panel::on_down(uint32_t dummy){ |
| 228 | int inc= (this->mode == CONTROL_MODE) ? -1 : 1; |
| 229 | *this->counter += inc; |
| 230 | this->counter_changed = true; |
| 231 | return 0; |
| 232 | } |
| 233 | |
| 234 | // on most menu screens will go back to previous higher menu |
| 235 | uint32_t Panel::on_back(uint32_t dummy){ |
| 236 | if(this->mode == MENU_MODE && this->current_screen->parent != NULL) { |
| 237 | this->enter_screen(this->current_screen->parent); |
| 238 | } |
| 239 | return 0; |
| 240 | } |
| 241 | |
| 242 | uint32_t Panel::on_select(uint32_t dummy){ |
| 243 | // TODO make configurable, including turning off |
| 244 | // buzz is ignored on panels that do not support buzz |
| 245 | this->click_changed = true; |
| 246 | this->idle_time= 0; |
| 247 | lcd->buzz(60,300); // 50ms 300Hz |
| 248 | return 0; |
| 249 | } |
| 250 | |
| 251 | uint32_t Panel::on_pause(uint32_t dummy){ |
| 252 | if(!paused) { |
| 253 | THEKERNEL->pauser->take(); |
| 254 | paused= true; |
| 255 | }else{ |
| 256 | THEKERNEL->pauser->release(); |
| 257 | paused= false; |
| 258 | } |
| 259 | return 0; |
| 260 | } |
| 261 | |
| 262 | bool Panel::counter_change(){ if( this->counter_changed ){ this->counter_changed = false; return true; }else{ return false; } } |
| 263 | bool Panel::click(){ if( this->click_changed ){ this->click_changed = false; return true; }else{ return false; } } |
| 264 | |
| 265 | |
| 266 | // Enter menu mode |
| 267 | void Panel::enter_menu_mode(){ |
| 268 | this->mode = MENU_MODE; |
| 269 | this->counter = &this->menu_selected_line; |
| 270 | this->menu_changed = false; |
| 271 | } |
| 272 | |
| 273 | void Panel::setup_menu(uint16_t rows){ |
| 274 | this->setup_menu(rows, min(rows, this->max_screen_lines())); |
| 275 | } |
| 276 | |
| 277 | void Panel::setup_menu(uint16_t rows, uint16_t lines){ |
| 278 | this->menu_selected_line = 0; |
| 279 | this->menu_start_line = 0; |
| 280 | this->menu_rows = rows; |
| 281 | this->menu_lines = lines; |
| 282 | } |
| 283 | |
| 284 | uint16_t Panel::menu_current_line(){ |
| 285 | return this->menu_selected_line >> this->menu_offset; |
| 286 | } |
| 287 | |
| 288 | void Panel::menu_update(){ |
| 289 | |
| 290 | // Limits, up and down |
| 291 | this->menu_selected_line = this->menu_selected_line % ( this->menu_rows<<this->menu_offset ); |
| 292 | while( this->menu_selected_line < 0 ){ this->menu_selected_line += this->menu_rows << this->menu_offset; } |
| 293 | |
| 294 | // What to display |
| 295 | this->menu_start_line = 0; |
| 296 | if( this->menu_rows > this->menu_lines ){ |
| 297 | if( this->menu_current_line() >= 2 ){ |
| 298 | this->menu_start_line = this->menu_current_line() - 1; |
| 299 | } |
| 300 | if( this->menu_current_line() > this->menu_rows - this->menu_lines ){ |
| 301 | this->menu_start_line = this->menu_rows - this->menu_lines; |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | this->menu_changed = true; |
| 306 | } |
| 307 | |
| 308 | bool Panel::menu_change(){ |
| 309 | if( this->menu_changed ){ this->menu_changed = false; return true; }else{ return false; } |
| 310 | } |
| 311 | |
| 312 | bool Panel::control_value_change(){ |
| 313 | if( this->control_value_changed ){ this->control_value_changed = false; return true; }else{ return false; } |
| 314 | } |
| 315 | |
| 316 | |
| 317 | bool Panel::enter_control_mode(double passed_normal_increment, double passed_pressed_increment){ |
| 318 | this->mode = CONTROL_MODE; |
| 319 | this->normal_increment = passed_normal_increment; |
| 320 | this->pressed_increment = passed_pressed_increment; |
| 321 | this->counter = &this->control_normal_counter; |
| 322 | this->control_normal_counter = 0; |
| 323 | this->control_pressed_counter = 0; |
| 324 | this->control_base_value = 0; |
| 325 | return true; |
| 326 | } |
| 327 | |
| 328 | void Panel::control_value_update(){ |
| 329 | // TODO what do we do here? |
| 330 | this->control_value_changed = true; |
| 331 | } |
| 332 | |
| 333 | void Panel::set_control_value(double value){ |
| 334 | this->control_base_value = value; |
| 335 | } |
| 336 | |
| 337 | double Panel::get_control_value(){ |
| 338 | return this->control_base_value + (this->control_normal_counter*this->normal_increment); |
| 339 | } |
| 340 | |
| 341 | bool Panel::is_playing() const { |
| 342 | void *returned_data; |
| 343 | |
| 344 | bool ok= THEKERNEL->public_data->get_value( player_checksum, is_playing_checksum, &returned_data ); |
| 345 | if(ok) { |
| 346 | bool b= *static_cast<bool*>(returned_data); |
| 347 | return b; |
| 348 | } |
| 349 | return false; |
| 350 | } |
| 351 | |
| 352 | void Panel::set_playing_file(string f) { |
| 353 | // just copy the first 20 characters after the first / if there |
| 354 | size_t n= f.find_last_of('/'); |
| 355 | if(n == string::npos) n= 0; |
| 356 | strncpy(playing_file, f.substr(n+1, 19).c_str(), sizeof(playing_file)); |
| 357 | playing_file[sizeof(playing_file)-1]= 0; |
| 358 | } |