Decoding Libert GXT2 serial protocol

Introduction

This note describes the serial protocol used by a Libert GXT2 3000. This is a 2U rack mount 3000 VA UPS, with 9 pin serial port that supports both contact closure (basic signalling) and proper serial signalling, which Libert refer to as the ESP II protocol. The specification of the ESP 2 protocol appears to be a closely guarded secret, the available documentation talk about its existance, but if you want your Libert UPS to talk to a daemon your only choice of software is the Libert's Multilink software. This is a group of Java programs, consisting of a daemon and a viewer. Seems to work ok, but for two drawbacks, i) Its java, and who on earth runs a java daemon for mission critical monitoring? ii) All the options related multi-machines (i.e., telling other machines about the status) appear to need the purchase of a licences to enable these features. Having just bought their hardware, why am I supposed to pay again for what should be included?

With all that, I went about trying to reverse engineer their protocol with the final goal of including an appropriate driver to NUT. At the moment, I can say the information is correct for the GXT2-3000RT230, but I can not be sure it doesn't change between models, it is possible the commands/locations change with each model, or even in each firmware. This is because this UPS sends 266 commands/requests to the UPS and gets 266 responses, but I could only assign a purpose to 56 of these, and any of these others could be instructing the software to use a specific offset value or command set.

ESP-II (ESP 2) Protocol

This information is based on observation, comparing the output of Multilink to the contents of packets send between the host computer and the UPS. As a result this is open to reinterpretation.

Communication is via the serial port at 2400 baud, 8N1. The host software (Multilink) sends 6 byte packets (5 bytes and a checksum) and the UPS responds with 8 byte packets (7 bytes and a checksum). In all recorded cases a command sent to the UPS receives a reply, though in 4 out of 106323 cases, the reply was late and arrived after a second command had been sent.

There were 266 recorded commands sent to the UPS, of those 56 had an identified function. Each reply has a 16 bit payload at the 6th and 7th byte. Numbers are 16 bit big endian. Alphanumberic fields are byte swapped (first is 7th byte, second is 6th byte), unused alphanumeric fields are filled with spaces. The numeric fields have various units (obviously), some are *10 the actual value to represent numbers with a resolution of one decimal place.

The identified commands were (decimal values including checksum):
001,136,002,001,004,144 to xxx,xxx,xxx,xxx,018,xxx : Model name, 30 bytes, i.e. "GXT2-3000RT230                "
001,136,002,001,019,159 to xxx,xxx,xxx,xxx,026,xxx : Firmware version, 16 bytes, i.e. "GXT2MR15D-2K3K  "
001,136,002,001,027,167 to xxx,xxx,xxx,xxx,036,xxx : Serial Number, 20 bytes, i.e. "2343010092FB591     "
001,136,002,001,037,177 to xxx,xxx,xxx,xxx,040,xxx : Date of manufacture, 4 words, i.e. "07OCT02 " for 7th October 2002
001,149,002,001,001,154 : Expected runtime, minutes
001,149,002,001,002,155 : Battery voltage, Volts, *10
001,149,002,001,004,157 : Battery charge, percent (unconfirmed)
001,149,002,001,005,158 : Real power, Watts
001,149,002,001,006,159 : Apparent power, VA
001,149,002,001,007,160 : Output load, percent
001,149,002,001,008,161 : Frequency, Hz, *10
001,149,002,001,009,162 : Frequency, Hz, *10
001,149,002,001,010,163 : Frequency, Hz, *10
001,149,002,001,014,167 : Temperature, C, *10
001,144,002,001,001,149 : Bypass voltage, Volts, *10 (unconfirmed)
001,144,002,001,003,151 : Output voltage, *10
001,144,002,001,004,152 : Output current?, *10
001,144,002,001,005,153 : Input voltage, *10
001,161,002,001,008,173 : Max load, VA
001,161,002,001,013,178 : Nominal battery voltage, Volts, *10

e.g. The battery voltage is requested by the command:
001,149,002,001,002,155
and the reply is:
001,149,004,001,002,003,040,200
which decodes to 3*256+40=808, or 80.8 Volts, or 81 Volts as reported by Multilink.

e.g. The first 16 bits of the model name is requested by the command:
001,136,002,001,004,144
and the reply is:
001,136,004,001,004,088,071,049
which decodes to X followed by G, which is the first two letters of GXT2-3000RT230

