USB to Serial link for Shell access <auto start>

ettill

New Member
Nov 22, 2025
14
14
3
Greetings everyone,

Im trying to see if I can get a USB-Serial adapter to work as a local shell access. I need it to start automatically and only have access to Proxmox and not via a VM.
My motherboard does NOT have a serial port on it, but does have USBs internal & External.

Im continuing my trek to have a OLED screen to display the CPU Usage & Temp, and Mem Usage information. I have a temp workaround of an ESP32 logging into it via WiFi and parsing the data from Glances. it works nicely... but it relies on the WiFi .... and i would prefer to keep in internal on a Serial interface.
I understand that the ESP32 will boot faster and will have to 'wait for' the log in prompt then log in (i will setup a Glances guest user just to run that) then parse the data coming from the shell accessed Glances page.

Just need some help getting Proxmox to make the USB-serial adapter into a Console port automatically when it boots.

Thanks in advance to any assistance or direction to where it has been done before.

Michael
 
Hey everyone ... I figured it all out .... .... lost about half my head of hair .. and my loved ones are avoiding me for being so grumpy .. but i WON...

OK so what i did was ... With the ESP32c6 Plugged into a USB port. in shell as root .... Identified it using
Code:
lsusb
udevadm info -a -n /dev/ttyACM0

Look for: idVendor and idProduct (e.g., Espressif 303a:1001) Serial number for uniqueness (optional)

Next create udev rule:
Code:
nano /etc/udev/rules.d/99-espressif-acm.rules
Contents :
Code:
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", SYMLINK+="ttyACM_espressif"

Save and exit nano ...... now reload rules & triggers
Code:
udevadm control --reload-rules
udevadm trigger
ls -l /dev/ttyACM_espressif

You should see /dev/ttyACM_espressif -> ttyACM0

Next I created a seperate Glances user and gave it serial 'dialout' control and set is path ** NOTE im using the addon not LXC so adjust if needed.
Code:
adduser --disabled-password --gecos "Glances User" glances
usermod -aG dialout glances
chsh -s /opt/glances/.venv/bin/glances glances

Next create a small script that executes Glances using a real shell :
Code:
cat << 'EOF' > /usr/local/bin/glances-shell
#!/bin/bash
exec /opt/glances/.venv/bin/glances --stdout cpu.total,mem.percent,sensors.Composite.value,network.nic2.bytes_recv_rate_per_sec,network.nic2.bytes_sent_rate_per_sec
EOF

#### This script monitors CPU Usage, Mem Usage, CPU Temp , Network 2 interface bytes Receive and Transmit per sec
#### CPU usage = cpu.total
#### MEM Usage = mem.percent
#### CPU Temp = sensors.Composite.value
#### NIC2 Rx/s = network.nic2.bytes_recv_rate_per_sec ** Change the nic2 to your label or remove it to monitor all
#### NIC2 Tx/s = network.nic2.bytes_sent_rate_per_sec ** Change the nic2 to your label or remove it to monitor all
Others can be added to it just update the script, I just played around with each module call to find what i wanted....
like just running glances in a shell with the --stdout then a module like 'cpu' will show all child extensions to it.
Example Prox:~# glances --stdout cpu
gave me this
Code:
 cpu: {'total': 4.4, 'user': 8.2, 'nice': 0.0, 'system': 1.3, 'idle': 87.1, 'iowait': 3.4, 'irq': 0.0, 'steal': 0.0, 'guest': 0.9,     'ctx_switches': 16290680, 'interrupts': 13606047, 'soft_interrupts': 9108293, 'syscalls': 0, 'cpucore': 8}
you can see each '' is a separate sub descriptor

