Interpretation of raw ADC values ​​in uTracer6

109 views
Skip to first unread message

S3b

unread,
Nov 13, 2025, 10:39:32 AMNov 13
to uTracer
Hello, I recently acquired a uTracer 6 and would like to customize it by making it portable and adding some options.

To avoid manually drawing a label for each of my tubes, I plan to use a small thermal receipt printer (the idea is to print the same information as the "Quick Test" section of the uTracer software, and possibly a curve or two). I would then like to control the uTracer with a smartphone interface (via ESP32), again using simple data (quick test and a curve or two), so that the device is portable and can be taken with me to flea markets and swap meets.

Unfortunately, all software development around uTracer is closed ... (it's as if open source is scary ?). What a pity ! But, that's not the point...

I managed to use an "Arduino" to intercept and transcribe the data frames. Thanks to this, I can display the frames (and hide the echoes) to understand how the uTracer and the PC communicate. (Note, in the picture, that I will also use the Metrix U61 method for the tube holders and the selector, in order to avoid long cables and limit oscillations.)

unnamed.jpg

I thought I could retrieve converted values from the uTracer to the PC, but unfortunately for me, these are raw values from the PIC's 10-bit ADC, and the calculations are done within the program. I managed to recover some values (like the supply voltage measurement) by deduction: (float Vpower = rawVpower * (5/1024.0f) * 11.0f * Calibration_Vsup; //Vref de 5V ? 11.0f pour 11K car R1 = 1K et R2 = 10K : (1K+10K)/1K). But I believe the task will not be so simple for Va, Vs, Ia, etc... Especially because of the use of PGA? And also, I seem to recall reading somewhere that for Va and Vs, Vpower had to be deduced.

I found the following information: https://www.dos4ever.com/uTracer3/code_blocks1.txt, https://www.dos4ever.com/uTracer3/code_blocks2.txt, but it seems to me that this information is for uTracer 3+, and I understand that you did things differently for uTracer 6 ?

I don't understand the relationship between Ia and Ia Comp, or Is and Is Comp (Comp. for compensation?).

Could you please help me implement the missing formulas and decode the returned voltages and currents ?

Capture d’écran 2025-11-12 210008.png



Have a good day and see you soon,
S3b

Ihor

unread,
Nov 13, 2025, 11:44:35 AMNov 13
to uTracer
I believe there is lots of information at this forum about this subject. The communication protocol is fully describer and open, with all the formulas

The solution with esp32 is also available already for more than 5 years :)

Ronald even published his code in Visual Basic how everythign is converted n the code, also available on this forum. 

There is even an implementation in pearl 

The different between 3+ and 6 is minor in terms of the formulas as long as you do not want to implement the grid current measurement module. 

Cheers, 

Ihor

S3b

unread,
Nov 13, 2025, 5:40:43 PMNov 13
to uTracer
Thank you so much for your feedback :)

