Michael Spencer / Smoothie

Dependencies:   mbed

Fork of Smoothie by Stéphane Cachat

modules/utils/panel/Panel.cpp

Committer:
Michael J. Spencer
Date:
2014-02-28
Revision:
2:1df0b61d3b5a

File content as of revision 2:1df0b61d3b5a:

/*
      This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl).
      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.
      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.
      You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
*/

#include "libs/Kernel.h"
#include "Panel.h"
#include "PanelScreen.h"

#include "libs/nuts_bolts.h"
#include "libs/utils.h"
#include "Button.h"

#include "modules/utils/player/PlayerPublicAccess.h"
#include "screens/CustomScreen.h"
#include "screens/MainMenuScreen.h"

#include "panels/I2CLCD.h"
#include "panels/VikiLCD.h"
#include "panels/Smoothiepanel.h"
#include "panels/ReprapDiscountGLCD.h"
#include "panels/ST7565.h"
#include "version.h"

#define panel_checksum             CHECKSUM("panel")
#define enable_checksum            CHECKSUM("enable")
#define lcd_checksum               CHECKSUM("lcd")
#define i2c_lcd_checksum           CHECKSUM("i2c_lcd")
#define viki_lcd_checksum          CHECKSUM("viki_lcd")
#define smoothiepanel_checksum     CHECKSUM("smoothiepanel")
#define panelolu2_checksum         CHECKSUM("panelolu2")
#define rrd_glcd_checksum          CHECKSUM("reprap_discount_glcd")
#define st7565_glcd_checksum       CHECKSUM("st7565_glcd")

#define menu_offset_checksum        CHECKSUM("menu_offset")
#define encoder_resolution_checksum CHECKSUM("encoder_resolution")
#define jog_x_feedrate_checksum     CHECKSUM("alpha_jog_feedrate")
#define jog_y_feedrate_checksum     CHECKSUM("beta_jog_feedrate")
#define jog_z_feedrate_checksum     CHECKSUM("gamma_jog_feedrate")
#define	longpress_delay_checksum	CHECKSUM("longpress_delay")

#define hotend_temp_checksum CHECKSUM("hotend_temperature")
#define bed_temp_checksum    CHECKSUM("bed_temperature")

Panel::Panel()
{
    this->counter_changed = false;
    this->click_changed = false;
    this->refresh_flag = false;
    this->enter_menu_mode();
    this->lcd = NULL;
    this->do_buttons = false;
    this->idle_time = 0;
    this->start_up = true;
    this->current_screen = NULL;
    strcpy(this->playing_file, "Playing file");
}

Panel::~Panel()
{
    delete this->lcd;
}

void Panel::on_module_loaded()
{
    // Exit if this module is not enabled
    if ( !THEKERNEL->config->value( panel_checksum, enable_checksum )->by_default(false)->as_bool() ) {
        delete this;
        return;
    }

    // Initialise the LCD, see which LCD to use
    if (this->lcd != NULL) delete this->lcd;
    int lcd_cksm = get_checksum(THEKERNEL->config->value(panel_checksum, lcd_checksum)->by_default("i2c")->as_string());

    // Note checksums are not const expressions when in debug mode, so don't use switch
    if (lcd_cksm == i2c_lcd_checksum) {
        this->lcd = new I2CLCD();
    } else if (lcd_cksm == viki_lcd_checksum) {
        this->lcd = new VikiLCD();
        this->lcd->set_variant(0);
    } else if (lcd_cksm == panelolu2_checksum) {
        this->lcd = new VikiLCD();
        this->lcd->set_variant(1);
    } else if (lcd_cksm == smoothiepanel_checksum) {
        this->lcd = new Smoothiepanel();
    } else if (lcd_cksm == rrd_glcd_checksum) {
        this->lcd = new ReprapDiscountGLCD();
    } else if (lcd_cksm == st7565_glcd_checksum) {
        this->lcd = new ST7565();
    } else {
        // no lcd type defined
        return;
    }

    this->custom_screen= new CustomScreen(); // this needs to be called here as it needs the config cache loaded

    // some panels may need access to this global info
    this->lcd->setPanel(this);

    // the number of screen lines the panel supports
    this->screen_lines = this->lcd->get_screen_lines();

    // some encoders may need more clicks to move menu, this is a divisor and is in config as it is
    // an end user usability issue
    this->menu_offset = THEKERNEL->config->value( panel_checksum, menu_offset_checksum )->by_default(0)->as_number();

    // override default encoder resolution if needed
    this->encoder_click_resolution = THEKERNEL->config->value( panel_checksum, encoder_resolution_checksum )->by_default(this->lcd->getEncoderResolution())->as_number();

    // load jogging feedrates in mm/min
    jogging_speed_mm_min[0] = THEKERNEL->config->value( panel_checksum, jog_x_feedrate_checksum )->by_default(3000.0f)->as_number();
    jogging_speed_mm_min[1] = THEKERNEL->config->value( panel_checksum, jog_y_feedrate_checksum )->by_default(3000.0f)->as_number();
    jogging_speed_mm_min[2] = THEKERNEL->config->value( panel_checksum, jog_z_feedrate_checksum )->by_default(300.0f )->as_number();

    // load the default preset temeratures
    default_hotend_temperature = THEKERNEL->config->value( panel_checksum, hotend_temp_checksum )->by_default(185.0f )->as_number();
    default_bed_temperature    = THEKERNEL->config->value( panel_checksum, bed_temp_checksum    )->by_default(60.0f  )->as_number();


    this->up_button.up_attach(    this, &Panel::on_up );
    this->down_button.up_attach(  this, &Panel::on_down );
    this->click_button.up_attach( this, &Panel::on_select );
    this->back_button.up_attach(  this, &Panel::on_back );
    this->pause_button.up_attach( this, &Panel::on_pause );


    //setting longpress_delay
    int longpress_delay =  THEKERNEL->config->value( panel_checksum, longpress_delay_checksum )->by_default(0)->as_number();
    this->up_button.set_longpress_delay(longpress_delay);
    this->down_button.set_longpress_delay(longpress_delay);
//    this->click_button.set_longpress_delay(longpress_delay);
//    this->back_button.set_longpress_delay(longpress_delay);
//    this->pause_button.set_longpress_delay(longpress_delay);
	
	
    THEKERNEL->slow_ticker->attach( 100,  this, &Panel::button_tick );
    THEKERNEL->slow_ticker->attach( 1000, this, &Panel::encoder_check );

    // Register for events
    this->register_for_event(ON_IDLE);
    this->register_for_event(ON_MAIN_LOOP);
    this->register_for_event(ON_GCODE_RECEIVED);

    // Refresh timer
    THEKERNEL->slow_ticker->attach( 20, this, &Panel::refresh_tick );
}

