#include <HardwareSerial.h>
#include <math.h>
#include <string.h>
//Settings for the thermal printer
HardwareSerial printerSerial(1);
const uint8_t RX_PIN = 4;
const uint8_t TX_PIN = 5;
const uint32_t BAUDRATE = 9600;
const int LINE_WIDTH = 32; // For the "EM5820" printer model and many other standard printers
//Debug serial monitor
const bool ECHO_TO_SERIAL = true;
//Function prototypes
void pPrint(const char* s);
void pPrintln(const char* s);
void pWriteCmd(const uint8_t* data, int len);
void setEmphasized(bool on);
void setUnderline(uint8_t mode);
void setFontSize(uint8_t n);
void resetFont();
void selectFontB(bool on);
String formatFloatFR(float value, int decimals);
String padRight(String s, int width);
String padLeft(String s, int width);
void printSeparator(int width = LINE_WIDTH);
void printMiniSeparator(int width = LINE_WIDTH);
void printCentered(const char* txt, int width = LINE_WIDTH, bool doubleWidth = false);
void printHeader(const char* refTube, int width = LINE_WIDTH);
void printSettings(float Vf, float Va, float Vs, float Vg, const char* mode, int width = LINE_WIDTH);
void printCurrentsMeasurements(float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, int width = LINE_WIDTH);
void printCharacteristicsTable(const char* labels[], float measured[], float nominal[], int rows, int width = LINE_WIDTH);
void printCurrentsMeasurementsGraph(float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, int width);
void printDate(int dd, int mm, int aaaa, const char* model, int width);
void printMeasurementAccuracy(float senseResistor, float maxCurrent, float Ia_meas);
void printTestReport(const char* refTube, float Vf, float Va, float Vs, float Vg, float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, const char* labels[], float measured[], float nominal[], int rows, int DD, int MM, int YYYY, const char* model, float senseResistor, float maxCurrent, const char* mode, int lineWidth = LINE_WIDTH);
// ---------- setup / loop ----------
void setup() {
// Serial monitor
Serial.begin(115200);
// Thermal printer
printerSerial.begin(BAUDRATE, SERIAL_8N1, RX_PIN, TX_PIN);
delay(200);
// Reset printer (ESC @)
uint8_t resetCmd[] = {0x1B, 0x40};
pWriteCmd(resetCmd, sizeof(resetCmd));
delay(100);
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=[ Data Examples ]=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
const char* ref = "EL6"; // Tube name
const char* mode = "Pentode (Ia, Is)"; //Or Pentode (Vg, gm)" or "Triode (Ia, Is)"
float Vf = 6.30, Va = 250.0, Vs = 200.0, Vg = -10.0, Ia_meas = 84.04, Ia_nom = 100.55, Is_meas = 9.77, Is_nom = 12.55; // Currents measurement parameters and results
// Derivatives :
const char* labels[] = {"Ra [kOhm]","gm [mA/V]","mu","Rs [kOhm]","gm2 [mA/V]","mus"};
float measured[] = { 12.72, 14.841, 188.8, 11.11, 1.772, 19.7 }; // derivatives
float nominal[] = { 2.60, 12.500, 33.0, 2.60, 12.500, 33.0 }; // Set nominal parameters by user
int rows = sizeof(measured) / sizeof(measured[0]); //automatically calculates the number of elements in an array
//Other parameters :
int DD = 30, MM = 11, AAAA = 2025; //Date (take from the user's terminal)
const char* model = "uTracer6"; // Or uTracer3, uTracer3+, etc ...
float senseResistor = 4.7; // Or 3.5, 18, etc ...
float maxCurrent = 750; // Or 1000, 450, etc ... / Current range
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Print the report
printTestReport(ref, Vf, Va, Vs, Vg, Ia_meas, Ia_nom, Is_meas, Is_nom, labels, measured, nominal, rows, DD, MM, AAAA, model, senseResistor, maxCurrent, mode, LINE_WIDTH);
}
void loop() {}
void pPrint(const char* s) {
printerSerial.print(s);
if (ECHO_TO_SERIAL) Serial.print(s);
}
void pPrintln(const char* s) {
printerSerial.println(s);
if (ECHO_TO_SERIAL) Serial.println(s);
}
//Send ESC/POS print commands
void pWriteCmd(const uint8_t* data, int len) {
printerSerial.write(data, len);
if (ECHO_TO_SERIAL) {
for (int i = 0; i < len; ++i) {
uint8_t b = data[i];
if (b >= 32 && b != 127) Serial.write(b);
else {
char buf[8];
snprintf(buf, sizeof(buf), "<0x%02X>", b);
Serial.print(buf);
}
}
}
}
//Sending commands to the printer (with ESC/POS commands).
void setEmphasized(bool on) { uint8_t cmd[] = {0x1B, 'E', (uint8_t)(on?1:0)}; pWriteCmd(cmd,3); }
void setUnderline(uint8_t mode) { uint8_t cmd[] = {0x1B, '-', mode}; pWriteCmd(cmd,3); }
void setFontSize(uint8_t n) { uint8_t cmd[] = {0x1D, '!', n}; pWriteCmd(cmd,3); }
void resetFont() { setFontSize(0); }
void selectFontB(bool on) { uint8_t cmd[] = {0x1B, 'M', (uint8_t)(on?1:0)}; pWriteCmd(cmd,3); }
String formatFloatFR(float value, int decimals) {
char buf[32]; snprintf(buf, sizeof(buf), "%.*f", decimals, value);
String s = String(buf); s.replace('.', ','); return s;
}
String padRight(String s, int width) {
int len = s.length(); if (len >= width) return s.substring(0, width);
for (int i=0;i<width-len;i++) s += ' '; return s;
}
String padLeft(String s, int width) {
int len = s.length(); if (len >= width) return s.substring(0, width);
String r=""; for (int i=0;i<width-len;i++) r += ' '; r += s; return r;
}
void printSeparator(int width) {
String line=""; for (int i=0;i<width;i++) line += '-'; pPrintln(line.c_str());
}
void printMiniSeparator(int width) {
String s=""; for(int i=0;i<width/2;i++) s += "- "; pPrintln(s.c_str());
}
void printCentered(const char* txt, int width, bool doubleWidth) {
String s = String(txt);
int effective = doubleWidth ? (width/2) : width;
int pad = (effective - s.length())/2; if (pad<0) pad=0;
String out=""; for (int i=0;i<pad;i++) out += ' '; out += s; pPrintln(out.c_str());
}
void printHeader(const char* refTube, int width) {
setEmphasized(true); setFontSize(0x11);
printCentered("TEST REPORT", width, true);
printCentered(refTube, width, true);
resetFont(); setEmphasized(false);
}
void printSettings(float Vf, float Va, float Vs, float Vg, const char* mode, int width) {
const char* prefix = "Settings : ";
pPrint(prefix); setEmphasized(true); pPrintln(mode); setEmphasized(false);
int indent = strlen(prefix);
String s1 = "Vf=" + formatFloatFR(Vf,2) + "V";
String s2 = "Va=" + formatFloatFR(Va,0) + "V";
String s3 = "Vs=" + formatFloatFR(Vs,0) + "V";
String s4 = "Vg=" + formatFloatFR(Vg,0) + "V";
String single = s1 + " " + s2 + " " + s3 + " " + s4;
if ((int)single.length() + indent <= width) {
String out=""; for (int i=0;i<indent;i++) out += ' '; out += single; pPrintln(out.c_str());
} else {
String l1 = s1 + " " + s2; String l2 = s3 + " " + s4;
String out1="", out2=""; for (int i=0;i<indent;i++){ out1 += ' '; out2 += ' '; }
out1 += l1; out2 += l2; pPrintln(out1.c_str()); pPrintln(out2.c_str());
}
}
void printCurrentsMeasurements(float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, int width) {
float Ia_pct = (Ia_nom != 0.0f) ? (Ia_meas / Ia_nom * 100.0f) : 0.0f;
float Is_pct = (Is_nom != 0.0f) ? (Is_meas / Is_nom * 100.0f) : 0.0f;
String sIa = "Ia = " + formatFloatFR(Ia_meas,2) + "mA (" + formatFloatFR(Ia_nom,0) + "mA)";
String sIs = "Is = " + formatFloatFR(Is_meas,2) + "mA (" + formatFloatFR(Is_nom,0) + "mA)";
pPrintln(sIa.c_str());
pPrintln(sIs.c_str());
}
void printCurrentsMeasurementsGraph(float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, int width) {
float Ia_pct = (Ia_nom != 0.0f) ? (Ia_meas / Ia_nom * 100.0f) : 0.0f;
float Is_pct = (Is_nom != 0.0f) ? (Is_meas / Is_nom * 100.0f) : 0.0f;
const char* labels[2] = {"Ia", "Is"};
float values[2] = {Ia_pct, Is_pct};
const int spaceBetween = 1; // espace entre la barre (avec crochets) et le pourcentage
for (int i = 0; i < 2; ++i) {
float pct = values[i];
// Label (ex: "Ia: ")
String lbl = String(labels[i]) + ": ";
int labelLen = lbl.length();
// Pourcentage sous forme de chaîne (ex: "183,0%")
String pctStr = formatFloatFR(pct, 1) + "%";
int pctWidth = pctStr.length();
// Calculer la largeur totale disponible pour la barre (crochets inclus)
// on veut : labelLen + barTotal + spaceBetween + pctWidth == width
int barTotal = width - labelLen - spaceBetween - pctWidth;
// BarTotal minimum (au moins "[ ]" + 1 char) -> 3 ( '[' + ' ' + ']' )
if (barTotal < 3) barTotal = 3;
// largeur intérieure = barTotal - 2 (sans les crochets)
int inside = barTotal - 2;
if (inside < 1) inside = 1;
// nombre de caractères remplis (proportionnel à 0..100%)
int filled = round((inside * min(pct, 100.0f)) / 100.0f);
if (filled > inside) filled = inside;
// Construire la barre (fixe en taille barTotal)
String bar = "[";
for (int j = 0; j < filled; ++j) bar += "=";
for (int j = filled; j < inside; ++j) bar += " ";
bar += "]";
// Construire la ligne : label + barre + espace + pourcentage (en gras)
// IMPORTANT : on n'inclut PAS les octets ESC dans le calcul de largeur (ils n'occupent pas de colonne visible)
pPrint(lbl.c_str());
pPrint(bar.c_str());
// espace entre barre et pourcentage
pPrint(" ");
// afficher le pourcentage en gras
setEmphasized(true);
pPrint(pctStr.c_str());
setEmphasized(false);
pPrintln(""); // saut de ligne
}
}
void printDate(int dd, int mm, int aaaa, const char* model, int width) {
// Date build
char dateBuf[16];
snprintf(dateBuf, sizeof(dateBuf), "%02d/%02d/%04d", dd, mm, aaaa);
// Combination : "JJ/MM/AAAA - txt"
String line = String(dateBuf);
if (strlen(model) > 0) {
line += " - ";
line += model;
}
// Center
int pad = (width - line.length()) / 2;
if (pad < 0) pad = 0; // sécurité (ne devrait jamais arriver)
String out = "";
for (int i = 0; i < pad; i++) out += ' ';
out += line;
pPrintln(out.c_str());
}
void printCharacteristicsTable(const char* labels[], float measured[], float nominal[], int rows, int width) {
selectFontB(true);
// minimal constraints
const int minLabel = 6; // allowed minimal label if extreme truncation needed
const int minMeas = 5;
const int minNom = 5;
const int minPct = 4;
// desired separators (try to keep these)
int sep1Width = 2; // gap Param - Meas (can be reduced)
int sep2Width = 2;
int sep3Width = 2;
String s1="", s2="", s3=""; // will be built later based on actual sep widths
// Prepare formatted arrays (safe upper bound)
const int MAX_ROWS = 64;
int rcount = min(rows, MAX_ROWS);
String measStrs[MAX_ROWS];
String nomStrs[MAX_ROWS];
String pctNumStrs[MAX_ROWS];
int maxMeasLen = 0;
int maxNomLen = 0;
int maxPctNumLen = 0;
int maxLabelLen = 0;
// compute lengths and formatted strings
for (int i = 0; i < rcount; ++i) {
measStrs[i] = formatFloatFR(measured[i], 1);
nomStrs[i] = formatFloatFR(nominal[i], 1);
float pct = (nominal[i] != 0.0f) ? (measured[i] / nominal[i] * 100.0f) : 0.0f;
pctNumStrs[i] = formatFloatFR(pct, 1);
maxMeasLen = max(maxMeasLen, (int)measStrs[i].length());
maxNomLen = max(maxNomLen, (int)nomStrs[i].length());
maxPctNumLen = max(maxPctNumLen, (int)pctNumStrs[i].length());
}
// compute max label length from provided array (don't mangle labels here)
for (int i = 0; i < rcount; ++i) {
int L = strlen(labels[i]);
if (L > maxLabelLen) maxLabelLen = L;
}
// consider header length "Param."
maxLabelLen = max(maxLabelLen, (int)String("Param.").length());
// ensure headers don't force too small numeric columns
const String headerMeasured = "Meas.";
const String headerNominal = "Nomin.";
maxMeasLen = max(maxMeasLen, (int)headerMeasured.length());
maxNomLen = max(maxNomLen, (int)headerNominal.length());
// column widths initial guess (reserve 1 char for '%' sign)
int wPctNumeric = max(maxPctNumLen, 1);
int wPct = max(minPct, wPctNumeric + 1);
int wMeas = max(maxMeasLen, minMeas);
int wNom = max(maxNomLen, minNom);
// total separators (will be adjusted)
int totalSep = sep1Width + sep2Width + sep3Width;
// target: try to set wLabel = maxLabelLen (prefer to keep labels intact)
int wLabel = max(maxLabelLen, minLabel);
// compute used width
int used = wLabel + wMeas + wNom + wPct + totalSep;
// If we are over width: first try to reduce separators (sep1,sep2,sep3) down to 0
if (used > width) {
int overflow = used - width;
// reduce sep1, sep2, sep3 in that order (we prefer to keep some gap after Param, so reduce others first)
int r;
// try reduce sep2 and sep3 first (they are small)
r = min(overflow, sep2Width); sep2Width -= r; overflow -= r;
r = min(overflow, sep3Width); sep3Width -= r; overflow -= r;
// then reduce sep1
r = min(overflow, sep1Width); sep1Width -= r; overflow -= r;
totalSep = sep1Width + sep2Width + sep3Width;
used = wLabel + wMeas + wNom + wPct + totalSep;
}
// If still overflow: reduce numeric columns (meas, nom, pct) down to their minima
if (used > width) {
int overflow = used - width;
int r;
r = min(overflow, wMeas - minMeas); wMeas -= r; overflow -= r;
r = min(overflow, wNom - minNom); wNom -= r; overflow -= r;
r = min(overflow, wPct - minPct); wPct -= r; overflow -= r;
used = wLabel + wMeas + wNom + wPct + totalSep;
}
// If still overflow: reluctantly trim label width (last resort)
if (used > width) {
int overflow = used - width;
wLabel = max(minLabel, wLabel - overflow);
used = wLabel + wMeas + wNom + wPct + totalSep;
}
// If under-used, give extra to label for nicer look
if (used < width) {
wLabel += (width - used);
used = wLabel + wMeas + wNom + wPct + totalSep;
}
// Build separator strings from final widths
s1 = ""; for (int i = 0; i < sep1Width; ++i) s1 += ' ';
s2 = ""; for (int i = 0; i < sep2Width; ++i) s2 += ' ';
s3 = ""; for (int i = 0; i < sep3Width; ++i) s3 += ' ';
// HEADER
setUnderline(1);
{
String h = "";
h += padRight("Param.", wLabel);
h += s1;
h += padLeft(headerMeasured, wMeas);
h += s2;
h += padLeft(headerNominal, wNom);
h += s3;
h += padLeft("%", wPct);
pPrintln(h.c_str());
}
setUnderline(0);
// ROWS
for (int i = 0; i < rcount; ++i) {
String lbl = String(labels[i]);
// If label longer than wLabel, truncate right (keep beginning): less destructive than removing suffixes
if ((int)lbl.length() > wLabel) lbl = lbl.substring(0, wLabel);
String meas = measStrs[i];
if ((int)meas.length() > wMeas) meas = meas.substring(meas.length() - wMeas); // keep rightmost numeric
String nom = nomStrs[i];
if ((int)nom.length() > wNom) nom = nom.substring(nom.length() - wNom);
// pct numeric: ensure fits wPct-1 (reserve 1 for '%'), else reduce decimals to 0, else truncate rightmost
String pctNum = pctNumStrs[i];
int maxNumAllowed = max(1, wPct - 1);
if ((int)pctNum.length() > maxNumAllowed) {
float pctVal = (nominal[i] != 0.0f) ? (measured[i] / nominal[i] * 100.0f) : 0.0f;
pctNum = formatFloatFR(pctVal, 0);
if ((int)pctNum.length() > maxNumAllowed) pctNum = pctNum.substring(pctNum.length() - maxNumAllowed);
}
String pctFull = pctNum + String("%");
if ((int)pctFull.length() > wPct) {
String shortNum = pctNum;
if ((int)shortNum.length() > (wPct - 1)) shortNum = shortNum.substring(shortNum.length() - (wPct - 1));
pctFull = shortNum + "%";
}
// Build line with explicit separators
String line = "";
line += padRight(lbl, wLabel);
line += s1;
line += padLeft(meas, wMeas);
line += s2;
line += padLeft(nom, wNom);
line += s3;
line += padLeft(pctFull, wPct);
pPrintln(line.c_str());
}
selectFontB(false);
}
void printMeasurementAccuracy(float senseResistor, float maxCurrent, float Ia_meas) {
// Determine accuracy category
const char* accuracy;
if (Ia_meas > maxCurrent / 3.0f) {
accuracy = "good";
} else if (Ia_meas < maxCurrent / 10.0f) {
accuracy = "bad";
} else {
accuracy = "moderate";
}
// Build output string
String line1 = "Ra/Ri : " + String(senseResistor, 1) + "R - Range : " + String(maxCurrent, 0) + "mA";
String line2 = "Meas. accuracy : " + String(accuracy);
// Print with newline between the two parts
pPrintln(line1.c_str());
pPrintln(line2.c_str());
}
// ---------- main report ----------
void printTestReport(const char* refTube, float Vf, float Va, float Vs, float Vg, float Ia_meas, float Ia_nom, float Is_meas, float Is_nom, const char* labels[], float measured[], float nominal[], int rows, int DD, int MM, int AAAA, const char* model, float senseResistor, float maxCurrent, const char* mode, int lineWidth) {
printSeparator(lineWidth);
printHeader(refTube, lineWidth);
printSeparator(lineWidth);
//""""""""""""""""""""""[Start Optn 1]""""""""""""""""""""""
printSettings(Vf, Va, Vs, Vg, mode, lineWidth);
printSeparator(lineWidth);
//""""""""""""""""""""""[ End Optn 1 ]""""""""""""""""""""""
printCentered("CHARACTERISTICS :", lineWidth, false);
printCurrentsMeasurements(Ia_meas, Ia_nom, Is_meas, Is_nom, lineWidth);
//""""""""""""""""""""""[Start Optn 2]""""""""""""""""""""""
printCurrentsMeasurementsGraph(Ia_meas, Ia_nom, Is_meas, Is_nom, lineWidth);
//""""""""""""""""""""""[ End Optn 2 ]""""""""""""""""""""""
//""""""""""""""""""""""[Start Optn 3]""""""""""""""""""""""
printMiniSeparator();
printCharacteristicsTable(labels, measured, nominal, rows, lineWidth);
//""""""""""""""""""""""[ End Optn 3 ]""""""""""""""""""""""
printSeparator(lineWidth);
//""""""""""""""""""""""[Start Optn 4]""""""""""""""""""""""
printDate(DD, MM, AAAA, model, lineWidth);
printSeparator(lineWidth);
//""""""""""""""""""""""[ End Optn 4 ]""""""""""""""""""""""
//""""""""""""""""""""""[Start Optn 5]""""""""""""""""""""""
printMeasurementAccuracy(senseResistor, maxCurrent, Ia_meas);
printSeparator(lineWidth);
//""""""""""""""""""""""[ End Optn 5 ]""""""""""""""""""""""
//pPrintln("");
// Paper feed & cutter (if the printer model allows it)
uint8_t cutCmd[] = {0x1D, 'V', 0x00};
pWriteCmd(cutCmd, sizeof(cutCmd));
Serial.println("Printing finished.");
}