diff --git a/.gitignore b/.gitignore index db6f29e..9b0c4bb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *~ +*.pyc build CMakeCache.txt CMakeFiles diff --git a/Arduino_Monitor.py b/Arduino_Monitor.py index ab356b1..3f655a2 100644 --- a/Arduino_Monitor.py +++ b/Arduino_Monitor.py @@ -4,12 +4,34 @@ Lots of help from here: http://stackoverflow.com/questions/1093598/pyserial-how-to-read-last-line-sent-from-serial-device """ from threading import Thread + import time import serial import struct -last_received = '' -profile = [] + +status = [0, 23, 23, 0, 0, 0] + +oven_connected = False + +profile = [ + 150, + 200, + 217, + 260, + 480, + 1, + 2, + 1, + 2, + -1, + -6, + 60, + 180, + 60, + 150, + 20, + 40] PI_TS_MIN = 0 PI_TS_MAX = 1 @@ -17,8 +39,10 @@ PI_TL = 2 PI_TP = 3 PI_TIME_MAX = 4 -PI_RAMP_UP_MIN = 5 -PI_RAMP_UP_MAX = 6 +PI_TS_RAMP_UP_MIN = 5 +PI_TS_RAMP_UP_MAX = 6 +PI_TP_RAMP_UP_MIN = 5 +PI_TP_RAMP_UP_MAX = 6 PI_RAMP_DOWN_MIN = 7 PI_RAMP_DOWN_MAX = 8 @@ -29,19 +53,29 @@ PI_TL_DURATION_MAX = 12 PI_TP_DURATION_MIN = 13 PI_TP_DURATION_MAX = 14 +def recv_config(ser): + global profile + ser.write(chr(255)) + ser.flush() + t = ser.read(30) + profile = struct.unpack("hhhhhhhhhhhhhhhhh", t) + ser.flushInput() + + def receiving(ser): - global last_received - buffer = '' - ser.write(chr(255)) + global status + try: + time.sleep(2) + recv_config(ser) + except Exception, e: + print e + pass + + while 1: + ser.write(chr(254)) ser.flush() - profile = struct.unpack("hhhhhhhhhhhhhhh", ser.read(30)) + status = struct.unpack("hhhhhb", ser.read(11)) ser.flushInput() - while 1: - ser.write(chr(254)) - ser.flush() - last_received = ser.read(11) - print repr(last_received) - ser.flushInput() class SerialData(object): @@ -57,15 +91,31 @@ class SerialData(object): Thread(target=receiving, args=(self.ser,)).start() def next(self): + global status if not self.ser: - return 100 #return anything so we can test when Arduino isn't connected + return status[1] try: - return int(struct.unpack("hhhhhb", last_received)[1]) - except Exception, e: - print e - return 0 + return status[1] + except Exception: + pass + def send_config(self): + if not self.ser: + return + + global profile + self.ser.write(struct.pack("hhhhhhhhhhhhhhhhh", profile)) + + def send_start(self): + if not self.ser: + return False + + self.ser.write(chr(251)) + return True + + def connected(self): + return self.ser != None def __del__(self): if self.ser: diff --git a/CMakeLists.txt b/CMakeLists.txt index c6f10a2..a63df69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,8 +16,14 @@ set(ARDUINO_DEFAULT_BOARD atmega328) # Default Board ID, when not specified set(ARDUINO_DEFAULT_PORT /dev/ttyUSB0) # Default Port, when not specified link_directories(/usr/share/arduino/libraries) +# +# generate_arduino_library(reflowctl_lib +# SRCS oven_control.cpp profile.cpp +# HDRS oven_control.h profile.h +# BOARD atmega328) generate_arduino_firmware(reflowctl - SKETCH reflowctl + SRCS oven_control.cpp profile.cpp main.cpp + HDRS oven_control.h profile.h PORT /dev/ttyUSB0 BOARD atmega328) diff --git a/burn.png b/burn.png new file mode 100644 index 0000000..70d83de Binary files /dev/null and b/burn.png differ diff --git a/libs/oven_control.cpp b/libs/oven_control.cpp deleted file mode 100644 index bb7b231..0000000 --- a/libs/oven_control.cpp +++ /dev/null @@ -1,456 +0,0 @@ -#include "oven_control.h" -#include -#include -#include "profile.h" - -//Pin assignments for SainSmart LCD Keypad Shield -LiquidCrystal _lcd(8, 9, 4, 5, 6, 7); -DFR_Key _keypad; -Profile _profile; - -OvenCtl::OvenCtl() { - - time = 0; - temperature = 1; - last_temperature = 1; - actual_dt = 0; - // timestamps of event beginnings/ends - Ts_time_start = 0; - Ts_time_end = 0; - Tl_time_start = 0; - Tl_time_end = 0; - Tp_time_start = 0; - Tp_time_end = 0; - - // thermostat - set_min = 0; - set_max = 0; - set_dt_min = 0; - set_dt_max = 0; - - state = 0; - error_condition = 0; - is_oven_heating = false; - - // ui stuff - led_on = false; - disable_checks = false; - lcd = &_lcd; - keypad = &_keypad; - profile = &_profile; - lcd->begin(16, 2); -} - -void OvenCtl::reset() { - digitalWrite(7, LOW); -} - - -void OvenCtl::send_state() { - Serial.write(time & 0xff); - Serial.write((time>>8) & 0xff); - Serial.write(temperature & 0xff); - Serial.write((temperature >> 8) & 0xff); - Serial.write(last_temperature & 0xff); - Serial.write((last_temperature >> 8) & 0xff); - Serial.write(state & 0xff); - Serial.write((state >> 8) & 0xff); - Serial.write(error_condition & 0xff); - Serial.write((error_condition >> 8) & 0xff); - Serial.write(is_oven_heating); - Serial.flush(); -} - -void OvenCtl::send_config() { - int tmp; - for (int i=0;i < PI_END; i++) - { - tmp = profile->data[i]; - Serial.write(tmp & 0xff); - Serial.write((tmp >> 8 ) & 0xff); - } - Serial.flush(); -} - -void OvenCtl::dispatch_input_config(int cmd) { - if (cmd == 255) - send_config(); - else if (cmd == 254) - recv_config(); - else if (cmd == 250) - reset(); - else if (cmd == 253) - ; -} - -void OvenCtl::recv_config() { - -} - -void OvenCtl::handle_states() { - int cmd = -1; - - if (state > 0) - { - time++; - get_temp(); - check_dt(); - } - - if (error_condition != 0) { - set_error_state(); - } - - if (Serial.available() > 0) { - cmd = Serial.read(); - if (cmd == 255) - send_config(); - else if (cmd == 254) - send_state(); - else if (cmd == 253) - recv_config(); - else if (cmd == 252) - reset(); - else if (cmd == 251) - set_start_state(); - } - - switch (state) { - case CONFIG_STATE: - if (profile->handle_config_state(lcd, keypad)) - set_start_state(); - break; - case START_STATE: - handle_start_state(); - break; - case PREHEAT_STATE: - handle_preheat_state(); - break; - case RAMP_UP_STATE: - handle_ramp_up_state(); - break; - case TAL_FIRST_STATE: - handle_tal_first_state(); - break; - case PEAK_STATE: - handle_peak_state(); - break; - case TAL_SECOND_STATE: - Tl_time_end = time; - handle_tal_second_state(); - break; - case RAMP_DOWN_STATE: - handle_ramp_down_state(); - break; - case END_STATE: - handle_end_state(); - break; - case ERROR_STATE: - handle_error_state(); - break; - default: - break; - } - - control_oven(); - if (state > 0) { - print_status(); - delay(1000); - } -} - - - -void OvenCtl::print_status() { - if (error_condition == 0) { - String tmp("T: "); - if (time < 10) - tmp += "00"; - else if (time < 100) - tmp += "0"; - tmp += time; - tmp += " Tmp: "; - if (temperature < 10) - tmp += "00"; - else if (temperature < 100) - tmp += "0"; - tmp += temperature; - lcd->setCursor(0, 0); - lcd->print(tmp); - - tmp = "Profile: "; - tmp += state; - tmp += "/"; - tmp += END_STATE; - lcd->setCursor(0, 1); - lcd->print(tmp); - lcd->setCursor(13, 1); - if (is_oven_heating) - lcd->print("on "); - else - lcd->print("off"); - } - else { - lcd->clear(); - lcd->print("Error:"); - lcd->setCursor(0, 1); - if (error_condition & E_DT_MIN) - lcd->print("K/s too low"); - if (error_condition & E_DT_MAX) - lcd->print("K/s too high"); - if (error_condition & E_TIME_MAX) - lcd->print("reflow too long"); - if (error_condition & E_TS_TOO_SHORT) - lcd->print("ts too short"); - if (error_condition & E_TS_TOO_LONG) - lcd->print("ts too long"); - if (error_condition & E_TL_TOO_SHORT) - lcd->print("tal too short"); - if (error_condition & E_TL_TOO_LONG) - lcd->print("tal too long"); - if (error_condition & E_TP_TOO_LONG) - lcd->print("peak too short"); - if (error_condition & E_TP_TOO_SHORT) - lcd->print("peak too long"); - } -} - - -void OvenCtl::control_oven() { - if (temperature < set_min && !is_oven_heating) { - is_oven_heating = true; -// Serial.println("Oven turned on"); - } - else if (temperature > set_min && is_oven_heating) { - is_oven_heating = false; - } -} - - -void OvenCtl::set_temp(int min, int max, int dt_min, int dt_max) { - set_min = min; - set_max = max; - set_dt_min = dt_min; - set_dt_max = dt_max; -} - - -void OvenCtl::get_temp() { - last_temperature = temperature; - temperature = int(float(analogRead(2)) * 0.2929); - actual_dt = temperature - last_temperature; -} - -void OvenCtl::check_dt() { - if (disable_checks) - return; - if (actual_dt > set_dt_max) { - error_condition |= E_DT_MAX; - } - if (actual_dt < set_dt_min) { - error_condition |= E_DT_MIN; - } -} - -void OvenCtl::check_max_duration() { - if (disable_checks) - return; - if (time > profile->data[PI_TIME_MAX]) { - error_condition |= E_TIME_MAX; - } -} - -void OvenCtl::check_Ts_duration_min() { - if (disable_checks) - return; - Tl_time_end = time; - if (time - Tl_time_start < profile->data[PI_TL_DURATION_MIN]) { - error_condition |= E_TL_TOO_SHORT; - } -} - -void OvenCtl::check_Ts_duration_max() { - if (disable_checks) - return; - if (time - Ts_time_start > profile->data[PI_TL_DURATION_MAX]) { - error_condition |= E_TS_TOO_LONG; - } -} - - -void OvenCtl::check_Tl_duration_min() { - if (disable_checks) - return; - Tl_time_end = time; - if (time - Tl_time_start < profile->data[PI_TL_DURATION_MIN]) { - error_condition |= E_TL_TOO_SHORT; - } -} - -void OvenCtl::check_Tl_duration_max() { - if (disable_checks) - return; - if (time - Tl_time_start > profile->data[PI_TL_DURATION_MAX]) { - error_condition |= E_TL_TOO_LONG; - } -} - -void OvenCtl::check_Tp_duration_min() { - Tp_time_end = time; - if (time - Tp_time_start < profile->data[PI_TP_DURATION_MIN]) { - error_condition |= E_TP_TOO_SHORT; - } -} - -void OvenCtl::check_Tp_duration_max() { - if (disable_checks) - return; - if (time - Tp_time_start > profile->data[PI_TP_DURATION_MAX]) { - error_condition |= E_TP_TOO_LONG; - } -} - -void OvenCtl::set_config_state() { - profile->print_config_state_0(lcd); -} - -void OvenCtl::set_start_state() { - led_on = false; - digitalWrite(13, LOW); - error_condition = 0; - state = START_STATE; - get_temp(); - last_temperature = temperature; - actual_dt = temperature - last_temperature; - set_temp(profile->data[PI_TP]-5, profile->data[PI_TP], 0, profile->data[PI_RAMP_UP_MAX]); - lcd->clear(); -} - - -void OvenCtl::set_preheat_state() { -// Serial.println("Changing state to PREHEAT_STATE"); - state++; -} - - -void OvenCtl::set_ramp_up_state() { - state++; -} - - -void OvenCtl::set_tal_first_state() { - state++; -} - - -void OvenCtl::set_peak_state() { - state++; -} - - -void OvenCtl::set_tal_second_state() { - set_temp(0, 25, -3, -6); - state++; -} - - -void OvenCtl::set_ramp_down_state() { - state++; -} - - -void OvenCtl::set_end_state() { - state++; -} - - -void OvenCtl::set_error_state() { - if (state != ERROR_STATE) { - set_temp(0, 0, 0, 0); - state = ERROR_STATE; - } -} - - -void OvenCtl::handle_config_state() { - if (profile->handle_config_state(lcd, keypad)) - state++; -} - -void OvenCtl::handle_start_state() { - check_max_duration(); - if (temperature > profile->data[PI_TS_MIN]) { - Ts_time_start = time; - set_preheat_state(); - } -} - - -void OvenCtl::handle_preheat_state() { - check_Ts_duration_max(); - check_max_duration(); - if (temperature > profile->data[PI_TS_MAX]) { - check_Ts_duration_min(); - set_ramp_up_state(); - } -} - - -void OvenCtl::handle_ramp_up_state() { - check_max_duration(); - if (temperature > profile->data[PI_TL]) { - Tl_time_start = time; - set_tal_first_state(); - } -} - - -void OvenCtl::handle_tal_first_state() { - check_max_duration(); - check_Tl_duration_max(); - if (temperature > profile->data[PI_TP] - 5) { - Tp_time_start = time; - set_peak_state(); - } -} - - -void OvenCtl::handle_peak_state() { - check_Tl_duration_max(); - check_Tp_duration_max(); - if (time - Tp_time_start > profile->data[PI_TP_DURATION_MAX]) { - check_Tp_duration_min(); - set_tal_second_state(); - } -} - - -void OvenCtl::handle_tal_second_state() { - check_Tl_duration_max(); - if (temperature < profile->data[PI_TL]) { - check_Tl_duration_min(); - set_ramp_down_state(); - } -} - -void OvenCtl::handle_ramp_down_state() { - if (temperature < profile->data[PI_TS_MIN]) { - set_end_state(); - } -} - - -void OvenCtl::handle_end_state() { -} - - -void OvenCtl::handle_error_state() { - if (led_on) { - digitalWrite(13, LOW); - led_on = false; - } - else { - digitalWrite(13, HIGH); - led_on = true; - } -} diff --git a/libs/oven_control.h b/libs/oven_control.h deleted file mode 100644 index 104f701..0000000 --- a/libs/oven_control.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef _H_OVEN_CTL -#define _H_OVEN_CTL - -// -// -// states -#define CONFIG_STATE 0 -#define START_STATE 1 -#define PREHEAT_STATE 2 -#define RAMP_UP_STATE 3 -#define TAL_FIRST_STATE 4 -#define PEAK_STATE 5 -#define TAL_SECOND_STATE 6 -#define RAMP_DOWN_STATE 7 -#define END_STATE 8 -#define ERROR_STATE 9 - -// error conditions -#define E_DT_MIN 1 // temperature dt too small -#define E_DT_MAX 2 // temperature dt too big -#define E_TIME_MAX 4 // reflow process does take too long -#define E_TS_TOO_SHORT 8 // Ts duration too short -#define E_TS_TOO_LONG 16 // Ts duration too long -#define E_TL_TOO_SHORT 32 // Tl duration too short -#define E_TL_TOO_LONG 64 // Tl duration too long -#define E_TP_TOO_SHORT 128 // Tp duration too short -#define E_TP_TOO_LONG 256 // Tp duration too long -#define E_CONFIG 512 // error happened in config state - -#include - -class LiquidCrystal; -class DFR_Key; -class Profile; - -class OvenCtl { -public: - - OvenCtl(); - void handle_states(); - void set_config_state(); - -private: - // system time, timestamps and temperatures from sensors - int time; // profile seconds - int temperature; // actual oven temp - int last_temperature; // last oven temp - int actual_dt; // actual difference from last to actual temperatur - - // timestamps of event beginnings/ends - int Ts_time_start; - int Ts_time_end; - int Tl_time_start; - int Tl_time_end; - int Tp_time_start; - int Tp_time_end; - - // thermostat - int set_min; - int set_max; - int set_dt_min; - int set_dt_max; - - // ui stuff - boolean led_on; - boolean disable_checks; - - // state machine - unsigned int error_condition; - unsigned int state; - boolean is_oven_heating; - - LiquidCrystal * lcd; - DFR_Key * keypad; - Profile * profile; - - void print_status(); - void control_oven(); - void set_temp(int, int, int, int); - void get_temp(); - void check_dt(); - void check_max_duration(); - - void set_start_state(); - void set_preheat_state(); - void set_tal_first_state(); - void set_ramp_up_state(); - void set_peak_state(); - void set_tal_second_state(); - void set_ramp_down_state(); - void set_end_state(); - void set_error_state(); - - void handle_config_state(); - void handle_start_state(); - void handle_ramp_up_state(); - void handle_preheat_state(); - void handle_tal_first_state(); - void handle_peak_state(); - void handle_tal_second_state(); - void handle_ramp_down_state(); - void handle_end_state(); - void handle_error_state(); - - void check_Ts_duration_min(); - void check_Ts_duration_max(); - void check_Tl_duration_min(); - void check_Tl_duration_max(); - void check_Tp_duration_min(); - void check_Tp_duration_max(); - - void send_state(); - void send_config(); - void reset(); - - void recv_config(); - - void dispatch_input_config(int); -}; - -#endif - diff --git a/libs/profile.cpp b/libs/profile.cpp deleted file mode 100644 index 1db9e35..0000000 --- a/libs/profile.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "profile.h" -#include "oven_control.h" -#include -#include - -#define P_TS_MIN 0 -#define P_TS_MAX 1 -#define P_TL 2 -#define P_TP 3 -#define P_TIME_MAX 4 - -// PROFILE TEMP PER SECOND RATES -#define P_RAMP_UP_RATE_MIN 5 -#define P_RAMP_UP_RATE_MAX 6 -#define P_RAMP_DOWN_MAX 7 -#define P_RAMP_DOWN_MIN 8 - -// PROFILE TEMP DURATIONS -#define P_TS_DURATION_MIN 9 -#define P_TS_DURATION_MAX 10 -#define P_TL_DURATION_MIN 11 -#define P_TL_DURATION_MAX 12 -#define P_TP_DURATION_MIN 13 -#define P_TP_DURATION_MAX 14 -#define P_END 15 - - -Profile::Profile() : - data({150, // °C - 200, // °C - 217, // °C - 260, // 245-260°C - 480, // seconds - - // profile temp per second rates - 0, // not used yet - 50, // 3°C/second - -2, // 2°C/seconds min - -6, // 6°C/seconds max - - // profile temp durations - 60, - 180, - 60, - 150, - 20, - 40}), - config_index(0), - config_state(0), - key(NO_KEY) {} - -boolean Profile::handle_config_state(LiquidCrystal * lcd, DFR_Key * keypad) { - boolean changed = false; - - key = keypad->getKey(); - - switch (config_state) { - case 0: - if (key == SELECT_KEY) { - config_state = 2; - } - else if (key > 0) { - config_state++; - print_config_state(lcd); - } - break; - case 1: - switch (key) { - case LEFT_KEY: - config_index = (config_index-1) % P_END; - changed = true; - break; - case RIGHT_KEY: - config_index = (config_index+1) % P_END; - changed = true; - break; - case UP_KEY: - data[config_index]++; - changed = true; - break; - case DOWN_KEY: - data[config_index]--; - changed = true; - break; - case SELECT_KEY: - config_state = 2; - break; - default: - ; - } - if (changed) - print_config_state(lcd); - break; - case 2: - return true; - } - return false; -} - -void Profile::print_config_state_0(LiquidCrystal * lcd) { - lcd->clear(); - lcd->setCursor(0, 0); - lcd->print("start | config"); - lcd->setCursor(0, 1); - lcd->print("[sel] | [other]"); -} - -void Profile::print_config_state(LiquidCrystal * lcd) { - lcd->clear(); - switch (config_index) { - case P_TS_MIN: - lcd->setCursor(0,0); - lcd->print("P_TS_MIN: "); - lcd->print(data[PI_TS_MIN]); - break; - case P_TS_MAX: - lcd->setCursor(0,0); - lcd->print("P_TS_MAX: "); - lcd->print(data[PI_TS_MAX]); - break; - case P_TL: - lcd->setCursor(0,0); - lcd->print("Tl: "); - lcd->print(data[PI_TL]); - break; - case P_TP: - lcd->setCursor(0,0); - lcd->print("Tp: "); - lcd->print(data[PI_TP]); - break; - case P_TIME_MAX: - lcd->setCursor(0,0); - lcd->print("time_max: "); - lcd->print(data[PI_TIME_MAX]); - break; - - // PROFILE TEMP PER SECOND RATES - case P_RAMP_UP_RATE_MIN: - lcd->setCursor(0,0); - lcd->print("ramp_up_min: "); - lcd->print(data[PI_RAMP_UP_MIN]); - break; - case P_RAMP_UP_RATE_MAX: - lcd->setCursor(0,0); - lcd->print("ramp_up_max: "); - lcd->print(data[PI_RAMP_UP_MAX]); - break; - case P_RAMP_DOWN_MAX: - lcd->setCursor(0,0); - lcd->print("ramp_down_min: "); - lcd->print(data[PI_RAMP_DOWN_MIN]); - break; - case P_RAMP_DOWN_MIN: - lcd->setCursor(0,0); - lcd->print("ramp_down_max: "); - lcd->print(data[PI_RAMP_DOWN_MAX]); - break; - - // PROFILE TEMP DURATIONS - case P_TS_DURATION_MIN: - lcd->setCursor(0,0); - lcd->print("Ts_duration_min: "); - lcd->print(data[PI_TS_DURATION_MIN]); - break; - case P_TS_DURATION_MAX: - lcd->setCursor(0,0); - lcd->print("Ts_duration_max: "); - lcd->print(data[PI_TS_DURATION_MAX]); - break; - case P_TL_DURATION_MIN: - lcd->setCursor(0,0); - lcd->print("Tl_duration_min: "); - lcd->print(data[PI_TL_DURATION_MIN]); - break; - case P_TL_DURATION_MAX: - lcd->setCursor(0,0); - lcd->print("Tl_duration_max: "); - lcd->print(data[PI_TL_DURATION_MAX]); - break; - case P_TP_DURATION_MIN: - lcd->setCursor(0,0); - lcd->print("Tp_duration_min: "); - lcd->print(data[PI_TP_DURATION_MIN]); - break; - case P_TP_DURATION_MAX: - lcd->setCursor(0,0); - lcd->print("Tp_duration_max: "); - lcd->print(data[PI_TP_DURATION_MAX]); - break; - } -} \ No newline at end of file diff --git a/libs/profile.h b/libs/profile.h deleted file mode 100644 index 34e0175..0000000 --- a/libs/profile.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef _H_PROFILE -#define _H_PROFILE - -#include - -class LiquidCrystal; -class DFR_Key; - -#define PI_TS_MIN 0 -#define PI_TS_MAX 1 -#define PI_TL 2 -#define PI_TP 3 -#define PI_TIME_MAX 4 - -// profile temp per second rates -#define PI_RAMP_UP_MIN 5 -#define PI_RAMP_UP_MAX 6 -#define PI_RAMP_DOWN_MIN 7 -#define PI_RAMP_DOWN_MAX 8 - -// profile temp durations -#define PI_TS_DURATION_MIN 9 -#define PI_TS_DURATION_MAX 10 -#define PI_TL_DURATION_MIN 11 -#define PI_TL_DURATION_MAX 12 -#define PI_TP_DURATION_MIN 13 -#define PI_TP_DURATION_MAX 14 -#define PI_END 15 - - -class Profile { -public: - int data[15]; - - unsigned int config_index; - int config_state; - int key; - - Profile(); - boolean handle_config_state(LiquidCrystal * lcd, DFR_Key * keypad); - void print_config_state(LiquidCrystal * lcd); - void print_config_state_0(LiquidCrystal * lcd); -}; - -#endif diff --git a/libs/ui.h b/libs/ui.h deleted file mode 100644 index 188fb82..0000000 --- a/libs/ui.h +++ /dev/null @@ -1,35 +0,0 @@ -// #ifndef _H_UI -// #define _H_UI -// -// #include -// #include -// -// class Profile { -// public: -// int Ts_min; -// int Ts_max; -// int Tl; -// int Tp; -// int time_max; -// -// // profile temp per second rates -// int ramp_up_min; -// int ramp_up_max; -// int ramp_down_max; -// int ramp_down_min; -// -// // profile temp durations -// int Ts_duration_min; -// int Ts_duration_max; -// int Tl_duration_min; -// int Tl_duration_max; -// int Tp_duration_min; -// int Tp_duration_max; -// int config_index; -// -// Profile(); -// void handle_config_state(LiquidCrystal & lcd, DFR_Key & keypad); -// void print_config_state(LiquidCrystal & lcd); -// }; -// -// #endif diff --git a/oven_control.cpp b/oven_control.cpp new file mode 100644 index 0000000..8a5c204 --- /dev/null +++ b/oven_control.cpp @@ -0,0 +1,251 @@ +#include "oven_control.h" +#include +#include +#include "profile.h" + +//Pin assignments for SainSmart LCD Keypad Shield +LiquidCrystal lcd(8, 9, 4, 5, 6, 7); +DFR_Key keypad; +Profile profile; + +OvenCtl::OvenCtl() { + + time = 0; + temperature = 1; + last_temperature = 1; + actual_dt = 0.f; + + + // timestamps of event beginnings/ends + Ts_time_start = 0; + Ts_time_end = 0; + Tl_time_start = 0; + Tl_time_end = 0; + Tp_time_start = 0; + Tp_time_end = 0; + + // thermostat + set_min = 0; + set_max = 0; + set_dt_min = 0; + set_dt_max = 0; + + op_state = OP_CONFIG; + profile_state = START_STATE; + error_condition = 0; + is_oven_heating = false; + + actual_hysteresis = ramp_up_hysteresis = 1.0; // s + ramp_down_hysteresis = 1.0; // s + + // ui stuff + disable_checks = false; + lcd.begin(16, 2); +} + +void OvenCtl::reset() { + digitalWrite(7, LOW); +} + + +void OvenCtl::send_state() { + Serial.write(time & 0xff); + Serial.write((time>>8) & 0xff); + Serial.write(temperature & 0xff); + Serial.write((temperature >> 8) & 0xff); + Serial.write(last_temperature & 0xff); + Serial.write((last_temperature >> 8) & 0xff); + Serial.write(profile_state & 0xff); + Serial.write((profile_state >> 8) & 0xff); + Serial.write(error_condition & 0xff); + Serial.write((error_condition >> 8) & 0xff); + Serial.write(is_oven_heating); + Serial.flush(); +} + +void OvenCtl::send_config() { + int tmp; + for (int i=0;i < PI_END; i++) + { + tmp = profile.data[i]; + Serial.write(tmp & 0xff); + Serial.write((tmp >> 8 ) & 0xff); + } + Serial.flush(); +} + + +void OvenCtl::recv_config() { + +} + + +void OvenCtl::dispatch_input_config(int cmd) { + if (cmd == 255) + send_config(); + else if (cmd == 254) + recv_config(); + else if (cmd == 250) + reset(); + else if (cmd == 253) + ; +} + +void OvenCtl::handle_stand_alone_state() { + time++; + get_temp(); + check_dt(); +} + +void OvenCtl::handle_remote_state() { + time++; + get_temp(); + check_dt(); +} + + + + +/** + * handles input, dispatching state dependend modes to state handlers + * + * + */ +void OvenCtl::loop() { + int cmd = -1; + + switch (op_state) { + case OP_CONFIG: +// if (profile.handle_config_state()) +// set_start_state(); + break; + case OP_STAND_ALONE: +// if (profile.handle_config_state()) +// set_start_state(); +// break; + case OP_REMOTE: +// if (profile.handle_config_state()) +// set_start_state(); + break; + } + +// if (error_condition != 0) { +// set_error_state(); +// } + + if (Serial.available() > 0) { + cmd = Serial.read(); + if (cmd == 255) + send_config(); + else if (cmd == 254) + send_state(); + else if (cmd == 253) + recv_config(); + else if (cmd == 252) + reset(); +// else if (cmd == 251) +// set_start_state(); + } + + + + control_oven(); + if (profile_state > 0) { + print_status(); + delay(1000); + } +} + + + +void OvenCtl::print_status() { + if (error_condition == 0) { + String tmp("T: "); + if (time < 10) + tmp += "00"; + else if (time < 100) + tmp += "0"; + tmp += time; + tmp += " Tmp: "; + if (temperature < 10) + tmp += "00"; + else if (temperature < 100) + tmp += "0"; + tmp += temperature; + lcd.setCursor(0, 0); + lcd.print(tmp); + + tmp = "Profile: "; + tmp += profile_state; + tmp += "/"; + tmp += END_STATE; + lcd.setCursor(0, 1); + lcd.print(tmp); + lcd.setCursor(13, 1); + if (is_oven_heating) + lcd.print("on "); + else + lcd.print("off"); + } + else { + lcd.clear(); + lcd.print("Error:"); + lcd.setCursor(0, 1); + if (error_condition & E_DT_MIN) + lcd.print("K/s too low"); + if (error_condition & E_DT_MAX) + lcd.print("K/s too high"); + if (error_condition & E_TIME_MAX) + lcd.print("reflow too long"); + if (error_condition & E_TS_TOO_SHORT) + lcd.print("ts too short"); + if (error_condition & E_TS_TOO_LONG) + lcd.print("ts too long"); + if (error_condition & E_TL_TOO_SHORT) + lcd.print("tal too short"); + if (error_condition & E_TL_TOO_LONG) + lcd.print("tal too long"); + if (error_condition & E_TP_TOO_LONG) + lcd.print("peak too short"); + if (error_condition & E_TP_TOO_SHORT) + lcd.print("peak too long"); + } +} + + +void OvenCtl::control_oven() { + if (temperature <= set_min + actual_hysteresis && !is_oven_heating) { + is_oven_heating = true; +// Serial.println("Oven turned on"); + } + else if (temperature >= set_min + actual_hysteresis && is_oven_heating) { + is_oven_heating = false; + } +} + + +void OvenCtl::set_temp(int min, int max, int dt_min, int dt_max) { + set_min = min; + set_max = max; + set_dt_min = dt_min; + set_dt_max = dt_max; +} + + +void OvenCtl::get_temp() { + last_temperature = temperature; + temperature = int(float(analogRead(2)) * 0.2929); + actual_dt = float(temperature) - last_temperature; +} + +void OvenCtl::check_dt() { + if (disable_checks) + return; + if (actual_dt > set_dt_max) { + error_condition |= E_DT_MAX; + } + if (actual_dt < set_dt_min) { + error_condition |= E_DT_MIN; + } +} + diff --git a/oven_control.h b/oven_control.h new file mode 100644 index 0000000..862b9bd --- /dev/null +++ b/oven_control.h @@ -0,0 +1,139 @@ +#ifndef _H_OVEN_CTL +#define _H_OVEN_CTL + +/* +// operational states +#define OP_CONFIG 0 +#define OP_STAND_ALONE 1 +#define OP_REMOTE 2*/ + +enum OP_STATE { + OP_CONFIG, + OP_STAND_ALONE, + OP_REMOTE +}; + +#define E_DT_MIN 1 // temperature dt too small +#define E_DT_MAX 2 // temperature dt too big +#define E_TIME_MAX 4 // reflow process does take too long +#define E_TS_TOO_SHORT 8 // Ts duration too short +#define E_TS_TOO_LONG 16 // Ts duration too long +#define E_TL_TOO_SHORT 32 // Tl duration too short +#define E_TL_TOO_LONG 64 // Tl duration too long +#define E_TP_TOO_SHORT 128 // Tp duration too short +#define E_TP_TOO_LONG 256 // Tp duration too long +#define E_CONFIG 512 // error happened in config state + + +// profile states +enum PROFILE_STATE { + START_STATE, + PREHEAT_STATE, + RAMP_UP_STATE, + TAL_FIRST_STATE, + PEAK_STATE, + TAL_SECOND_STATE, + RAMP_DOWN_STATE, + END_STATE, + ERROR_STATE +}; + + +#include + +class LiquidCrystal; +class DFR_Key; +class Profile; + +extern Profile profile; + +class OvenCtl { +public: + + OvenCtl(); + void loop(); +// void set_config_state(); + +private: + // system time, timestamps and temperatures from sensors + int time; // profile seconds + int temperature; // actual oven temp + int last_temperature; // last oven temp + float actual_dt; // actual difference from last to actual temperatur + + // timestamps of event beginnings/ends + int Ts_time_start; + int Ts_time_end; + int Tl_time_start; + int Tl_time_end; + int Tp_time_start; + int Tp_time_end; + + // thermostat + float set_min; + float set_max; + int set_dt_min; + int set_dt_max; + + float ramp_up_hysteresis; // duration in seconds + float ramp_down_hysteresis; // duration in seconds + float actual_hysteresis; // duration in seconds + + // ui stuff + boolean disable_checks; + + // state machine + unsigned int error_condition; + OP_STATE op_state; + PROFILE_STATE profile_state; + boolean is_oven_heating; + + void print_status(); + void control_oven(); + void set_temp(int, int, int, int); + void get_temp(); + void check_dt(); + void check_max_duration(); + +// void set_start_state(); +// void set_preheat_state(); +// void set_tal_first_state(); +// void set_ramp_up_state(); +// void set_peak_state(); +// void set_tal_second_state(); +// void set_ramp_down_state(); +// void set_end_state(); +// void set_error_state(); + +// void handle_profile_states(); + void handle_stand_alone_state(); + void handle_remote_state(); + +// void handle_config_state(); +// void handle_start_state(); +// void handle_ramp_up_state(); +// void handle_preheat_state(); +// void handle_tal_first_state(); +// void handle_peak_state(); +// void handle_tal_second_state(); +// void handle_ramp_down_state(); +// void handle_end_state(); +// void handle_error_state(); + +// void check_Ts_duration_min(); +// void check_Ts_duration_max(); +// void check_Tl_duration_min(); +// void check_Tl_duration_max(); +// void check_Tp_duration_min(); +// void check_Tp_duration_max(); + + void send_state(); + void send_config(); + void reset(); + + void recv_config(); + + void dispatch_input_config(int); +}; + +#endif diff --git a/plot.py b/plot.py index 6748b58..fd2b910 100644 --- a/plot.py +++ b/plot.py @@ -1,30 +1,5 @@ # -*- coding: utf-8 -*- -""" -GP: -Changed datasource, title, and refresh interval to use -as a poor man's Arduino oscilliscope. - -This demo demonstrates how to draw a dynamic mpl (matplotlib) -plot in a wxPython application. - -It allows "live" plotting as well as manual zooming to specific -regions. - -Both X and Y axes allow "auto" or "manual" settings. For Y, auto -mode sets the scaling of the graph to see all the data points. -For X, auto mode makes the graph "follow" the data. Set it X min -to manual 0 to always see the whole data from the beginning. - -Note: press Enter in the 'manual' text box to make a new value -affect the plot. - -Eli Bendersky (eliben@gmail.com) -License: this code is in the public domain -Last modified: 31.07.2008 -""" - - import os import pprint import random @@ -33,85 +8,40 @@ import wx REFRESH_INTERVAL_MS = 1000 -# The recommended way to use wx with mpl is with the WXAgg -# backend. -# import matplotlib matplotlib.use('WXAgg') +import matplotlib.lines from matplotlib.figure import Figure +from matplotlib.pyplot import legend from matplotlib.backends.backend_wxagg import \ FigureCanvasWxAgg as FigCanvas, \ NavigationToolbar2WxAgg as NavigationToolbar from matplotlib.path import Path import matplotlib.patches as patches +import wx.lib.buttons as buttons import numpy as np import pylab -#Data comes from here + from Arduino_Monitor import SerialData as DataGen - - -class BoundControlBox(wx.Panel): - """ A static box with a couple of radio buttons and a text - box. Allows to switch between an automatic mode and a - manual mode with an associated value. - """ - def __init__(self, parent, ID, label, initval): - wx.Panel.__init__(self, parent, ID) - - self.value = initval - - box = wx.StaticBox(self, -1, label) - sizer = wx.StaticBoxSizer(box, wx.VERTICAL) - - self.radio_auto = wx.RadioButton(self, -1, - label="Auto", style=wx.RB_GROUP) - self.radio_manual = wx.RadioButton(self, -1, - label="Manual") - self.manual_text = wx.TextCtrl(self, -1, - size=(35,-1), - value=str(initval), - style=wx.TE_PROCESS_ENTER) - - self.Bind(wx.EVT_UPDATE_UI, self.on_update_manual_text, self.manual_text) - self.Bind(wx.EVT_TEXT_ENTER, self.on_text_enter, self.manual_text) - - manual_box = wx.BoxSizer(wx.HORIZONTAL) - manual_box.Add(self.radio_manual, flag=wx.ALIGN_CENTER_VERTICAL) - manual_box.Add(self.manual_text, flag=wx.ALIGN_CENTER_VERTICAL) - - sizer.Add(self.radio_auto, 0, wx.ALL, 10) - sizer.Add(manual_box, 0, wx.ALL, 10) - self.radio_auto.SetValue(False); - self.radio_manual.SetValue(True); - - self.SetSizer(sizer) - sizer.Fit(self) - - def on_update_manual_text(self, event): - self.manual_text.Enable(self.radio_manual.GetValue()) - - def on_text_enter(self, event): - self.value = self.manual_text.GetValue() - - def is_auto(self): - return self.radio_auto.GetValue() - - def manual_value(self): - return self.value +import Arduino_Monitor class GraphFrame(wx.Frame): """ The main frame of the application """ - title = 'Demo: dynamic matplotlib graph' + title = 'reflowctl gui' def __init__(self): wx.Frame.__init__(self, None, -1, self.title) self.datagen = DataGen() self.data = [self.datagen.next()] - self.paused = False + self.started = False + + self.profile = [] + self.state = [] + self.count = 0 self.create_menu() self.create_status_bar() @@ -121,6 +51,7 @@ class GraphFrame(wx.Frame): self.Bind(wx.EVT_TIMER, self.on_redraw_timer, self.redraw_timer) self.redraw_timer.Start(REFRESH_INTERVAL_MS) + def create_menu(self): self.menubar = wx.MenuBar() @@ -137,110 +68,274 @@ class GraphFrame(wx.Frame): def create_main_panel(self): self.panel = wx.Panel(self) + self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) + self.init_profile() + self.init_log() + self.init_oven_status() self.init_plot() self.canvas = FigCanvas(self.panel, -1, self.fig) - self.xmin_control = BoundControlBox(self.panel, -1, "X min", 0) - self.xmax_control = BoundControlBox(self.panel, -1, "X max", 250) - self.ymin_control = BoundControlBox(self.panel, -1, "Y min", 0) - self.ymax_control = BoundControlBox(self.panel, -1, "Y max", 280) + self.recv_config_button = wx.Button(self.panel, -1, "Receive Config") + self.Bind(wx.EVT_BUTTON, self.on_recv_config_button, self.recv_config_button) - self.pause_button = wx.Button(self.panel, -1, "Pause") - self.Bind(wx.EVT_BUTTON, self.on_pause_button, self.pause_button) - self.Bind(wx.EVT_UPDATE_UI, self.on_update_pause_button, self.pause_button) + self.send_button = wx.Button(self.panel, -1, "Send Config") + self.Bind(wx.EVT_BUTTON, self.on_send_button, self.send_button) - self.cb_grid = wx.CheckBox(self.panel, -1, - "Show Grid", - style=wx.ALIGN_RIGHT) - self.Bind(wx.EVT_CHECKBOX, self.on_cb_grid, self.cb_grid) - self.cb_grid.SetValue(True) + self.start_button = buttons.GenToggleButton(self.panel, -1, "Start") + self.Bind(wx.EVT_BUTTON, self.on_start_button, self.start_button) - self.cb_xlab = wx.CheckBox(self.panel, -1, - "Show X labels", - style=wx.ALIGN_RIGHT) - self.Bind(wx.EVT_CHECKBOX, self.on_cb_xlab, self.cb_xlab) - self.cb_xlab.SetValue(True) + #self.on_bitmap = wx.Image('burn.png', wx.BITMAP_TYPE_PNG).Scale(32, 32, wx.IMAGE_QUALITY_HIGH).ConvertToBitmap() + #self.off_bitmap = wx.Image('unburn.png', wx.BITMAP_TYPE_PNG).ConvertToBitmap() - self.hbox1 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox1.Add(self.pause_button, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) - self.hbox1.AddSpacer(5) - self.hbox1.Add(self.cb_grid, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) - self.hbox1.AddSpacer(5) - self.hbox1.Add(self.cb_xlab, border=5, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL) - self.hbox2 = wx.BoxSizer(wx.HORIZONTAL) - self.hbox2.Add(self.xmin_control, border=5, flag=wx.ALL) - self.hbox2.Add(self.xmax_control, border=5, flag=wx.ALL) - self.hbox2.AddSpacer(24) - self.hbox2.Add(self.ymin_control, border=5, flag=wx.ALL) - self.hbox2.Add(self.ymax_control, border=5, flag=wx.ALL) + self.ctrls = wx.BoxSizer(wx.VERTICAL) + self.ctrls.Add(self.recv_config_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) + self.ctrls.Add(self.send_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) + self.ctrls.Add(self.start_button, border=5, flag=wx.ALL | wx.ALIGN_LEFT) + self.hbox1.Add(self.ctrls, border=5, flag=wx.ALL | wx.ALIGN_TOP) self.vbox = wx.BoxSizer(wx.VERTICAL) + self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.ALL) self.vbox.Add(self.canvas, 1, flag=wx.LEFT | wx.TOP | wx.GROW) - self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP) - self.vbox.Add(self.hbox2, 0, flag=wx.ALIGN_LEFT | wx.TOP) + #self.vbox.Add(self.hbox1, 0, flag=wx.ALIGN_LEFT | wx.TOP) + self.panel.SetSizer(self.vbox) self.vbox.Fit(self) + def profile_spin_changed(self, event): + print dir(event) + + + def add_profile_item(self, title, sizer, min_=1, max_=250): + + mc = 8 + + item = wx.SpinCtrl(self.panel, -1, "", (30, 50)) + item.SetRange(min_, max_) + item.SetValue(Arduino_Monitor.profile[self.count]) + + self.Bind(wx.EVT_SPIN, self.profile_spin_changed, item) + + sizer.Add(wx.StaticText(self.panel, -1, title), (self.count, 0)) + sizer.Add(item, (self.count, 1)) + + self.count += 1 + self.profile.append(item) + + def init_profile(self): + self.preheat_sizer = wx.GridBagSizer(5, 5) + self.rampup_sizer = wx.GridBagSizer(5, 5) + self.peak_sizer = wx.GridBagSizer(5, 5) + self.rampdown_sizer = wx.GridBagSizer(5, 5) + + self.add_profile_item("Ts_min (°C)", self.preheat_sizer, 0, 300) + self.add_profile_item("Ts_max (°C)", self.preheat_sizer, 0, 300) + self.add_profile_item("Ts duration min (s)", self.preheat_sizer, 0, 300) + self.add_profile_item("Ts duration max (s)", self.preheat_sizer, 0, 300) + + self.add_profile_item("ts ramp up min (°C/s)", self.preheat_sizer, 1, 100) + self.add_profile_item("ts ramp up max (°C/s)", self.preheat_sizer, 1, 100) + self.add_profile_item("tp ramp up min (°C/s)", self.peak_sizer, 1, 100) + self.add_profile_item("tp ramp up max (°C/s)", self.peak_sizer1, 100) + + self.add_profile_item("Tl duration min (s)", self.rampup_sizer, 0, 300) + self.add_profile_item("Tl duration max (s)", self.rampup_sizer, 0, 300) + + self.add_profile_item("Tp (°C)", self.peak_sizer, 0, 300) + self.add_profile_item("Tp duration min (s)", self.peak_sizer, 0, 300) + self.add_profile_item("Tp duration max (s)", self.peak_sizer, 0, 300) + + self.add_profile_item("ramp down min (°C/s)", self.rampdown_sizer, -100, 0) + self.add_profile_item("ramp down max (°C/s)", self.rampdown_sizer, -100, 0) + + self.add_profile_item("time max (s)", 0, 800) + + self.box = wx.StaticBox(self.panel, -1, "Profile Settings") + self.bsizer = wx.StaticBoxSizer(self.box, wx.VERTICAL) + self.bsizer.Add(self.profile_sizer, 0, flag=wx.ALL, border=5) + self.hbox1.Add(self.bsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) + + def init_oven_status(self): + self.oven_status_sizer = wx.GridBagSizer(5, 5) + + #set_min = 0; + #set_max = 0; + #set_dt_min = 0; + #set_dt_max = 0; + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Connected"), (0, 0)) + self.oven_connected = wx.StaticText(self.panel, -1, str(self.datagen.connected())) + self.oven_status_sizer.Add(self.oven_connected, (0, 1)) + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Temperature"), (1, 0)) + self.temperature = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[1])) + self.oven_status_sizer.Add(self.temperature, (1, 1)) + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Time"), (2, 0)) + self.time = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[0])) + self.oven_status_sizer.Add(self.time, (2, 1)) + + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "State"), (3, 0)) + self.pstate = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[3])) + self.oven_status_sizer.Add(self.pstate, (3, 1)) + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Error"), (4, 0)) + self.perror = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[4])) + self.oven_status_sizer.Add(self.perror, (4, 1)) + + self.oven_status_sizer.Add(wx.StaticText(self.panel, -1, "Heating"), (5, 0)) + self.is_oven_heating = wx.TextCtrl(self.panel, -1, str(Arduino_Monitor.status[5])) + self.oven_status_sizer.Add(self.is_oven_heating, (5, 1)) + + self.obox = wx.StaticBox(self.panel, -1, "Oven status") + self.osizer = wx.StaticBoxSizer(self.obox, wx.VERTICAL) + self.osizer.Add(self.oven_status_sizer, 0, flag=wx.ALL, border=5) + self.hbox1.Add(self.osizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) + + + def init_log(self): + self.log_sizer = wx.GridBagSizer(5, 5) + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_start"), (0, 0)) + self.ts_time_start = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.ts_time_start, (0, 1)) + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Ts_time_end"), (1, 0)) + self.ts_time_end = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.ts_time_end, (1, 1)) + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_start"), (2, 0)) + self.tl_time_start = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.tl_time_start, (2, 1)) + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tl_time_end"), (3, 0)) + self.tl_time_end = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.tl_time_end, (3, 1)) + + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_start"), (4, 0)) + self.tp_time_start = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.tp_time_start, (4, 1)) + + self.log_sizer.Add(wx.StaticText(self.panel, -1, "Tp_time_end"), (5, 0)) + self.tp_time_end = wx.TextCtrl(self.panel, -1) + self.log_sizer.Add(self.tp_time_end, (5, 1)) + + self.lbox = wx.StaticBox(self.panel, -1, "Profile Log") + self.lsizer = wx.StaticBoxSizer(self.lbox, wx.VERTICAL) + self.lsizer.Add(self.log_sizer, 0, flag=wx.ALL, border=5) + self.hbox1.Add(self.lsizer, border=5, flag=wx.ALL | wx.ALIGN_TOP) + def create_status_bar(self): self.statusbar = self.CreateStatusBar() def init_plot(self): self.dpi = 100 - self.fig = Figure((3.0, 3.0), dpi=self.dpi) + self.fig = Figure((4.0, 4.0), dpi=self.dpi) self.axes = self.fig.add_subplot(111) self.axes.set_axis_bgcolor('black') self.axes.set_title(u'Reflow Temperature', size=12) self.axes.set_xlabel(u'Time / seconds', size=12) - self.axes.set_ylabel(u'Temperature / °C', size=12) + self.axes.set_ylabel(u'Temperature (°C)', size=12) pylab.setp(self.axes.get_xticklabels(), fontsize=8) pylab.setp(self.axes.get_yticklabels(), fontsize=8) - # plot the data as a line series, and save the reference - # to the plotted line series - # - + # no 1 ts_min_x_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - ts_min_y_min = ts_min + ts_min_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] + # no 2 ts_max_x_min = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MIN] - ts_max_y_min = ts_max + ts_max_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] + # no t1 ts_max_x_max = ts_min_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TS_DURATION_MAX] - ts_max_y_max = ts_max + ts_max_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] - ts_min_x_max = ts_max_x_max - (ts_max_y_max - ts_min_y) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]) - ts_min_y_max = ts_min + # no t2 + ts_min_x_max = ts_max_x_max - (ts_max_y_max - ts_min_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + ts_min_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] - tl_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TL] - Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - tl_x_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + # no 10 + t0_x_max = ts_min_x_max - (ts_max_x_max / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX]) + t0_y_max = 0 - tl_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TL] - Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX]) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] - tl_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + # no 4 + tp_x_min = ts_max_x_min + (Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - ts_max_y_min) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + tp_y_min = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] - tl_x_max = tl_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TL_DURATION_MIN] - tl_y_max = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + # no 5 + tp_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MAX] + tp_y_max = tp_y_min + + # no 8 + tp5_x_max = tp_x_min + Arduino_Monitor.profile[Arduino_Monitor.PI_TP_DURATION_MIN] + tp5_y_max = tp_y_max - 5 + + # no 9 + tp5_x_min = ts_max_x_max + (tp5_y_max - ts_max_y_max) / Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_UP_MAX] + tp5_y_min = tp5_y_max + + # no 6 + end_x_max = tp_x_max + tp_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MIN]) + end_y_max = 0 + + self.xmax = end_x_max + 20 + self.ymax = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] + 20 + + # no 7 + end_x_min = tp5_x_max + tp5_y_max / abs(Arduino_Monitor.profile[Arduino_Monitor.PI_RAMP_DOWN_MAX]) + end_y_min = 0 + + tsmin = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MIN] + tsmax = Arduino_Monitor.profile[Arduino_Monitor.PI_TS_MAX] + tl = Arduino_Monitor.profile[Arduino_Monitor.PI_TL] + tp = Arduino_Monitor.profile[Arduino_Monitor.PI_TP] + self.ts_line_min = matplotlib.lines.Line2D([0, self.xmax], [tsmin, tsmin], + transform=self.axes.transData, figure=self.fig, color='green') + self.ts_line_max = matplotlib.lines.Line2D([0, self.xmax], [tsmax, tsmax], + transform=self.axes.transData, figure=self.fig, label="Ts_max", color='lightgreen') + self.tl_line = matplotlib.lines.Line2D([0, self.xmax], [tl, tl], + transform=self.axes.transData, figure=self.fig, label="Tl", color='yellow') + self.tp_line = matplotlib.lines.Line2D([0, self.xmax], [tp, tp], + transform=self.axes.transData, figure=self.fig, label="Tp", color='blue') + + self.ts_line_min.set_label("Ts_min") + self.ts_line_min.set_label("Ts_max") + self.tl_line.set_label("Tl") + self.tp_line.set_label("Tp") + self.fig.lines.extend([self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line]) verts = [ - [ 0.0, 0.0], - [ 75.0, 150.0], - [100.0, 200.0], - [108.5, 217.0], - [130.0, 260.0], - [170.0, 260.0], - [300.0, 0.0], - [ 0.0, 0.0]] + [0.0, 0.0], + [ts_min_x_min, ts_min_y_min], + [ts_max_x_min, ts_max_y_min], + [ts_max_x_max, ts_max_y_max], + [ts_min_x_max, ts_min_y_max], + #[tp_x_min, tp_y_min], + #[tp_x_max, tp_y_max], + #[end_x_max, end_y_max], + #[end_x_min, end_y_min], + #[tp5_x_max, tp5_y_max], + #[tp5_x_min, tp5_y_min], + [t0_x_max, t0_y_max], + [0.0, 0.0]] - codes = [Path.MOVETO, - Path.LINETO, + codes = [ + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, + #Path.LINETO, + #Path.LINETO, + #Path.LINETO, + #Path.LINETO, Path.CLOSEPOLY] self.plot_data = self.axes.plot( @@ -249,86 +344,62 @@ class GraphFrame(wx.Frame): color=(1, 1, 0), )[0] + print "verts", verts + path = Path(verts, codes) self.patch = patches.PathPatch(path, edgecolor="red", facecolor='orange', lw=2) self.axes.add_patch(self.patch) + self.axes.legend( (self.ts_line_min, self.ts_line_max, self.tl_line, self.tp_line), ('Ts_min', 'Ts_max', 'Tl', 'Tp'), loc=2) def draw_plot(self): """ Redraws the plot """ - # when xmin is on auto, it "follows" xmax to produce a - # sliding window effect. therefore, xmin is assigned after - # xmax. - # - if self.xmax_control.is_auto(): - xmax = len(self.data) if len(self.data) > 50 else 50 - else: - xmax = int(self.xmax_control.manual_value()) - if self.xmin_control.is_auto(): - xmin = xmax - 50 - else: - xmin = int(self.xmin_control.manual_value()) + self.axes.set_xbound(lower=0, upper=self.xmax) + self.axes.set_ybound(lower=0, upper=self.ymax) - #xmax = 480 + self.axes.grid(True, color='gray') - # for ymin and ymax, find the minimal and maximal values - # in the data set and add a mininal margin. - # - # note that it's easy to change this scheme to the - # minimal/maximal value in the current display, and not - # the whole data set. - # - if self.ymin_control.is_auto(): - ymin = round(min(self.data), 0) - 1 - else: - ymin = int(self.ymin_control.manual_value()) - - if self.ymax_control.is_auto(): - ymax = round(max(self.data), 0) + 1 - else: - ymax = int(self.ymax_control.manual_value()) - - #ymax = 300 - - self.axes.set_xbound(lower=xmin, upper=xmax) - self.axes.set_ybound(lower=ymin, upper=ymax) - - # anecdote: axes.grid assumes b=True if any other flag is - # given even if b is set to False. - # so just passing the flag into the first statement won't - # work. - # - if self.cb_grid.IsChecked(): - self.axes.grid(True, color='gray') - else: - self.axes.grid(False) - - # Using setp here is convenient, because get_xticklabels - # returns a list over which one needs to explicitly - # iterate, and setp already handles this. - # - pylab.setp(self.axes.get_xticklabels(), - visible=self.cb_xlab.IsChecked()) + pylab.setp(self.axes.get_xticklabels(), visible=True) self.plot_data.set_xdata(np.arange(len(self.data))) self.plot_data.set_ydata(np.array(self.data)) self.canvas.draw() - def on_pause_button(self, event): - self.paused = not self.paused + def update_config(self): + for ix, i in enumerate(self.profile): + i.SetValue(str(Arduino_Monitor.profile[i])) - def on_update_pause_button(self, event): - label = "Resume" if self.paused else "Pause" - self.pause_button.SetLabel(label) + def update_state(self): + if Arduino_Monitor.status[3] > 0: + self.started = True - def on_cb_grid(self, event): - self.draw_plot() + self.time.SetValue(str(Arduino_Monitor.status[0])) + self.temperature.SetValue(str(Arduino_Monitor.status[1])) + self.pstate.SetValue(str(Arduino_Monitor.status[3])) + self.perror.SetValue(str(Arduino_Monitor.status[4])) + self.is_oven_heating.SetValue(str(Arduino_Monitor.status[5])) - def on_cb_xlab(self, event): - self.draw_plot() + + def on_start_button(self, event): + self.started = self.datagen.send_start() + self.recv_config_button.Disable() + self.send_button.Disable() + self.profile = [] + for i in range(30): + self.profile_sizer.Remove(i) + self.profile_sizer.Layout() + + def on_recv_config_button(self, event): + if not self.started: + self.datagen.recv_config() + + + def on_send_button(self, event): + if not self.started: + self.datagen.send_config() def on_save_plot(self, event): file_choices = "PNG (*.png)|*.png" @@ -347,12 +418,11 @@ class GraphFrame(wx.Frame): self.flash_status_message("Saved to %s" % path) def on_redraw_timer(self, event): - # if paused do not add data, but still redraw the plot - # (to respond to scale modifications, grid change, etc.) - # - if not self.paused: + + if self.started: self.data.append(self.datagen.next()) + self.update_state() self.draw_plot() def on_exit(self, event): @@ -376,3 +446,4 @@ if __name__ == '__main__': app.frame = GraphFrame() app.frame.Show() app.MainLoop() + diff --git a/profile.cpp b/profile.cpp new file mode 100644 index 0000000..29e52a6 --- /dev/null +++ b/profile.cpp @@ -0,0 +1,219 @@ +#include "profile.h" +#include "oven_control.h" +#include +#include + + + +// +// state: temp_min, temp_max, duration +// +// +// + +// x0 = time of last_state.temp_max +// y0 = time of last_state.temp_max + +// x1 = xa + state.duration +// y1 = state.temp_max + + +// x = actual_time +// y = y0 + ((x - x0) * y1 - (x - x0) * y0) / (x1 - x0) + +// preheat, soak, tal1, soak, tal2, rampdown + + +Profile::Profile() : + data {150, // °C + 200, // °C + 217, // °C + 260, // 245-260°C + 480, // seconds + + 0, // ts ramp up rates + 2, + 0, // tp ramp up rates + 2, + -1, // ramp down rates + -6, + + // profile temp durations + 60, + 180, + 60, + 150, + 20, + 40}, + config_index(0), + config_state(CS_MENU), + key(NO_KEY) {} + +boolean Profile::handle_config_state() { + boolean changed = false; + + key = keypad.getKey(); + + switch (config_state) { + case CS_MENU: + if (key == SELECT_KEY) { + config_state = CS_END; + } + else if (key > 0) { + config_state = CS_EDIT; + print_config_state(); + } + break; + case CS_EDIT: + switch (key) { + case LEFT_KEY: + config_index = (config_index-1) % PI_END; + changed = true; + break; + case RIGHT_KEY: + config_index = (config_index+1) % PI_END; + changed = true; + break; + case UP_KEY: + data[config_index]++; + changed = true; + break; + case DOWN_KEY: + data[config_index]--; + changed = true; + break; + case SELECT_KEY: + config_state = CS_END; + break; + default: + ; + } + if (changed) + print_config_state(); + break; + case 2: + return true; + } + return false; +} + +void Profile::print_config_state_0() { + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("start | config"); + lcd.setCursor(0, 1); + lcd.print("[sel] | [other]"); +} + +void Profile::print_config_state() { + lcd.clear(); + switch (config_index) { + case PI_TS_MIN: + lcd.setCursor(0,0); + lcd.print("PI_TS_MIN: "); + lcd.print(data[PI_TS_MIN]); + break; + case PI_TS_MAX: + lcd.setCursor(0,0); + lcd.print("PI_TS_MAX: "); + lcd.print(data[PI_TS_MAX]); + break; + case PI_TL: + lcd.setCursor(0,0); + lcd.print("Tl: "); + lcd.print(data[PI_TL]); + break; + case PI_TP_MIN: + lcd.setCursor(0,0); + lcd.print("Tp min: "); + lcd.print(data[PI_TP_MIN]); + break; + case PI_TP_MAX: + lcd.setCursor(0,0); + lcd.print("Tp max: "); + lcd.print(data[PI_TP_MAX]); + break; + case PI_TIME_MAX: + lcd.setCursor(0,0); + lcd.print("time_max: "); + lcd.print(data[PI_TIME_MAX]); + break; + + // PROFILE TEMP PER SECOND RATES + case PI_TS_RAMP_UP_MIN: + lcd.setCursor(0,0); + lcd.print("ramp_up_min: "); + lcd.print(data[PI_TS_RAMP_UP_MIN]); + break; + case PI_TS_RAMP_UP_MAX: + lcd.setCursor(0,0); + lcd.print("ramp_up_max: "); + lcd.print(data[PI_TS_RAMP_UP_MAX]); + break; + case PI_TP_RAMP_UP_MIN: + lcd.setCursor(0,0); + lcd.print("ramp_up_min: "); + lcd.print(data[PI_TP_RAMP_UP_MIN]); + break; + case PI_TP_RAMP_UP_MAX: + lcd.setCursor(0,0); + lcd.print("ramp_up_max: "); + lcd.print(data[PI_TP_RAMP_UP_MAX]); + break; + case PI_RAMP_DOWN_MAX: + lcd.setCursor(0,0); + lcd.print("ramp_down_min: "); + lcd.print(data[PI_RAMP_DOWN_MIN]); + break; + case PI_RAMP_DOWN_MIN: + lcd.setCursor(0,0); + lcd.print("ramp_down_max: "); + lcd.print(data[PI_RAMP_DOWN_MAX]); + break; + + // PROFILE TEMP DURATIONS + case PI_TS_DURATION_MIN: + lcd.setCursor(0,0); + lcd.print("Ts_duration_min: "); + lcd.print(data[PI_TS_DURATION_MIN]); + break; + case PI_TS_DURATION_MAX: + lcd.setCursor(0,0); + lcd.print("Ts_duration_max: "); + lcd.print(data[PI_TS_DURATION_MAX]); + break; + case PI_TL_DURATION_MIN: + lcd.setCursor(0,0); + lcd.print("Tl_duration_min: "); + lcd.print(data[PI_TL_DURATION_MIN]); + break; + case PI_TL_DURATION_MAX: + lcd.setCursor(0,0); + lcd.print("Tl_duration_max: "); + lcd.print(data[PI_TL_DURATION_MAX]); + break; + case PI_TP_DURATION_MIN: + lcd.setCursor(0,0); + lcd.print("Tp_duration_min: "); + lcd.print(data[PI_TP_DURATION_MIN]); + break; + case PI_TP_DURATION_MAX: + lcd.setCursor(0,0); + lcd.print("Tp_duration_max: "); + lcd.print(data[PI_TP_DURATION_MAX]); + break; + } +} + +void set_std_profile() { +// ProfileState * ps = &states[0]; +// ps->temp_min = 150; +// ps->duration = -1; +// +// ps = &states[1]; +// ps->temp_min = 200; +// ps->duration = 100; +} + +Profile2 std_profile = Profile2(); + diff --git a/profile.h b/profile.h new file mode 100644 index 0000000..96d236b --- /dev/null +++ b/profile.h @@ -0,0 +1,87 @@ +#ifndef _H_PROFILE +#define _H_PROFILE + +#include + +class LiquidCrystal; +class DFR_Key; + +#define PI_TS_MIN 0 +#define PI_TS_MAX 1 + +#define PI_TL 2 + +#define PI_TP_MIN 3 +#define PI_TP_MAX 4 +#define PI_TIME_MAX 5 + +// profile temp per second rates +#define PI_TS_RAMP_UP_MIN 6 +#define PI_TS_RAMP_UP_MAX 7 +#define PI_TP_RAMP_UP_MIN 8 +#define PI_TP_RAMP_UP_MAX 9 +#define PI_RAMP_DOWN_MIN 10 +#define PI_RAMP_DOWN_MAX 11 + +// profile temp durations +#define PI_TS_DURATION_MIN 12 +#define PI_TS_DURATION_MAX 13 +#define PI_TL_DURATION_MIN 14 +#define PI_TL_DURATION_MAX 15 +#define PI_TP_DURATION_MIN 16 +#define PI_TP_DURATION_MAX 17 +#define PI_END 18 + +// config states +#define CS_MENU 0 +#define CS_EDIT 1 +#define CS_END 2 + +class LiquidCrystal; +class DFR_Key; + +extern LiquidCrystal lcd; +extern DFR_Key keypad; + +class Profile { +public: + int data[18]; + + unsigned int config_index; + int config_state; + int key; + + Profile(); + boolean handle_config_state(); + void print_config_state(); + void print_config_state_0(); +}; + + +class ProfileState { +public: + int temp_min; + int duration; + int temp_max; + String name; + + ProfileState() : temp_min(0), duration(0), temp_max(0) {} + ProfileState(int min, int dur, int max = -1, const char & name = NULL) : temp_min(min), duration(dur), temp_max(max), name(name) {} +}; + +class SolderType {}; + + + +class Profile2 { +public: + ProfileState states[3]; + +}; + + +extern Profile2 std_profile; + +void set_std_profile(); + +#endif diff --git a/qtplot.py b/qtplot.py new file mode 100644 index 0000000..7c99886 --- /dev/null +++ b/qtplot.py @@ -0,0 +1,172 @@ +import sys, os, random +from PyQt4 import QtGui, QtCore + +from numpy import arange, sin, pi, array, linspace +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +from matplotlib.path import Path +import matplotlib.patches as patches + +from scipy.interpolate import * + +progname = os.path.basename(sys.argv[0]) +progversion = "0.1" + + +class MyMplCanvas(FigureCanvas): + """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.).""" + def __init__(self, parent=None, width=5, height=4, dpi=100): + fig = Figure(figsize=(width, height), dpi=dpi) + self.axes = fig.add_subplot(111) + # We want the axes cleared every time plot() is called + self.axes.hold(False) + + self.compute_initial_figure() + + # + FigureCanvas.__init__(self, fig) + self.setParent(parent) + + FigureCanvas.setSizePolicy(self, + QtGui.QSizePolicy.Expanding, + QtGui.QSizePolicy.Expanding) + FigureCanvas.updateGeometry(self) + + def compute_initial_figure(self): + pass + + +class MyDynamicMplCanvas(MyMplCanvas): + """A canvas that updates itself every second with a new plot.""" + def __init__(self, *args, **kwargs): + MyMplCanvas.__init__(self, *args, **kwargs) + timer = QtCore.QTimer(self) + QtCore.QObject.connect(timer, QtCore.SIGNAL("timeout()"), self.update_figure) + timer.start(1000) + + def compute_initial_figure(self): + x1_min = 150-20. + y1_min = 150. + + x2_min = x1_min + 100. + y2_min = 200. + + x3_min = x2_min + 20. + y3_min = 220. + + x4_min = x3_min + 249. - y3_min + y4_min = 249. + + x5_min = x4_min + (y4_min - y3_min) / 3 * 2 + y5_min = y3_min + + x6_min = x5_min + y5_min-20. + y6_min = 20. + + p1x = array([0., x1_min, x2_min, x3_min, x4_min, x5_min, x6_min]) + p1y = array([20., y1_min, y2_min, y3_min, y4_min, y5_min, y6_min]) + + interp = pchip(p1x, p1y) + + xx = linspace(0., x6_min, x6_min) + + ynew = interp(xx) + + print "len xx", len(xx) + print "len p1x", len(p1x) + print "xy", zip(p1x, p1y) + print "ynew", ynew + + dtp_min = 99999. + dtp_max = 0. + dtpi = -1 + + dtn_min = -99999. + dtn_max = 0. + dtni = -1 + for i in xrange(1, len(ynew)): + tmp = ynew[i] - ynew[i-1] + if tmp > 0: + if tmp < dtp_min: + dtp_min = tmp + dtpi = i + elif tmp > dtp_max: + dtp_max = tmp + dtpi = i + elif tmp < 0: + if tmp > dtn_min: + dtn_min = tmp + dtni = i + elif tmp < dtn_max: + dtn_max = tmp + dtni = i + print "max negative", dtn_min, dtn_max, dtni + print "max positive", dtp_min, dtp_max, dtpi + + self.axes.plot(p1x, p1y, "bo", xx, ynew, "r-") + #self.axes.plot(p1x, p1y, 'r-o') + + def update_figure(self): + # Build a list of 4 random integers between 0 and 10 (both inclusive) + #l = [ random.randint(0, 10) for i in xrange(4) ] + + #self.axes.plot([0, 1, 2, 3], l, 'r') + #self.draw() + pass + + +class ApplicationWindow(QtGui.QMainWindow): + def __init__(self): + QtGui.QMainWindow.__init__(self) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("application main window") + + self.file_menu = QtGui.QMenu('&File', self) + self.file_menu.addAction('&Quit', self.fileQuit, + QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + self.menuBar().addMenu(self.file_menu) + + self.help_menu = QtGui.QMenu('&Help', self) + self.menuBar().addSeparator() + self.menuBar().addMenu(self.help_menu) + + self.help_menu.addAction('&About', self.about) + + self.main_widget = QtGui.QWidget(self) + + l = QtGui.QVBoxLayout(self.main_widget) + #sc = MyStaticMplCanvas(self.main_widget, width=5, height=4, dpi=100) + dc = MyDynamicMplCanvas(self.main_widget, width=5, height=4, dpi=100) + #l.addWidget(sc) + l.addWidget(dc) + + self.main_widget.setFocus() + self.setCentralWidget(self.main_widget) + + self.statusBar().showMessage("All hail matplotlib!", 2000) + + def fileQuit(self): + self.close() + + def closeEvent(self, ce): + self.fileQuit() + + def about(self): + QtGui.QMessageBox.about(self, "About %s" % progname, +u"""%(prog)s version %(version)s +Copyright \N{COPYRIGHT SIGN} 2005 Florent Rougon, 2006 Darren Dale + +This program is a simple example of a Qt4 application embedding matplotlib +canvases. + +It may be used and modified with no restriction; raw copies as well as +modified versions may be distributed without limitation.""" +% {"prog": progname, "version": progversion}) + + +qApp = QtGui.QApplication(sys.argv) + +aw = ApplicationWindow() +aw.setWindowTitle("%s" % progname) +aw.show() +sys.exit(qApp.exec_()) diff --git a/unburn.png b/unburn.png new file mode 100644 index 0000000..074b65a Binary files /dev/null and b/unburn.png differ