And thank you for the link explaining the uTracer 6 formulas! (Since I was using the GUI manual for the uTracer 3, I didn't realize I was on the wrong page...).

That said, in my screenshot, with the decimal value set to "203", the GUI software displays "197.7V".

However, when applying the formula: Vtubes = 1.0448*n-Vsup -> I get 1.0448*205-19.8 = 194.3V, which is not 197.7V (I've tried recalculating it every which way, but I can't find anything that matches...). Also, what should I do with my correction factor of Va = "0.974" in the "Cal." file?

Capture d’écran 2025-11-12 210008.pngCapture d’écran 2025-11-13 230535.png

- - - 

"The solution with esp32 is also available already for more than 5 years :)

Thanks, I did look at that solution, it looks really great!
But again, the code seems closed (at least, I haven't found an open-source repository or anything like that) and it's impossible to implement my own functions...
Since AI can't yet properly disassemble programs (one can always dream, haha), I'm planning to reinvent the wheel with a version on ESP32 (lighter, unfortunately) to implement my thermal printing function.

I'll try to find the VBA code on the forum :)

Thanks again for all this information and have a good evening,
S3b

Ihor

unread,
Nov 14, 2025, 7:26:48 AMNov 14
to uTracer
The formulas on the webside by the way differ from the actual implementation by using 1024 instead of 1023, but also the formulas for conversion are in the two visual basic source code files that you attached
Va = Va_ * (5 / 1024) * ((AnodeDivR1 + AnodeDivR2) / (AnodeDivR1 * CalVar1)) - VsupSystem

S3b

unread,
Nov 19, 2025, 5:41:05 PMNov 19
to uTracer
Thank you very much.
Okay for the Va and VS formulas :) 

Capture d’écran 2025-11-19 223404.png

But, when I try to apply the Vneg formula to the code files, with R1 = 200000, R2 = 4700, n = 641, CalVneg = 0.978 (and dblVmin = -15,6) :

Case 8 'The negative power supply voltage frmCom.lblStr8.Caption = strWord frmCom.lblInt8.Caption = lngWord dblVmin = (CDbl(lngWord) / 1024#) - 1# dblVmin = 5# * ((dblVminR1 + dblVminR2) / dblVminR1) * dblVmin + 5# 
frmCom.lblVal8.Caption = Decimals(dblVmin, 1)

Or, as explained here:
Capture d’écran 2025-11-19 231404.png

dblVmin = 5# * ((dblVminR1 + dblVminR2) / dblVminR1) * ((CDbl(lngWord) / 1024#) - 1#) + 5#
dblVmin = 5 * ((200000 + 4700) / 200000) * (641 / 1024) - 1) + 5
dblVmin = 7.2V -15.6V


Or, with : dblAnodeRs = 4.7, n = 1216, dblCalVar3 = 1.006 (et Ia = 635,4) :

Case 1 'dblIa is the anode current frmCom.lblStr1.Caption = strWord frmCom.lblInt1.Caption = lngWord dblIa = (CDbl(lngWord) * (5# / 1024#) * 1000#) * dblCalVar3 / dblAnodeRs dblMeasMX(I, intIa) = dblIa

dblIa = (CDbl(lngWord) * (5 / 1024) * 1000) * dblCalVar3 / dblAnodeRs
dblIa = (1216 * (5 / 1024) * 1000) * 1.006 / 4.7
dblIa = 1270,8  635.4
 

I admit that understanding this is a bit confusing for me. I apologize for all these questions.

Ihor

unread,
Nov 19, 2025, 6:06:24 PMNov 19
to uTracer
dblVmin = 5# * ((dblVminR1 + dblVminR2) / dblVminR1) * ((CDbl(lngWord) / 1024#) - 1#) + 5#
That formula is correct, your R1 and R2 values for uTracer6 are wrong. 
5 * ((4700 + 470) / 470) * ((641 / 1024) - 1) + 5 = -15.5712890625

Ihor


S3b

unread,
Nov 20, 2025, 9:21:33 AMNov 20
to uTracer
Thank you so much!
Arf, indeed! The mistake was mine; I wasn't looking at the right information (since there's so much information scattered around, I have trouble organizing everything I find and get confused very easily). It's really kind of you to help me anyway!

I'm wondering why retrieve the negative voltage value of -15V? Is it simply to check that the "Step-Down" is working correctly? Or is this value used for measurements?

All that's left for me to understand is the current. From the documents and code snippets I've read, it seems that the formula is the same for all four values ​​(Ia, Ia Comp, Is, Is Comp).

From my interpretation, what we're retrieving aren't the raw values ​​from the ADC for measuring the voltages across the shunt resistors. Otherwise, we won't be able to define a value of "1216" (the maximum of 10-bit ADC values ​​being 1024). I imagine there's already some initial processing with the PGA113? And therefore, that the returned Gain values ​​(Ia and Is) are indicative?

S3b

