console: values in a column, the curve as a graph, and Ctrl+C
Three more things the original did better, and two bugs found doing them. `curve` walks every whole degree from 10 to 60 with a bar, which is the original's. The port sampled it every five degrees and printed a bare percentage — ten numbers for a cubic, showing none of its shape. The bar is the duty itself, so it needs no scale. `show` and `statistics` print one value per line behind a dotted label instead of a run-on line. That reads the same either way for a single reading and is the whole difference when `monitor` emits one a second forever. The label renderer is now shared with the help, since it is the same thing three times; the flash overload takes its width from the string's type, so the padding needs no hand-counted constant and the labels stay out of SRAM. `statistics` gains the sample total, and says "not available" rather than a zero it never measured. Ctrl+C echoes `^C` and gives a fresh prompt, abandoning whatever was half-typed, and it is what stops `monitor` now. Stopping on *any* byte was the port's own invention and it reads fine until a host sends a line ending: `monitor\r\n` stopped itself on the `\n` it arrived with, one reading in, which is why monitoring looked broken from a script and fine by hand. The other bug is arithmetic. A temperature's fraction came from `(quarters % 4) * 25`, and C++ gives a negative remainder for a negative dividend — so -40.25 C printed as "-40.-25". The sign comes off first now, and the fraction is two digits, so the column lines up: -40.00, -40.25. Verified against v1.8b on the board, which was flashed back to compare against directly: same 51 curve rows over the same span with the same 100-column bars, agreeing within the one percentage point the consteval table costs against the legacy runtime doubles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
13
README.md
13
README.md
@@ -1,6 +1,6 @@
|
||||
# fantemp
|
||||
|
||||
**v2.1.** Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on
|
||||
**v2.2.** Temperature-controlled fan firmware (ATmega328P, 16 MHz), rewritten on
|
||||
[libavr](https://git.blackmark.me/avr/libavr): thermistor on ADC0 sampled
|
||||
free-running and averaged over 1000 conversions, fan on OC0B at 50 kHz,
|
||||
115200 Bd serial console (`help` lists the commands), temperature
|
||||
@@ -21,6 +21,17 @@ command that cannot be abbreviated, because `r` should not be able to wipe the
|
||||
histogram. `save` (new) forces a writeback, which otherwise happens every 30
|
||||
minutes and on the way into the bootloader.
|
||||
|
||||
`show`, `statistics` and the histogram print one value per line behind a dotted
|
||||
label, the way the original did — a run-on line is fine for one reading and
|
||||
unreadable when `monitor` emits one a second. `curve` walks every whole degree
|
||||
from 10 to 60 with a bar, because the curve is a cubic and five-degree samples
|
||||
without a graph show none of its shape.
|
||||
|
||||
**Ctrl+C** abandons a half-typed line and gives a fresh prompt, echoing `^C`, and
|
||||
it is what stops `monitor`. Stopping on *any* byte, which is what the port did
|
||||
first, reads well right up until a host sends a line ending: `monitor\r\n` then
|
||||
stopped itself on the `\n` it arrived with, one reading in.
|
||||
|
||||
## Reaching the bootloader
|
||||
|
||||
`bootloader` **jumps**; it does not reset. That is not a style choice:
|
||||
|
||||
137
src/terminal.hpp
137
src/terminal.hpp
@@ -14,10 +14,12 @@
|
||||
#include "thermistor.hpp"
|
||||
|
||||
// The serial console: line-buffered commands over the hardware UART.
|
||||
// `help` lists everything; `monitor` streams until any key.
|
||||
// `help` lists everything, command names may be abbreviated, `monitor` streams
|
||||
// until Ctrl+C, and Ctrl+C abandons a half-typed line anywhere else.
|
||||
namespace app {
|
||||
|
||||
class terminal {
|
||||
static constexpr char ctrl_c = 0x03;
|
||||
static constexpr std::uint8_t line_max = 24;
|
||||
static inline char line[line_max]{};
|
||||
static inline std::uint8_t at = 0;
|
||||
@@ -61,16 +63,46 @@ class terminal {
|
||||
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)
|
||||
// `name ....: ` — the dotted label the original used everywhere it printed a
|
||||
// list of things, which is what makes a column of values readable without
|
||||
// counting spaces. One renderer for all three users; only the column differs.
|
||||
static void label(std::string_view text, std::uint8_t column)
|
||||
{
|
||||
serial << name << ' ';
|
||||
for (auto i = name.size() + 1; i < help_column; ++i)
|
||||
serial << text << ' ';
|
||||
for (auto i = text.size() + 1; i < column; ++i)
|
||||
serial << '.';
|
||||
serial << ": "_P;
|
||||
}
|
||||
|
||||
// The same, for a label that is a literal rather than a command name: it
|
||||
// stays in flash, and its width comes from the type, so the padding needs no
|
||||
// hand-counted constant. The command names cannot use this — they are
|
||||
// string_views because they are matched at run time.
|
||||
template <typename Flash>
|
||||
static void label(Flash text, std::uint8_t column)
|
||||
{
|
||||
serial << text << ' ';
|
||||
for (auto i = Flash::size + 1; i < column; ++i)
|
||||
serial << '.';
|
||||
serial << ": "_P;
|
||||
}
|
||||
|
||||
static void help_row(std::string_view name)
|
||||
{
|
||||
label(name, help_column);
|
||||
}
|
||||
|
||||
// Quarter-°C as a signed decimal with a two-digit fraction. The sign is taken
|
||||
// off first: C++ gives a negative remainder for a negative dividend, so
|
||||
// `(q % 4) * 25` on -40.25 C yields -25 and prints "-40.-25".
|
||||
static void temperature(std::int16_t quarters)
|
||||
{
|
||||
if (quarters < 0)
|
||||
serial << '-';
|
||||
auto magnitude = static_cast<std::uint16_t>(quarters < 0 ? -quarters : quarters);
|
||||
serial << magnitude / 4 << '.' << avr::dec<{.width = 2, .fill = '0'}>((magnitude % 4) * 25);
|
||||
}
|
||||
|
||||
static void help()
|
||||
{
|
||||
serial << "\r\nFanTemp "_P << version << " command overview\r\n"_P;
|
||||
@@ -81,7 +113,7 @@ class terminal {
|
||||
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;
|
||||
serial << "loops the show command until Ctrl+C is pressed\r\n"_P;
|
||||
help_row(commands[4].name);
|
||||
serial << "enters the bootloader\r\n"_P;
|
||||
help_row(commands[5].name);
|
||||
@@ -115,31 +147,50 @@ class terminal {
|
||||
return static_cast<std::uint32_t>(thermistor::series_resistor) * adc / (1023u - adc);
|
||||
}
|
||||
|
||||
// One value per line behind a dotted label, as the original had it. A single
|
||||
// run-on line is fine for one reading and unreadable when `monitor` prints
|
||||
// one a second forever.
|
||||
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;
|
||||
label("ADC value"_P, reading_column);
|
||||
serial << controller::last_adc() << " / 1023\r\n"_P;
|
||||
|
||||
label("Resistance"_P, reading_column);
|
||||
if (auto ohms = resistance(); ohms == open_circuit)
|
||||
serial << "open circuit\r\n"_P;
|
||||
else
|
||||
serial << ohms << " Ohm"_P;
|
||||
serial << ", fan "_P << controller::fan_percent() << " %, "_P;
|
||||
serial << ohms << " Ohm\r\n"_P;
|
||||
|
||||
label("Temperature"_P, reading_column);
|
||||
temperature(controller::temperature_quarters());
|
||||
serial << " C\r\n"_P;
|
||||
|
||||
label("Fan speed"_P, reading_column);
|
||||
serial << controller::fan_percent() << "% "_P;
|
||||
if (controller::automatic())
|
||||
serial << "auto"_P;
|
||||
serial << "auto\r\n"_P;
|
||||
else
|
||||
serial << "manual"_P;
|
||||
serial << "\r\n"_P;
|
||||
serial << "manual\r\n"_P;
|
||||
}
|
||||
|
||||
// Every whole degree from 10 to 60 with a bar, which is the original's and is
|
||||
// the point of the command: the curve is a cubic, and five-degree samples
|
||||
// without a graph show none of its shape. The bar is the duty itself, so it
|
||||
// reads as a percentage without needing a scale.
|
||||
static void print_curve()
|
||||
{
|
||||
for (std::int8_t t = 15; t <= 60; t += 5)
|
||||
serial << t << " C -> "_P << curve::duty(t) << " %\r\n"_P;
|
||||
for (std::uint8_t t = curve_low; t <= curve_high; ++t) {
|
||||
auto duty = curve::duty(static_cast<std::int8_t>(t));
|
||||
serial << avr::dec<{.width = 2, .fill = '0'}>(t) << " C = "_P << avr::dec<{.width = 3, .fill = ' '}>(duty)
|
||||
<< "% |"_P;
|
||||
for (std::uint8_t i = 0; i < duty; ++i)
|
||||
serial << '#';
|
||||
serial << "\r\n"_P;
|
||||
}
|
||||
}
|
||||
|
||||
static void print_uptime()
|
||||
@@ -151,12 +202,21 @@ class terminal {
|
||||
|
||||
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<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
||||
auto empty = statistics::total_samples() == 0;
|
||||
label("Minimum temperature"_P, stat_column);
|
||||
if (empty)
|
||||
serial << "not available\r\n"_P;
|
||||
else
|
||||
serial << statistics::min_temperature() << " C\r\n"_P;
|
||||
|
||||
label("Maximum temperature"_P, stat_column);
|
||||
if (empty)
|
||||
serial << "not available\r\n"_P;
|
||||
else
|
||||
serial << statistics::max_temperature() << " C\r\n"_P;
|
||||
|
||||
label("Total samples"_P, stat_column);
|
||||
serial << static_cast<std::uint32_t>(statistics::total_samples()) << "\r\n"_P;
|
||||
}
|
||||
|
||||
static void print_histogram()
|
||||
@@ -300,9 +360,19 @@ class terminal {
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr std::string_view version = "v2.1";
|
||||
static constexpr std::string_view version = "v2.2";
|
||||
static constexpr std::uint8_t bar_max = 100;
|
||||
|
||||
// Columns the dotted labels' colons land in, and the curve's span. All four
|
||||
// are the original's.
|
||||
static constexpr std::uint8_t reading_column = 13;
|
||||
static constexpr std::uint8_t stat_column = 21;
|
||||
static constexpr std::uint8_t curve_low = 10;
|
||||
static constexpr std::uint8_t curve_high = 60;
|
||||
|
||||
// A railed ADC leaves the divider with no solution rather than a huge one.
|
||||
static constexpr std::uint32_t open_circuit = 0xffffffff;
|
||||
|
||||
static void init()
|
||||
{
|
||||
serial << "\r\nFanTemp "_P << version << " on libavr -- 'help' for commands\r\n"_P;
|
||||
@@ -316,7 +386,11 @@ class terminal {
|
||||
show();
|
||||
last_monitor = uptime::millis();
|
||||
}
|
||||
if (serial_t::read()) { // any key stops
|
||||
// Ctrl+C only, as the original had it. Stopping on *any* byte reads
|
||||
// well until a host sends a line ending: `monitor\r\n` then stops
|
||||
// itself on the `\n` it arrived with, one reading in.
|
||||
if (auto in = serial_t::read(); in && *in == ctrl_c) {
|
||||
serial << "^C\r\n"_P;
|
||||
monitoring = false;
|
||||
prompt();
|
||||
}
|
||||
@@ -324,7 +398,14 @@ class terminal {
|
||||
}
|
||||
while (auto in = serial_t::read()) {
|
||||
char c = static_cast<char>(*in);
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (c == ctrl_c) {
|
||||
// Abandon whatever was typed and start a fresh line, which is
|
||||
// what Ctrl+C means at every other prompt in the world.
|
||||
serial << "^C\r\n"_P;
|
||||
at = 0;
|
||||
overflowed = false;
|
||||
prompt();
|
||||
} else if (c == '\r' || c == '\n') {
|
||||
serial << "\r\n"_P;
|
||||
if (at == 0 && !overflowed) {
|
||||
prompt(); // a bare Enter just reprompts, no gap needed
|
||||
|
||||
Reference in New Issue
Block a user