#pragma once #include #include #include #include #include "board.hpp" #include "bootloader.hpp" #include "controller.hpp" #include "curve.hpp" #include "statistics.hpp" #include "thermistor.hpp" // The serial console: line-buffered commands over the hardware UART. // `help` lists everything; `monitor` streams until any key. namespace app { class terminal { static constexpr std::uint8_t line_max = 24; static inline char line[line_max]{}; static inline std::uint8_t at = 0; static inline bool overflowed = false; static inline bool monitoring = false; static inline std::uint64_t last_monitor = 0; // Commands, in the order they are matched — which is the order the original // firmware matched them in, and that order is load-bearing. An abbreviation // resolves to the *first* entry it prefixes, so `s` is show (not statistics, // not set) exactly as it always was, and anything appended to this list // cannot steal an abbreviation that already meant something else. struct command { std::string_view name; bool exact; // reset only: an abbreviation must not be able to wipe data }; static constexpr std::array commands{{ {"help", false}, {"show", false}, {"curve", false}, {"monitor", false}, {"bootloader", false}, {"uptime", false}, {"statistics", false}, {"histogram", false}, {"reset", true}, {"set", false}, {"auto", false}, {"version", false}, {"save", false}, }}; // Column the descriptions' colons line up in, counted from the start of the // name. The longest name is `bootloader` at 10, so 12 leaves it a space and // one dot — the original's layout exactly. static constexpr std::uint8_t help_column = 12; static void prompt() { serial << "> "_P; } // `name ....: ` — the dots are what make a dozen descriptions readable in a // terminal, and they cost nothing but a loop. static void help_row(std::string_view name) { serial << name << ' '; for (auto i = name.size() + 1; i < help_column; ++i) serial << '.'; serial << ": "_P; } static void help() { serial << "\r\nFanTemp "_P << version << " command overview\r\n"_P; help_row(commands[0].name); serial << "prints this help message\r\n"_P; help_row(commands[1].name); serial << "shows current temperature and fan speed\r\n"_P; help_row(commands[2].name); serial << "shows mapping from temperature to fan speed\r\n"_P; help_row(commands[3].name); serial << "loops the show command until a key is pressed\r\n"_P; help_row(commands[4].name); serial << "enters the bootloader\r\n"_P; help_row(commands[5].name); serial << "shows system uptime\r\n"_P; help_row(commands[6].name); serial << "prints overall statistics like min and max temp\r\n"_P; help_row(commands[7].name); serial << "prints a histogram of the temperature\r\n"_P; help_row(commands[8].name); serial << "resets statistics to 0 in EEPROM and RAM (no abbreviation)\r\n"_P; help_row(commands[9].name); serial << "sets the fan speed to the provided value, 0-100\r\n"_P; help_row(commands[10].name); serial << "turns on automatic fan control\r\n"_P; help_row(commands[11].name); serial << "displays firmware version\r\n"_P; help_row(commands[12].name); serial << "writes the statistics to EEPROM now\r\n"_P; serial << "commands may be abbreviated: 'up' is uptime\r\n"_P; } // The thermistor's resistance from the divider, in whole ohms. The original // printed this beside the reading and it is the one number that says *why* a // temperature is wrong: an open sensor rails the ADC and the resistance goes // to the tens of megohms, a shorted one to zero. static std::uint32_t resistance() { auto adc = controller::last_adc(); if (adc >= 1023) return 0xffffffff; // open circuit: the divider has no solution return static_cast(thermistor::series_resistor) * adc / (1023u - adc); } static void show() { if (!controller::data_available()) { serial << "no data yet\r\n"_P; return; } auto quarters = controller::temperature_quarters(); serial << "temperature "_P << quarters / 4 << '.' << (quarters % 4) * 25 << " C, adc "_P << controller::last_adc() << ", resistance "_P; if (auto ohms = resistance(); ohms == 0xffffffff) serial << "open"_P; else serial << ohms << " Ohm"_P; serial << ", fan "_P << controller::fan_percent() << " %, "_P; if (controller::automatic()) serial << "auto"_P; else serial << "manual"_P; serial << "\r\n"_P; } static void print_curve() { for (std::int8_t t = 15; t <= 60; t += 5) serial << t << " C -> "_P << curve::duty(t) << " %\r\n"_P; } static void print_uptime() { auto seconds = static_cast(uptime::millis() / 1000); serial << seconds / 86400 << "d "_P << (seconds / 3600) % 24 << "h "_P << (seconds / 60) % 60 << "m "_P << seconds % 60 << "s\r\n"_P; } static void print_statistics() { if (statistics::total_samples() == 0) { serial << "no data yet\r\n"_P; return; } serial << "min "_P << statistics::min_temperature() << " C, max "_P << statistics::max_temperature() << " C, samples "_P << static_cast(statistics::total_samples()) << "\r\n"_P; } static void print_histogram() { auto highest = statistics::highest_bucket(); if (highest == 0) { serial << "no data yet\r\n"_P; return; } // The original's normalisation, and its resolution: divide by whatever // makes the tallest bucket fit in a hundred columns, not forty. A bar // that tops out at 40 throws away most of the difference between // neighbouring buckets, which on a distribution this narrow is the whole // picture. std::uint32_t factor = highest / bar_max > 1 ? highest / bar_max : 1; while (highest / factor > bar_max) ++factor; for (std::uint8_t t = statistics::min_temperature(); t <= statistics::max_temperature(); ++t) { auto count = statistics::bucket(t); // Count first, in a fixed column, so the numbers read as a table // instead of trailing off the ragged right-hand end of the bars. serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C : "_P << avr::dec<{.width = 10, .fill = ' '}>(count) << " |"_P; for (std::uint32_t i = 0; i < count / factor; ++i) serial << '#'; serial << "\r\n"_P; } } // Abbreviations: the input matches a command when it is a non-empty prefix // of it. `reset` is the exception and must be typed in full. // // starts_with, not substr: substr throws std::out_of_range, and one // potentially-throwing call is enough to pull in std::terminate, which does // not exist in a freestanding AVR build. The link fails rather than the // firmware, so this is a build-time trap rather than a runtime one — but it // is a trap, and the whole file avoids substr for that reason. static bool matches(std::string_view input, const command &c) { if (input.empty()) return false; if (c.exact) return input == c.name; return c.name.starts_with(input); } static void dispatch(std::string_view input) { // A line that overflowed the buffer is not a command — it is the tail of // one. Acting on it is how a truncated `reset` becomes a surprise. if (overflowed) { serial << "input too long, ignored\r\n"_P; overflowed = false; return; } // Split on the first space with the (pointer, length) constructor rather // than substr, which throws — see matches(). const auto space = input.find(' '); const auto word = space == std::string_view::npos ? input : std::string_view{input.data(), space}; const auto rest = space == std::string_view::npos ? std::string_view{} : std::string_view{input.data() + space + 1, input.size() - space - 1}; if (word.empty()) return; std::uint8_t which = commands.size(); for (std::uint8_t i = 0; i < commands.size(); ++i) if (matches(word, commands[i])) { which = i; break; } switch (which) { case 0: help(); return; case 1: show(); return; case 2: print_curve(); return; case 3: monitoring = true; return; case 4: serial << "entering bootloader\r\n"_P; statistics::save(); serial.drain(); bootloader::enter(); case 5: print_uptime(); return; case 6: print_statistics(); return; case 7: print_histogram(); return; case 8: statistics::reset(); serial << "statistics cleared in EEPROM and RAM\r\n"_P; return; case 9: { std::uint16_t percent = 0; bool valid = !rest.empty(); for (char c : rest) { if (c < '0' || c > '9') { valid = false; break; } percent = static_cast(percent * 10 + (c - '0')); if (percent > 100) valid = false; } if (valid) { controller::set_manual(static_cast(percent)); serial << "fan "_P << percent << " %, manual\r\n"_P; } else { serial << "set 0..100\r\n"_P; } return; } case 10: controller::set_automatic(); serial << "automatic fan control\r\n"_P; return; case 11: serial << "FanTemp "_P << version << " on libavr\r\n"_P; return; case 12: statistics::save(); serial << "statistics written to EEPROM\r\n"_P; return; default: serial << '\'' << word << "' is not a command; 'help' for the list\r\n"_P; return; } } public: static constexpr std::string_view version = "v2.1"; static constexpr std::uint8_t bar_max = 100; static void init() { serial << "\r\nFanTemp "_P << version << " on libavr -- 'help' for commands\r\n"_P; prompt(); } static void poll() { if (monitoring) { if (uptime::millis() >= last_monitor + 1000) { show(); last_monitor = uptime::millis(); } if (serial_t::read()) { // any key stops monitoring = false; prompt(); } return; } while (auto in = serial_t::read()) { char c = static_cast(*in); if (c == '\r' || c == '\n') { serial << "\r\n"_P; if (at == 0 && !overflowed) { prompt(); // a bare Enter just reprompts, no gap needed continue; } dispatch(std::string_view{line, at}); at = 0; if (!monitoring) { // A blank line between a command's output and the next // prompt: without it the answer and the thing you type // next run together and a screen of them is unreadable. serial << "\r\n"_P; prompt(); } } else if (c == 0x7f || c == 0x08) { if (at) { --at; serial << "\b \b"_P; } } else if (c >= ' ') { if (at < line_max) { line[at++] = c; serial << c; // echo } else { overflowed = true; // reported when the line is submitted } } } } }; } // namespace app