unread,
Nov 20, 2025, 5:37:47 PMNov 20
to uTracer
120*(5/1024)*(1000/4,7)*1,006 = 125,4
13*(5/1024)*(1000/4,7)*1,006 = 13,5

S3b

unread,
Nov 23, 2025, 6:06:34 PMNov 23
to uTracer
Okay, I managed to convert the most important data for interpretation. Thank you so much for your help. 

In the meantime, I tried the ESP32/uTracerJS version from https://boffin.nl and I think it's a real shame to reinvent the wheel (knowing that I wouldn't do any better than that version, that's for sure!). I also understand that you developed that version? If so, would it be possible to integrate the ability to print a quick test report using a small thermal printer? (Exemple : EM5820) (If needed, I can write a module/functions for this purpose?) 

Have a good evening, 
Sébastien

Ihor

unread,
Nov 24, 2025, 5:22:21 PMNov 24
to uTracer
I checked quickly and it would not be difficult to include that printer, as it supports TTL levels and even has a arduino (thermalprinting lib). I do not have one so indeed if there would be some tested piece of code for esp32 that works I can add it to work with the quick test functionality for example. 

I briefly checked EM5820 online, does it also supports self-adhesive labels or just standard paper for thermal printing? 

S3b

unread,
Nov 25, 2025, 4:53:38 AMNov 25
to uTracer
Great! For now, I have a piece of code that displays "test" on thermal paper, using an ESP32. I'm going to work on the code so it displays quick test information :) (and later on, it would be great if it could print a graph or two). Are you using ESP-IDF? Or the Arduino IDE?

Unfortunately, it seems to me that this printer doesn't support labels. Or maybe it does? If you're okay with it, I'll order a roll of thermal labels and do some testing! Because it would really be great to have that option!

S3b

unread,
Nov 30, 2025, 3:26:20 PMNov 30
to uTracer
Good evening,

I've finalized the various functions for printing the results of the quick test :) Since Arduino libraries were mentioned, I developed the code using the Arduino IDE.

The printer is controlled via TTL, and the printer's RX and TX (and GND) pins are directly connected to the ESP32 pins (because we're only using data transmission from the ESP32 to the printer. However, for advanced functions, it's preferable to use a logic level adapter or a simple resistor bridge).

