From 852183414336e59034a9593e9c4ce6b7200de21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 10 Nov 2012 15:27:43 +0100 Subject: [PATCH] sync --- .gitignore | 1 + Arduino_Monitor.py | 88 ++++++-- CMakeLists.txt | 8 +- burn.png | Bin 0 -> 12626 bytes libs/oven_control.cpp | 456 ---------------------------------------- libs/oven_control.h | 122 ----------- libs/profile.cpp | 191 ----------------- libs/profile.h | 45 ---- libs/ui.h | 35 ---- oven_control.cpp | 251 ++++++++++++++++++++++ oven_control.h | 139 ++++++++++++ plot.py | 477 ++++++++++++++++++++++++------------------ profile.cpp | 219 +++++++++++++++++++ profile.h | 87 ++++++++ qtplot.py | 172 +++++++++++++++ unburn.png | Bin 0 -> 8711 bytes 16 files changed, 1219 insertions(+), 1072 deletions(-) create mode 100644 burn.png delete mode 100644 libs/oven_control.cpp delete mode 100644 libs/oven_control.h delete mode 100644 libs/profile.cpp delete mode 100644 libs/profile.h delete mode 100644 libs/ui.h create mode 100644 oven_control.cpp create mode 100644 oven_control.h create mode 100644 profile.cpp create mode 100644 profile.h create mode 100644 qtplot.py create mode 100644 unburn.png 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 0000000000000000000000000000000000000000..70d83de90d480bb667f2ca859952b5df3cfc1a0e GIT binary patch literal 12626 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 Dq(c3a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..074b65a25548e8710603ec527bc1e3f53787b099 GIT binary patch literal 8711 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