be careful some have mutli like in the 'sensors' .. and they are motherboard specific ... when I ran the same type of command for sensors.
example Prox:~# glances --stout sensors
I get alot of stuff
Code:
sensors: [{'label': 'Composite', 'unit': 'C', 'value': 32, 'warning': 74, 'critical': 79, 'type': 'temperature_core', 'key': 'label'},     {'label': 'Composite 1', 'unit': 'C', 'value': 33, 'warning': 83, 'critical': 87, 'type': 'temperature_core', 'key': 'label'}, {'label':     'Core 0', 'unit': 'C', 'value': 35, 'warning': 105, 'critical': 105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 1',     'unit': 'C', 'value': 35, 'warning': 105, 'critical': 105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 2', 'unit': 'C',     'value': 35, 'warning': 105, 'critical': 105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 3', 'unit': 'C', 'value': 35,     'warning': 105, 'critical': 105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 4', 'unit': 'C', 'value': 36, 'warning': 105,     'critical': 105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 5', 'unit': 'C', 'value': 36, 'warning': 105, 'critical':     105, 'type': 'temperature_core', 'key': 'label'}, {'label': 'Core 6', 'unit': 'C', 'value': 36, 'warning': 105, 'critical': 105, 'type':     'temperature_core', 'key': 'label'}, {'label': 'Core 7', 'unit': 'C', 'value': 37, 'warning': 105, 'critical': 105, 'type':     'temperature_core', 'key': 'label'}, {'label': 'Package id 0', 'unit': 'C', 'value': 37, 'warning': 105, 'critical': 105, 'type':     'temperature_core', 'key': 'label'}, {'label': 'Sensor 1', 'unit': 'C', 'value': 40, 'warning': 65261, 'critical': 65261, 'type':     'temperature_core', 'key': 'label'}, {'label': 'Sensor 2', 'unit': 'C', 'value': 46, 'warning': 65261, 'critical': 65261, 'type':     'temperature_core', 'key': 'label'}, {'label': 'Sensor 2 2', 'unit': 'C', 'value': 33, 'warning': 65261, 'critical': 65261, 'type':     'temperature_core', 'key': 'label'}, {'label': 'acpitz 0', 'unit': 'C', 'value': 27, 'warning': None, 'critical': None, 'type':     'temperature_core', 'key': 'label'}]
as you can see they are weird. also they are case sensitive .. but after a little trial and error was able to find 'sensors.Composite.value" worked

Make the script executable:
Code:
chmod +x /usr/local/bin/glances-shell
usermod -s /usr/local/bin/glances-shell glances

Next setup systemd serial-getty for esp32 by creating a fold and a config file
Code:
mkdir -p /etc/systemd/system/serial-getty@ttyACM_espressif.service.d
nano /etc/systemd/system/serial-getty@ttyACM_espressif.service.d/override.conf
it's contents :
Code:
[Service]
ExecStart=
ExecStart=-/sbin/agetty -L -i 115200 %I vt102 --autologin glances

This will auto start Glances apon boot.
... land last reload systemd and enable service
Code:
systemctl daemon-reexec
systemctl enable serial-getty@ttyACM_espressif.service
systemctl start serial-getty@ttyACM_espressif.service
systemctl status serial-getty@ttyACM_espressif.service


Now the ESP32 side :
Code:
#include <U8g2lib.h>

// ----- OLED setup (ESP32-C6 Xiao I2C pins) -----
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /*SCL=*/ 23, /*SDA=*/ 22);

// ----- Page timing -----
const unsigned long PAGE_INTERVAL = 3000;
unsigned long lastPageSwitch = 0;
bool pageCPU = true;

// ----- Glances values -----
float cpuUsage = 0;
float memUsage = 0;
float cpuTemp = 0;
float rxRate = 0;
float txRate = 0;

String lineBuffer = "";

// ----- Function prototypes -----
void parseLine(String &line);
void drawCPUPage();
void drawNetworkPage();
void drawBar(int x,int y,int w,int h,float percent);

void setup() {
  Serial.begin(115200);
  delay(500); // allow USB Serial to initialize
  //Serial.println("Glances reader starting...");

  u8g2.begin();
  u8g2.setFont(u8g2_font_8x13_tf);
  u8g2.clearBuffer();
  u8g2.setCursor(0,12);
  //u8g2.print("Waiting for data...");
  u8g2.sendBuffer();
}

void loop() {
  // --- Read Glances output ---
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n') {
      parseLine(lineBuffer);
      lineBuffer = "";
    } else if (c != '\r') {
      lineBuffer += c;
    }
  }

  // --- Page switching ---
  if (millis() - lastPageSwitch > PAGE_INTERVAL) {
    lastPageSwitch = millis();
    pageCPU = !pageCPU;
  }

  // --- Draw current page ---
  if (pageCPU) drawCPUPage();
  else drawNetworkPage();
}

