The USB port presents as 0665:5161 ("Cypress USB to Serial" — the name is misleading). There's no CDC/serial interface, so no /dev/ttyUSB0 ever appears. Linux binds it as a raw HID device, /dev/hidrawN, and axpert.py talks to it through hidapi.
On the tested unit the product string reads "STMicroelectronics HID in FS Mode" — the inverter's own STM32 speaks USB directly, borrowing Cypress's ID so the vendor's Windows software recognises it.
The same protocol is available on the RS232/RJ45 port at 2400 baud 8N1, if you'd rather use a UART.
request: <ASCII command> <CRC hi> <CRC lo> 0x0D
reply: ( <ASCII data> <CRC hi> <CRC lo> 0x0D
- CRC is CRC-16/XMODEM (poly
0x1021, init 0, no reflection). - If a CRC byte is
0x28(,0x0ALF or0x0DCR, the protocol adds 1 to it, so it can't be mistaken for a delimiter. - The reply's CRC covers the leading
(. Evidence: the error reply(NAKss—crc("(NAK")isss. axpert.pychecks every reply's CRC and drops corrupted ones.
Example — QPIGS is exactly 8 bytes: 51 50 49 47 53 B7 A9 0D.
- Commands go out in 8-byte HID output reports, zero-padded. A 14-byte command is two reports; the inverter buffers until it sees CR.
- On Linux,
hid_write()wants a report-ID byte first (0x00here). Some units of this family want it, some don't —axpert.pytries one, falls back to the other, and remembers which worked. - Replies come back as 8-byte input reports; ~14 of them for a
QPIGSreply. Read until0x0D. - The ESP32 bridge does the same as SET_REPORT control transfers out and interrupt IN for replies — what the Linux kernel does behind
/dev/hidraw.
All reads are Q… commands. Setting changes start with P, M, D or F; this project only ever sends POP0x, PCP0x and DAT…, and only when you ask.
| Command | Gives |
|---|---|
QPI |
Protocol (PI30) |
QMN, QID, QSID, QVFW |
Model, serial, long serial (contains QID), firmware |
QMOD |
Mode: L grid, B battery, S standby, F fault… |
QPIGS |
Live status (21 fields, or 24 on some firmware) |
QPIRI |
Ratings and current settings |
QPIWS |
Warning bits (bit 5 = line fail) |
QFLAG |
Feature flags (buzzer, auto-restart, …) |
QBMS |
What the battery's BMS reports, when one is linked |
QOPPT, QCHPT |
24 hourly output / charger priority slots (+4 trailing values) |
QET, QLT |
Lifetime solar / load, Wh |
QEDyyyymmdd, QLDyyyymmdd, QEMyyyymm, QEYyyyy… |
Daily, monthly, yearly energy |
QT |
Inverter clock |
| Field | Finding | How |
|---|---|---|
load_percent |
Exactly floor(VA ÷ rated VA × 100) — apparent power, truncated |
45/45 samples |
pv_charging_power |
Solar power reaching the battery, after charger losses | Always a few W under PV volts × amps, never above |
| Battery current | Whole amps only — ±27 W per sample at 53 V | Raw values |
| Energy balance | Solar + battery = load + ~10–30 W (the inverter's own draw) | Closes on every run |
| Status bits b2/b1/b0 | Charging / from solar / from grid | Flip in step with the measured charge current |
QOPPT/QCHPT |
First 24 values are hourly slots | Match the priorities in QPIRI |
Battery type 8 |
Lithium with a BMS link (LIb) | QBMS answers, SOC and charge limit sync |
- Energy history follows the inverter's clock.
QED20260816returns nothing if the inverter thinks it's 2027. ReadQTfirst and build dates from that (the dashboard does). After correcting the clock, older days stay filed under the old dates. - 24-field
QPIGS. Some firmware appends feed-to-grid status, country code and exported watts.parse()keeps any extra fields asunmapped_Nrather than dropping them. - In BMS mode,
QPIRI's battery voltages can be stale. The BMS's values (QBMS) are what's in force. Saving any setting on the screen refreshes them. - The BMS changes the charge limit as the battery fills (seen 10 A → 80 A → 60 A on one unit). A low limit near full is normal.
- Programs 12/13/29 in percent (lithium mode) aren't reported over PI30; only voltage-mode values appear in
QPIRI.