// Enter a screen, we only care about it now
void Panel::enter_screen(PanelScreen *screen)
{
    screen->panel = this;
    this->current_screen = screen;
    this->reset_counter();
    this->current_screen->on_enter();
}

// Reset the counter
void Panel::reset_counter()
{
    *this->counter = 0;
    this->counter_changed = false;
}

// Indicate the idle loop we want to call the refresh hook in the current screen
// called 20 times a second
uint32_t Panel::refresh_tick(uint32_t dummy)
{
    this->refresh_flag = true;
    this->idle_time++;
    return 0;
}

// Encoder pins changed in interrupt
uint32_t Panel::encoder_check(uint32_t dummy)
{
    // TODO if encoder reads go through i2c like on smoothie panel this needs to be
    // optionally done in idle loop, however when reading encoder directly it needs to be done
    // frequently, smoothie panel will return an actual delta count so won't miss any if polled slowly
    // NOTE this code will not work if change is not -1,0,-1 anything greater (as in above case) will not work properly
    static int encoder_counter = 0;
    int change = lcd->readEncoderDelta();
    encoder_counter += change;

    if ( change != 0 && encoder_counter % this->encoder_click_resolution == 0 ) {
        this->counter_changed = true;
        (*this->counter) += change;
        this->idle_time = 0;
    }
    return 0;
}

// Read and update each button
uint32_t Panel::button_tick(uint32_t dummy)
{
    this->do_buttons = true;
    return 0;
}

void Panel::on_gcode_received(void *argument)
{
    Gcode *gcode = static_cast<Gcode *>(argument);
    if ( gcode->has_m) {
        if ( gcode->m == 117 ) { // set LCD message
            this->message = get_arguments(gcode->command);
            if (this->message.size() > 20) this->message = this->message.substr(0, 20);
            gcode->mark_as_taken();
        }
    }
}

// on main loop, we can send gcodes or do anything that waits in this loop
void Panel::on_main_loop(void *argument)
{
    if (this->current_screen != NULL) {
        this->current_screen->on_main_loop();
        this->lcd->on_main_loop();
    }
}


