The Grandstream GXP2020EXT is an accessory “sidecar” (or in Grandstream-speak “Expansion Module”) that adds 56 speed dials with BLF to Grandstream model GXP20xx VoIP telephones.

In the article below I’ve provided two scripts to help GXP2020EXT users keep their units programed with fresh directory information and matching card inserts.

The Problem

As the phone system administrator at work, one of the more annoying duties I have is updating the labels on telephone sidecars. You see these on receptionist phones, switchboard operator phones, lobby phones, etc. Grandstream’s GXP2020EXT is a 56 button panel with an indicator light to see if the line is busy (Busy Lamp Field, or BLF). You can actually connect two GXP2020EXT units together to support 112 lines.

In addition to programming the GXP2020EXT to make it aware of which line is which, there are also some supplied card inserts for writing down names associated with each line.

I wanted an easy way to not only program the GXP2020EXT, but also create a printable card insert.

The layout of the GXP2020EXT buttons and card insert leave something to be desired in my opinion. The bottom row of buttons lines up pretty well with the bottom line of the card insert, but as you work your way up from the bottom toward the top, the buttons start ending up more or less in the middle of the space for the name entry on the card. By the time you get all the way to the top the button is at the very top of the name position.

The Solution

I created two scripts to help me turn a list of names and extensions into a printable card insert and fully provisoned GXP2020EXT.

I spent a lot of time trying different spacings with the card insert to see if I could work out lines that were less confusing to my users, but ultimately I just opted to skip the first and last rows, and this seems to have cleared up the user confusion I was getting occasional reports about.

Feeding data to two scripts seemed not only easier, but less likely to introduce error (displaying one name, but programming for a different line, etc).

I also added a vertical line down the center of the card as a demarcation between the sides. The GXP2020EXT is pretty flexible, and can be programmed in different ways. Each line on the card could have two buttons for example, one that shows the user’s status (busy etc) and another one that calls the user, maybe one that transfers, or even one that just taps into the line to let a call center supervisor listen in, all depending on how you go about programming it and your phone system capabilities. I just use the BLF feature, pressing the corresponding button calls the user, so the vertical separator made sense to me. This way I’ll put two names on each line of the card (one party on the left, and one on the right).

Below is my printed card (right) shown next to a Grandstream supplied card (left). If all you want is a blank card like this you can just download a PDF of it here: gxp202ext_blank.pdf

Here is what it looks like with some names filled in

Template Script

The script I wrote to create these template files requires ImageMagick (with Ghostscript support). I’m using Ubuntu in this example. To install ImageMagick (if it isn’t already), just run:

$ sudo apt install imagemagick

Choose (y)es to accept the recommended packages and perform the install.

In order to create a PDF with ImageMagick on Ubuntu you’ll probably also want to modify /etc/ImageMagick-6/policy.xml, otherwise you might see an error message like:

