├── .gitignore ├── zeroii_issues.png ├── Makefile ├── zeroii-analyzer ├── button.h ├── log.h ├── menu_manager.h ├── analyzer.h ├── graph.h ├── process.h ├── shell.h ├── persistence.h └── zeroii-analyzer.ino ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /zeroii_issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gerner/zeroii-analyzer/HEAD/zeroii_issues.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | SOURCES=$(shell find . -name *.cpp -o -name *.h -o -name *.ino) 3 | 4 | FQBN=arduino:renesas_uno:minima 5 | 6 | build/build_flag: $(SOURCES) 7 | arduino-cli compile --fqbn $(FQBN) zeroii-analyzer 8 | touch build/build_flag 9 | 10 | all: build/build_flag 11 | 12 | upload: all 13 | arduino-cli upload zeroii-analyzer -p /dev/ttyACM0 --fqbn $(FQBN) 14 | 15 | clean: 16 | rm build/build_flag 17 | -------------------------------------------------------------------------------- /zeroii-analyzer/button.h: -------------------------------------------------------------------------------- 1 | #ifndef _BUTTON_H 2 | #define _BUTTON_H 3 | 4 | class Button { 5 | public: 6 | Button(const int SW) { 7 | SW_ = SW; 8 | press_ = false; 9 | last_button_press_ = 0; 10 | } 11 | void begin() { 12 | pinMode(SW_, INPUT_PULLUP); 13 | } 14 | bool read() { 15 | bool click = false; 16 | if (digitalRead(SW_) == LOW) { 17 | uint32_t now = millis(); 18 | if (!press_ && now - last_button_press_ > 100) { 19 | click = true; 20 | } 21 | press_ = true; 22 | last_button_press_ = now; 23 | } else { 24 | press_ = false; 25 | } 26 | return click; 27 | } 28 | private: 29 | int SW_; 30 | bool press_; 31 | uint32_t last_button_press_; 32 | 33 | }; 34 | 35 | #endif //_BUTTON_H 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zeroii-analyzer 2 | Antenna analyzer based on RigExpert Zero II and Arduino 3 | 4 | # Gotchas with the Zero II 5 | 6 | When I first got my Zero II I could not get it to respond to UART, I2C or be 7 | recognized as a USB device. It seemed like the device was dead. I found this 8 | [video by Ed March, aka Professor Vector](https://www.youtube.com/watch?v=3K6qOPxwzps) 9 | that answered some of my questions. The same guy has 10 | [updated libraries for accessing the board](https://wb9raa.com/aazero2/). 11 | 12 | Basically, there's a power switch on the board that determines if the board 13 | gets power from the power pins or from USB. I switched it to "ext" and suddenly 14 | I could power the thing. 15 | 16 | ![fixes for my RigExpert Zero II](zeroii_issues.png) 17 | 18 | I ended up using I2C and not flashing the firmware (contrary to the 19 | [RigExpert guide](https://rigexpert.com/news/zero-ii-the-tiny-and-powerful-analyzer/)) 20 | # Dependencies 21 | 22 | * `RigExpertZeroII_I2C` my version of it 23 | * `MD_REncoder` 24 | * `TFTLCD-Library` my version of it 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nick Gerner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /zeroii-analyzer/log.h: -------------------------------------------------------------------------------- 1 | #ifndef _LOG_H 2 | #define _LOG_H 3 | 4 | //#define DISABLE_LOG 5 | 6 | #define LOG_ERROR 1 7 | #define LOG_WARNING 2 8 | #define LOG_INFO 3 9 | #define LOG_DEBUG 4 10 | #define LOG_TRACE 5 11 | 12 | const char* level_names[] = {"CRIT", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" }; 13 | 14 | class Logger { 15 | public: 16 | Logger(const char* name) { 17 | #ifndef DISABLE_LOG 18 | name_ = String(name); 19 | level_ = LOG_INFO; 20 | #endif 21 | } 22 | 23 | Logger(const char* name, uint8_t level) { 24 | #ifndef DISABLE_LOG 25 | name_ = String(name); 26 | level_ = level; 27 | #endif 28 | } 29 | 30 | void log(uint8_t level, const char* message) { 31 | #ifndef DISABLE_LOG 32 | if(level_ >= level) { 33 | Serial.print("["); 34 | Serial.print(level_names[level]); 35 | Serial.print("]\t"); 36 | Serial.print(name_); 37 | Serial.print("\t"); 38 | Serial.println(message); 39 | Serial.flush(); 40 | } 41 | #endif 42 | } 43 | 44 | void log(uint8_t level, const __FlashStringHelper* message) { 45 | #ifndef DISABLE_LOG 46 | if(level_ >= level) { 47 | Serial.print("["); 48 | Serial.print(level_names[level]); 49 | Serial.print("]\t"); 50 | Serial.print(name_); 51 | Serial.print("\t"); 52 | Serial.println(message); 53 | Serial.flush(); 54 | } 55 | #endif 56 | } 57 | 58 | void error(const char* message) { 59 | log(LOG_ERROR, message); 60 | } 61 | void warn(const char* message) { 62 | log(LOG_WARNING, message); 63 | } 64 | void info(const char* message) { 65 | log(LOG_INFO, message); 66 | } 67 | void debug(const char* message) { 68 | log(LOG_DEBUG, message); 69 | } 70 | 71 | void error(const __FlashStringHelper* message) { 72 | log(LOG_ERROR, message); 73 | } 74 | void warn(const __FlashStringHelper* message) { 75 | log(LOG_WARNING, message); 76 | } 77 | void info(const __FlashStringHelper* message) { 78 | log(LOG_INFO, message); 79 | } 80 | void debug(const __FlashStringHelper* message) { 81 | log(LOG_DEBUG, message); 82 | } 83 | 84 | void log(uint8_t level, String message) { 85 | log(level, message.c_str()); 86 | } 87 | void error(String message) { 88 | error(message.c_str()); 89 | } 90 | void warn(String message) { 91 | warn(message.c_str()); 92 | } 93 | void info(String message) { 94 | info(message.c_str()); 95 | } 96 | void debug(String message) { 97 | debug(message.c_str()); 98 | } 99 | 100 | private: 101 | #ifndef DISABLE_LOG 102 | String name_; 103 | uint8_t level_; 104 | #endif 105 | }; 106 | 107 | #endif 108 | -------------------------------------------------------------------------------- /zeroii-analyzer/menu_manager.h: -------------------------------------------------------------------------------- 1 | #ifndef _MENU_MANAGER_H 2 | #define _MENU_MANAGER_H 3 | 4 | #include 5 | 6 | struct Menu; 7 | 8 | struct MenuOption { 9 | MenuOption() : sub_menu(NULL) { } 10 | MenuOption(String l, uint16_t oid, Menu* sm) { 11 | label = l; 12 | option_id = oid; 13 | sub_menu = sm; 14 | } 15 | String label; 16 | uint8_t option_id; 17 | Menu* sub_menu; 18 | }; 19 | 20 | struct Menu { 21 | Menu(Menu* p, const MenuOption* o, size_t o_count) { 22 | parent = p; 23 | options = o; 24 | option_count = o_count; 25 | selected_option = 0; 26 | 27 | //set us as parent 28 | for (size_t i=0; iparent = this; 31 | } 32 | } 33 | } 34 | 35 | Menu* parent; 36 | const MenuOption* options; 37 | size_t option_count; 38 | size_t selected_option; 39 | }; 40 | 41 | class MenuManager { 42 | public: 43 | MenuManager(Menu* root_menu) { 44 | root_menu_ = root_menu; 45 | current_menu_ = root_menu; 46 | current_option_ = -1; 47 | } 48 | 49 | void select(size_t i) { 50 | if (i < current_menu_->option_count) { 51 | current_menu_->selected_option = i; 52 | } 53 | } 54 | 55 | void select_rel(int32_t delta) { 56 | if (current_menu_->option_count > 0) { 57 | select(constrain((int32_t)current_menu_->selected_option+delta, 0, current_menu_->option_count-1)); 58 | } 59 | } 60 | 61 | void select_up() { 62 | select(current_menu_->selected_option+1); 63 | } 64 | 65 | void select_down() { 66 | select(current_menu_->selected_option-1); 67 | } 68 | 69 | bool select_option(uint16_t option_id) { 70 | return select_option(root_menu_, option_id); 71 | } 72 | 73 | void expand() { 74 | assert(current_menu_->selected_option < current_menu_->option_count); 75 | const MenuOption* current_option = &(current_menu_->options[current_menu_->selected_option]); 76 | if (current_option_ >= 0) { 77 | return; 78 | } 79 | if (current_option->sub_menu == NULL) { 80 | current_option_ = current_option->option_id; 81 | } else { 82 | current_menu_ = current_option->sub_menu; 83 | assert(current_option_ == -1); 84 | } 85 | } 86 | 87 | void collapse() { 88 | if (current_option_ >= 0) { 89 | current_option_ = -1; 90 | } else { 91 | if (current_menu_ == root_menu_) { 92 | return; 93 | } 94 | assert(current_menu_->parent != NULL); 95 | current_menu_->selected_option = 0; 96 | current_menu_ = current_menu_->parent; 97 | } 98 | } 99 | 100 | Menu* root_menu_; 101 | Menu* current_menu_; 102 | int16_t current_option_; 103 | 104 | private: 105 | bool select_option(Menu* m, uint16_t option_id) { 106 | for(size_t i; ioption_count; i++) { 107 | if(m->options[i].option_id == option_id) { 108 | current_menu_ = m; 109 | select(i); 110 | return true; 111 | } else if(m->options[i].sub_menu) { 112 | if(select_option(m->options[i].sub_menu, option_id)) { 113 | return true; 114 | } 115 | } 116 | } 117 | return false; 118 | } 119 | }; 120 | 121 | #endif 122 | -------------------------------------------------------------------------------- /zeroii-analyzer/analyzer.h: -------------------------------------------------------------------------------- 1 | #ifndef _ANALYZER_H 2 | #define _ANALYZER_H 3 | 4 | #include 5 | #include "Complex.h" 6 | 7 | #include "log.h" 8 | 9 | Logger analysis_logger("analysis"); 10 | 11 | Complex compute_gamma(const Complex z, const float z0_real) { 12 | // gamma = (z - z0) / (z + z0) 13 | // z = r + xj 14 | 15 | Complex z0(z0_real); 16 | 17 | return (z - z0) / (z + z0); 18 | } 19 | 20 | Complex compute_z(const Complex gamma, const float z0_real) { 21 | Complex z0(z0_real); 22 | return z0*((one+gamma) / (one-gamma)); 23 | } 24 | 25 | /*Complex calibrate_reflection(Complex sc, Complex oc, Complex load, Complex reflection) { 26 | // inspired by NanoVNA 27 | Complex a=load, b=oc, c=sc, d = reflection; 28 | return -(a - d)*(b - c)/(a*(b - c) + Complex(2.f)*c*(a - b) + d*(Complex(-2.f)*a + b + c)); 29 | }*/ 30 | 31 | Complex calibrate_reflection(Complex sc, Complex oc, Complex load, Complex reflection) { 32 | // via: https://hsinjulit.com/sol-calibration/ 33 | // e01 * e10 = 2 * (oc - load)(load - sc)/(oc - sc) 34 | // e11 = (sc + oc - 2*load)/(oc - sc) 35 | // cal_gamma = (uncal_gamma - load)/(e01*e10 + e11(uncal_gamma - load)) 36 | Complex two = Complex(2); 37 | Complex uncal_gamma = reflection; 38 | Complex e01e10 = two * (oc - load)*(load - sc)/(oc - sc); 39 | Complex e11 = (sc + oc - two*load)/(oc - sc); 40 | Complex cal_gamma = (uncal_gamma - load)/(e01e10 + e11*(uncal_gamma - load)); 41 | return cal_gamma; 42 | } 43 | 44 | float compute_swr(Complex gamma) { 45 | float mag = gamma.modulus(); 46 | if (mag > 1) { 47 | return INFINITY; 48 | } else { 49 | return (1 + mag) / (1 - mag); 50 | } 51 | } 52 | 53 | struct AnalysisPoint { 54 | uint32_t fq; 55 | Complex uncal_z; 56 | 57 | AnalysisPoint(): fq(0) {} 58 | AnalysisPoint(const AnalysisPoint &p): fq(p.fq), uncal_z(p.uncal_z) {} 59 | AnalysisPoint(uint32_t a_fq, Complex a_uncal_z) : fq(a_fq), uncal_z(a_uncal_z) {} 60 | 61 | static const size_t data_size = sizeof(uint32_t)+sizeof(Complex); 62 | 63 | static AnalysisPoint from_bytes(const uint8_t* data) { 64 | // assume data has enough elements 65 | uint32_t fq = *(uint32_t*)data; 66 | Complex c = *(Complex*)(data+sizeof(uint32_t)); 67 | 68 | return AnalysisPoint(fq, c); 69 | } 70 | 71 | static void to_bytes(AnalysisPoint point, uint8_t* data) { 72 | *(uint32_t*)data = point.fq; 73 | *(Complex *)(data+sizeof(uint32_t)) = point.uncal_z; 74 | } 75 | }; 76 | 77 | struct CalibrationPoint { 78 | CalibrationPoint() { 79 | } 80 | 81 | CalibrationPoint(const CalibrationPoint &p) { 82 | fq = p.fq; 83 | cal_short = p.cal_short; 84 | cal_open = p.cal_open; 85 | cal_load = p.cal_load; 86 | } 87 | 88 | CalibrationPoint(uint32_t f, Complex s, Complex o, Complex l) { 89 | fq = f; 90 | cal_short = s; 91 | cal_open = o; 92 | cal_load = l; 93 | } 94 | 95 | uint32_t fq; 96 | Complex cal_short; 97 | Complex cal_open; 98 | Complex cal_load; 99 | }; 100 | 101 | bool CalibrationCmp(const CalibrationPoint &lhs, const uint32_t &fq) { 102 | return lhs.fq < fq; 103 | } 104 | 105 | CalibrationPoint uncalibrated_point = CalibrationPoint(0, Complex(-1), Complex(1), Complex(0)); 106 | 107 | class Analyzer { 108 | public: 109 | Analyzer(float z0, CalibrationPoint* calibration_results) { 110 | z0_ = z0; 111 | calibration_len_ = 0; 112 | calibration_results_ = calibration_results; 113 | } 114 | 115 | Complex uncalibrated_measure(uint32_t fq) { 116 | zeroii_.startMeasure(fq); 117 | 118 | float R = zeroii_.getR(); 119 | float X = zeroii_.getX(); 120 | 121 | analysis_logger.debug(String("")+R+" + "+X+" i"); 122 | 123 | return Complex(R, X); 124 | } 125 | 126 | Complex calibrated_gamma(const AnalysisPoint &p) const { 127 | return calibrated_gamma(p.fq, p.uncal_z); 128 | } 129 | 130 | Complex calibrated_gamma(uint32_t fq, Complex uncalibrated_z) const { 131 | CalibrationPoint* cal = find_calibration(fq); 132 | return calibrate_reflection(cal->cal_short, cal->cal_open, cal->cal_load, compute_gamma(uncalibrated_z, z0_)); 133 | } 134 | 135 | CalibrationPoint* find_calibration(uint32_t fq) const { 136 | if(calibration_len_ == 0) { 137 | return &uncalibrated_point; 138 | } else { 139 | size_t i; 140 | return std::lower_bound(calibration_results_, &calibration_results_[calibration_len_], fq, CalibrationCmp); 141 | } 142 | } 143 | 144 | RigExpertZeroII_I2C zeroii_; 145 | 146 | float z0_; 147 | size_t calibration_len_; 148 | CalibrationPoint *calibration_results_; 149 | }; 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /zeroii-analyzer/graph.h: -------------------------------------------------------------------------------- 1 | #ifndef _GRAPH_H 2 | #define _GRAPH_H 3 | 4 | #include "log.h" 5 | 6 | Logger graph_logger = Logger("graph"); 7 | 8 | String frequency_formatter(const int32_t fq) { 9 | int int_part, dec_part; 10 | char buf[3+1+3+3+1]; 11 | if (fq >= 1ul * 1000ul * 1000ul * 1000ul) { 12 | int_part = fq / 1000ul / 1000ul / 1000ul; 13 | dec_part = fq / 1000ul / 1000ul % 1000ul; 14 | snprintf(buf, sizeof(buf), "%d.%03dGHz", int_part, dec_part); 15 | return String(buf); 16 | } else if (fq >= 1ul * 1000ul * 1000ul) { 17 | int_part = fq / 1000ul / 1000ul; 18 | dec_part = fq / 1000ul % 1000ul; 19 | snprintf(buf, sizeof(buf), "%d.%03dMHz", int_part, dec_part); 20 | return String(buf); 21 | } else if (fq >= 1ul * 1000ul) { 22 | int_part = fq / 1000ul; 23 | dec_part = fq % 1000ul; 24 | snprintf(buf, sizeof(buf), "%d.%03dkHz", int_part, dec_part); 25 | return String(buf); 26 | } else { 27 | int_part = fq; 28 | dec_part = 0; 29 | snprintf(buf, sizeof(buf), "%d.%03dHz", int_part, dec_part); 30 | return String(buf); 31 | } 32 | } 33 | 34 | void read_patch(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t* patch) { 35 | for (int16_t j = 0; j < h; j++, y++) { 36 | for (int16_t i = 0; i < w; i++) { 37 | patch[j * w + i] = tft.readPixel(x+i, y); 38 | } 39 | } 40 | } 41 | 42 | #define POINTER_WIDTH 8 43 | #define POINTER_HEIGHT 8 44 | 45 | class GraphContext { 46 | public: 47 | GraphContext(const AnalysisPoint* results, const size_t results_len, const Analyzer* analyzer) : swr_i_(0), results_(results), results_len_(results_len), analyzer_(analyzer) {} 48 | 49 | void initialize_swr() { 50 | uint32_t start_fq; 51 | uint32_t end_fq; 52 | if(results_len_ == 0) { 53 | start_fq = MIN_FQ; 54 | end_fq = MAX_FQ; 55 | } else if (results_len_ == 1) { 56 | start_fq = results_[0].fq-100; 57 | end_fq = results_[0].fq+100; 58 | } else { 59 | start_fq = results_[0].fq; 60 | end_fq = results_[results_len_-1].fq; 61 | } 62 | // x ranges from start fq to end fq 63 | // y ranges from 1 to 5 64 | x_min_ = start_fq; 65 | x_max_ = end_fq; 66 | y_min_ = 5; 67 | y_max_ = 1; 68 | 69 | x_screen_ = 8*4; 70 | y_screen_ = 8*TITLE_TEXT_SIZE*2; 71 | width_ = tft.width()-x_screen_-5*6*LABEL_TEXT_SIZE; 72 | height_ = tft.height()-y_screen_-8*2; 73 | } 74 | 75 | void initialize_smith(bool zoom) { 76 | // gamma range, x is real, y is imag 77 | x_min_ = -1; 78 | x_max_ = 1; 79 | y_min_ = 1; 80 | y_max_ = -1; 81 | 82 | // zoomed in so plot fills x dimension 83 | if (zoom) { 84 | x_screen_ = 8*2; 85 | y_screen_ = -(tft.width()-x_screen_-tft.height())/2; 86 | width_ = tft.width()-x_screen_; 87 | height_ = width_; 88 | } else { 89 | // zoomed out so all of plot fits y dimension 90 | y_screen_ = 8*2; 91 | x_screen_ = (tft.width()-(tft.height()-y_screen_))/2; 92 | height_ = tft.height()-y_screen_-1; 93 | width_ = height_; 94 | } 95 | } 96 | 97 | template 98 | void draw_swr_label(const T label, uint32_t fq, float swr, const Analyzer* analyzer) { 99 | int16_t label_xy[2]; 100 | 101 | translate_to_screen(fq, swr, label_xy); 102 | tft.fillCircle(label_xy[0], label_xy[1], 2, WHITE); 103 | tft.setCursor(label_xy[0]-6*LABEL_TEXT_SIZE/2-4*6*LABEL_TEXT_SIZE, label_xy[1]+8*LABEL_TEXT_SIZE/2); 104 | tft.print(label); 105 | } 106 | 107 | void graph_swr() { 108 | graph_logger.info(String("graphing swr plot with ")+results_len_+" points"); 109 | 110 | // set the pointer patch outside the graph area 111 | pointer_patch_x = tft.width(); 112 | pointer_patch_y = tft.height(); 113 | 114 | // area is just below the title 115 | tft.fillRect(0, 5*2*8, tft.width(), 3*8*3, BLACK); 116 | tft.fillRect(0, 8*TITLE_TEXT_SIZE, tft.width(), tft.height()-8*TITLE_TEXT_SIZE, BLACK); 117 | 118 | // draw axes 119 | tft.drawFastHLine(x_screen_, y_screen_, width_, WHITE); 120 | tft.drawFastHLine(x_screen_, y_screen_+height_, width_, WHITE); 121 | tft.drawFastVLine(x_screen_, y_screen_, height_, WHITE); 122 | tft.drawFastVLine(x_screen_+width_, y_screen_, height_, WHITE); 123 | 124 | uint32_t start_fq = results_[0].fq; 125 | uint32_t end_fq = results_[results_len_-1].fq; 126 | 127 | // add some axes labels fq min/max, swr 1.5, 3 128 | tft.setTextSize(LABEL_TEXT_SIZE); 129 | tft.setTextColor(GRAY); 130 | draw_swr_label(frequency_formatter(start_fq), start_fq, 1.0, analyzer_); 131 | draw_swr_label(frequency_formatter(end_fq), end_fq, 1.0, analyzer_); 132 | draw_swr_label(1.5, start_fq, 1.5, analyzer_); 133 | draw_swr_label(3.0, start_fq, 3.0, analyzer_); 134 | tft.setTextColor(WHITE); 135 | 136 | int16_t xy_cutoff[2]; 137 | translate_to_screen(0, 3, xy_cutoff); 138 | tft.drawFastHLine(x_screen_, xy_cutoff[1], width_, RED); 139 | translate_to_screen(0, 1.5, xy_cutoff); 140 | tft.drawFastHLine(x_screen_, xy_cutoff[1], width_, MAGENTA); 141 | // draw all the analysis points 142 | if (results_len_ == 0) { 143 | graph_logger.info(F("no results to plot")); 144 | return; 145 | } 146 | 147 | //TODO: what do we do if none of the SWR falls into plottable region? 148 | 149 | if (results_len_ == 1) { 150 | int16_t xy[2]; 151 | translate_to_screen(results_[0].fq, compute_swr(analyzer_->calibrated_gamma(results_[0])), xy); 152 | tft.fillCircle(xy[0], xy[1], 3, YELLOW); 153 | } else { 154 | for (size_t i=0; icalibrated_gamma(results_[i])); 156 | int16_t xy_start[2]; 157 | translate_to_screen(results_[i].fq, swr, xy_start); 158 | int16_t xy_end[2]; 159 | translate_to_screen(results_[i+1].fq, compute_swr(analyzer_->calibrated_gamma(results_[i+1])), xy_end); 160 | graph_logger.debug(String("drawing line ")+xy_start[0]+","+xy_start[1]+" to "+xy_end[0]+","+xy_end[1]); 161 | tft.drawLine(xy_start[0], xy_start[1], xy_end[0], xy_end[1], YELLOW); 162 | } 163 | } 164 | } 165 | 166 | void draw_swr_pointer() { 167 | if (results_len_ == 0) { 168 | return; 169 | } 170 | uint32_t start_fq = results_[0].fq; 171 | uint32_t end_fq = results_[results_len_-1].fq; 172 | 173 | //first clear the old swr pointer 174 | if(pointer_patch_x < tft.width() && pointer_patch_y < tft.height()) { 175 | tft.drawRGBBitmap(pointer_patch_x, pointer_patch_y, pointer_patch, POINTER_WIDTH, POINTER_HEIGHT); 176 | } 177 | 178 | //draw the "pointer" 179 | int16_t xy_pointer[2]; 180 | translate_to_screen(results_[swr_i_].fq, compute_swr(analyzer_->calibrated_gamma(results_[swr_i_])), xy_pointer); 181 | pointer_patch_x = xy_pointer[0]-POINTER_WIDTH/2; 182 | pointer_patch_y = xy_pointer[1]; 183 | read_patch(pointer_patch_x, pointer_patch_y, POINTER_WIDTH, POINTER_HEIGHT, pointer_patch); 184 | tft.drawTriangle(xy_pointer[0], xy_pointer[1], xy_pointer[0]-POINTER_WIDTH/2, xy_pointer[1]+POINTER_HEIGHT-1, xy_pointer[0]+POINTER_WIDTH/2-1, xy_pointer[1]+POINTER_HEIGHT-1, GREEN); 185 | } 186 | 187 | size_t draw_swr_title() { 188 | if (results_len_ == 0) { 189 | tft.fillRect(0, 0, tft.width()-8*TITLE_TEXT_SIZE*5, 8*TITLE_TEXT_SIZE, BLACK); 190 | tft.setCursor(0,0); 191 | tft.setTextSize(TITLE_TEXT_SIZE); 192 | tft.println("No SWR results"); 193 | return 0; 194 | } 195 | 196 | size_t min_swr_i = 0; 197 | float min_swr = compute_swr(analyzer_->calibrated_gamma(results_[0])); 198 | for (size_t i=0; icalibrated_gamma(results_[i])); 200 | if (swr < min_swr) { 201 | min_swr = swr; 202 | min_swr_i = i; 203 | } 204 | } 205 | 206 | //draw the title 207 | tft.fillRect(0, 0, tft.width()-8*TITLE_TEXT_SIZE*5, 8*TITLE_TEXT_SIZE*2, BLACK); 208 | tft.setCursor(0,0); 209 | tft.setTextSize(TITLE_TEXT_SIZE); 210 | 211 | Complex min_g = analyzer_->calibrated_gamma(results_[min_swr_i]); 212 | Complex sel_g = analyzer_->calibrated_gamma(results_[swr_i_]); 213 | 214 | tft.println(String("Min @ ")+frequency_formatter(results_[min_swr_i].fq)+" "+compute_swr(min_g)+"SWR"); 215 | tft.println(String("Sel @ ")+frequency_formatter(results_[swr_i_].fq)+" "+compute_swr(sel_g)+"SWR"); 216 | 217 | return min_swr_i; 218 | } 219 | 220 | void draw_smith_coords(size_t min_swr_i) { 221 | if(results_len_ == 0) { 222 | return; 223 | } 224 | Complex min_g = analyzer_->calibrated_gamma(results_[min_swr_i]); 225 | Complex min_z = compute_z(min_g, analyzer_->z0_); 226 | Complex sel_g = analyzer_->calibrated_gamma(results_[swr_i_]); 227 | Complex sel_z = compute_z(sel_g, analyzer_->z0_); 228 | 229 | tft.fillRect(0, tft.height()-2*8*TITLE_TEXT_SIZE, tft.width(), 8*TITLE_TEXT_SIZE*2, BLACK); 230 | tft.setCursor(0, tft.height()-2*8*TITLE_TEXT_SIZE); 231 | tft.setTextSize(TITLE_TEXT_SIZE); 232 | tft.println(String("Min X: ")+min_z.real()+" R: "+min_z.imag()); 233 | tft.println(String("Sel X: ")+sel_z.real()+" R: "+sel_z.imag()); 234 | } 235 | 236 | void draw_smith_title() { 237 | size_t min_swr_i = draw_swr_title(); 238 | draw_smith_coords(min_swr_i); 239 | } 240 | 241 | template 242 | void draw_smith_label(const T label, const Complex z) { 243 | int16_t label_xy[2]; 244 | Complex label_g; 245 | 246 | label_g = compute_gamma(z, analyzer_->z0_); 247 | translate_to_screen(label_g.real(), label_g.imag(), label_xy); 248 | tft.fillCircle(label_xy[0], label_xy[1], 2, WHITE); 249 | tft.setCursor(label_xy[0]+6*LABEL_TEXT_SIZE/2, label_xy[1]+8*LABEL_TEXT_SIZE/2); 250 | tft.print(label); 251 | } 252 | 253 | void graph_smith() { 254 | graph_logger.info(String("graphing swr plot with ")+results_len_+" points"); 255 | pointer_patch_x = tft.width(); 256 | pointer_patch_y = tft.height(); 257 | 258 | // area is just below the title 259 | tft.fillRect(0, 5*2*8, tft.width(), 3*8*3, BLACK); 260 | tft.fillRect(0, 8*TITLE_TEXT_SIZE, tft.width(), tft.height()-8*TITLE_TEXT_SIZE, BLACK); 261 | 262 | // draw axes 263 | // horizontal (and arcs) resistance axis 264 | tft.drawFastHLine(x_screen_, y_screen_+(height_/2), width_, WHITE); 265 | // circular reactance axis (radius 1, centered at (0.5, 0)) 266 | tft.drawCircle(x_screen_+(width_*3/4), y_screen_+(height_/2), width_/4, WHITE); 267 | // and draw the outer circle for reference 268 | tft.drawCircle(x_screen_+(width_/2), y_screen_+(height_/2), width_/2, WHITE); 269 | 270 | // draw some axes labels: z=z0, z=z0*2, z=z0/2, z=1j, z=-1j 271 | tft.setTextSize(LABEL_TEXT_SIZE); 272 | tft.setTextColor(GRAY); 273 | 274 | draw_smith_label(analyzer_->z0_, Complex(analyzer_->z0_, 0)); 275 | draw_smith_label(analyzer_->z0_*2, Complex(analyzer_->z0_*2.0, 0)); 276 | draw_smith_label(analyzer_->z0_/2.0, Complex(analyzer_->z0_/2.0, 0)); 277 | draw_smith_label(analyzer_->z0_, Complex(analyzer_->z0_, analyzer_->z0_)); 278 | draw_smith_label(analyzer_->z0_, Complex(analyzer_->z0_, -analyzer_->z0_)); 279 | 280 | tft.setTextColor(WHITE); 281 | 282 | //cutoff swr circles 283 | int16_t center[2]; 284 | translate_to_screen(0, 0, center); 285 | 286 | int16_t swr_3[2]; 287 | translate_to_screen(0.5, 0, swr_3); 288 | assert(abs(compute_swr(Complex(0.5, 0))-3.0) < 0.001); 289 | swr_3[0] -= center[0]; 290 | tft.drawCircle(x_screen_+(width_/2), y_screen_+(height_/2), swr_3[0], RED); 291 | 292 | int16_t swr_15[2]; 293 | translate_to_screen(0.2, 0, swr_15); 294 | assert(abs(compute_swr(Complex(0.2, 0))-1.5) < 0.001); 295 | swr_15[0] -= center[0]; 296 | tft.drawCircle(x_screen_+width_/2, y_screen_+height_/2, swr_15[0], MAGENTA); 297 | 298 | // draw all the analysis points 299 | if (results_len_ == 0) { 300 | graph_logger.info(F("no results to plot")); 301 | return; 302 | } else if (results_len_ == 1) { 303 | int16_t xy[2]; 304 | Complex g = analyzer_->calibrated_gamma(results_[0]); 305 | translate_to_screen(g.real(), g.imag(), xy); 306 | tft.fillCircle(xy[0], xy[1], 3, YELLOW); 307 | } else { 308 | for (size_t i=0; icalibrated_gamma(results_[i]); 310 | Complex g_end = analyzer_->calibrated_gamma(results_[i+1]); 311 | int16_t xy_start[2]; 312 | translate_to_screen(g_start.real(), g_start.imag(), xy_start); 313 | int16_t xy_end[2]; 314 | translate_to_screen(g_end.real(), g_end.imag(), xy_end); 315 | graph_logger.debug(String("drawing line ")+xy_start[0]+","+xy_start[1]+" to "+xy_end[0]+","+xy_end[1]); 316 | tft.drawLine(xy_start[0], xy_start[1], xy_end[0], xy_end[1], YELLOW); 317 | } 318 | } 319 | } 320 | 321 | void draw_smith_pointer() { 322 | if (results_len_ == 0) { 323 | return; 324 | } 325 | 326 | //first clear the old swr pointer 327 | if(pointer_patch_x < tft.width() && pointer_patch_y < tft.height()) { 328 | tft.drawRGBBitmap(pointer_patch_x, pointer_patch_y, pointer_patch, POINTER_WIDTH, POINTER_HEIGHT); 329 | } 330 | 331 | //draw the "pointer" 332 | int16_t xy_pointer[2]; 333 | Complex gamma_pointer = analyzer_->calibrated_gamma(results_[swr_i_]); 334 | translate_to_screen(gamma_pointer.real(), gamma_pointer.imag(), xy_pointer); 335 | pointer_patch_x = xy_pointer[0]-POINTER_WIDTH/2; 336 | pointer_patch_y = xy_pointer[1]; 337 | read_patch(pointer_patch_x, pointer_patch_y, POINTER_WIDTH, POINTER_HEIGHT, pointer_patch); 338 | tft.drawTriangle(xy_pointer[0], xy_pointer[1], xy_pointer[0]-POINTER_WIDTH/2, xy_pointer[1]+POINTER_HEIGHT-1, xy_pointer[0]+POINTER_WIDTH/2-1, xy_pointer[1]+POINTER_HEIGHT-1, GREEN); 339 | } 340 | 341 | void incr_swri(int32_t turn) { 342 | swr_i_ = constrain((int32_t)swr_i_+turn, 0, results_len_-1); 343 | } 344 | 345 | private: 346 | int16_t pointer_patch_x, pointer_patch_y; 347 | uint16_t pointer_patch[POINTER_WIDTH*POINTER_HEIGHT]; 348 | size_t swr_i_; 349 | const AnalysisPoint* results_; 350 | size_t results_len_; 351 | const Analyzer* analyzer_; 352 | 353 | float x_min_; 354 | float x_max_; 355 | float y_min_; 356 | float y_max_; 357 | 358 | int16_t x_screen_; 359 | int16_t y_screen_; 360 | int16_t width_; 361 | int16_t height_; 362 | 363 | void translate_to_screen(float x_in, float y_in, int16_t* xy) { 364 | float x_range = x_max_ - x_min_; 365 | float y_range = y_max_ - y_min_; 366 | xy[0] = (x_in - x_min_) / x_range * width_ + x_screen_; 367 | xy[1] = (y_in - y_min_) / y_range * height_ + y_screen_; 368 | 369 | graph_logger.debug(String(x_in)+" -> "+xy[0]+" "+y_in+" -> "+xy[1]); 370 | } 371 | }; 372 | 373 | #endif //_GRAPH_H 374 | -------------------------------------------------------------------------------- /zeroii-analyzer/process.h: -------------------------------------------------------------------------------- 1 | #ifndef _PROCESS_H 2 | #define _PROCESS_H 3 | 4 | #include "log.h" 5 | 6 | Logger process_logger("process"); 7 | 8 | class AnalysisProcessor { 9 | public: 10 | void initialize(uint32_t start_fq, uint32_t end_fq, uint16_t steps, AnalysisPoint* results) { 11 | start_fq_ = start_fq; 12 | end_fq_ = end_fq; 13 | fq_ = start_fq; 14 | if(end_fq > start_fq && steps > 1) { 15 | steps_ = steps; 16 | step_fq_ = pow(((double)end_fq)/((double)start_fq), 1.0/((double)steps_-1)); 17 | } else { 18 | steps_ = 0; 19 | step_fq_ = 1.0; 20 | } 21 | results_ = results; 22 | result_idx_ = 0; 23 | 24 | process_logger.info(String("analyzing startFq ")+start_fq+" endFq "+end_fq+" steps "+steps+" step_fq "+step_fq_); 25 | tft.fillScreen(BLACK); 26 | draw_title(); 27 | initialize_progress_meter("Analyzing..."); 28 | } 29 | 30 | bool analyze() { 31 | if (result_idx_ >= steps_) { 32 | return true; 33 | } 34 | 35 | process_logger.debug(String("analyzing fq ")+fq_+" idx "+result_idx_); 36 | Complex z = analyzer.uncalibrated_measure(fq_); 37 | results_[result_idx_] = AnalysisPoint(fq_, z); 38 | result_idx_++; 39 | fq_ = next_fq(fq_, result_idx_); 40 | 41 | // update progress meter 42 | draw_progress_meter(steps_, result_idx_); 43 | 44 | return false; 45 | } 46 | 47 | 48 | private: 49 | AnalysisPoint* results_; 50 | uint32_t fq_; 51 | double step_fq_; 52 | size_t result_idx_; 53 | 54 | uint32_t start_fq_; 55 | uint32_t end_fq_; 56 | size_t steps_; 57 | 58 | uint32_t next_fq(uint32_t fq_, size_t result_idx_) { 59 | if(result_idx_ == steps_-1) { 60 | return end_fq_; 61 | } else { 62 | return constrain((uint32_t)round((double)fq_ * step_fq_), start_fq_, end_fq_); 63 | } 64 | } 65 | }; 66 | 67 | enum CAL_STEP { CAL_START, CAL_S_START, CAL_S, CAL_O_START, CAL_O, CAL_L_START, CAL_L, CAL_END }; 68 | class Calibrator { 69 | public: 70 | Calibrator(Analyzer* analyzer) { 71 | analyzer_ = analyzer; 72 | } 73 | void initialize(uint32_t start_fq, uint32_t end_fq, uint16_t steps, CalibrationPoint* results) { 74 | calibration_state_ = CAL_START; 75 | start_fq_ = start_fq; 76 | end_fq_ = end_fq; 77 | fq_ = start_fq; 78 | if(end_fq > start_fq && steps > 1) { 79 | steps_ = steps; 80 | step_fq_ = pow(((double)end_fq)/((double)start_fq), 1.0/((double)steps_-1)); 81 | } else { 82 | steps_ = 0; 83 | step_fq_ = 1.0; 84 | } 85 | results_ = results; 86 | result_idx_ = 0; 87 | 88 | process_logger.info(String("calibrating startFq ")+start_fq+" endFq "+end_fq+" steps "+steps+" step_fq "+step_fq_); 89 | tft.fillScreen(BLACK); 90 | draw_title(); 91 | } 92 | bool calibration_step() { 93 | switch(calibration_state_) { 94 | case CAL_START: 95 | tft.setTextSize(2); 96 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 97 | tft.setCursor(0, 7*2*8); 98 | tft.println(F("connect short and press knob")); 99 | calibration_state_ = CAL_S_START; 100 | break; 101 | // all three "start" cases are the same 102 | // just increment the state on click 103 | case CAL_S_START: 104 | case CAL_O_START: 105 | case CAL_L_START: 106 | if (click) { 107 | tft.fillRect(0, 7*2*8, tft.width(), 2*8, BLACK); 108 | process_logger.info(String("calibration start state ")+calibration_state_); 109 | calibration_state_++; 110 | fq_ = start_fq_; 111 | result_idx_ = 0; 112 | initialize_progress_meter("Calibrating..."); 113 | } 114 | break; 115 | case CAL_S: 116 | if(result_idx_ < steps_) { 117 | process_logger.debug(String("calibrating ")+fq_); 118 | results_[result_idx_].cal_short = compute_gamma(analyzer_->uncalibrated_measure(fq_), analyzer_->z0_); 119 | results_[result_idx_].fq = fq_; 120 | result_idx_++; 121 | fq_ = next_fq(fq_, result_idx_); 122 | draw_progress_meter(steps_, result_idx_); 123 | } else { 124 | process_logger.info(F("done calibrating short.")); 125 | tft.setTextSize(2); 126 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 127 | tft.setCursor(0, 7*2*8); 128 | tft.println(F("connect open and press knob")); 129 | calibration_state_ = CAL_O_START; 130 | } 131 | break; 132 | case CAL_O: 133 | if(result_idx_ < steps_) { 134 | process_logger.debug(String("calibrating ")+fq_); 135 | results_[result_idx_].cal_open = compute_gamma(analyzer_->uncalibrated_measure(fq_), analyzer_->z0_); 136 | result_idx_++; 137 | fq_ = next_fq(fq_, result_idx_); 138 | draw_progress_meter(steps_, result_idx_); 139 | } else { 140 | process_logger.info(F("done calibrating open.")); 141 | tft.setTextSize(2); 142 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 143 | tft.setCursor(0, 7*2*8); 144 | tft.println(F("connect load and press knob")); 145 | calibration_state_ = CAL_L_START; 146 | } 147 | break; 148 | case CAL_L: 149 | if(result_idx_ < steps_) { 150 | process_logger.debug(String("calibrating ")+fq_); 151 | results_[result_idx_].cal_load = compute_gamma(analyzer_->uncalibrated_measure(fq_), analyzer_->z0_); 152 | result_idx_++; 153 | fq_ = next_fq(fq_, result_idx_); 154 | draw_progress_meter(steps_, result_idx_); 155 | } else { 156 | process_logger.info(F("done calibrating load.")); 157 | tft.setTextSize(2); 158 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 159 | tft.setCursor(0, 6*2*8); 160 | tft.println(F("done calibrating.")); 161 | calibration_state_ = CAL_END; 162 | // we're done! 163 | return true; 164 | } 165 | break; 166 | } 167 | return false; 168 | } 169 | 170 | private: 171 | Analyzer* analyzer_; 172 | uint8_t calibration_state_; 173 | 174 | uint32_t start_fq_; 175 | uint32_t end_fq_; 176 | 177 | uint32_t fq_; 178 | double step_fq_; 179 | CalibrationPoint* results_; 180 | size_t steps_; 181 | size_t result_idx_; 182 | 183 | uint32_t next_fq(uint32_t fq_, size_t result_idx_) { 184 | if(result_idx_ == steps_-1) { 185 | return end_fq_; 186 | } else { 187 | return constrain((uint32_t)round((double)fq_ * step_fq_), start_fq_, end_fq_); 188 | } 189 | } 190 | }; 191 | 192 | String frequency_parts_formatter(const uint32_t fq) { 193 | uint16_t ghz_part, mhz_part, khz_part, hz_part; 194 | //char buf[1+3*4+3+1]; 195 | char buf[64]; 196 | ghz_part = fq / 1000 / 1000 / 1000; 197 | mhz_part = fq / 1000 / 1000 % 1000ul; 198 | khz_part = fq / 1000 % 1000ul; 199 | hz_part = fq % 1000ul; 200 | 201 | snprintf(buf, sizeof(buf), "%d.%03d.%03d.%03d Hz", ghz_part, mhz_part, khz_part, hz_part); 202 | return String(buf); 203 | } 204 | 205 | enum FQ_SETTING_STATE { FQ_SETTING_START, FQ_SETTING_GHZ, FQ_SETTING_MHZ, FQ_SETTING_KHZ, FQ_SETTING_HZ, FQ_SETTING_END }; 206 | 207 | class FqSetter { 208 | public: 209 | void initialize(const uint32_t fq) { 210 | process_logger.info("initialized FqSetter"); 211 | fq_state_ = FQ_SETTING_START; 212 | fq_ = fq; 213 | } 214 | 215 | String frequency_parts_indicator() const { 216 | switch(fq_state_) { 217 | case FQ_SETTING_GHZ: return String(" ^"); 218 | case FQ_SETTING_MHZ: return String(" ^^^"); 219 | case FQ_SETTING_KHZ: return String(" ^^^"); 220 | case FQ_SETTING_HZ: return String(" ^^^"); 221 | } 222 | } 223 | 224 | void draw_fq_setting(const String label) const { 225 | process_logger.debug(String("drawing frequency ")+fq_); 226 | tft.setTextSize(3); 227 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 228 | tft.setCursor(0, 6*2*8); 229 | tft.print(" "); 230 | process_logger.debug("fq parts"); 231 | tft.println(frequency_parts_formatter(fq_)); 232 | process_logger.debug("fq indicator"); 233 | tft.println(frequency_parts_indicator()); 234 | process_logger.debug("drew fq"); 235 | } 236 | 237 | bool set_fq_value(const uint32_t min_fq, const uint32_t max_fq, const String label) { 238 | // clicking advances through fields in the fq (GHz, MHz, ...) or sets the value 239 | if (click) { 240 | if (fq_state_ == FQ_SETTING_HZ) { 241 | fq_state_ = FQ_SETTING_END; 242 | return true; 243 | } else { 244 | fq_state_++; 245 | draw_fq_setting(label); 246 | return false; 247 | } 248 | } else if (fq_state_ == FQ_SETTING_START) { 249 | fq_state_ = FQ_SETTING_GHZ; 250 | tft.fillRect(0, 5*2*8, tft.width(), 3*8*3, BLACK); 251 | tft.setCursor(0, 5*2*8); 252 | tft.print(label); 253 | tft.println(":"); 254 | draw_fq_setting(label); 255 | return false; 256 | } else if (turn != 0) { 257 | // rotating changes value 258 | int32_t inc = 1ul; 259 | switch(fq_state_) { 260 | case FQ_SETTING_GHZ: inc = 1ul * 1000 * 1000 * 1000; break; 261 | case FQ_SETTING_MHZ: inc = 1ul * 1000 * 1000; break; 262 | case FQ_SETTING_KHZ: inc = 1ul * 1000; break; 263 | } 264 | process_logger.debug(String("constraining fq ")+fq_+" "+turn+" "+inc+" "+min_fq+" "+max_fq); 265 | process_logger.debug(String(fq_ + turn*inc)); 266 | fq_ = constrain(fq_ + turn * inc, min_fq, max_fq); 267 | process_logger.debug(String(fq_)); 268 | draw_fq_setting(label); 269 | return false; 270 | } else { 271 | return false; 272 | } 273 | } 274 | 275 | uint32_t fq() const { return fq_; } 276 | private: 277 | uint8_t fq_state_; 278 | uint32_t fq_; 279 | }; 280 | 281 | #define NUM_BANDS 19 282 | #define BAND_10M 11 283 | class BandSetter { 284 | public: 285 | void initialize(uint32_t start_fq, uint32_t end_fq) { 286 | // try and find the matching band if there is one 287 | for(band_idx_=0; band_idx_ < NUM_BANDS; band_idx_++) { 288 | if(band_fqs[band_idx_][0] == start_fq && band_fqs[band_idx_][1] == end_fq) { 289 | break; 290 | } 291 | } 292 | if(band_idx_ == NUM_BANDS) { 293 | band_idx_ = 0; 294 | } 295 | tft.fillScreen(BLACK); 296 | draw_title(); 297 | tft.setCursor(0, 5*2*8); 298 | tft.println(F("Band:")); 299 | draw_band_setting(); 300 | } 301 | bool set_band() { 302 | if (click) { 303 | return true; 304 | } else if (turn != 0) { 305 | band_idx_ = constrain((int32_t)band_idx_+turn, 0, NUM_BANDS-1); 306 | draw_band_setting(); 307 | } 308 | return false; 309 | } 310 | void draw_band_setting() { 311 | tft.setTextSize(3); 312 | tft.fillRect(0, 6*2*8, tft.width(), 2*8*3, BLACK); 313 | tft.setCursor(0, 6*2*8); 314 | tft.print(" "); 315 | tft.println(band_names[band_idx_]); 316 | } 317 | 318 | void band(uint32_t* start_fq, uint32_t* end_fq) { 319 | *start_fq = band_fqs[band_idx_][0]; 320 | *end_fq = band_fqs[band_idx_][1]; 321 | } 322 | 323 | const uint32_t band_fqs[NUM_BANDS][2] = { {135700, 137800}, {472000, 479000}, {1800000, 2000000}, {3500000, 4000000}, {5330500, 5406400}, {7000000, 7300000}, {10100000, 10150000}, {14000000, 14350000}, {18068000, 18168000}, {2100000, 21450000}, {24890000, 24990000}, {28000000, 29700000}, {50000000, 54000000}, {144000000, 148000000}, {219000000, 225000000}, {420000000, 450000000}, {902000000, 928000000}, {100000, 600000000}, {100000, 1000000000} }; 324 | const char* band_names[NUM_BANDS] = {F("2200m"), F("630m"), F("160m"), F("80m"), F("60m"), F("40m"), F("30m"), F("20m"), F("17m"), F("15m"), F("12m"), F("10m"), F("6m"), F("VHF"), F("1.25m"), F("UHF"), F("33cm"), F("Reference RF"), F("Full Range")}; 325 | 326 | private: 327 | size_t band_idx_; 328 | }; 329 | 330 | class UserValueSetter { 331 | public: 332 | UserValueSetter() { 333 | } 334 | 335 | void initialize(String label, int32_t initial_value, int32_t min_value, int32_t max_value) { 336 | label_ = label; 337 | value_ = initial_value; 338 | min_value_ = min_value; 339 | max_value_ = max_value; 340 | 341 | tft.fillScreen(BLACK); 342 | draw_title(); 343 | 344 | /*tft.setTextSize(3); 345 | tft.fillRect(0, 5*2*8, tft.width(), 2*8*3, BLACK); 346 | tft.setCursor(0, 5*2*8); 347 | tft.print(label_); 348 | tft.println(":"); 349 | tft.print(" "); 350 | tft.println(value_);*/ 351 | initialize_progress_meter(label_); 352 | draw_progress_meter(max_value_, value_, min_value_); 353 | } 354 | 355 | bool set_value() { 356 | if (click) { 357 | return true; 358 | } 359 | // rotating changes value 360 | if (turn != 0) { 361 | value_ = constrain(value_ + turn, min_value_, max_value_); 362 | /*tft.setTextSize(3); 363 | tft.fillRect(0, 5*2*8, tft.width(), 2*8*3, BLACK); 364 | tft.setCursor(0, 5*2*8); 365 | tft.print(label_); 366 | tft.println(":"); 367 | tft.print(" "); 368 | tft.println(value_);*/ 369 | draw_progress_meter(max_value_, value_, min_value_); 370 | } 371 | return false; 372 | } 373 | 374 | int32_t value_; 375 | private: 376 | String label_; 377 | int32_t min_value_; 378 | int32_t max_value_; 379 | }; 380 | 381 | class FileBrowser { 382 | public: 383 | FileBrowser() : file_menu_(NULL), file_options_(NULL) {} 384 | 385 | ~FileBrowser() { 386 | if (file_menu_) { 387 | delete file_menu_; 388 | delete [] file_options_; 389 | } 390 | } 391 | 392 | bool initialize(FsFile* directory, bool with_new) { 393 | process_logger.debug(F("initializing file browser")); 394 | 395 | tft.fillScreen(BLACK); 396 | draw_title(); 397 | 398 | if(!directory->isOpen() || !directory->isDirectory()) { 399 | return false; 400 | } 401 | if (file_menu_) { 402 | delete file_menu_; 403 | delete [] file_options_; 404 | file_menu_ = NULL; 405 | file_options_ = NULL; 406 | } 407 | 408 | with_new_ = with_new; 409 | 410 | process_logger.debug(F("counting files in directory")); 411 | 412 | // awkward to iterate through directory once to get count and a second 413 | // time to actually get the names into the array. not sure how else to 414 | // do it with an array of MenuOptions (no stl shenanigans) 415 | size_t file_count = 0; 416 | directory->rewindDirectory(); 417 | FsFile entry; 418 | while(entry.openNext(directory, O_RDONLY)) { 419 | file_count++; 420 | } 421 | 422 | process_logger.debug(F("allocating file options")); 423 | 424 | size_t idx; 425 | if (with_new_) { 426 | file_count++; 427 | file_options_ = new MenuOption[file_count]; 428 | file_options_[0].label = String("New File"); 429 | idx = 1; 430 | } else { 431 | file_options_ = new MenuOption[file_count]; 432 | idx = 0; 433 | } 434 | 435 | process_logger.debug(F("iterating through directory")); 436 | 437 | directory->rewindDirectory(); 438 | while(entry.openNext(directory, O_RDONLY) && idx < file_count) { 439 | char filename[128]; 440 | entry.getName(filename, sizeof(filename)); 441 | file_options_[idx++].label = String(filename); 442 | } 443 | 444 | file_menu_ = new Menu(NULL, file_options_, file_count); 445 | 446 | draw_menu(file_menu_, -1, true); 447 | return true; 448 | } 449 | 450 | bool choose_file() { 451 | if (click) { 452 | return true; 453 | } else if (turn != 0 && file_menu_->option_count > 0) { 454 | file_menu_->selected_option = constrain((int32_t)file_menu_->selected_option+turn, 0, file_menu_->option_count-1); 455 | draw_menu(file_menu_, -1, false); 456 | return false; 457 | } else { 458 | return false; 459 | } 460 | } 461 | 462 | bool is_new() { 463 | return with_new_ && file_menu_->selected_option == 0; 464 | } 465 | 466 | void file(char* filename, size_t max_len) { 467 | file_options_[file_menu_->selected_option].label.toCharArray(filename, max_len); 468 | } 469 | 470 | private: 471 | bool with_new_; 472 | MenuOption* file_options_; 473 | Menu* file_menu_; 474 | }; 475 | 476 | typedef bool (*ProgressFn)(void); 477 | 478 | class ConfirmDialog { 479 | public: 480 | void initialize(ProgressFn progress_fn) { 481 | confirmation_menu.selected_option = 0; 482 | progress_fn_ = progress_fn; 483 | progress_ = true; 484 | } 485 | 486 | bool progress() { 487 | if (progress_) { 488 | if(progress_fn_()) { 489 | process_logger.debug(F("inner progress fn returned true, proceeding with confirmation.")); 490 | progress_ = false; 491 | tft.fillScreen(BLACK); 492 | tft.setCursor(CONFIRM_ORIG_X-6*TITLE_TEXT_SIZE, CONFIRM_ORIG_Y-8*TITLE_TEXT_SIZE); 493 | tft.print(F("Are you sure?")); 494 | draw_title(); 495 | draw_menu(&confirmation_menu, -1, true, CONFIRM_ORIG_X, CONFIRM_ORIG_Y); 496 | } 497 | } else { 498 | if(click) { 499 | return true; 500 | } else if(turn != 0 && confirmation_menu.option_count > 0) { 501 | confirmation_menu.selected_option = constrain((int32_t)confirmation_menu.selected_option+turn, 0, confirmation_menu.option_count-1); 502 | draw_menu(&confirmation_menu, -1, false, CONFIRM_ORIG_X, CONFIRM_ORIG_Y); 503 | } 504 | } 505 | return false; 506 | } 507 | 508 | bool confirm() { 509 | return !progress_ && confirmation_menu.selected_option == 0; 510 | } 511 | 512 | private: 513 | MenuOption confirmation_menu_options[2] = { 514 | MenuOption("Yes", 0, NULL), 515 | MenuOption("Cancel", 0, NULL), 516 | }; 517 | Menu confirmation_menu = Menu(NULL, confirmation_menu_options, sizeof(confirmation_menu_options)/sizeof(confirmation_menu_options[0])); 518 | 519 | bool progress_; 520 | ProgressFn progress_fn_; 521 | }; 522 | 523 | #endif 524 | -------------------------------------------------------------------------------- /zeroii-analyzer/shell.h: -------------------------------------------------------------------------------- 1 | #ifndef _SHELL_H 2 | #define _SHELL_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #ifdef __arm__ 11 | // should use uinstd.h to define sbrk but Due causes a conflict 12 | extern "C" char* sbrk(int incr); 13 | #else // __ARM__ 14 | extern char *__brkval; 15 | #endif // __arm__ 16 | 17 | int freeMemory() { 18 | char top; 19 | #ifdef __arm__ 20 | return &top - reinterpret_cast(sbrk(0)); 21 | #elif defined(CORE_TEENSY) || (ARDUINO > 103 && ARDUINO != 151) 22 | return &top - __brkval; 23 | #else // __arm__ 24 | return __brkval ? &top - __brkval : &top - __malloc_heap_start; 25 | #endif // __arm__ 26 | } 27 | 28 | size_t total_memory; 29 | void shell_begin() { 30 | #ifndef RAMEND 31 | total_memory = freeMemory(); 32 | #else 33 | total_memory = RAMEND; 34 | #endif 35 | } 36 | 37 | #define MAX_SERIAL_COMMAND 128 38 | char serial_command[MAX_SERIAL_COMMAND+1]; 39 | size_t serial_command_len = 0; 40 | bool read_serial_command() { 41 | int c; 42 | while(serial_command_len < MAX_SERIAL_COMMAND && ((c = Serial.read()) > 0)) { 43 | if(c == '\n') { 44 | serial_command[serial_command_len] = '\0'; 45 | return true; 46 | } 47 | serial_command[serial_command_len++] = c; 48 | } 49 | if(serial_command_len == MAX_SERIAL_COMMAND) { 50 | serial_command_len = 0; 51 | } 52 | return false; 53 | } 54 | 55 | void wait_for_serial() { 56 | char c; 57 | while(((c = Serial.read()) == 0) || (c != '\n')) { 58 | delay(100); 59 | } 60 | } 61 | 62 | int str2int(const char* str, int len, char** ptr=NULL) 63 | { 64 | int i; 65 | int ret = 0; 66 | for(i = 0; i < len; ++i) 67 | { 68 | if(str[i] < '0' || str [i] > '9') { 69 | break; 70 | } else { 71 | ret = ret * 10 + (str[i] - '0'); 72 | } 73 | } 74 | if(ptr) { 75 | *ptr = (char *)((void *)str+i); 76 | } 77 | return ret; 78 | } 79 | 80 | size_t lstrip(const char* str, size_t len) { 81 | size_t i; 82 | for(i=0; i> 9) + 1980; 94 | *m = (fs_date >> 5) & 15; 95 | *d = fs_date & 31; 96 | } 97 | 98 | void fstime_to_hms(uint16_t fs_time, uint8_t* H, uint8_t* M, uint8_t* S) { 99 | // fs_time packs 5 bit hour shifted up 11, 6-bit minute shifted up 5, 5-bit second shifted down 1 (2 second resolution) 100 | *H = fs_time >> 11; 101 | *M = (fs_time >> 5) & 0X3F; 102 | *S = 2 * (fs_time & 0X1F); 103 | } 104 | 105 | void print_directory(FsBaseFile* dir, int numTabs=0) { 106 | FsFile entry; 107 | size_t filename_max_len = 128; 108 | char filename[filename_max_len]; 109 | while (entry.openNext(dir, O_RDONLY)) { 110 | for (uint8_t i = 0; i < numTabs; i++) { 111 | Serial.print('\t'); 112 | } 113 | entry.getName(filename, filename_max_len); 114 | Serial.print(filename); 115 | if (entry.isDir()) { 116 | Serial.println("/"); 117 | print_directory(&entry, numTabs + 1); 118 | } else { 119 | // files have sizes, directories do not 120 | Serial.print("\t\t"); 121 | Serial.print(entry.size(), DEC); 122 | Serial.print("\t"); 123 | 124 | uint16_t fs_date; 125 | uint16_t fs_time; 126 | entry.getModifyDateTime(&fs_date, &fs_time); 127 | 128 | uint16_t y; 129 | uint8_t m, d, H, M, S; 130 | fsdate_to_ymd(fs_date, &y, &m, &d); 131 | fstime_to_hms(fs_time, &H, &M, &S); 132 | 133 | 134 | DateTime modify_time(y, m, d, H, M, S); 135 | time_t t(modify_time.unixtime()); 136 | char formatted_date[20]; 137 | strftime(formatted_date, sizeof(formatted_date), "%Y-%m-%dT%H:%M:%S", localtime(&t)); 138 | 139 | Serial.println(formatted_date); 140 | 141 | } 142 | entry.close(); 143 | } 144 | } 145 | 146 | void shellfn_reset(size_t argc, char* argv[]) { 147 | Serial.println("resetting"); 148 | NVIC_SystemReset(); 149 | } 150 | 151 | void shellfn_eeprom(size_t argc, char* argv[]) { 152 | if(argc < 2) { 153 | Serial.println("usage: `eeprom IDX` where IDX is an index into eeprom"); 154 | return; 155 | } 156 | int idx = atoi(argv[1]); 157 | Serial.println(String("eeprom idx\t")+idx+":\t0x"+String(EEPROM.read(idx), HEX)); 158 | } 159 | 160 | void shellfn_result(size_t argc, char* argv[]) { 161 | if(argc < 2) { 162 | Serial.println("usage: `results IDX` where IDX is a index into result array"); 163 | return; 164 | } 165 | 166 | int idx = atoi(argv[1]); 167 | if(idx >= analysis_results_len) { 168 | Serial.println(String("idx ")+idx+" >= "+analysis_results_len); 169 | } else { 170 | Serial.println(String("result idx\t")+idx); 171 | Serial.print("Raw:\t"); 172 | Serial.println(analysis_results[idx].uncal_z); 173 | Serial.print("Uncal gamma:\t"); 174 | Serial.println(compute_gamma(analysis_results[idx].uncal_z, 50)); 175 | Serial.print("Cal gamma:\t"); 176 | Serial.println(analyzer.calibrated_gamma(analysis_results[idx])); 177 | Serial.print("SWR:\t"); 178 | Serial.println(compute_swr(analyzer.calibrated_gamma(analysis_results[idx]))); 179 | } 180 | } 181 | 182 | void shellfn_results(size_t argc, char* argv[]) { 183 | for (size_t i=0; i 222 | extern uint32_t __StackTop; 223 | void shellfn_free(size_t argc, char* argv[]) { 224 | size_t free = freeMemory(); 225 | size_t used = mallinfo().arena + ((size_t*)__StackTop - &used); 226 | Serial.println("\ttotal\tused\tfree"); 227 | Serial.println(String("Mem:\t")+(free+used)+"\t"+used+"\t"+free); 228 | } 229 | 230 | void shellfn_dir(size_t argc, char* argv[]) { 231 | FsFile root; 232 | if (!root.open("/")) { 233 | Serial.println("could not open root!"); 234 | } else { 235 | print_directory(&root); 236 | root.close(); 237 | } 238 | } 239 | 240 | void shellfn_df(size_t argc, char* argv[]) { 241 | 242 | uint32_t blocks_per_cluster = sd.vol()->sectorsPerCluster(); 243 | uint32_t cluster_count = sd.vol()->clusterCount(); 244 | uint32_t free_cluster_count = sd.vol()->freeClusterCount(); 245 | Serial.println("Filesystem\t512 byte blocks\tUsed\tAvailable\tUse%"); 246 | Serial.print("/"); 247 | Serial.print("\t"); 248 | Serial.print((uint64_t)cluster_count * (uint64_t)blocks_per_cluster); 249 | Serial.print("\t"); 250 | Serial.print(((uint64_t)(cluster_count-free_cluster_count)*(uint64_t)blocks_per_cluster)); 251 | Serial.print("\t"); 252 | Serial.print((uint64_t)(free_cluster_count)*(uint64_t)blocks_per_cluster); 253 | Serial.print("\t"); 254 | Serial.print(((float)free_cluster_count)/((float)cluster_count)); 255 | Serial.print("%\n"); 256 | } 257 | 258 | void shellfn_fstype(size_t argc, char* argv[]) { 259 | uint8_t fat_type = sd.fatType(); 260 | switch(fat_type) { 261 | case FAT_TYPE_EXFAT: 262 | Serial.println("exFat"); 263 | break; 264 | case FAT_TYPE_FAT32: 265 | Serial.println("Fat32"); 266 | break; 267 | case FAT_TYPE_FAT16: 268 | Serial.println("Fat16"); 269 | break; 270 | default: 271 | Serial.println(String("unknown fat type: ")+fat_type); 272 | } 273 | } 274 | 275 | void shellfn_rm(size_t argc, char* argv[]) { 276 | 277 | if (argc < 2) { 278 | Serial.println("remove what file?"); 279 | return; 280 | } 281 | const char* target_name = argv[1]; 282 | if(strcmp(target_name, "/") == 0) { 283 | Serial.println("can't remove root"); 284 | return; 285 | } 286 | 287 | if(!sd.exists(target_name)) { 288 | Serial.println(String("no such file ")+target_name); 289 | return; 290 | } 291 | 292 | FsFile target; 293 | if(!target.open(target_name)) { 294 | Serial.println(String("could not open")+target_name); 295 | return; 296 | } 297 | if(target.isDirectory()) { 298 | FsFile child; 299 | if(target.openNext(&child)) { 300 | Serial.println(String(target_name)+" is a non-empty directory"); 301 | return; 302 | } 303 | } 304 | target.close(); 305 | 306 | if(!sd.remove(target_name)) { 307 | Serial.println("remove failed"); 308 | return; 309 | } 310 | Serial.println(String("removed ")+target_name); 311 | } 312 | 313 | void shellfn_mv(size_t argc, char* argv[]) { 314 | 315 | if (argc < 2) { 316 | Serial.println("move what file?"); 317 | return; 318 | } 319 | const char* target_name = argv[1]; 320 | if(strcmp(target_name, "/") == 0) { 321 | Serial.println("can't move root"); 322 | return; 323 | } 324 | 325 | if(argc < 3) { 326 | Serial.println("move it to what new name?"); 327 | return; 328 | } 329 | const char* new_name = argv[2]; 330 | 331 | if(!sd.exists(target_name)) { 332 | Serial.println(String("no such file ")+target_name); 333 | return; 334 | } 335 | 336 | FsFile target; 337 | if(!target.open(target_name)) { 338 | Serial.println(String("could not open")+target_name); 339 | return; 340 | } 341 | if(target.isDirectory()) { 342 | FsFile child; 343 | if(target.openNext(&child)) { 344 | Serial.println(String(target_name)+" is a non-empty directory"); 345 | return; 346 | } 347 | } 348 | target.close(); 349 | 350 | if(!sd.rename(target_name, new_name)) { 351 | Serial.println("move failed"); 352 | return; 353 | } 354 | Serial.println(String("moved ")+target_name+" to "+new_name); 355 | } 356 | 357 | void shellfn_touch(size_t argc, char* argv[]) { 358 | if (argc < 2) { 359 | Serial.println("touch what file?"); 360 | return; 361 | } 362 | const char* target_name = argv[1]; 363 | 364 | FsFile target; 365 | if(!target.open(target_name, O_RDWR | O_CREAT | O_AT_END)) { 366 | Serial.println(String("could not open ")+target_name+" for append"); 367 | return; 368 | } 369 | target.sync(); 370 | target.close(); 371 | 372 | Serial.println("touched"); 373 | } 374 | 375 | void shellfn_cat(size_t argc, char* argv[]) { 376 | if (argc < 2) { 377 | Serial.println("cat what file?"); 378 | return; 379 | } 380 | const char* target_name = argv[1]; 381 | 382 | FsFile target; 383 | if(!target.open(target_name, O_RDONLY)) { 384 | Serial.println(String("could not open ")+target_name+" for read-only"); 385 | return; 386 | } 387 | 388 | while(target.available()) { 389 | Serial.write(target.read()); 390 | } 391 | 392 | target.close(); 393 | } 394 | 395 | void shellfn_mkdir(size_t argc, char* argv[]) { 396 | if (argc < 2 || argc > 3) { 397 | Serial.println("usage: mkdir [-p] dirpath"); 398 | return; 399 | } 400 | 401 | const char* target_name = argv[1]; 402 | bool mk_parents = false; 403 | if (argc > 2) { 404 | if(strcmp("-p", argv[1]) == 0) { 405 | mk_parents = true; 406 | target_name = argv[2]; 407 | } else if(strcmp("-p", argv[2]) == 0) { 408 | mk_parents = true; 409 | } else { 410 | Serial.println("usage: mkdir [-p] dirpath"); 411 | return; 412 | } 413 | } 414 | 415 | FsFile root; 416 | if (!root.open("/")) { 417 | Serial.println("could not open root!"); 418 | } 419 | 420 | FsFile new_dir; 421 | if(!new_dir.mkdir(&root, target_name, mk_parents)) { 422 | Serial.println("error creating directory"); 423 | } 424 | 425 | root.close(); 426 | } 427 | 428 | void shellfn_pixel(size_t argc, char* argv[]) { 429 | //pixel x y [color] 430 | //without color, print the color of pixel x y 431 | //with color set pixel x y to color 432 | 433 | if (argc < 3) { 434 | Serial.println("usage: pixel x y [color]"); 435 | return; 436 | } 437 | int x = atoi(argv[1]); 438 | int y = atoi(argv[2]); 439 | if (argc > 3) { 440 | int color = atoi(argv[3]); 441 | Serial.println(String("drawing pixel at ")+x+","+y+" "+color); 442 | tft.drawPixel(x, y, color); 443 | } else { 444 | Serial.println(String("reading at ")+x+","+y); 445 | Serial.println(tft.readPixel(x, y)); 446 | } 447 | } 448 | 449 | template 450 | void write16(T &writer, const uint16_t v) { 451 | writer.write(((uint8_t *)&v)[0]); 452 | writer.write(((uint8_t *)&v)[1]); 453 | } 454 | 455 | template 456 | void write32(T &writer, const uint32_t v) { 457 | writer.write(((uint8_t *)&v)[0]); 458 | writer.write(((uint8_t *)&v)[1]); 459 | writer.write(((uint8_t *)&v)[2]); 460 | writer.write(((uint8_t *)&v)[3]); 461 | } 462 | 463 | template 464 | void write_color16_as_24(T &writer, uint16_t pixel) { 465 | // per AdafruitTFT lib color565 function (24bit color -> 16bit color): 466 | // ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) 467 | // pixel is 565 format 468 | uint8_t r = (pixel >> 8) & 0xF8; 469 | uint8_t g = (pixel >> 3) & 0xFC; 470 | uint8_t b = (pixel << 3) & 0xFF; 471 | 472 | writer.write(r); 473 | writer.write(g); 474 | writer.write(b); 475 | } 476 | 477 | void shellfn_screenshot(size_t argc, char* argv[]) { 478 | if (argc < 2) { 479 | Serial.println("usage: screenshot filepath"); 480 | return; 481 | } 482 | 483 | const char* target_name = argv[1]; 484 | 485 | const uint16_t bmp_depth = 24; 486 | const uint32_t width = tft.width(); 487 | const uint32_t height = tft.height(); 488 | const uint32_t image_offset = 14 + 40; 489 | // row_size has some padding 490 | // this calculation taken from Adafruit TFT bmp example 491 | // this assumes 24 bits per pixel (compare to bmp_depth above) 492 | const uint32_t row_size = (width * 3 + 3) & ~3; 493 | const uint32_t bitmap_size = row_size * height; 494 | const uint32_t file_size = bitmap_size + image_offset; 495 | const uint8_t padding = row_size - width*bmp_depth/8; 496 | 497 | FsFile target; 498 | if(!target.open(target_name, O_RDWR | O_CREAT | O_TRUNC)) { 499 | Serial.println(String("could not open ")+target_name+" for append"); 500 | return; 501 | } 502 | 503 | Serial.println("writing header"); 504 | // BMP header (14 bytes) 505 | // magic bytes 506 | target.write(0x42); 507 | target.write(0x4D); 508 | //file size 509 | write32(target, file_size); 510 | // creator bytes 511 | write32(target, 0u); 512 | // image offset 513 | write32(target, image_offset); 514 | 515 | // DIB header (Windows BITMAPINFO format) (40 bytes) 516 | // header size 517 | write32(target, 40u); 518 | // width/height 519 | write32(target, width); 520 | write32(target, height); 521 | // planes == 1 522 | write16(target, 1u); 523 | // bits per pixel 524 | write16(target, bmp_depth); 525 | // compression == 0 no compression 526 | write32(target, 0u); 527 | // raw bitmap size 528 | write32(target, bitmap_size); 529 | // horizontal/vertical resolutions 2835 ~72dpi 530 | write32(target, 2835); 531 | write32(target, 2835); 532 | // colors and important colors in palette, 0 is default 533 | write32(target, 0u); 534 | write32(target, 0u); 535 | 536 | Serial.println(String("file size: ")+file_size); 537 | Serial.println(String("image offset: ")+image_offset); 538 | 539 | Serial.println(String("rows: ")+height); 540 | Serial.println(String("cols: ")+width); 541 | Serial.println(String("bitmap size: ")+bitmap_size); 542 | 543 | Serial.println(String("rowsize: ")+row_size); 544 | Serial.println(String("padding: ")+padding); 545 | 546 | // pixel array (at last) 547 | // rows are padded to multiple of 32 bits (row_size) 548 | // rows are stored bottom to top 549 | // tft pixels are 16-bit 565 format and we need to explode that into 24-bit 550 | Serial.print("writing pixel data"); 551 | 552 | // unsigned value will wrap around to a large number after row 0, 553 | // so we check that row < height for the loop condition 554 | for(size_t row = height-1; row < height; row--) { 555 | for(size_t col = 0; col 1){ 592 | //assume argv[1] is 8601 593 | DateTime new_now(argv[1]); 594 | rtc.adjust(new_now); 595 | Serial.println("set time"); 596 | } 597 | } 598 | 599 | const char* SHELL_COMMANDS[] = { 600 | "help", 601 | "reset", 602 | "eeprom", 603 | "batt", 604 | "aref", 605 | "free", 606 | 607 | //fs stuff 608 | "dir", 609 | "df", 610 | "fstype", 611 | "rm", 612 | "mv", 613 | "touch", 614 | "cat", 615 | "mkdir", 616 | 617 | //tft stuff 618 | "pixel", 619 | "screenshot", 620 | 621 | //rtc stuff 622 | "date", 623 | 624 | //stuff specific to antenna analyzer 625 | "result", 626 | "results", 627 | "menu_state", 628 | }; 629 | 630 | 631 | 632 | void shellfn_help(size_t argc, char* argv[]) { 633 | Serial.println(String("available commands (")+sizeof(SHELL_COMMANDS)/sizeof(SHELL_COMMANDS[0])+"):"); 634 | for(size_t i=0; i 5 | #include 6 | #include 7 | #include 8 | 9 | #include "log.h" 10 | #include "analyzer.h" 11 | 12 | Logger persistence_logger("persistence"); 13 | 14 | // Tools for managing settings and results persistence 15 | 16 | enum SettingsListenerState { SETTINGS_START, SETTINGS_Z0, SETTINGS_CAL, SETTINGS_CAL_POINT, SETTINGS_CAL_FQ, SETTINGS_CAL_S, SETTINGS_CAL_S_R, SETTINGS_CAL_S_I, SETTINGS_CAL_O, SETTINGS_CAL_O_R, SETTINGS_CAL_O_I, SETTINGS_CAL_L, SETTINGS_CAL_L_R, SETTINGS_CAL_L_I }; 17 | 18 | class SettingsJsonListener : public JsonListener { 19 | public: 20 | SettingsJsonListener(size_t max_steps) { 21 | max_steps_ = max_steps; 22 | calibration_results_ = new CalibrationPoint[max_steps]; 23 | } 24 | 25 | ~SettingsJsonListener() { 26 | delete [] calibration_results_; 27 | } 28 | 29 | void initialize() { 30 | state_ = SETTINGS_START; 31 | has_error_ = false; 32 | calibration_len_ = 0; 33 | saw_z0_ = false; 34 | saw_end_ = false; 35 | } 36 | 37 | void complete() { 38 | if(!saw_end_) { 39 | persistence_logger.warn(F("never saw valid end of document")); 40 | has_error_ = true; 41 | } 42 | } 43 | 44 | void key(String k) { 45 | if (has_error_) { 46 | return; 47 | } 48 | switch(state_) { 49 | case SETTINGS_START: 50 | if (k == "z0") { 51 | state_ = SETTINGS_Z0; 52 | } else if(k == "calibration") { 53 | state_ = SETTINGS_CAL; 54 | } 55 | break; 56 | case SETTINGS_CAL_POINT: 57 | if (k == "fq") { 58 | state_ = SETTINGS_CAL_FQ; 59 | } else if (k == "cal_short") { 60 | state_ = SETTINGS_CAL_S; 61 | } else if (k == "cal_open") { 62 | state_ = SETTINGS_CAL_O; 63 | } else if (k == "cal_load") { 64 | state_ = SETTINGS_CAL_L; 65 | } 66 | break; 67 | default: 68 | // only allow keys in the root or inside a calibration point 69 | has_error_ = true; 70 | persistence_logger.warn(String("key \"")+k+"\" found in state "+state_); 71 | } 72 | } 73 | 74 | void value(String v) { 75 | if (has_error_) { 76 | return; 77 | } 78 | switch(state_) { 79 | case SETTINGS_START: 80 | case SETTINGS_CAL_POINT: 81 | break; 82 | case SETTINGS_Z0: 83 | z0_ = atof(v.c_str()); 84 | state_ = SETTINGS_START; 85 | saw_z0_ = true; 86 | break; 87 | case SETTINGS_CAL_FQ: 88 | calibration_results_[calibration_len_].fq = atoi(v.c_str()); 89 | state_ = SETTINGS_CAL_POINT; 90 | saw_fq_ = true; 91 | break; 92 | case SETTINGS_CAL_S_R: 93 | case SETTINGS_CAL_O_R: 94 | case SETTINGS_CAL_L_R: 95 | cal_val_.setReal(atof(v.c_str())); 96 | state_ += 1; 97 | break; 98 | case SETTINGS_CAL_S_I: 99 | case SETTINGS_CAL_O_I: 100 | case SETTINGS_CAL_L_I: 101 | cal_val_.setImag(atof(v.c_str())); 102 | break; 103 | default: 104 | has_error_ = true; 105 | persistence_logger.warn(String("value \"")+v+"\" found in state "+state_); 106 | } 107 | } 108 | 109 | void startArray() { 110 | if (has_error_) { 111 | return; 112 | } 113 | switch(state_) { 114 | case SETTINGS_START: 115 | case SETTINGS_CAL: 116 | break; 117 | case SETTINGS_CAL_S: 118 | case SETTINGS_CAL_O: 119 | case SETTINGS_CAL_L: 120 | state_++; 121 | break; 122 | default: 123 | has_error_ = true; 124 | persistence_logger.warn(String("start array found in state ")+state_); 125 | } 126 | } 127 | 128 | void endArray() { 129 | if (has_error_) { 130 | return; 131 | } 132 | switch(state_) { 133 | case SETTINGS_START: 134 | break; 135 | case SETTINGS_CAL: 136 | state_ = SETTINGS_START; 137 | break; 138 | case SETTINGS_CAL_S_I: 139 | calibration_results_[calibration_len_].cal_short = cal_val_; 140 | saw_cal_short_ = true; 141 | state_ = SETTINGS_CAL_POINT; 142 | break; 143 | case SETTINGS_CAL_O_I: 144 | calibration_results_[calibration_len_].cal_open = cal_val_; 145 | saw_cal_open_ = true; 146 | state_ = SETTINGS_CAL_POINT; 147 | break; 148 | case SETTINGS_CAL_L_I: 149 | calibration_results_[calibration_len_].cal_load = cal_val_; 150 | saw_cal_load_ = true; 151 | state_ = SETTINGS_CAL_POINT; 152 | break; 153 | default: 154 | has_error_ = true; 155 | persistence_logger.warn(String("end array found in state ")+state_); 156 | } 157 | 158 | } 159 | 160 | void startObject() { 161 | if (has_error_) { 162 | return; 163 | } 164 | switch(state_) { 165 | case SETTINGS_START: 166 | break; 167 | case SETTINGS_CAL: 168 | if (calibration_len_ == max_steps_) { 169 | has_error_ = true; 170 | persistence_logger.warn("too many calibration points"); 171 | } else { 172 | state_ = SETTINGS_CAL_POINT; 173 | saw_fq_ = false; 174 | saw_cal_short_ = false; 175 | saw_cal_open_ = false; 176 | saw_cal_load_ = false; 177 | } 178 | break; 179 | default: 180 | has_error_ = true; 181 | persistence_logger.warn(String("start object found in state ")+state_); 182 | } 183 | } 184 | 185 | void endObject() { 186 | if (has_error_) { 187 | return; 188 | } 189 | switch(state_) { 190 | case SETTINGS_START: 191 | break; 192 | case SETTINGS_CAL_POINT: 193 | if (!(saw_fq_ && saw_cal_short_ && saw_cal_open_ && saw_cal_load_)) { 194 | has_error_ = true; 195 | persistence_logger.warn(F("didn't see all required elements in calibration point")); 196 | persistence_logger.warn(String("point: ")+calibration_len_); 197 | } else { 198 | state_ = SETTINGS_CAL; 199 | calibration_len_++; 200 | } 201 | break; 202 | default: 203 | has_error_ = true; 204 | persistence_logger.warn(String("end object found in state ")+state_); 205 | } 206 | } 207 | 208 | void startDocument() { 209 | } 210 | 211 | void endDocument() { 212 | if (has_error_) { 213 | return; 214 | } 215 | if (!saw_z0_) { 216 | has_error_ = true; 217 | persistence_logger.warn(F("didn't see z0")); 218 | } 219 | if (state_ != SETTINGS_START) { 220 | has_error_ = true; 221 | persistence_logger.warn(String("end document in wrong state: ")+state_); 222 | } 223 | saw_end_ = true; 224 | } 225 | 226 | void whitespace(char c) { 227 | } 228 | 229 | bool has_error_; 230 | float z0_; 231 | size_t calibration_len_; 232 | CalibrationPoint* calibration_results_; 233 | private: 234 | uint8_t state_; 235 | size_t max_steps_; 236 | 237 | Complex cal_val_; 238 | bool saw_z0_; 239 | bool saw_fq_; 240 | bool saw_cal_short_; 241 | bool saw_cal_open_; 242 | bool saw_cal_load_; 243 | bool saw_end_; 244 | }; 245 | 246 | enum ResultsListenerState { RESULTS_START, RESULTS_POINT, RESULTS_FQ, RESULTS_Z, RESULTS_Z_R, RESULTS_Z_I }; 247 | 248 | class ResultsJsonListener : public JsonListener { 249 | public: 250 | ResultsJsonListener(size_t max_steps) { 251 | max_steps_ = max_steps; 252 | results_ = new AnalysisPoint[max_steps]; 253 | } 254 | 255 | ~ResultsJsonListener() { 256 | delete [] results_; 257 | } 258 | 259 | void initialize() { 260 | results_len_ = 0; 261 | state_ = RESULTS_START; 262 | has_error_ = false; 263 | saw_end_ = false; 264 | } 265 | 266 | void complete() { 267 | if(!saw_end_) { 268 | persistence_logger.warn("never saw valid end of document"); 269 | has_error_ = true; 270 | } 271 | } 272 | 273 | void key(String k) { 274 | if(has_error_) { 275 | return; 276 | } 277 | switch(state_) { 278 | case RESULTS_POINT: 279 | if (k == "fq") { 280 | state_ = RESULTS_FQ; 281 | } else if (k =="uncal_z") { 282 | state_ = RESULTS_Z; 283 | } 284 | break; 285 | default: 286 | has_error_ = true; 287 | persistence_logger.warn(String("key \"")+k+"\" found in state "+state_); 288 | } 289 | } 290 | 291 | void value(String v) { 292 | if(has_error_) { 293 | return; 294 | } 295 | switch(state_) { 296 | case RESULTS_FQ: 297 | results_[results_len_].fq = atoi(v.c_str()); 298 | state_ = RESULTS_POINT; 299 | saw_fq_ = true; 300 | break; 301 | case RESULTS_Z_R: 302 | results_[results_len_].uncal_z.setReal(atof(v.c_str())); 303 | state_ = RESULTS_Z_I; 304 | break; 305 | case RESULTS_Z_I: 306 | results_[results_len_].uncal_z.setImag(atof(v.c_str())); 307 | break; 308 | case RESULTS_POINT: 309 | break; 310 | default: 311 | has_error_ = true; 312 | persistence_logger.warn(String("value \"")+v+"\" found in state "+state_); 313 | } 314 | } 315 | 316 | void startArray() { 317 | if(has_error_) { 318 | return; 319 | } 320 | switch(state_) { 321 | case RESULTS_START: 322 | break; 323 | case RESULTS_Z: 324 | state_ = RESULTS_Z_R; 325 | break; 326 | default: 327 | has_error_ = true; 328 | persistence_logger.warn(String("start array found in state ")+state_); 329 | } 330 | } 331 | 332 | void endArray() { 333 | if(has_error_) { 334 | return; 335 | } 336 | switch(state_) { 337 | case RESULTS_START: 338 | break; 339 | case RESULTS_Z_I: 340 | state_ = RESULTS_POINT; 341 | saw_z_ = true; 342 | break; 343 | default: 344 | has_error_ = true; 345 | persistence_logger.warn(String("end array found in state ")+state_); 346 | } 347 | } 348 | 349 | void startObject() { 350 | if(has_error_) { 351 | return; 352 | } 353 | switch(state_) { 354 | case RESULTS_START: 355 | if (results_len_ == max_steps_) { 356 | has_error_ = true; 357 | persistence_logger.warn("too many result points"); 358 | } else { 359 | state_ = RESULTS_POINT; 360 | saw_fq_ = false; 361 | saw_z_ = false; 362 | } 363 | break; 364 | case RESULTS_POINT: 365 | break; 366 | default: 367 | has_error_ = true; 368 | persistence_logger.warn(String("start object found in state ")+state_); 369 | } 370 | } 371 | 372 | void endObject() { 373 | if(has_error_) { 374 | return; 375 | } 376 | switch(state_) { 377 | case RESULTS_POINT: 378 | if(!(saw_fq_ && saw_z_)) { 379 | has_error_ = true; 380 | persistence_logger.warn("didn't see all required fields in result point"); 381 | } else { 382 | results_len_++; 383 | } 384 | state_ = RESULTS_START; 385 | break; 386 | default: 387 | has_error_ = true; 388 | persistence_logger.warn(String("end object found in state ")+state_); 389 | } 390 | } 391 | 392 | void startDocument() { 393 | persistence_logger.info("startDocument"); 394 | } 395 | 396 | void endDocument() { 397 | if(state_ != RESULTS_START) { 398 | persistence_logger.warn(String("results doc ended not in correct state: ")+state_); 399 | has_error_ = true; 400 | } else { 401 | saw_end_ = true; 402 | } 403 | } 404 | 405 | void whitespace(char c) { 406 | } 407 | 408 | size_t results_len_; 409 | AnalysisPoint* results_; 410 | 411 | bool has_error_; 412 | private: 413 | uint8_t state_; 414 | size_t max_steps_; 415 | bool saw_fq_; 416 | bool saw_z_; 417 | bool saw_end_; 418 | }; 419 | 420 | #define DEFAULT_ANALYZER_PERSISTENCE_NAME "zeroii-analyzer" 421 | #define SETTINGS_PREFIX "settings_" 422 | #define RESULTS_PREFIX "results_" 423 | 424 | class AnalyzerPersistence { 425 | public: 426 | // save named settings 427 | bool save_settings(const char* name, const Analyzer* analyzer) { 428 | FsFile entry; 429 | if(!entry.open(&settings_dir_, name, O_WRONLY | O_CREAT | O_TRUNC)) { 430 | persistence_logger.error(String("settings saving could not open ")+name); 431 | return false; 432 | } 433 | 434 | char buf[32]; 435 | entry.write("{\"z0\":"); 436 | entry.write(dtostrf(analyzer->z0_, 1, 6, buf)); 437 | 438 | entry.write(",\"calibration\":["); 439 | bool is_first = true; 440 | for(size_t i; icalibration_len_; i++) { 441 | if(!is_first) { 442 | entry.write(","); 443 | } else { 444 | is_first = false; 445 | } 446 | entry.write("{\"fq\":"); 447 | entry.write(itoa(analyzer->calibration_results_[i].fq, buf, 10)); 448 | 449 | entry.write(",\"cal_short\":["); 450 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_short.real(), 1, 6, buf)); 451 | entry.write(","); 452 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_short.imag(), 1, 6, buf)); 453 | entry.write("]"); 454 | 455 | entry.write(",\"cal_open\":["); 456 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_open.real(), 1, 6, buf)); 457 | entry.write(","); 458 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_open.imag(), 1, 6, buf)); 459 | entry.write("]"); 460 | 461 | entry.write(",\"cal_load\":["); 462 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_load.real(), 1, 6, buf)); 463 | entry.write(","); 464 | entry.write(dtostrf(analyzer->calibration_results_[i].cal_load.imag(), 1, 6, buf)); 465 | entry.write("]"); 466 | 467 | entry.write("}"); 468 | } 469 | entry.write("]"); 470 | entry.write("}"); 471 | 472 | entry.close(); 473 | persistence_logger.info(String("saved settings to ")+name); 474 | return true; 475 | } 476 | 477 | bool load_settings(FsFile* entry, Analyzer* analyzer, size_t max_cal_len) { 478 | JsonStreamingParser parser; 479 | SettingsJsonListener listener(max_cal_len); 480 | listener.initialize(); 481 | parser.setListener(&listener); 482 | 483 | int c; 484 | while((c = entry->read()) >= 0) { 485 | parser.parse(c); 486 | } 487 | 488 | if(entry->available() > 0) { 489 | persistence_logger.error(String("failed to read settings file error ")+entry->getError()); 490 | return false; 491 | } 492 | listener.complete(); 493 | 494 | if (listener.has_error_) { 495 | persistence_logger.error("failed to load settings"); 496 | return false; 497 | } 498 | 499 | analyzer->z0_ = listener.z0_; 500 | for(size_t i; icalibration_results_[i] = listener.calibration_results_[i]; 502 | } 503 | analyzer->calibration_len_ = listener.calibration_len_; 504 | 505 | persistence_logger.info("loaded settings"); 506 | return true; 507 | } 508 | 509 | // load named settings 510 | bool load_settings(const char* name, Analyzer* analyzer, size_t max_cal_len) { 511 | FsFile entry; 512 | if(!entry.open(&settings_dir_, name, O_RDONLY)){ 513 | return false; 514 | } 515 | return load_settings(&entry, analyzer, max_cal_len) && entry.close(); 516 | } 517 | 518 | // save settings to automatically named file 519 | bool save_settings(const Analyzer* analyzer) { 520 | FsFile entry; 521 | char filename[128]; 522 | if(find_latest_file(&settings_dir_, &entry, SETTINGS_PREFIX)) { 523 | size_t filename_len = entry.getName(filename, sizeof(filename));; 524 | entry.close(); 525 | persistence_logger.info(String("latest file is ")+filename); 526 | persistence_logger.info(String("suffix is \"")+(filename+sizeof(SETTINGS_PREFIX))+("\"")); 527 | int file_number = str2int(filename+sizeof(SETTINGS_PREFIX)-1, filename_len-sizeof(SETTINGS_PREFIX)+1); 528 | persistence_logger.info(String("number is ")+file_number); 529 | (String(SETTINGS_PREFIX)+(file_number+1)+".json").toCharArray(filename, sizeof(filename)); 530 | persistence_logger.info(String("saving settings to additional settings file ")+filename); 531 | return save_settings(filename, analyzer); 532 | } else { 533 | (String(SETTINGS_PREFIX)+"0.json").toCharArray(filename, sizeof(filename)); 534 | persistence_logger.info(String("saving settings to first settings file ")+filename); 535 | return save_settings(filename, analyzer); 536 | } 537 | } 538 | 539 | // load most recent settings 540 | bool load_settings(Analyzer* analyzer, size_t max_cal_len) { 541 | FsFile entry; 542 | if(find_latest_file(&settings_dir_, &entry, SETTINGS_PREFIX)) { 543 | return load_settings(&entry, analyzer, max_cal_len) && entry.close(); 544 | } else { 545 | persistence_logger.warn("no settings found"); 546 | return false; 547 | } 548 | } 549 | 550 | // save named results 551 | bool save_results(const char* name, const AnalysisPoint* results, const size_t results_len) { 552 | FsFile entry; 553 | if(!entry.open(&results_dir_, name, O_WRONLY | O_CREAT | O_TRUNC)) { 554 | persistence_logger.error(String("could not open ") + name); 555 | return false; 556 | } 557 | 558 | entry.write("["); 559 | bool is_first = true; 560 | char buf[32]; 561 | for(size_t i; iread()) >= 0) { 590 | parser.parse(c); 591 | } 592 | 593 | if(entry->available() > 0) { 594 | persistence_logger.error(String("failed to read results file error ")+entry->getError()); 595 | return false; 596 | } 597 | listener.complete(); 598 | 599 | if(listener.has_error_) { 600 | persistence_logger.error("failed to load results"); 601 | return false; 602 | } 603 | 604 | for(size_t i=0; i '9') { 718 | return ret; 719 | } else { 720 | ret = ret * 10 + (str[i] - '0'); 721 | } 722 | } 723 | return ret; 724 | } 725 | 726 | bool find_latest_file(FsFile* directory, FsFile* entry, const char* prefix) { 727 | size_t prefix_len = strlen(prefix); 728 | // iterate through directory 729 | char filename[128]; 730 | char max_filename[128]; 731 | int max_file_number = -1; 732 | directory->rewindDirectory(); 733 | while(entry->openNext(directory, O_RDONLY)) { 734 | // check for an int at the end of each file name after prefix 735 | size_t name_len = entry->getName(filename, sizeof(filename)); 736 | if (strncmp(filename, prefix, min(name_len, prefix_len)) != 0) { 737 | entry->close(); 738 | continue; 739 | } 740 | 741 | int file_number = str2int(filename+prefix_len, name_len-prefix_len); 742 | if (file_number > max_file_number) { 743 | max_file_number = file_number; 744 | strcpy(max_filename, filename); 745 | } 746 | 747 | entry->close(); 748 | } 749 | 750 | if (max_file_number >= 0) { 751 | if(!entry->open(directory, max_filename, O_RDONLY)) { 752 | char dirname[128]; 753 | directory->getName(dirname, 128); 754 | persistence_logger.error(String("could not open ")+max_filename+" in "+dirname); 755 | return false; 756 | } 757 | return true; 758 | } else { 759 | return false; 760 | } 761 | } 762 | }; 763 | 764 | #endif //_SETTINGS_H 765 | -------------------------------------------------------------------------------- /zeroii-analyzer/zeroii-analyzer.ino: -------------------------------------------------------------------------------- 1 | #include "RigExpertZeroII_I2C.h" 2 | #include "Complex.h" 3 | 4 | #include // Core graphics library 5 | #include "Adafruit_TFTLCD.h" 6 | 7 | const __FlashStringHelper* test = F("foobar"); 8 | 9 | #include 10 | 11 | #include "RTClib.h" 12 | 13 | #include 14 | 15 | #include "log.h" 16 | #include "analyzer.h" 17 | #include "menu_manager.h" 18 | #include "persistence.h" 19 | 20 | Logger loop_logger("loop"); 21 | 22 | #define WAIT_FOR_SERIAL 0 23 | 24 | #define VBATT_ALPHA 0.05 25 | #define BATT_SENSE_PIN A3 26 | #define BATT_SENSE_PERIOD 3000 27 | float vbatt = 8.0; 28 | uint32_t last_vbatt = 0; 29 | 30 | float measure_vbatt() { 31 | // experimentally calibrated using a linear transformation 32 | uint16_t batt_raw = analogRead(BATT_SENSE_PIN); 33 | float vbatt = batt_raw * 0.0113 - 0.434; 34 | return vbatt; 35 | } 36 | void init_vbatt() { 37 | vbatt = measure_vbatt(); 38 | last_vbatt = millis(); 39 | } 40 | void update_vbatt() { 41 | vbatt = ((1.0-VBATT_ALPHA) * vbatt) + ((VBATT_ALPHA) * measure_vbatt()); 42 | last_vbatt = millis(); 43 | } 44 | 45 | // These are the four touchscreen analog pin 46 | #define YP A2 // must be an analog pin, use "An" notation! 47 | #define XM A3 // must be an analog pin, use "An" notation! 48 | #define YM 7 // can be a digital pin 49 | #define XP 8 // can be a digital pin 50 | 51 | // This is calibration data for the raw touch data to the screen coordinates 52 | #define TS_MINX 110 53 | #define TS_MINY 80 54 | #define TS_MAXX 900 55 | #define TS_MAXY 940 56 | 57 | #define MINPRESSURE 10 58 | #define MAXPRESSURE 1000 59 | 60 | #define YP A2 // must be an analog pin, use "An" notation! 61 | #define XM A3 // must be an analog pin, use "An" notation! 62 | #define YM 7 // can be a digital pin 63 | #define XP 8 // can be a digital pin 64 | 65 | // This is calibration data for the raw touch data to the screen coordinates 66 | #define TS_MINX 110 67 | #define TS_MINY 80 68 | #define TS_MAXX 900 69 | #define TS_MAXY 940 70 | 71 | #define MINPRESSURE 10 72 | #define MAXPRESSURE 1000 73 | 74 | // The control pins for the LCD can be assigned to any digital or 75 | // analog pins...but we'll use the analog pins as this allows us to 76 | // double up the pins with the touch screen (see the TFT paint example). 77 | #define LCD_CS 1 //A3 // Chip Select goes to Analog 3 78 | #define LCD_CD 0 // Command/Data goes to Analog 2 79 | #define LCD_WR A1 // LCD Write goes to Analog 1 80 | #define LCD_RD A0 // LCD Read goes to Analog 0 81 | #define LCD_RESET -1 82 | 83 | #define TFT_ROTATION 3 84 | #define BLACK 0x0000 85 | #define BLUE 0x001F 86 | #define RED 0xF800 87 | #define GREEN 0x07E0 88 | #define CYAN 0x07FF 89 | #define MAGENTA 0xF81F 90 | #define YELLOW 0xFFE0 91 | #define WHITE 0xFFFF 92 | #define GRAY 0xBDF7 93 | 94 | //Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST); 95 | Adafruit_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET); 96 | 97 | // Some display configs 98 | #define TITLE_TEXT_SIZE 2 99 | #define LABEL_TEXT_SIZE 1 100 | #define MENU_TEXT_SIZE 2 101 | #define MENU_ORIG_X 0 102 | #define MENU_ORIG_Y TITLE_TEXT_SIZE*8*2 103 | #define CONFIRM_ORIG_X TITLE_TEXT_SIZE*6 104 | #define CONFIRM_ORIG_Y TITLE_TEXT_SIZE*8*3 105 | 106 | #define ERROR_MESSAGE_DWELL_TIME 7000 107 | 108 | // For better pressure precision, we need to know the resistance 109 | // between X+ and X- Use any multimeter to read it 110 | // For the one we're using, its 300 ohms across the X plate 111 | //TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300); 112 | 113 | //100kHz 114 | #define MIN_FQ 100000 115 | //1GHz 116 | #define MAX_FQ 1000000000 117 | #define MAX_STEPS 128 118 | 119 | //TODO: cleanup graph.h so it doesn't have to be included here 120 | #include "graph.h" 121 | 122 | #define ZERO_I2C_ADDRESS 0x5B 123 | #define CLK A2 124 | #define DT A4 125 | #define SW A5 126 | 127 | #define Z0 50 128 | 129 | #define PROGRESS_METER_X 8*2*4 130 | #define PROGRESS_METER_Y 8*2*4 131 | #define PROGRESS_METER_WIDTH (tft.width()-PROGRESS_METER_Y*2) 132 | 133 | // holds the most recent set of analysis results, initialize to zero length 134 | // array so we can realloc it below 135 | size_t analysis_results_len = 0; 136 | AnalysisPoint analysis_results[MAX_STEPS]; 137 | 138 | size_t calibration_len = 0; 139 | CalibrationPoint calibration_results[MAX_STEPS]; 140 | 141 | Analyzer analyzer(Z0, calibration_results); 142 | 143 | AnalyzerPersistence persistence; 144 | 145 | void initialize_progress_meter(String label) { 146 | tft.fillRect(PROGRESS_METER_X, PROGRESS_METER_Y, PROGRESS_METER_WIDTH, 8*2*2, BLACK); 147 | tft.setTextSize(2); 148 | tft.setCursor(PROGRESS_METER_X, PROGRESS_METER_Y); 149 | tft.print(label); 150 | tft.drawRect(PROGRESS_METER_X, PROGRESS_METER_Y+8*2, PROGRESS_METER_WIDTH, 7*2, WHITE); 151 | } 152 | 153 | void draw_progress_meter(size_t total, size_t current, size_t min_value=0) { 154 | tft.fillRect(PROGRESS_METER_X, PROGRESS_METER_Y+8*2, PROGRESS_METER_WIDTH, 7*2, BLACK); 155 | tft.drawRect(PROGRESS_METER_X, PROGRESS_METER_Y+8*2, PROGRESS_METER_WIDTH, 7*2, WHITE); 156 | tft.fillRect(PROGRESS_METER_X, PROGRESS_METER_Y+8*2, PROGRESS_METER_WIDTH*(current-min_value)/(total-min_value), 7*2, WHITE); 157 | tft.fillRect(PROGRESS_METER_X, PROGRESS_METER_Y+8*2*2, PROGRESS_METER_WIDTH, 8*2, BLACK); 158 | tft.setCursor(PROGRESS_METER_X, PROGRESS_METER_Y+8*2*2); 159 | tft.print(current); 160 | if (min_value == 0) { 161 | tft.print("/"); 162 | tft.print(total); 163 | } 164 | } 165 | 166 | RTC_DS3231 rtc; 167 | 168 | // Call back for file timestamps. Only called for file create and sync(). 169 | void date_callback(uint16_t* date, uint16_t* time) { 170 | DateTime now = rtc.now(); 171 | // Return date using FS_DATE macro to format fields. 172 | *date = FS_DATE(now.year(), now.month(), now.day()); 173 | 174 | // Return time using FS_TIME macro to format fields. 175 | *time = FS_TIME(now.hour(), now.minute(), now.second()); 176 | } 177 | 178 | SdFs sd; 179 | SerialWombatChip sw; 180 | 181 | SerialWombatQuadEnc quad_enc(sw); 182 | SerialWombatDebouncedInput debounced_input(sw); 183 | 184 | int32_t turn = 0; 185 | uint16_t last_quad_enc = 32768; 186 | bool click = false; 187 | 188 | enum MOPT { 189 | MOPT_ANALYZE, 190 | MOPT_FQ, 191 | MOPT_RESULTS, 192 | MOPT_SETTINGS, 193 | 194 | MOPT_FQCENTER, 195 | MOPT_FQWINDOW, 196 | MOPT_FQSTART, 197 | MOPT_FQEND, 198 | MOPT_FQBAND, 199 | MOPT_FQSTEPS, 200 | 201 | MOPT_SWR, 202 | MOPT_SMITH, 203 | MOPT_SAVE_RESULTS, 204 | MOPT_LOAD_RESULTS, 205 | 206 | MOPT_CALIBRATE, 207 | MOPT_Z0, 208 | MOPT_SAVE_SETTINGS, 209 | MOPT_LOAD_SETTINGS, 210 | MOPT_ZOOM_SMITH, 211 | 212 | MOPT_BACK, 213 | }; 214 | 215 | const MenuOption fq_menu_options[] = { 216 | MenuOption(F("Fq Start"), MOPT_FQSTART, NULL), 217 | MenuOption(F("Fq End"), MOPT_FQEND, NULL), 218 | MenuOption(F("Fq Center"), MOPT_FQCENTER, NULL), 219 | MenuOption(F("Fq Range"), MOPT_FQWINDOW, NULL), 220 | MenuOption(F("Fq Band"), MOPT_FQBAND, NULL), 221 | MenuOption(F("Steps"), MOPT_FQSTEPS, NULL), 222 | MenuOption(F("Back"), MOPT_BACK, NULL), 223 | }; 224 | Menu fq_menu(NULL, fq_menu_options, sizeof(fq_menu_options)/sizeof(fq_menu_options[0])); 225 | 226 | const MenuOption results_menu_options[] { 227 | MenuOption(F("SWR graph"), MOPT_SWR, NULL), 228 | MenuOption(F("Smith chart"), MOPT_SMITH, NULL), 229 | MenuOption(F("Save Results"), MOPT_SAVE_RESULTS, NULL), 230 | MenuOption(F("Load Results"), MOPT_LOAD_RESULTS, NULL), 231 | MenuOption(F("Back"), MOPT_BACK, NULL), 232 | }; 233 | Menu results_menu(NULL, results_menu_options, sizeof(results_menu_options)/sizeof(results_menu_options[0])); 234 | 235 | const MenuOption settings_menu_options[] = { 236 | MenuOption(F("Calibration"), MOPT_CALIBRATE, NULL), 237 | MenuOption(F("Z0"), MOPT_Z0, NULL), 238 | MenuOption(F("Save Settings"), MOPT_SAVE_SETTINGS, NULL), 239 | MenuOption(F("Load Settings"), MOPT_LOAD_SETTINGS, NULL), 240 | MenuOption(F("Zoom Smith Chart"), MOPT_ZOOM_SMITH, NULL), 241 | MenuOption(F("Back"), MOPT_BACK, NULL), 242 | }; 243 | Menu settings_menu(NULL, settings_menu_options, sizeof(settings_menu_options)/sizeof(settings_menu_options[0])); 244 | 245 | const MenuOption root_menu_options[] = { 246 | MenuOption(F("Analyze"), MOPT_ANALYZE, NULL), 247 | MenuOption(F("Frequencies"), MOPT_FQ, &fq_menu), 248 | MenuOption(F("Results"), MOPT_RESULTS, &results_menu), 249 | MenuOption(F("Settings"), MOPT_SETTINGS, &settings_menu), 250 | }; 251 | Menu root_menu(NULL, root_menu_options, sizeof(root_menu_options)/sizeof(root_menu_options[0])); 252 | 253 | MenuManager menu_manager(&root_menu); 254 | 255 | uint32_t start_fq = MIN_FQ; 256 | uint32_t end_fq = MAX_FQ; 257 | uint16_t step_count = MAX_STEPS/2; 258 | 259 | bool set_analysis_from_calibration() { 260 | if(analyzer.calibration_len_ == 0) { 261 | return false; 262 | } 263 | 264 | start_fq = constrain(analyzer.calibration_results_[0].fq, MIN_FQ, MAX_FQ); 265 | end_fq = constrain(analyzer.calibration_results_[analyzer.calibration_len_-1].fq, MIN_FQ, MAX_FQ); 266 | step_count = constrain(analyzer.calibration_len_, 1, MAX_STEPS); 267 | return true; 268 | } 269 | 270 | void draw_title() { 271 | tft.fillRect(0, 0, tft.width(), 8*TITLE_TEXT_SIZE, BLACK); 272 | tft.setCursor(0,0); 273 | tft.setTextSize(TITLE_TEXT_SIZE); 274 | 275 | tft.print(frequency_formatter(start_fq) + "-" + frequency_formatter(end_fq)); 276 | tft.print(" St:"); 277 | tft.print(step_count); 278 | tft.print(" Z0:"); 279 | tft.print((uint32_t)analyzer.z0_); 280 | 281 | draw_error(); 282 | } 283 | 284 | uint32_t last_error_print = 0; 285 | uint32_t last_error_time = 0; 286 | char error_message[64]; 287 | 288 | void clear_error_display() { 289 | current_error(""); 290 | tft.fillRect(0, tft.height()-8*TITLE_TEXT_SIZE, tft.width(), 8*TITLE_TEXT_SIZE, BLACK); 291 | } 292 | 293 | void current_error(const char* error_msg) { 294 | strncpy(error_message, error_msg, sizeof(error_message)); 295 | last_error_time = millis(); 296 | draw_error(); 297 | } 298 | 299 | void draw_error() { 300 | if (error_message[0] && millis() - last_error_time < ERROR_MESSAGE_DWELL_TIME) { 301 | tft.fillRect(0, tft.height()-8*TITLE_TEXT_SIZE, tft.width(), 8*TITLE_TEXT_SIZE, BLACK); 302 | tft.setCursor(0, tft.height()-8*TITLE_TEXT_SIZE); 303 | tft.setTextSize(TITLE_TEXT_SIZE); 304 | tft.setTextColor(RED); 305 | 306 | tft.print(error_message); 307 | 308 | tft.setTextColor(WHITE); 309 | } 310 | } 311 | 312 | void draw_vbatt() { 313 | if (vbatt >= 100 || vbatt < 0) { 314 | //something weird is going on 315 | return; 316 | } 317 | tft.fillRect(tft.width() - 6*TITLE_TEXT_SIZE*5, 0, 6*TITLE_TEXT_SIZE*5, 8*TITLE_TEXT_SIZE, BLACK); 318 | tft.setCursor(tft.width() - 6*TITLE_TEXT_SIZE*5, 0); 319 | tft.setTextSize(TITLE_TEXT_SIZE); 320 | if (vbatt >= 10) { 321 | tft.print((int)vbatt); 322 | tft.print("."); 323 | tft.print((int)vbatt*10%10); 324 | } else { 325 | tft.print(vbatt); 326 | } 327 | tft.print("v"); 328 | } 329 | 330 | void clear_menu(Menu* current_menu, int16_t menu_x=MENU_ORIG_X, int16_t menu_y=MENU_ORIG_Y) { 331 | int16_t w = 1; 332 | int16_t h = 8*current_menu->option_count*MENU_TEXT_SIZE; 333 | for(int i=0; ioption_count; i++) { 334 | if(current_menu->options[i].label.length()+1 > w) { 335 | w = current_menu->options[i].label.length()+1; 336 | } 337 | } 338 | w = 6*MENU_TEXT_SIZE*w; 339 | tft.fillRect(menu_x, menu_y, w, h, BLACK); 340 | } 341 | 342 | // draws menu on the tft 343 | void draw_menu(Menu* current_menu, int current_option, bool fresh=true, int16_t menu_x=MENU_ORIG_X, int16_t menu_y=MENU_ORIG_Y) { 344 | if (fresh) { 345 | clear_menu(current_menu, menu_x, menu_y); 346 | } else { 347 | // just blank the cursor area 348 | tft.fillRect(menu_x, menu_y, 6*MENU_TEXT_SIZE, 8*current_menu->option_count*MENU_TEXT_SIZE, BLACK); 349 | } 350 | tft.setTextSize(MENU_TEXT_SIZE); 351 | for(int i=0; ioption_count; i++) { 352 | tft.setCursor(menu_x, menu_y+i*8*MENU_TEXT_SIZE); 353 | // either: 354 | // this is the current option 355 | // this is the selected option 356 | // this is just an option 357 | if (current_option >= 0 && current_menu->options[i].option_id == current_option) { 358 | tft.print("+"); 359 | } else if(current_menu->selected_option == i) { 360 | tft.print(">"); 361 | } else { 362 | tft.print(" "); 363 | } 364 | tft.print(current_menu->options[i].label); 365 | } 366 | } 367 | 368 | void menu_back() { 369 | loop_logger.debug(F("menu back")); 370 | leave_option(menu_manager.current_option_); 371 | clear_menu(menu_manager.current_menu_); 372 | menu_manager.collapse(); 373 | tft.fillScreen(BLACK); 374 | draw_menu(menu_manager.current_menu_, menu_manager.current_option_, true); 375 | draw_title(); 376 | } 377 | 378 | typedef String (*int_formatter) (const int32_t); 379 | String decimal_int_formatter(const int32_t v) { 380 | return String(v); 381 | } 382 | 383 | int32_t set_user_value(int32_t current_value, int32_t min_value, int32_t max_value, String label, int32_t multiplier=1, int_formatter formatter=&decimal_int_formatter) { 384 | // clicking backs out of this option 385 | if (click) { 386 | tft.fillScreen(BLACK); 387 | menu_back(); 388 | return current_value; 389 | } 390 | // rotating changes value 391 | if (turn != 0) { 392 | // inc scales with how fast you're turning it 393 | // inc is direction * 2 ^ speed / 10 394 | int32_t inc = turn * multiplier;// * (uint32_t(1) << (uint32_t(encoder.speed()/2))) * multiplier; 395 | int32_t updated_value = constrain(current_value + inc, min_value, max_value); 396 | tft.setTextSize(3); 397 | tft.fillRect(0, 5*2*8, tft.width(), 2*8*3, BLACK); 398 | tft.setCursor(0, 5*2*8); 399 | tft.print(label); 400 | tft.println(":"); 401 | tft.print(" "); 402 | tft.println(formatter(updated_value)); 403 | return updated_value; 404 | } 405 | return current_value; 406 | } 407 | 408 | /************* 409 | * A set of state machines to handle different kinds of activities that cross 410 | * loop invocations. 411 | ************/ 412 | #include "process.h" 413 | AnalysisProcessor* analysis_processor = NULL; 414 | Calibrator* calibrator = NULL; 415 | FqSetter* fq_setter = NULL; 416 | BandSetter* band_setter = NULL; 417 | UserValueSetter* value_setter = NULL; 418 | FileBrowser* file_browser = NULL; 419 | ConfirmDialog* confirm_dialog = NULL; 420 | GraphContext* graph_context = NULL; 421 | bool zoom_smith = true; 422 | 423 | bool browse_progress() { 424 | return file_browser->choose_file(); 425 | } 426 | 427 | void enter_option(int32_t option_id) { 428 | loop_logger.debug(String("entering ")+option_id); 429 | switch(option_id) { 430 | case MOPT_ANALYZE: 431 | analysis_results_len = step_count; 432 | analysis_processor = new AnalysisProcessor(); 433 | if(analysis_processor == NULL) { 434 | loop_logger.error(F("could not make an AnalysisProcessor")); 435 | } 436 | analysis_processor->initialize(start_fq, end_fq, step_count, analysis_results); 437 | break; 438 | case MOPT_FQCENTER: { 439 | int32_t centerFq = start_fq + (end_fq-start_fq)/2; 440 | fq_setter = new FqSetter(); 441 | if(fq_setter == NULL) { 442 | loop_logger.error("could not make an FqSetter"); 443 | } 444 | fq_setter->initialize(centerFq); 445 | break; 446 | } 447 | case MOPT_FQWINDOW: { 448 | int32_t rangeFq = end_fq - start_fq; 449 | fq_setter = new FqSetter(); 450 | if(fq_setter == NULL) { 451 | loop_logger.error("could not make an FqSetter"); 452 | } 453 | fq_setter->initialize(rangeFq); 454 | break; 455 | } 456 | case MOPT_FQSTART: { 457 | assert(fq_setter == NULL); 458 | fq_setter = new FqSetter(); 459 | if(fq_setter == NULL) { 460 | loop_logger.error("could not make an FqSetter"); 461 | } 462 | fq_setter->initialize(start_fq); 463 | break; 464 | } 465 | case MOPT_FQEND: { 466 | fq_setter = new FqSetter(); 467 | if(fq_setter == NULL) { 468 | loop_logger.error("could not make an FqSetter"); 469 | } 470 | fq_setter->initialize(end_fq); 471 | break; 472 | } 473 | case MOPT_FQBAND: 474 | band_setter = new BandSetter(); 475 | if(band_setter == NULL) { 476 | loop_logger.error("could not make an BandSetter"); 477 | } 478 | band_setter->initialize(start_fq, end_fq); 479 | break; 480 | case MOPT_FQSTEPS: 481 | value_setter = new UserValueSetter(); 482 | value_setter->initialize("Steps", step_count, 1, 128); 483 | break; 484 | case MOPT_Z0: 485 | value_setter = new UserValueSetter(); 486 | value_setter->initialize("Z0", analyzer.z0_, 1, 999); 487 | break; 488 | case MOPT_CALIBRATE: 489 | calibration_len = step_count; 490 | calibrator = new Calibrator(&analyzer); 491 | if(calibrator == NULL) { 492 | loop_logger.error("could not make a Calibrator"); 493 | } 494 | calibrator->initialize(start_fq, end_fq, step_count, calibration_results); 495 | break; 496 | case MOPT_SWR: { 497 | graph_context = new GraphContext(analysis_results, analysis_results_len, &analyzer); 498 | if(graph_context == NULL) { 499 | loop_logger.error("could not make a GraphContext"); 500 | } 501 | graph_context->initialize_swr(); 502 | graph_context->graph_swr(); 503 | graph_context->draw_swr_pointer(); 504 | graph_context->draw_swr_title(); 505 | break; 506 | } 507 | case MOPT_SMITH: { 508 | graph_context = new GraphContext(analysis_results, analysis_results_len, &analyzer); 509 | if(graph_context == NULL) { 510 | loop_logger.error("could not make a GraphContext"); 511 | } 512 | graph_context->initialize_smith(zoom_smith); 513 | graph_context->graph_smith(); 514 | graph_context->draw_smith_pointer(); 515 | graph_context->draw_smith_title(); 516 | break; 517 | } 518 | case MOPT_SAVE_RESULTS: 519 | file_browser = new FileBrowser(); 520 | if(file_browser == NULL) { 521 | loop_logger.error("could not make a FileBrowser"); 522 | } 523 | file_browser->initialize(&persistence.results_dir_, true); 524 | confirm_dialog = new ConfirmDialog(); 525 | if(confirm_dialog == NULL) { 526 | loop_logger.error("could not make a ConfirmDialog"); 527 | } 528 | confirm_dialog->initialize(&browse_progress); 529 | break; 530 | case MOPT_LOAD_RESULTS: 531 | file_browser = new FileBrowser(); 532 | if(file_browser == NULL) { 533 | loop_logger.error("could not make a FileBrowser"); 534 | } 535 | file_browser->initialize(&persistence.results_dir_, false); 536 | confirm_dialog = new ConfirmDialog(); 537 | if(confirm_dialog == NULL) { 538 | loop_logger.error("could not make a ConfirmDialog"); 539 | } 540 | confirm_dialog->initialize(&browse_progress); 541 | break; 542 | case MOPT_SAVE_SETTINGS: 543 | file_browser = new FileBrowser(); 544 | if(file_browser == NULL) { 545 | loop_logger.error("could not make a FileBrowser"); 546 | } 547 | file_browser->initialize(&persistence.settings_dir_, true); 548 | confirm_dialog = new ConfirmDialog(); 549 | if(confirm_dialog == NULL) { 550 | loop_logger.error("could not make a ConfirmDialog"); 551 | } 552 | confirm_dialog->initialize(&browse_progress); 553 | break; 554 | case MOPT_LOAD_SETTINGS: 555 | file_browser = new FileBrowser(); 556 | if(file_browser == NULL) { 557 | loop_logger.error("could not make a FileBrowser"); 558 | } 559 | file_browser->initialize(&persistence.settings_dir_, false); 560 | confirm_dialog = new ConfirmDialog(); 561 | if(confirm_dialog == NULL) { 562 | loop_logger.error("could not make a ConfirmDialog"); 563 | } 564 | confirm_dialog->initialize(&browse_progress); 565 | break; 566 | case MOPT_ZOOM_SMITH: 567 | value_setter = new UserValueSetter(); 568 | value_setter->initialize("Zoom Smith Chart", zoom_smith, 0, 1); 569 | break; 570 | } 571 | } 572 | 573 | void leave_option(int32_t option_id) { 574 | loop_logger.debug(String("leaving ")+option_id); 575 | switch(option_id) { 576 | case MOPT_FQCENTER: 577 | loop_logger.info(String("setting center fq to: ") + fq_setter->fq()); 578 | // move [start_fq, end_fq] so it's centered on desired value 579 | /*start_fq = constrain(fq_setter->fq() - (end_fq - start_fq)/2, MIN_FQ, MAX_FQ); 580 | end_fq = constrain(fq_setter->fq() + (end_fq - start_fq)/2, MIN_FQ, MAX_FQ);*/ 581 | delete fq_setter; 582 | fq_setter = NULL; 583 | break; 584 | case MOPT_FQWINDOW: { 585 | loop_logger.info(String("setting window fq to: ") + fq_setter->fq()); 586 | // narrow/expand [start_fq, end_fq] remaining centered 587 | int32_t cntFq = start_fq + (end_fq - start_fq)/2; 588 | start_fq = constrain(cntFq - fq_setter->fq()/2, MIN_FQ, MAX_FQ); 589 | end_fq = constrain(cntFq + fq_setter->fq()/2, MIN_FQ, MAX_FQ); 590 | delete fq_setter; 591 | fq_setter = NULL; 592 | break; 593 | } 594 | case MOPT_FQSTART: 595 | loop_logger.info(String("setting start fq to: ") + fq_setter->fq()); 596 | start_fq = fq_setter->fq(); 597 | end_fq = constrain(end_fq, start_fq+1, MAX_FQ); 598 | delete fq_setter; 599 | fq_setter = NULL; 600 | break; 601 | case MOPT_FQEND: 602 | loop_logger.info(String("setting end fq to: ") + fq_setter->fq()); 603 | end_fq = fq_setter->fq(); 604 | start_fq = constrain(start_fq, MIN_FQ, end_fq-1); 605 | delete fq_setter; 606 | fq_setter = NULL; 607 | break; 608 | case MOPT_FQBAND: 609 | band_setter->band(&start_fq, &end_fq); 610 | loop_logger.info(String("setting start/end to: ") + start_fq + "/" + end_fq); 611 | delete band_setter; 612 | band_setter = NULL; 613 | break; 614 | case MOPT_FQSTEPS: 615 | loop_logger.info(String("setting steps to: ") + value_setter->value_); 616 | step_count = value_setter->value_; 617 | delete value_setter; 618 | value_setter = NULL; 619 | break; 620 | case MOPT_Z0: 621 | loop_logger.info(String("setting z0 to: ") + value_setter->value_); 622 | analyzer.z0_ = value_setter->value_; 623 | delete value_setter; 624 | value_setter = NULL; 625 | break; 626 | case MOPT_ANALYZE: 627 | delete analysis_processor; 628 | analysis_processor = NULL; 629 | break; 630 | case MOPT_CALIBRATE: 631 | delete calibrator; 632 | calibrator = NULL; 633 | analyzer.calibration_len_ = calibration_len; 634 | break; 635 | case MOPT_SAVE_RESULTS: 636 | if(confirm_dialog->confirm()) { 637 | if(file_browser->is_new()) { 638 | if(!persistence.save_results(analysis_results, analysis_results_len)) { 639 | loop_logger.error(F("could not save results")); 640 | current_error("could not save results"); 641 | } 642 | } else { 643 | char filename[128]; 644 | file_browser->file(filename, sizeof(filename)); 645 | if(!persistence.save_results(filename, analysis_results, analysis_results_len)) { 646 | loop_logger.error(F("could not save results")); 647 | current_error("could not save results"); 648 | } 649 | } 650 | } else { 651 | loop_logger.info(F("cancelled saving results")); 652 | current_error("cancelled saving results"); 653 | } 654 | delete file_browser; 655 | file_browser = NULL; 656 | delete confirm_dialog; 657 | confirm_dialog = NULL; 658 | break; 659 | case MOPT_LOAD_RESULTS: 660 | if(confirm_dialog->confirm()) { 661 | char filename[128]; 662 | file_browser->file(filename, sizeof(filename)); 663 | if(!persistence.load_results(filename, analysis_results, &analysis_results_len, MAX_STEPS)) { 664 | loop_logger.error(F("could not load results")); 665 | current_error("could not load results"); 666 | } 667 | } else { 668 | loop_logger.info(F("cancelled loading results")); 669 | current_error("cancelled loading results"); 670 | } 671 | delete file_browser; 672 | file_browser = NULL; 673 | delete confirm_dialog; 674 | confirm_dialog = NULL; 675 | break; 676 | case MOPT_SAVE_SETTINGS: 677 | if(confirm_dialog->confirm()) { 678 | if(file_browser->is_new()) { 679 | if(!persistence.save_settings(&analyzer)) { 680 | loop_logger.error(F("could not save settings")); 681 | current_error("could not save settings"); 682 | } 683 | } else { 684 | char filename[128]; 685 | file_browser->file(filename, sizeof(filename)); 686 | if(!persistence.save_settings(filename, &analyzer)) { 687 | loop_logger.error(F("could not save settings")); 688 | current_error("could not save settings"); 689 | } 690 | } 691 | } else { 692 | loop_logger.info(F("cancelled saving settings")); 693 | current_error("cancelled saving settings"); 694 | } 695 | delete file_browser; 696 | file_browser = NULL; 697 | delete confirm_dialog; 698 | confirm_dialog = NULL; 699 | break; 700 | case MOPT_LOAD_SETTINGS: 701 | if(confirm_dialog->confirm()) { 702 | char filename[128]; 703 | file_browser->file(filename, sizeof(filename)); 704 | if(!persistence.load_settings(filename, &analyzer, MAX_STEPS)) { 705 | loop_logger.error(F("could not load settings")); 706 | current_error("could not load settings"); 707 | } else { 708 | set_analysis_from_calibration(); 709 | } 710 | } else { 711 | loop_logger.info(F("cancelled loading settings")); 712 | current_error("cancelled loading settings"); 713 | } 714 | delete file_browser; 715 | file_browser = NULL; 716 | delete confirm_dialog; 717 | confirm_dialog = NULL; 718 | break; 719 | case MOPT_ZOOM_SMITH: 720 | loop_logger.info(String("setting zoom smith: ") + value_setter->value_); 721 | zoom_smith = value_setter->value_; 722 | delete value_setter; 723 | value_setter = NULL; 724 | break; 725 | case MOPT_SWR: 726 | delete graph_context; 727 | graph_context = NULL; 728 | break; 729 | case MOPT_SMITH: 730 | delete graph_context; 731 | graph_context = NULL; 732 | break; 733 | } 734 | } 735 | 736 | void choose_option() { 737 | clear_menu(menu_manager.current_menu_); 738 | menu_manager.expand(); 739 | draw_menu(menu_manager.current_menu_, menu_manager.current_option_); 740 | enter_option(menu_manager.current_option_); 741 | } 742 | 743 | void handle_option() { 744 | switch(menu_manager.current_option_) { 745 | case -1: 746 | // clicking does whatever the cursor is on in the menu 747 | // we'll handle that action on the next loop 748 | if (click) { 749 | choose_option(); 750 | } else if (turn != 0) { 751 | menu_manager.select_rel(turn); 752 | draw_menu(menu_manager.current_menu_, menu_manager.current_option_, false); 753 | } 754 | break; 755 | case MOPT_BACK: 756 | // need to back out of being in BACK and back out of parent menu 757 | clear_menu(menu_manager.current_menu_); 758 | menu_manager.collapse(); 759 | menu_back(); 760 | break; 761 | case MOPT_ANALYZE: 762 | if (analysis_processor->analyze()) { 763 | menu_back(); 764 | if(!menu_manager.select_option(MOPT_SWR)) { 765 | loop_logger.error(F("could not find SWR option")); 766 | } else { 767 | choose_option(); 768 | } 769 | } 770 | break; 771 | case MOPT_SWR: 772 | if (click) { 773 | menu_back(); 774 | } else if (turn != 0 && analysis_results_len > 0) { 775 | // move the "pointer" on the swr graph 776 | graph_context->incr_swri(turn); 777 | graph_context->draw_swr_pointer(); 778 | graph_context->draw_swr_title(); 779 | } 780 | break; 781 | case MOPT_SMITH: 782 | if (click) { 783 | menu_back(); 784 | } else if (turn != 0 && analysis_results_len > 0) { 785 | // move the "pointer" on the smith chart 786 | graph_context->incr_swri(turn); 787 | graph_context->draw_smith_pointer(); 788 | graph_context->draw_smith_title(); 789 | } 790 | break; 791 | case MOPT_SAVE_RESULTS: 792 | case MOPT_LOAD_RESULTS: 793 | case MOPT_SAVE_SETTINGS: 794 | case MOPT_LOAD_SETTINGS: 795 | if(confirm_dialog->progress()) { 796 | menu_back(); 797 | } 798 | break; 799 | case MOPT_FQCENTER: 800 | if(fq_setter->set_fq_value(MIN_FQ, MAX_FQ, "Center Frequency")) { 801 | menu_back(); 802 | } 803 | break; 804 | case MOPT_FQWINDOW: 805 | if(fq_setter->set_fq_value(0, MAX_FQ/2, "Frequency Range")) { 806 | menu_back(); 807 | } 808 | break; 809 | case MOPT_FQSTART: 810 | if(fq_setter->set_fq_value(MIN_FQ, MAX_FQ, "Start Frequency")) { 811 | menu_back(); 812 | } 813 | break; 814 | case MOPT_FQEND: 815 | if(fq_setter->set_fq_value(MIN_FQ, MAX_FQ, "End Frequency")) { 816 | menu_back(); 817 | } 818 | break; 819 | case MOPT_FQBAND: 820 | if (band_setter->set_band()) { 821 | menu_back(); 822 | } 823 | break; 824 | case MOPT_FQSTEPS: 825 | if(value_setter->set_value()) { 826 | menu_back(); 827 | } 828 | break; 829 | case MOPT_CALIBRATE: { 830 | if(calibrator->calibration_step()) { 831 | menu_back(); 832 | } 833 | break; 834 | } 835 | case MOPT_Z0: 836 | if(value_setter->set_value()) { 837 | menu_back(); 838 | } 839 | break; 840 | case MOPT_ZOOM_SMITH: 841 | if(value_setter->set_value()) { 842 | menu_back(); 843 | } 844 | break; 845 | default: 846 | loop_logger.error(String("don't know what to do with option ")+menu_manager.current_option_); 847 | menu_back(); 848 | break; 849 | } 850 | } 851 | 852 | int digitalReadOutputPin(uint8_t pin) 853 | { 854 | uint8_t bit = digitalPinToBitMask(pin); 855 | uint8_t port = digitalPinToPort(pin); 856 | //if (port == NOT_A_PIN) 857 | // return LOW; 858 | 859 | return (*portOutputRegister(port) & bit) ? HIGH : LOW; 860 | } 861 | 862 | #define UNKNOWN_PIN 0xFF 863 | 864 | uint8_t getPinMode(uint8_t pin) 865 | { 866 | uint16_t bit = digitalPinToBitMask(pin); 867 | uint16_t port = digitalPinToPort(pin); 868 | 869 | // I don't see an option for mega to return this, but whatever... 870 | //if (NOT_A_PIN == port) return UNKNOWN_PIN; 871 | 872 | // Is there a bit we can check? 873 | if (0 == bit) return UNKNOWN_PIN; 874 | 875 | // Is there only a single bit set? 876 | if (bit & bit - 1) return UNKNOWN_PIN; 877 | 878 | volatile uint16_t *reg, *out; 879 | reg = portModeRegister(port); 880 | out = portOutputRegister(port); 881 | 882 | if (*reg & bit) 883 | return OUTPUT; 884 | else if (*out & bit) 885 | return INPUT_PULLUP; 886 | else 887 | return INPUT; 888 | } 889 | 890 | void setPinInput(uint8_t pin) 891 | { 892 | uint16_t bit = digitalPinToBitMask(pin); 893 | uint16_t port = digitalPinToPort(pin); 894 | volatile uint16_t *reg; 895 | reg = portModeRegister(port); 896 | *reg &= ~bit; 897 | } 898 | 899 | void setPinOutput(uint8_t pin) 900 | { 901 | uint16_t bit = digitalPinToBitMask(pin); 902 | uint16_t port = digitalPinToPort(pin); 903 | volatile uint16_t *reg; 904 | reg = portModeRegister(port); 905 | *reg |= bit; 906 | } 907 | 908 | //TODO: refactor things so we don't have to include shell.h here 909 | #include "shell.h" 910 | 911 | void setup_failed() { 912 | int led_state = 0; 913 | while(1) { 914 | delay(1000); 915 | led_state = !led_state; 916 | } 917 | } 918 | 919 | void setup() { 920 | Serial.begin(38400); 921 | Serial.flush(); 922 | 923 | if (WAIT_FOR_SERIAL) { 924 | wait_for_serial(); 925 | } 926 | 927 | init_vbatt(); 928 | current_error(""); 929 | 930 | loop_logger.info(F("starting TFT...")); 931 | 932 | uint16_t tft_id = tft.readID(); 933 | if (tft_id != 0x8357) { 934 | loop_logger.error(F("got unexpected tft id 0x")); 935 | Serial.println(tft_id, HEX); 936 | setup_failed(); 937 | } 938 | tft.begin(tft_id); 939 | tft.fillScreen(BLACK); 940 | tft.setRotation(TFT_ROTATION); 941 | tft.setCursor(0, 0); 942 | tft.setTextColor(WHITE); 943 | tft.setTextSize(2); 944 | loop_logger.info(F("TFT started.")); 945 | tft.println(F("Initializing...")); 946 | 947 | loop_logger.info(F("starting RTC...")); 948 | tft.println(F("starting RTC...")); 949 | if (!rtc.begin()) { 950 | delay(100); 951 | if(!rtc.begin()) { 952 | loop_logger.error(F("RTC failed to begin")); 953 | tft.println(F("RTC failed to begin")); 954 | setup_failed(); 955 | } 956 | } 957 | 958 | if (rtc.lostPower()) { 959 | loop_logger.warn(F("RTC lost power, let's set the time!")); 960 | // following line sets the RTC to the date & time this sketch was compiled 961 | rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); 962 | } 963 | 964 | loop_logger.info(F("starting SD...")); 965 | tft.println(F("starting SD...")); 966 | if(!sd.begin(SdSpiConfig(10, DEDICATED_SPI, SPI_HALF_SPEED))) { 967 | loop_logger.error(F("SD failed to begin")); 968 | tft.println(F("SD failed to start")); 969 | setup_failed(); 970 | } 971 | FsDateTime::setCallback(date_callback); 972 | 973 | loop_logger.info(F("starting ZEROII...")); 974 | tft.println(F("Starting ZEROII...")); 975 | if(!analyzer.zeroii_.startZeroII()) { 976 | loop_logger.error(F("failed to start zeroii")); 977 | tft.println(F("Failed to start ZeroII. Aborting.")); 978 | setup_failed(); 979 | return; 980 | } 981 | String str = "Version: "; 982 | loop_logger.info(str + analyzer.zeroii_.getMajorVersion() + "." + analyzer.zeroii_.getMinorVersion() + 983 | ", HW Revision: " + analyzer.zeroii_.getHwRevision() + 984 | ", SN: " + analyzer.zeroii_.getSerialNumber() 985 | ); 986 | loop_logger.info(F("ZEROII started.")); 987 | 988 | loop_logger.info(F("starting serial wombat...")); 989 | if(!sw.begin(Wire, 0x6C)) { 990 | loop_logger.error(F("serial wombat failed to begin")); 991 | tft.println(F("serial wombat failed to start")); 992 | setup_failed(); 993 | } 994 | uint32_t sw_version = sw.readVersion_uint32(); 995 | if (sw_version == 0) { 996 | loop_logger.error(String("serial wombat version was bad: ")+sw_version); 997 | } 998 | loop_logger.info(String("serial wombat version: ")+sw_version+" '"+sw.readVersion()+"'"); 999 | quad_enc.begin(2, 1, 10, false, QE_ONLOW_POLL); 1000 | quad_enc.read(32768); 1001 | debounced_input.begin(0, 30, false, false); 1002 | debounced_input.readTransitionsState(); 1003 | 1004 | loop_logger.info(F("setting some initial start/end fq")); 1005 | { 1006 | BandSetter bs; 1007 | start_fq = bs.band_fqs[BAND_10M][0]; 1008 | end_fq = bs.band_fqs[BAND_10M][1]; 1009 | } 1010 | 1011 | loop_logger.info(F("checking for settings...")); 1012 | if(!persistence.begin()) { 1013 | loop_logger.error(F("persistence failed to begin")); 1014 | tft.println(F("persistence failed to start")); 1015 | setup_failed(); 1016 | } 1017 | if(!persistence.load_settings(&analyzer, MAX_STEPS)) { 1018 | loop_logger.error(F("could not load existing settings")); 1019 | } else { 1020 | set_analysis_from_calibration(); 1021 | loop_logger.info(F("loaded settings")); 1022 | } 1023 | if(!persistence.load_results(analysis_results, &analysis_results_len, MAX_STEPS)) { 1024 | loop_logger.error(F("could not load existing results")); 1025 | } else { 1026 | loop_logger.info(F("loaded results")); 1027 | } 1028 | 1029 | loop_logger.info(F("Initialization complete.")); 1030 | tft.println(F("Initializing complete.")); 1031 | 1032 | tft.fillScreen(BLACK); 1033 | draw_title(); 1034 | draw_menu(menu_manager.current_menu_, menu_manager.current_option_); 1035 | } 1036 | 1037 | void loop() { 1038 | loop_logger.debug(F("entering loop")); 1039 | uint32_t now = millis(); 1040 | if (last_vbatt + BATT_SENSE_PERIOD < now) { 1041 | //loop_logger.debug("updating battery measurement"); 1042 | update_vbatt(); 1043 | draw_vbatt(); 1044 | //loop_logger.debug("battery measurement updated"); 1045 | } 1046 | 1047 | if (error_message[0] && now - last_error_time > ERROR_MESSAGE_DWELL_TIME) { 1048 | clear_error_display(); 1049 | } 1050 | 1051 | debounced_input.readTransitionsState(); 1052 | click = debounced_input.transitions > 0 && !debounced_input.digitalRead(); 1053 | 1054 | if(click && error_message[0]) { 1055 | //clear errors on positive user interaction 1056 | clear_error_display(); 1057 | } 1058 | 1059 | uint16_t next_quad_enc = quad_enc.read(32768); 1060 | turn = next_quad_enc - last_quad_enc; 1061 | 1062 | if(read_serial_command()) { 1063 | handle_serial_command(); 1064 | serial_command_len = 0; 1065 | } 1066 | 1067 | handle_option(); 1068 | } 1069 | 1070 | /* 1071 | Complex compute_gamma(float r, float x, float z0_real) { 1072 | // gamma = (z - z0) / (z + z0) 1073 | // z = r + xj 1074 | 1075 | Complex z(r, x); 1076 | Complex z0(z0_real); 1077 | 1078 | return (z - z0) / (z + z0); 1079 | } 1080 | 1081 | Complex calibrate_reflection(Complex sc, Complex oc, Complex load, Complex reflection) { 1082 | // inspired by NanoVNA 1083 | Complex a=load, b=oc, c=sc, d = reflection; 1084 | return -(a - d)*(b - c)/(a*(b - c) + Complex(2.f)*c*(a - b) + d*(Complex(-2.f)*a + b + c)); 1085 | } 1086 | 1087 | float resistance(Complex v) { 1088 | float re = v.real(), im = v.imag(); 1089 | float z0 = 50; 1090 | float d = z0 / ((1-re)*(1-re)+im*im); 1091 | float zr = ((1+re)*(1-re) - im*im) * d; 1092 | return zr; 1093 | } 1094 | 1095 | float reactance(Complex v) { 1096 | float re = v.real(), im = v.imag(); 1097 | float z0 = 50; 1098 | float d = z0 / ((1-re)*(1-re)+im*im); 1099 | float zi = 2*im * d; 1100 | return zi; 1101 | } 1102 | */ 1103 | 1104 | /* 1105 | // some thoughts on getting capacitance/inductance 1106 | // X = - 1/(2*pi*f*C) 1107 | // X = 2*pi*f*L 1108 | // courtesy nanovna-v2/NanoVNA-QT 1109 | inline double capacitance_inductance(double freq, double Z) { 1110 | if(Z>0) return Z/(2*M_PI*freq); 1111 | return 1./(2*Z*M_PI*freq); 1112 | } 1113 | 1114 | case SParamViewSource::TYPE_Z_CAP: // capacitance in pF 1115 | y = -capacitance_inductance(freqHz, Z.imag()) * 1e12; 1116 | break; 1117 | case SParamViewSource::TYPE_Z_IND: // inductance in nH 1118 | y = capacitance_inductance(freqHz, Z.imag()) * 1e9; 1119 | break; 1120 | */ 1121 | --------------------------------------------------------------------------------