#define ohw_logo_antipixel_width 80
#define ohw_logo_antipixel_height 15
static const uint8_t ohw_logo_antipixel_bits[] = {
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x01, 0x80, 0x0C, 0x00, 0x33, 0x18, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x5E,
    0x80, 0x2D, 0x6B, 0x9B, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x18, 0xAB, 0xFF, 0xFF,
    0xFF, 0xFD, 0x80, 0xFF, 0xC0, 0x2D, 0x7B, 0xB3, 0xFF, 0xFF, 0xFF, 0xFD, 0x80, 0x7F, 0x80, 0x33,
    0x78, 0xBB, 0xFF, 0xFF, 0xFF, 0xFD, 0x81, 0xF3, 0xE0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFD,
    0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xB3, 0x18, 0xDD, 0x98, 0xC5, 0x81, 0xF3, 0xE0, 0x3F, 0xFD, 0xAD,
    0x6B, 0x5D, 0x6B, 0x5D, 0x80, 0x73, 0x80, 0x3F, 0xFC, 0x21, 0x1B, 0x55, 0x08, 0xC5, 0x80, 0xF3,
    0xC0, 0x3F, 0xFD, 0xAD, 0x5B, 0x49, 0x6A, 0xDD, 0x80, 0xE1, 0xC0, 0x3F, 0xFD, 0xAD, 0x68, 0xDD,
    0x6B, 0x45, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

// On idle things, we don't want to do shit in interrupts
// don't queue gcodes in this
void Panel::on_idle(void *argument)
{
    if (this->start_up) {
        this->lcd->init();

        Version v;
        string build(v.get_build());
        string date(v.get_build_date());
        this->lcd->clear();
        this->lcd->setCursor(0, 0); this->lcd->printf("Welcome to Smoothie");
        this->lcd->setCursor(0, 1); this->lcd->printf("%s", build.substr(0, 20).c_str());
        this->lcd->setCursor(0, 2); this->lcd->printf("%s", date.substr(0, 20).c_str());
        this->lcd->setCursor(0, 3); this->lcd->printf("Please wait....");

        if (this->lcd->hasGraphics()) {
            this->lcd->bltGlyph(24, 40, ohw_logo_antipixel_width, ohw_logo_antipixel_height, ohw_logo_antipixel_bits);
        }

        this->lcd->on_refresh(true); // tell lcd to display now

        // Default top screen
        this->top_screen= new MainMenuScreen();
        this->top_screen->set_panel(this);
        this->custom_screen->set_parent(this->top_screen);
        this->start_up = false;
        return;
    }

    MainMenuScreen *mms= static_cast<MainMenuScreen*>(this->top_screen);
    // after being idle for a while switch to Watch screen
    if (this->current_screen != NULL && this->idle_time > this->current_screen->idle_timeout_secs()*20) {
        this->idle_time = 0;
        if (mms->watch_screen != this->current_screen) {
            this->enter_screen(mms->watch_screen);
            // TODO do we need to reset any state?
        }

        return;
    }

    if(current_screen == NULL && this->idle_time > 20*4) {
        this->enter_screen(mms->watch_screen);
        return;
    }

    if (this->do_buttons) {
        // we don't want to do I2C in interrupt mode
        this->do_buttons = false;

        // read the actual buttons
        int but = lcd->readButtons();
        if (but != 0) {
            this->idle_time = 0;
            if(current_screen == NULL) {
                // we were in startup screen so go to watch screen
                this->enter_screen(mms->watch_screen);
                return;
            }
        }

        // fire events if the buttons are active and debounce is satisfied
        this->up_button.check_signal(but & BUTTON_UP);
        this->down_button.check_signal(but & BUTTON_DOWN);
        this->back_button.check_signal(but & BUTTON_LEFT);
        this->click_button.check_signal(but & BUTTON_SELECT);
        this->pause_button.check_signal(but & BUTTON_PAUSE);
    }

    // If we are in menu mode and the position has changed
    if ( this->mode == MENU_MODE && this->counter_change() ) {
        this->menu_update();
    }

    // If we are in control mode
    if ( this->mode == CONTROL_MODE && this->counter_change() ) {
        this->control_value_update();
    }

    // If we must refresh
    if ( this->refresh_flag ) {
        this->refresh_flag = false;
        if (this->current_screen != NULL) {
            this->current_screen->on_refresh();
            this->lcd->on_refresh();
        }
    }
}

// Hooks for button clicks
uint32_t Panel::on_up(uint32_t dummy)
{
    // this is simulating encoder clicks, but needs to be inverted to
    // increment values on up,increment by
    int inc = (this->mode == CONTROL_MODE) ? 1 : -(this->menu_offset+1);
    *this->counter += inc;
    this->counter_changed = true;
    return 0;
}

uint32_t Panel::on_down(uint32_t dummy)
{
    int inc = (this->mode == CONTROL_MODE) ? -1 : (this->menu_offset+1);
    *this->counter += inc;
    this->counter_changed = true;
    return 0;
}

// on most menu screens will go back to previous higher menu
uint32_t Panel::on_back(uint32_t dummy)
{
    if (this->mode == MENU_MODE && this->current_screen != NULL && this->current_screen->parent != NULL) {
        this->enter_screen(this->current_screen->parent);
    }
    return 0;
}

uint32_t Panel::on_select(uint32_t dummy)
{
    // TODO make configurable, including turning off
    // buzz is ignored on panels that do not support buzz
    this->click_changed = true;
    this->idle_time = 0;
    lcd->buzz(60, 300); // 50ms 300Hz
    return 0;
}

uint32_t Panel::on_pause(uint32_t dummy)
{
    if (!paused) {
        THEKERNEL->pauser->take();
        paused = true;
    } else {
        THEKERNEL->pauser->release();
        paused = false;
    }
    return 0;
}

bool Panel::counter_change()
{
    if ( this->counter_changed ) {
        this->counter_changed = false;
        return true;
    } else {
        return false;
    }
}
bool Panel::click()
{
    if ( this->click_changed ) {
        this->click_changed = false;
        return true;
    } else {
        return false;
    }
}


// Enter menu mode
void Panel::enter_menu_mode()
{
    this->mode = MENU_MODE;
    this->counter = &this->menu_selected_line;
    this->menu_changed = false;
}

void Panel::setup_menu(uint16_t rows)
{
    this->setup_menu(rows, min(rows, this->max_screen_lines()));
}

void Panel::setup_menu(uint16_t rows, uint16_t lines)
{
    this->menu_selected_line = 0;
    this->menu_current_line = 0;
    this->menu_start_line = 0;
    this->menu_rows = rows;
    this->panel_lines = lines;
}

void Panel::menu_update()
{
    // Limits, up and down
    // NOTE menu_selected_line is changed in an interrupt and can change at any time
    int msl = this->menu_selected_line; // hopefully this is atomic

    #if 0
    // this allows it to wrap but with new method we dont; want to wrap
    msl = msl % ( this->menu_rows << this->menu_offset );
    while ( msl < 0 ) {
        msl += this->menu_rows << this->menu_offset;
    }
    #else
        // limit selected line to screen lines available
        if(msl >= this->menu_rows<<this->menu_offset){
            msl= (this->menu_rows-1)<<this->menu_offset;
        }else if(msl < 0) msl= 0;
    #endif

    this->menu_selected_line = msl; // update atomically we hope
    // figure out which actual line to select, if we have a menu offset it means we want to move one line per two clicks
    if(msl % (this->menu_offset+1) == 0) { // only if divisible by offset
        this->menu_current_line = msl >> this->menu_offset;
    }

    // What to display
    if ( this->menu_rows > this->panel_lines ) {
        #if 0
        // old way of scrolling not nice....
        if ( this->menu_current_line >= 2 ) {
            this->menu_start_line = this->menu_current_line - 1;
        }
        if ( this->menu_current_line > this->menu_rows - this->panel_lines ) {
            this->menu_start_line = this->menu_rows - this->panel_lines;
        }
        #else
        // new way we only scroll the lines when the cursor hits the bottom of the screen or the top of the screen
        // do we want to scroll up?
        int sl= this->menu_current_line - this->menu_start_line; // screen line we are on
        if(sl >= this->panel_lines) {
            this->menu_start_line += ((sl+1)-this->panel_lines); // scroll up to keep it on the screen

        }else if(sl < 0 ) { // do we want to scroll down?
            this->menu_start_line += sl; // scroll down
        }
        #endif

    }else{
        this->menu_start_line = 0;
    }

    this->menu_changed = true;
}

bool Panel::menu_change()
{
    if ( this->menu_changed ) {
        this->menu_changed = false;
        return true;
    } else {
        return false;
    }
}

bool Panel::control_value_change()
{
    if ( this->control_value_changed ) {
        this->control_value_changed = false;
        return true;
    } else {
        return false;
    }
}

bool Panel::enter_control_mode(float passed_normal_increment, float passed_pressed_increment)
{
    this->mode = CONTROL_MODE;
    this->normal_increment  = passed_normal_increment;
    this->counter = &this->control_normal_counter;
    this->control_normal_counter = 0;
    this->control_base_value = 0;
    return true;
}

void Panel::control_value_update()
{
    // TODO what do we do here?
    this->control_value_changed = true;
}

void Panel::set_control_value(float value)
{
    this->control_base_value = value;
}

float Panel::get_control_value()
{
    return this->control_base_value + (this->control_normal_counter * this->normal_increment);
}

bool Panel::is_playing() const
{
    void *returned_data;

    bool ok = THEKERNEL->public_data->get_value( player_checksum, is_playing_checksum, &returned_data );
    if (ok) {
        bool b = *static_cast<bool *>(returned_data);
        return b;
    }
    return false;
}

void  Panel::set_playing_file(string f)
{
    // just copy the first 20 characters after the first / if there
    size_t n = f.find_last_of('/');
    if (n == string::npos) n = 0;
    strncpy(playing_file, f.substr(n + 1, 19).c_str(), sizeof(playing_file));
    playing_file[sizeof(playing_file) - 1] = 0;
}