The printer is powered between 5V to 9V (it's recommended to power it with 9V to improve print quality).

"const bool ECHO_TO_SERIAL = true;" Allows you to display the printed results (and the ESC/POS commands that will be interpreted by the printer). This gives an idea of ​​the result if you don't have a printer.


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 ]""""""""""""""""""""""

  // Paper feed & cutter (if the printer model allows it)
  uint8_t cutCmd[] = {0x1D, 'V', 0x00};
  pWriteCmd(cutCmd, sizeof(cutCmd));
 
  Serial.println("Printing finished.");
}

This function groups all the others and allows you to adjust what you want to print. 
Here are some examples with options enabled or disabled : 

 c1_20251130_21072381.jpeg

To display the date (Option 4), you'll need to retrieve this data from the user's device (phone, tablet, PC, etc.).

Option 5 allows you to specify an estimated measurement quality. If a measurement of a few milliamps is taken with a range of 0 to 750mA, the measurement won't necessarily be very accurate, and option 5 will indicate this. :)

If anything needs to be modified (like passing parameters via arrays, for example), feel free to ask. :)

(I also plan, if there's interest in the future, to develop a function for plotting curves with the thermal printer.)

Attached is a demo program containing all the functions. Let me know what you think and if you'd like to integrate it into your program. :)

#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.");
}

Ihor

unread,
Dec 2, 2025, 3:53:21 PMDec 2
to uTracer
Looks nice! I think I am going to order that printer as well, so I can see what happens with the code. You also mentioned that you ordered sticky labels, I was wondering if that works. 

Did you run it from 5V or 9V? (v in principle very easy to get from 5V with cheap 1$ Chinese voltage boosters (XL6009), I used then before with good results, but probably the quality with 5V is also fine? 

I am also curious if the quality of the printing stays good. I remember having documents printed using a FAX on a thermal papers and some of them fade away over years even without exposure to direct light, but it does takes a couple of years:) 

Martin Manning

unread,
Dec 3, 2025, 9:06:55 AMDec 3
to uTracer
>> I am also curious if the quality of the printing stays good. I remember having documents printed using a FAX on a thermal papers and some of them fade away over years even without exposure to direct light, but it does takes a couple of years:) 

This is a function of the type of thermal printer. Direct thermal printing may only be good for a few months, but some direct thermal printing papers may be more durable than others. Thermal Transfer printing, commonly used for label making in laboratory and industrial settings can remain legible for years, but the cost of the machine and media is much higher.

Deejay Davo

unread,
Dec 3, 2025, 10:00:13 AMDec 3
to utr...@googlegroups.com
Hi Sebastian,
Looks really nice though!
Great addon! 
Quite a lazy add-on  haha no more writing on stickers or boxes..
So if someone can make a robot arm with some Al injection we can automate the whole process and can go straight into the Matrix lol


Will this work on a Dymo labelwriter also?
You can buy them quite cheap 2nd also, and they hardly go bad.. Just print a label and you can stick it to anywhere
The Dymo 450 can print aftermarket labels which are quite cheap, but  the 550 uses a chip reader, that can be disabled…


Martin, lol a fax that is quite old school nowadays, those papers must be faded…
I use a Dymo label printer which uses thermal printing also, but you have special durable labels also… 
I though it was a polypropylene base..
The best would be with a toner like a laserprinter, but I guess a labelprinter with toner should be quite expensive…



Op wo 3 dec 2025 om 15:06 schreef Martin Manning <mman...@fuse.net>

Sébastien D.

unread,
Dec 3, 2025, 12:57:51 PM (14 days ago) Dec 3
to uTracer
Great :) I haven't ordered any labels for the thermal printer yet (to be honest, I can't seem to find any good quality ones).

I am indeed using a step-down converter to get 9V (with ~16V to the Li-Ion battery). It seems to me that when the printer is running (motor + thermal printhead), it can draw a lot of current (I aimed for 2A to be on the safe side).
I'm going to try using 5V instead of 9V for the power supply. Here's my wiring:

IMG_20251203_152043.jpg

Indeed, with thermal printing, the marking will always disappear eventually. It's one of the limitations of this technology. The factors that accelerate degradation are: UV (sunlight), humidity, and grease/chemicals (hand grease, alcohol, gasoline, acetone, etc.).
The quality of the thermal paper also has a significant impact. Good thermal paper will last over 10 years (even 25 years for some). Poor thermal paper will only last a few years.

For me, a 10-year lifespan for measurement information is acceptable.

Because I've already tested NOS tubes in sealed boxes and realized they were completely unusable. Time always takes its toll !

That's why testing my tubes every 10 years doesn't seem like a bad idea, and if the measurement notes fade after 10 years, it will force me to redo them and see, or not, if my tubes are aging well!

Message has been deleted

Sébastien D.

unread,
Dec 5, 2025, 8:32:04 AM (12 days ago) Dec 5
to uTracer
Hello,

There's space in the enclosure for a step-up or step-down voltage regulator.

c1_20251205_10394084.jpeg
c1_20251205_10394110.jpeg

I've done some tests with different voltage levels.

-> 9V: the paper comes out in 2 seconds, the print is very dark and slightly blurry.
-> 5V: it takes 12 seconds for the paper to come out, the print is very sharp.
-> 3.3V: the print is too slow and the print is completely illegible.

IMG_20251205_001257.jpg


It seems to me that it's possible to use a Dymo label maker, but it requires a major hack. Some people manage to do it by emulating the keyboard using multiplexers !
Reply all
Reply to author
Forward
0 new messages