convert-im6.q16: attempt to perform an operation not allowed by the security policy `PDF' @ error/constitute.c/IsCoderAuthorized/408.

Edit /etc/ImageMagick-6/policy.xml and in the <policymap> section, find the “ghostscript” comment area

<!-- disable ghostscript format types -->
  <policy domain="coder" rights="none" pattern="PS" />
  <policy domain="coder" rights="none" pattern="EPS" />
  <policy domain="coder" rights="none" pattern="PDF" />
  <policy domain="coder" rights="none" pattern="XPS" />

Change the pattern=PDF line to read

  <policy domain="coder" rights="read|write" pattern="PDF" />

Keep in mind there are security implications to doing this, which is why the rights weren’t set in the first place. I’m running this script on my desktop, and I don’t have any processes likely to be a vector to throw malicious or untrustworthy data at ImageMagick, so I feel OK changing this on my system, but your situation may be different. Strictly speaking the step of turning the PNGs into PDF isn’t required, it’s just helpful to get them to print at the right size. Alternatively if you import the PNGs into LibreOffice and print from there you’ll probably be fine too.

If you want to run this on the same system as an ancient CentOS 5.5 Elastix server you’ll want to temporarily move your yum repos, create a new one to the CentOS vault, install ImageMagick, and then move your broken old yum repos back in to place.

$ sudo su -
$ cd /etc/yum.repos.d/
$ mkdir /root/backup
$ mv * !$
$ cat <<EOF >vault.repo
[vault]
name=CentOS Vault
baseurl=http://vault.centos.org/5.5/os/i386/
enable=1
EOF
$ yum check-update
$ yum install ImageMagick
# optionally get some extra fonts
$ yum install liberation-font
$ mv /root/backup .
$ rm vault.repo

This is all you need, but ImageMagick might still try to use the Microsoft Arial font. You can get the redistributable MS Core Fonts and use cabextract to dump them into /usr/share/fonts/default/TrueType/ or you could specify an alternative font in make_template.sh, or you could just upload a bunch of TTF fonts into /usr/share/fonts/default/TrueType/ from another system.

Once the ImageMagick requirements are met, the next thing you’ll need is your list of names. You’ll also want a list of extension numbers too, this will be handy when we move on to provisioning the GXP2020EXT. You can use the same list for both tasks. I’d suggest making a list in the following format:

Eduardo Chamberlain <1122>
Linda Fields <1104>
Venessa Lovett <1115>
Kevin Robertson <1172>
Andrew Dial <1108>
Paula Dahil <1165>
Jerry Hudson <1112>
Roger Holder <1126>
Ashley Deblois <1116>
...

My office runs Elastix/FreePBX/Asterisk. The format above is the same as you might expect to see in the extensions list of the Elastix web interface (PBX -> PBX Configuration -> Extensions). You can just copy those and paste them into a new file. If you’re using a different system or a newer version of Elastix you could just make the list by hand in a pinch. I’m going to save the list to a file named names.txt for the purpose of example and documentation below.

Due to the limited space, we probably don’t want to include the extension number in the printed card (only the name). However we do want to have those extensions in the file for later use by the provisioning script.

An easy solution is to use awk to output only the name portion (the words before the ‘<‘ character appears) on each line of our file

$ awk 'BEGIN { FS = "<" }; {print $1}' names.txt
Eduardo Chamberlain
Linda Fields
Venessa Lovett
Kevin Robertson
Andrew Dial
Paula Dahil
Jerry Hudson
Roger Holder
Ashley Deblois
...

You probably also want to be sure to alphabetically sort your list so that it’s easier to find a particular name on the display card. You can use the sort command to accomplish this. Pipe the output of sort into awk.

$ sort names.txt |awk 'BEGIN { FS = "<" }; {print $1}'
Andrew Dial
Ashley Deblois
Eduardo Chamberlain
Jerry Hudson
Kevin Robertson
Linda Fields
Paula Dahil
Roger Holder
Venessa Lovett
...

OK, so put it all together and pipe it into the make_template.sh script (supplied below)

$ sort names.txt |awk 'BEGIN { FS = "<" }; {print $1}' |./make_template.sh

You should now have a file named template.pdf that you can print out. Cut along the dashed lines with a pair of scissors and if everything worked out right the two sheets of paper you have left should fit in the card slots of the GXP2020EXT sidecar.

If like me you’d prefer to keep the top and bottom row of each card blank, then you’ll want to manually edit your names.txt file. Perform a sort ahead of time on the file, you won’t want to sort it in a pipeline as the order of the items in the file will need to be preserved to accomplish this task. Edit the file and insert a blank/empty line on each line-n where n % 14 = 1 or 0, or more verbosely, add a blank line to lines number 1, 14, 15, 28, 29, 42, 43, and 56.

download make_template.sh

#!/bin/bash

i=0
while read -t1 line
do
    name_part1[$i]="${line%' '*}"
    name_part2[$i]="${line##*' '}"
    ((++i))
done
unset i

if [ -z "${name_part1[0]}" ] && [ "${#name_part1[@]}" == "1" ]; then
    echo -e "\nUsage: [ pipe | ] $0 [ < stdin ]"
    echo -e "\nExamples:"
    echo -e "\t$0 < names.txt"
    echo -e "\t\t - or -"
    echo -e "\tcat names.txt | $0"
    echo -e "\nScript requires an input stream!\n"
    exit 1
fi
   

get_names() {
    echo "-fill black -pointsize 14"
    i=0

    # Two maintain alphabetical order vertically rather
    # than horizontal + vertically we put the draw
    # statements into two different loops to process
    # all of one side of the card first

    # Left side of card
    while [ "$i" -lt "14" ]
    do
        # adjust placement for bottom half
        if [ "$i" -lt "8" ]; then
            y1=20
            y2=34
        else
            y1=16
            y2=30
        fi
        echo -e "-draw "text 8,$(($y1 + 43 * $i))  '${name_part1[$i]}'""
        echo -e "-draw "text 8,$(($y2 + 43 * $i))  '${name_part2[$i]}'""
        ((i++))

    done
    # Right side of card
    while [ "$i" -lt "28" ]
    do
        # adjust placement for bottom half
        if [ "$i" -lt "15" ]; then
            y1=20
            y2=34
        else
            y1=16
            y2=30
        fi
        echo -e "-draw "text 103,$(($y1 + 43 * $((i - 14)))) '${name_part1[$i]}'""
        echo -e "-draw "text 103,$(($y2 + 43 * $((i - 14)))) '${name_part2[$i]}'""
        ((i++))

    done
}

offset() {
    skew="$1"
    ((skew--))
    skew="$((skew * 10))"
    skew="$((skew * 5 / 10))"

    if [ "${skew: -1:1}" == "5" ]; then
        skew="$((skew+ 10))"
    fi

    skew="${skew:0:1}"

    echo "$skew"
}

draw_box() {
    echo "-fill none -strokewidth 2 -stroke black "
    echo "-draw 'stroke-dasharray 5 3  rectangle 1,1,187,595' "
    echo "-fill none -strokewidth 1 -stroke black "
    echo "-draw 'line 95,0 95,609' "
    row=1
    while [ "$row" -le "13" ];
    do
        echo "-draw 'line 0,$((43 * $row - $(offset $row) )) 190,$((43 * $row - $(offset $row) ))' "
        ((++row))
    done
}


# Generate left template
bash -c "$(echo convert -size 189x597 xc:white $(get_names) $(draw_box) template1.png)"

# shift remaining names down the array
if [ "${#name_part1[@]}" -gt "27" ]; then
    i=0
    while [ "$i" -le "27" ]
    do
        name_part1[$i]="${name_part1[$((i + 28))]}"
        name_part2[$i]="${name_part2[$((i + 28))]}"
        ((i++))
    done
fi

# Generate right template
bash -c "$(echo convert -size 189x597 xc:white $(get_names) $(draw_box) template2.png)"


# side by side on one peice of paper
#montage -units PixelsPerInch -density 96 template?.png -mode concatenate -tile 2x template.pdf

# Or one per page
convert -units PixelsPerInch -density 96 template?.png template.pdf

if [ -f "template.pdf" ]; then
    rm template1.png template2.png
fi

Provisioning Script

Some of this script is repurposed from my earlier gspush.sh script which can be used to program other features beyond the GXP2020EXT. (If you work a lot with older Grandstream phones you might be interested in that article as well).

This script is meant to be used with the GXP2020EXT connected to a Grandstream GXP21xx phone. If you somehow have it hooked up to something else you may need to adjust the values in the script first.

The provisioning script is easy to use. Feed it the list of names and specify the IP address of the phone with the GXP2020EXT connected to it. If you are using a password other than the default specify it after the IP address. If you forget how to run the command just run it without input for a reminder:

$ ./provision_gxp2020ext.sh
Usage: stdout |./provision_gxp2020ext.sh <IP Address of Grandstream GXP2020EXT device> [password]
    Supply a list of names and extensions (one per line) via stdio in the format:
    Name <Extension>

Output looks something like this, but will vary depending on the firmware of the GXP21xx phone

$ cat names.txt |./provision_gxp2020ext.sh 10.0.0.152 secret
Sid: 8beeff00d42
Status:
Status: 1
Done.

download provision_gxp2020ext.sh

#!/bin/bash

GXP_PHONE_IP="$1"

if [ -n "$2" ]; then
    GXP_PASSWD="$2"
else
    GXP_PASSWD="admin" # default
fi

get_sid() {
        while read -r || [[ -n "$REPLY" ]]
        do
                if [ -n "$sid" ]; then
                        break
                fi
                if [ "${REPLY:0:23}" == "Set-Cookie: session_id="  ]; then
                        sid="${REPLY:23}"
            sid="${sid//;}"
                fi
                if [ "${REPLY//'"sid"'}" != "$REPLY" ]; then
                        sid="${REPLY#*'"sid"'}"
            sid="${sid#*'"'}"
                        sid="${sid%%'"'*}"
                fi
        done
        echo "$sid"
}

post_status() {
        # Older firmware says need to reboot, newer firmware says success
        while read -r || [[ -n "$REPLY" ]]
        do
                if [ "${REPLY//eboot}" != "$REPLY" ]; then
                        echo "1"
                        break
                fi
                if [ "${REPLY//success}" != "$REPLY" ]; then
                        echo "1"
                        break
                fi
        done
}

urlencode () {
        tab="`echo -en "\x9"`"
        i="$@"
        i=${i//%/%25}  ; i=${i//' '/%20} ; i=${i//$tab/%09}
        i=${i//!/%21}  ; i=${i//'"'/%22}  ; i=${i//#/%23}
        i=${i//\$/%24} ; i=${i//\&/%26}  ; i=${i//\'/%27}
        i=${i//(/%28}  ; i=${i//)/%29}   ; i=${i//\*/%2a}
        i=${i//+/%2b}  ; i=${i//,/%2c}   ; i=${i//-/%2d}
        i=${i//\./%2e} ; i=${i//\//%2f}  ; i=${i//:/%3a}
        i=${i//;/%3b}  ; i=${i//</%3c}   ; i=${i//=/%3d}
        i=${i//>/%3e}  ; i=${i//\?/%3f}  ; i=${i//@/%40}
        i=${i//\[/%5b} ; i=${i//\\/%5c}  ; i=${i//\]/%5d}
        i=${i//\^/%5e} ; i=${i//_/%5f}   ; i=${i//\`/%60}
        i=${i//\{/%7b} ; i=${i//|/%7c}   ; i=${i//\}/%7d}
        i=${i//\~/%7e}
        echo "$i"
        i=""
}


# Read stdin and create the string of Grandstream config values


# Valid Key mode values:
# 0 = Speed Dial, 1 = BLF, 2 = Presence Watcher, 3 = eventlist BLF,
# 4 = Speed Dial via active account, 5 = DialDTMF, 6 = Voicemail,
# 7 = CallReturn, 8 = Transfer, 9 = CallPark, 10 = Intercom,
# 11 = LDAP Search

Pvalues=""
i=0
while read -t1 line
do
    name="${line%%'<'*}"
    extension="${line##*'<'}"
    extension="${extension%%'>'*}"
    if [ -z "$name" ]; then # Turn off any unused/old BLF
        Pvalues+="P$((6001 + i))=0&" # Key mode, speed dial
    else
        Pvalues+="P$((6001 + i))=1&" # Key mode, BLF
    fi
    Pvalues+="P$((6201 + i))=0&" # Account
    Pvalues+="P$((6401 + i))=$(urlencode "$name")&" # Name
    Pvalues+="P$((6601 + i))=$(urlencode "$extension")&" # UserID
    ((i++))
done

if [ -z "$Pvalues" ] || [ -z "$GXP_PHONE_IP" ]; then
    echo "Usage: stdout |$0 <IP Address of Grandstream GXP2020EXT device> [password]" >&2
    echo -e "\tSupply a list of names and extensions (one per line) via stdio in the format:" >&2
    echo -e "\tName <Extension>" >&2
    exit 1
fi

# Provision the GXP2020EXT

# Older firmware passed password as "P2", newer firmware passes as "password"
# <form> action is the same
sid="$(curl -s -i -d "P2=$GXP_PASSWD&password=$GXP_PASSWD" "http://${GXP_PHONE_IP}/cgi-bin/dologin" |get_sid)"
echo "Sid: $sid"

# <form> action for config update is different across firmware versions
# Try old firmware style first
status="$(curl -s -i -b "session_id=$sid" -d "${Pvalues}update=Update" "http://${GXP_PHONE_IP}/cgi-bin/update" |post_status)"
echo "Status: $status"

# Try new firmware way if the last attempt failed
if [ -z "$status" ]; then
#   # Try new firmware config update action
    status="$(curl -s -i -b "session_id=$sid" -d "$Pvalues&sid=$sid" "http://${GXP_PHONE_IP}/cgi-bin/api.values.post" |post_status)"
    echo "Status: $status"
fi
if [ -z "$status" ]; then
    # Couldn't update speed dial on phone
    echo "Could not update speed dial correctly"
    exit 1
else
    echo "Done."
fi

That’s all there is to do now when we have a change of staff. Actually, at our company I took this one step further, by using my Grandstream / Elastix Phonebook Provisioning URL in a cron task to periodically export the names and look for changes between polling intervals. When changes are detected, the GXP2020EXT is automatically re-provisoned to reflect them, and an updated template is emailed to IT staff to print, cut, and replace. I’m now free to ignore this task completely.

Post your comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.