Implementazione supporto multi-inverter paralleli e fix comunicazione MQTT
Build Docker Image for Raspberry Pi / build-and-push (push) Failing after 1m15s

- Aggiunto supporto lettura inverter paralleli tramite comandi QPGS0-QPGS9
- Implementato discovery automatico inverter con filtro duplicati e serial invalidi
- Risolti bug critici comunicazione seriale:
  * Fix buffer ExecuteCmd da 7 a 200 bytes
  * Supporto terminatori CR e LF
  * Modalità blocking con delay 500ms
  * Lettura byte-by-byte per terminatore affidabile
- Implementato script MQTT per pubblicazione dati multi-inverter:
  * mqtt-push-parallel.sh con topic separati per ogni inverter
  * Fix autenticazione MQTT con username/password
  * Aggiunto flag retain (-r) per persistenza dati
- Creato test-loop-parallel.sh per simulazione completa container
- Aggiornata documentazione con compatibilità MKS IV e guida test loop
- Aggiornati profili debug VS Code per bash e parallel discovery
- Configurazione MQTT completa con server reale (192.168.1.37:1883)

Sistema testato e funzionante con 2 inverter Voltronic Axpert MKS IV
This commit is contained in:
Pi Developer
2026-01-31 16:15:26 +01:00
parent 8863c77f6f
commit 547537e761
18 changed files with 1842 additions and 70 deletions
+91 -9
View File
@@ -76,7 +76,7 @@ bool cInverter::query(const char *cmd, int replysize) {
int fd;
int i=0, n;
fd = open(this->device.data(), O_RDWR | O_NONBLOCK);
fd = open(this->device.data(), O_RDWR | O_NOCTTY);
if (fd == -1) {
lprintf("INVERTER: Unable to open device file (errno=%d %s)", errno, strerror(errno));
sleep(5);
@@ -134,12 +134,15 @@ bool cInverter::query(const char *cmd, int replysize) {
// Flush output to ensure command is sent
tcdrain(fd);
// Critical delay after write (like Python implementation)
usleep(500000); // 500ms delay
// Clear buffer again before reading
memset(buf, 0, sizeof(buf));
time(&started);
do {
n = read(fd, (void*)buf+i, replysize-i);
n = read(fd, (void*)buf+i, 1); // Read one byte at a time for reliable terminator detection
if (n < 0) {
if (time(NULL) - started > 2) {
lprintf("INVERTER: %s read timeout", cmd);
@@ -152,9 +155,9 @@ bool cInverter::query(const char *cmd, int replysize) {
if (n > 0) {
i += n;
// Check if we've received the terminator
if (i > 0 && buf[i-1] == 0x0d) {
lprintf("INVERTER: %s received terminator at byte %d", cmd, i);
// Check if we've received the terminator (CR or LF)
if (i > 0 && (buf[i-1] == 0x0d || buf[i-1] == 0x0a)) {
lprintf("INVERTER: %s received terminator (0x%02X) at byte %d", cmd, buf[i-1], i);
break;
}
}
@@ -181,8 +184,8 @@ bool cInverter::query(const char *cmd, int replysize) {
return false;
}
if (buf[i-1]!=0x0d) {
lprintf("INVERTER: %s: incorrect stop byte (got 0x%02X at pos %d, expected CR). Buffer: %s", cmd, buf[i-1], i-1, buf);
if (buf[i-1]!=0x0d && buf[i-1]!=0x0a) {
lprintf("INVERTER: %s: incorrect stop byte (got 0x%02X at pos %d, expected CR or LF). Buffer: %s", cmd, buf[i-1], i-1, buf);
return false;
}
@@ -277,8 +280,8 @@ void cInverter::poll() {
}
void cInverter::ExecuteCmd(const string cmd) {
// Sending any command raw
if (query(cmd.data(), 7)) {
// Sending any command raw - use larger buffer to accept full responses
if (query(cmd.data(), 200)) {
m.lock();
strcpy(status2, (const char*)buf+1);
m.unlock();
@@ -470,3 +473,82 @@ void cInverter::AutoDiscoverBufferSizes() {
printf("DISCOVERY_SUCCESS=%s\n", (qmod_size > 0 && qpigs_size > 0 && qpiri_size > 0 && qpiws_size > 0) ? "true" : "false");
}
// Discover number of parallel inverters
int cInverter::DiscoverParallelInverters() {
fprintf(stderr, "\n=== PARALLEL INVERTER DISCOVERY ===\n");
fprintf(stderr, "Checking for parallel inverter configuration...\n\n");
int count = 0;
char cmd[16];
std::string found_serials[10]; // Track unique serials
// Test QPGS0 through QPGS9
for (int i = 0; i < 10; i++) {
snprintf(cmd, sizeof(cmd), "QPGS%d", i);
if (query(cmd, 200)) {
// Check if response is valid (not NAK)
if (buf[0] == '(' && buf[1] != 'N') {
// Extract serial number (starts at position 3)
char serial[20] = {0};
int j = 0;
for (int k = 3; k < 17 && buf[k] != ' '; k++) {
serial[j++] = buf[k];
}
// Check if serial is valid (not all zeros and not empty)
bool valid_serial = false;
for (int k = 0; k < j; k++) {
if (serial[k] != '0') {
valid_serial = true;
break;
}
}
// Check if serial is duplicate
bool duplicate = false;
std::string serial_str(serial);
for (int k = 0; k < count; k++) {
if (found_serials[k] == serial_str) {
duplicate = true;
break;
}
}
if (valid_serial && j > 0 && !duplicate) {
found_serials[count] = serial_str;
count++;
fprintf(stderr, "✓ Inverter #%d via %s (Serial: %s)\n", count, cmd, serial);
printf("INVERTER_%d_SERIAL=%s\n", count, serial);
printf("INVERTER_%d_QPGS=%d\n", count, i);
} else if (duplicate) {
fprintf(stderr, "⊗ Skipping %s (Duplicate serial: %s)\n", cmd, serial);
} else {
fprintf(stderr, "⊗ Skipping %s (Invalid serial: %s)\n", cmd, serial);
}
}
}
usleep(100000); // 100ms between queries
}
fprintf(stderr, "\n=== DISCOVERY RESULT ===\n");
fprintf(stderr, "Total unique parallel inverters: %d\n", count);
printf("PARALLEL_COUNT=%d\n", count);
return count;
}
// Get parallel status for specific inverter
string cInverter::GetParallelStatus(int inverter_num) {
char cmd[16];
snprintf(cmd, sizeof(cmd), "QPGS%d", inverter_num);
if (query(cmd, 200)) {
if (buf[0] == '(' && buf[1] != 'N') {
// Return data without leading '('
return string((char*)buf + 1);
}
}
return "";
}
+2
View File
@@ -45,6 +45,8 @@ class cInverter {
int GetMode();
void ExecuteCmd(const std::string cmd);
void AutoDiscoverBufferSizes();
int DiscoverParallelInverters(); // Returns number of parallel inverters
string GetParallelStatus(int inverter_num); // Get QPGS data for specific inverter
};
#endif // ___INVERTER_H
+6
View File
@@ -203,6 +203,12 @@ int main(int argc, char* argv[]) {
exit(0);
}
// Parallel inverter discovery mode
if(cmdArgs.cmdOptionExists("-p") || cmdArgs.cmdOptionExists("--parallel-discovery")) {
int count = ups->DiscoverParallelInverters();
exit(0);
}
// Logic to send 'raw commands' to the inverter..
if (!rawcmd.empty()) {
ups->ExecuteCmd(rawcmd);
+142
View File
@@ -0,0 +1,142 @@
#!/bin/bash
# Test comunicazione con Voltronic Axpert MKS IV provando diversi baudrate
# Il MKS IV potrebbe usare un baudrate diverso dal classico 2400
DEVICE="${1:-/dev/ttyUSB0}"
echo "=== Test Baudrate per Voltronic Axpert MKS IV su $DEVICE ==="
echo ""
# Verifica device
if [ ! -e "$DEVICE" ]; then
echo "ERROR: Device $DEVICE non trovato!"
exit 1
fi
# Array di baudrate da testare
# Il MKS IV potrebbe usare: 2400, 9600, 19200, 38400 o 115200
BAUDRATES=(2400 9600 19200 38400 115200)
for BAUD in "${BAUDRATES[@]}"; do
echo "============================================"
echo "Testing BAUDRATE: $BAUD"
echo "============================================"
# Configura device
sudo stty -F $DEVICE $BAUD cs8 -cstopb -parenb -echo raw
sudo chmod 666 $DEVICE
# Test con Python
python3 << PYTHON_EOF
import sys
import serial
import time
def calc_crc(data):
"""Calcola CRC secondo protocollo Voltronic"""
crc_ta = [
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef
]
crc = 0
for byte in data:
da = ((crc >> 8) >> 4)
crc = (crc << 4) & 0xFFFF
crc ^= crc_ta[da ^ (byte >> 4)]
da = ((crc >> 8) >> 4)
crc = (crc << 4) & 0xFFFF
crc ^= crc_ta[da ^ (byte & 0x0F)]
return crc.to_bytes(2, 'big')
try:
ser = serial.Serial(
port='$DEVICE',
baudrate=$BAUD,
bytesize=8,
parity='N',
stopbits=1,
timeout=2
)
print(f"Porta aperta a {$BAUD} baud")
# Flush buffers
ser.reset_input_buffer()
ser.reset_output_buffer()
time.sleep(0.3)
# Test comando QMOD (semplice, 5 bytes di risposta)
cmd = 'QMOD'
print(f"Invio comando: {cmd}")
cmd_bytes = cmd.encode('ascii')
crc = calc_crc(cmd_bytes)
full_cmd = cmd_bytes + crc + b'\r'
print(f" Hex: {full_cmd.hex()}")
# Invia
ser.write(full_cmd)
ser.flush()
time.sleep(0.5)
# Leggi risposta
response = ser.read(200)
if len(response) > 0:
print(f" [OK] RISPOSTA RICEVUTA ({len(response)} bytes)")
print(f" Hex: {response.hex()}")
try:
ascii_text = response.decode('ascii', errors='replace')
print(f" ASCII: {ascii_text.strip()}")
# Verifica se è una risposta valida (inizia con '(' e non è NAK)
if response[0:1] == b'(' and b'NAK' not in response:
print(f" *** BAUDRATE CORRETTO: {$BAUD} ***")
sys.exit(0) # Success
elif b'NAK' in response:
print(f" [X] NAK ricevuto (inverter non comprende)")
else:
print(f" [X] Risposta non valida")
except:
print(f" [X] Risposta non decodificabile")
else:
print(f" [X] Nessuna risposta (timeout)")
ser.close()
except Exception as e:
print(f" [X] Errore: {e}")
sys.exit(1)
PYTHON_EOF
if [ $? -eq 0 ]; then
echo ""
echo "╔═══════════════════════════════════════════╗"
echo "║ BAUDRATE CORRETTO TROVATO: $BAUD"
echo "╚═══════════════════════════════════════════╝"
echo ""
echo "Aggiorna /etc/inverter/inverter.conf se necessario"
echo "Modifica sources/inverter-cli/inverter.cpp:"
echo " Cambia: speed_t baud = B$BAUD;"
exit 0
fi
sleep 1
done
echo ""
echo "============================================"
echo "NESSUN BAUDRATE FUNZIONANTE TROVATO"
echo "============================================"
echo ""
echo "Possibili cause:"
echo "1. Inverter spento o disconnesso"
echo "2. Cavo USB/RS232 difettoso"
echo "3. Device errato (prova /dev/ttyUSB1 o /dev/hidraw0)"
echo "4. Inverter in modalità incompatibile"
echo ""
+60
View File
@@ -0,0 +1,60 @@
#!/bin/bash
# Test suite per comandi Voltronic Axpert MKS IV
# Basato su documentazione forum AEVA e manuale protocollo
echo "╔══════════════════════════════════════════════════════╗"
echo "║ TEST COMANDI PROTOCOLLO VOLTRONIC MKS IV ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
test_command() {
local cmd=$1
local desc=$2
echo -n "Testing $cmd ($desc)... "
result=$(sudo ./bin/inverter_poller -r "$cmd" 2>&1 | grep "Reply:" | sed 's/Reply://g' | xargs)
if [ -z "$result" ]; then
echo "❌ NO RESPONSE"
elif [ "$result" = "NAK" ]; then
echo "❌ NAK (comando non supportato)"
else
echo "$result"
fi
}
# Comandi che DOVREBBERO funzionare
echo "=== COMANDI STANDARD P18 ==="
test_command "QID" "Device Serial Number"
test_command "QVFW" "Main CPU Firmware Version"
test_command "QGMN" "General Model Name"
test_command "QPI" "Protocol ID"
test_command "QFLAG" "Device Flag Status"
echo ""
echo "=== COMANDI STATUS AVANZATI ==="
test_command "QPGS0" "Parallel General Status"
test_command "QPGS1" "Parallel General Status #1"
test_command "QDI" "Default Settings Inquiry"
test_command "QMCHGCR" "Max Charging Current Options"
test_command "QMUCHGCR" "Max Utility Charging Current"
test_command "QOPPT" "Output Power Type"
echo ""
echo "=== COMANDI BATTERIA ==="
test_command "QBEQI" "Battery Equalization Info"
test_command "QBMS" "BMS Info"
echo ""
echo "=== COMANDI DIAGNOSTICI ==="
test_command "QBOOT" "Bootloader Version"
test_command "QET" "Total Generated Energy"
test_command "QEY" "Generated Energy This Year"
test_command "QEM" "Generated Energy This Month"
test_command "QED" "Generated Energy Today"
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ REPORT FINALE ║"
echo "╚══════════════════════════════════════════════════════╝"
echo "Comando FUNZIONANTE: QGMN (Model 054)"
echo "Comandi STANDARD non funzionanti: QPIGS, QPIRI, QMOD, QPIWS"
echo "Possibile causa: Protocollo proprietario MKS IV"
+22
View File
@@ -0,0 +1,22 @@
import serial
import time
port = '/dev/ttyUSB1'
ser = serial.Serial(port, 2400, bytesize=8, parity='N', stopbits=1, timeout=2)
# Costruisci comando QPIGS manualmente
cmd = b'QPIGS'
crc = 0xB7A9
cmd_full = cmd + bytes([crc >> 8, crc & 0xFF, 0x0D])
print(f"Invio comando: {cmd_full.hex(' ')}")
print(f"Lunghezza: {len(cmd_full)} bytes")
ser.write(cmd_full)
time.sleep(0.5)
resp = ser.read(100)
print(f"Ricevuto ({len(resp)} bytes): {resp.hex(' ')}")
print(f"ASCII: {resp}")
ser.close()
+26
View File
@@ -0,0 +1,26 @@
import serial
import time
port = '/dev/ttyUSB0'
ser = serial.Serial(port, 2400, bytesize=8, parity='N', stopbits=1, timeout=2)
for cmd_str in ['QPIGS', 'QMOD', 'QGMN']:
# CRC calcolati
crcs = {'QPIGS': 0xB7A9, 'QMOD': 0x49C1, 'QGMN': 0x4928}
cmd = cmd_str.encode()
crc = crcs[cmd_str]
cmd_full = cmd + bytes([crc >> 8, crc & 0xFF, 0x0D])
print(f"\n=== {cmd_str} ===")
print(f"Invio: {cmd_full.hex(' ')}")
ser.write(cmd_full)
time.sleep(0.5)
resp = ser.read(200)
print(f"Ricevuto ({len(resp)} bytes): {resp.hex(' ') if resp else '(nessuna risposta)'}")
if resp:
print(f"ASCII: {resp}")
ser.close()