Note: Max load field stores upto 65KVA, so there must be another format for larger UPSs.
Note: Some of these commands weren't polled on a regular basis, Multilink just seemed to /know/ a change had taken place and asked for an updated value. I've no idea where this cue came from.
Note: The reply is a repeat of the command, but for the third byte being 4 instead of 2, and the 2 reply bytes before the checksum.
Note: USB serial adapters can return bytes a few at time, meaing the 8 byte reply can arrive as multiple read()s. Your serial reading code will need to account for this.
Note: This code has been tested with model(firmware)s: GXT2-3000RT230(GXT2MR15D-2K3K), GXT3000RT-230(G_X.10.02) and GXT2-10000T230(GXT2-00896V04)

Test Program

A test program was written to request the above data from the connected UPS, the program source is available here, which can be compiled with:
gcc -O liebert.c -o liebert
and run in place. The output from the test UPS is:

./liebert /dev/ttyS0
Model name: GXT2-3000RT230                
Firmware version: GXT2MR15D-2K3K  
Serial #: 0228100092AF491     
Date of manufacture: 07OCT02 
Estimated runtime: 59 mins
Battery voltage: 80.8 Volts
Battery charge: 100 %
Real power: 363 VA
Apparent power: 468 Watts
Output load: 17 %
Frequency1: 50.0 Hz
Frequency2: 50.0 Hz
Frequency3: 50.0 Hz
Tempurature: 29.0 C
Input voltage: 231.8 Volts
Output voltage: 230.4 Volts
Output current: 2.0 Amps
Bypass voltage: 233.2 Volts
Maximum load: 3000 VA
Nominal battery voltage: 82.8 Volts

Methodology

The above information was generated by monitoring the serial conversation between the Multilink software and the Liebert GXT2-3000 UPS. Transparent monitoring was achieved using slsnif, a serial line sniffer which creates a serial device like device, typically /dev/ttyp0, while attaching to a real named serial port. It then prints to stdout data that passed in both directions, prefixing each with Host and Device.

Using the output of slsnif, I wrote an awk script that extracted the replies, matched them with commands/requests, and gave that command an index number which was assigned starting at 1 for each unique command seen. This was outputed to stdout, and fed into another awk script that listed changes between replies for the same command while also decoding the values to ascii and to a 16 bit number. With this I was able to run Multilink, see the various fields change in its GUI while also seeing the output of the awk script report changes. Using the unique index number made it easier to see patterns, and if needed, grep for a particular index number.

The actual slsniff line was:

slsnif -s 2400 /dev/ttyS0 > liebert.log
Note: The logfile option didn't work for me.

And then the output was parsed with some bash/awk:

tail -n 1000000 -f liebert.log | awk -- '/Host|Device/ {printf "%s\t",$1; for(i=2;i<=NF;i++)if(match($i,"(([0-9][0-9][0-9]))",a)>0)printf "<%s>",a[1]; printf "\n";}' |\
awk -- 'BEGIN {c=0; system("rm -f cmd2col.txt");} /^Host/ {command=$2; if(!($2 in col)){seen[$2]=1; col[$2]=c++; printf "%d\t%s\n",col[$2],$2 >> "cmd2col.txt"; \
fflush("cmd2col.txt");} else seen[$2]++;} /^Device/ {printf "%d\t%d\t%s\t%s\n",col[command],seen[command],command,$2;}' | \
sed "s/[<>]/ /g" |\
awk -- '{printf "%s\t",$0; if($14>=32 && $14<=127)printf "%c",$14; else printf "_"; if($15>=32 && $15<=127)printf " %c",$15; else printf " _"; printf "\n"; }' | \
awk -- '{if(colshort[$1]!=$14*256+$15){printf "%3d\t%03d %03d (%5d) -> %03d %03d (%5d)\n",$1,colp1[$1],colp2[$1],colshort[$1],$14,$15,$14*256+$15; } colp1[$1]=$14; colp2[$1]=$15; colshort[$1]=$14*256+$15;}'
Note: Comment out the last awk if you want the ascii columns (which are only useful for the model name etc fields, and are only requested once.
Note: Using tail -f means I can ^C the script, change something and run it again using all the previous input, without having to restart slsnif (which means restarting Multilink, which means restarting the Multilink deamon).

Finally, these commands were tested without Multilink running, to confirm Multilink didn't need to send some initialisation commands. Some C code sends each command in turn and prints the result, this is the liebert.c code whose output is shown above.

Written by Greg: greg at csc liv ac uk. Would welcome any comments. 27/7/08