// ----- Parse Glances line robustly -----
void parseLine(String &line) {
  int sep = line.indexOf(':');
  if(sep < 0) return;

  String key = line.substring(0, sep);
  String valueStr = line.substring(sep + 1);
  float value = valueStr.toFloat();

  if(key == "cpu.total") cpuUsage = value;
  else if(key == "mem.percent") memUsage = value;
  else if(key == "sensors.Composite.value") cpuTemp = value;
  else if(key == "network.nic2.bytes_recv_rate_per_sec") rxRate = value;
  else if(key == "network.nic2.bytes_sent_rate_per_sec") txRate = value;

 // Serial.println(line); // debug echo
}

// ----- Draw CPU page -----
void drawCPUPage() {
  u8g2.clearBuffer();

// line 1: CPU Usasge
  u8g2.setCursor(0,14);
  u8g2.print("CPU ");
  u8g2.print(cpuUsage,1);
  u8g2.print("%");
  drawBar(75, 2, 50, 14, cpuUsage);

// line 2: Memory Usage
  u8g2.setCursor(0,34);
  u8g2.print("MEM ");
  u8g2.print(memUsage,1);
  u8g2.print("%");
  drawBar(75,22,50,14, memUsage);

// line 3: CPU temp
  u8g2.setCursor(0,54);
  u8g2.print("TEMP ");
  u8g2.print(cpuTemp,1);
  u8g2.print("C");

  u8g2.sendBuffer();
}

// ----- Draw Network page with auto-unit formatting -----
void drawNetworkPage() {
  u8g2.clearBuffer();

  // --- Lambda-style formatter ---
  auto formatRate = [](float bytesPerSec, String &unit) -> float {
    float value = bytesPerSec;
    unit = "B";
    if (value >= 1024.0) { value /= 1024.0; unit = "K"; }
    if (value >= 1024.0) { value /= 1024.0; unit = "M"; }
    if (value >= 1024.0) { value /= 1024.0; unit = "G"; }
    return value;
  };

  String rxUnit, txUnit;
  float rxValue = formatRate(rxRate, rxUnit);
  float txValue = formatRate(txRate, txUnit);

  float rxPercent = (rxRate / 1e9) * 100.0; // bargraph 0-1Gb/s
  float txPercent = (txRate / 1e9) * 100.0;

  u8g2.setCursor(0,14);
  u8g2.print(" nic2 interface");

  u8g2.setCursor(0,34);
  u8g2.print("R ");
  u8g2.print(rxValue,1);
  u8g2.print(" ");
  u8g2.print(rxUnit);
  drawBar(80,22,45,14, rxPercent);

  u8g2.setCursor(0,54);
  u8g2.print("T ");
  u8g2.print(txValue,1);
  u8g2.print(" ");
  u8g2.print(txUnit);
  drawBar(80,44,45,14, txPercent);

  u8g2.sendBuffer();
}

// ----- Draw a horizontal bar 0-100% -----
void drawBar(int x,int y,int w,int h,float percent) {
  if(percent<0) percent=0;
  if(percent>100) percent=100;
  u8g2.drawFrame(x,y,w,h);
  int fill = (int)((percent/100.0)*(w-2));
  if(fill>0) u8g2.drawBox(x+1,y+1,fill,h-2);
}

Now this is TOTAL isolated and not on the network ... .. just a simple usb plugin.