The World’s First Cloud Texting Enabled Raspberry Pi Powered Espresso Machine

Zipwhip is a cloud texting company. That means our goal is to give users a way to send and receive their texts from the cloud. Texting is a very important medium in our lives and yet it’s been stuck on our mobile phones forever. Why can’t we send a text from the web, our desktops, tablets, iPads, Kindle Fires, or even the family T.V.? We can send email from all those places so why not texts? With Zipwhip you can.

The first thing Zipwhip has to do to solve cloud texting is get your texts into the cloud. Once they’re there you can do all sorts of fancy things. By installing our background app on your Android phone your texts are synced in real-time. In addition to our mobile solution, we’ve recently launched our landline service that  allows users to send and receive texts from their existing business or residential landlines. That’s right, you can now order dinner via text from a restaurant with a Zipwhip enabled landline. It’s a powerful platform with thousands of use cases. So powerful that last year, as you may recall, we were able to create the world’s first text enabled espresso machine as a way to show off  just how cool the SMS medium really is.

We were already working on a version of Textspresso you could own yourself when we heard that the Raspberry Pi team had moved into their first office. We knew they needed one. It was meant to be a nice way to thank them for revolutionizing the computing industry with their $25 credit card sized Linux computer. This project wouldn’t have been possible without their amazing new device. We decided to document the whole process so you can make your own over a long weekend. Below you’ll find all the gory details.

Completed Circuit Board

pscircuitboard

Circuit Board Attached to Raspberry Pi

psraspi507

Part 1

Steps to Build Your Own

  1. Get yourself a Textspresso circuit board
  2. Get the parts
  3. Buy the machine
  4. Setup Raspberry Pi
  5. Create a Zipwhip account
  6. Configure your Zipwhip account to the machine (the software step)
  7. Text yourself a coffee

1.) Get Yourself a Textpsresso Circuit Board

We used to have our circuit boards available on Batch PCB but the site has closed. You can download the Eagle files below and then upload them directly to OSH park (they take the files natively). Unfortunately you’ll have to buy the boards in batches of 3 for $17.60 and upload them yourself. You can also download Eagle via the link below if you don’t already have it.

Eagle: http://www.cadsoftusa.com/

Hardware Files: http://ftp.zipwhip.com/public/textspresso/textspressohardware/schematics.zip

2.) Get the Parts

We created a list of parts from four different sites: Digi-Key, Samtec, Allied, and Amazon. The heavy lifting is done. You can click right through to the exact part you need inside the table. Although the capacitors aren’t absolutely necessary, they help smooth out the electrical currents running through the board and lessen the chance that you’ll have isues with your circuit board. The parts at the bottom of the list are for your Raspberry Pi. Instructions on how to configure your Model A are below. You’ll be soldering the circuit board yourself, so get that magnifying glass ready. Some of these parts are extremely small.

 

Supplier Quantity Part Number Item
Digi-Key 2 445-1265-1-ND Capacitor 0.1uF
Digi-Key 6 445-1237-1-ND Capacitor 15pF
Digi-Key 1 541-348LCT-ND Resistor 350OHM
Digi-Key 4 541-499LCT-ND Resistor 499OHM
Digi-Key 1 541-1.21KLCT-ND Resistor 1.2k
Digi-Key 4 541-3.57KLCT-ND Resistor 3.57k
Digi-Key 3 541-100KLCT-ND Resistor 100k
Digi-Key 1 541-4.7KJCT-ND Resistor 4.7k
Digi-Key 1 541-20KJCT-ND Resistor 20k
Digi-Key 4 568-1633-1-ND NPN Transistors
Digi-Key 1 497-1857-1-ND Shift Register
Digi-Key 3 HCPL2631SDCT-ND Optocouplers
Samtec 1 TSW-111-26-F-S Ribbon Cable
Samtec 1 SSW-113-01-F-D GPIO Port
Sametc 1 ESQ-111-24-G-S ESPR_BRD Connector
Allied 1 70266830 Raspberry Pi Model A
Allied 1 70232557 Power Supply
Allied 1 70235238  SD Card
Amazon 1 EW-7811UnR2 Wireless USB Adaptor

3.) Buy the Machine

Seattle Coffee Gear has the Delonghi Magnifica Super Automatic espresso machine available for purchase on their site. They are a great company with stellar customer service, but you’ll be hacking this machine at your own risk. Once that cover comes off there is no going back. The direct link to the machine is below.

http://www.seattlecoffeegear.com/delonghi-magnifica-esam3300-superautomatic-espresso-machine

4.) Setup Raspberry Pi

When you start this step, you’ll probably have all of your parts in a nice pile ready to go. If you already have an imaged card you can jump right to the second paragraph in this section which covers enabling WiFi. If you have a blank SD card (we recommend 8GB) copy the Raspian Wheezy OS on it. If you’re not sure how to do that, follow the link below. Just a heads-up for newbies to the code/tech/electronics hobby, the “image” you’ll see referenced isn’t a picture. It’s the operating system that will be running on your Pi and acting as the brains of the operation. All you have to do is download a free disk-imager, download and extract the latest version of Raspian Wheezy, get the image onto your card and you’re done. Below you see a screen grab of a successful “write.”

Once your SD card has the Raspian Wheezy OS on it, you can insert it and complete the initial setup of the Pi. If you’re working with a keyboard made in the U.S., remember to make that change as U.K. is the default style. Once that’s done you can setup WiFi for your Pi. How exciting! Plug in your dongle and then use sudo nano /etc/network/interfaces to access your network configuration files. Then, enter the WiFi file information below (you have to add your own network name and password). Once you’ve saved and exited out of that file, you can check to see if you are setup correctly with a ping http://www.XXXX.com command. You’ll get an error message back if something isn’t setup correctly.

WiFi network interface file:

auto wlan0
iface wlan0 inet dhcp
wpa-ssid wifi_ssid
wpa-psk wifi_password
wpa-key-mgmt WPA-PSK
wpa-pairwise TKIP CCMP
wpa-group TKIP CCMP
wpa-proto WPA RSN
wpa-ap-scan 1
wpa-scan-ssid 1

diskimagerdone

5.) Create a Zipwhip Account

In order for all of this to work, you need to have an account for either your Android smartphone, 800 number, or landline. All coffee orders sent to the number associated with the machine will be routed through our cloud and then down to the machine. Links to our registration pages are below.

To register with your Android smartphone: http://webapp.zipwhip.com/#register

To register with your landline: http://zipwhip.com/landlines

To register with your 800 number: http://zipwhip.com/text-enable-your-1-800-number

6.) Configure Your Zipwhip Account to the Machine (the software step)

On your Pi run the following commands to download and install the Zipwhip Textspresso software package:

sudo wget http://ftp.zipwhip.com/public/textspresso/install-textspresso.sh
sudo chmod 755 install-textspresso.sh
sudo ./install-textspresso.sh

Configure the software with your account info:

sudo python /opt/zipwhip/bin/cmdconfig.py –write –write-phonenum <your-zipwhip-phonenumber> –write-password <your-zipwhip-pasword> –write-getnewsessionandclientid

Start it up and watch the logs:
sudo service zwtextspressod start; tail -f /opt/zipwhip/log/zipwhip-textspresso.log

7.) Text Yourself a Coffee

You can text in 3 different orders: “coffee single” for a small coffee, “coffee double” for a bigger coffee, and “The Zipwhip” for a triple coffee . You can also text in “status” to see if the machine is on or off, and “menu” just in case you forget what your order options are.

Part 2

A Deeper Look Into the Hack

  1. The machine
  2. Hacking into the machine
  3. Ribbon cable
  4. The control panel circuit board
  5. The shift register
  6. How the push buttons work
  7. The board layout
  8. Our schematic design
  9. Octocouplers
  10. Powering the pi

1.) The Machine

Below you’ll see a shot of the front of the machine. With the custom software and hardware of this hack, you’ll be recreating the pushing of a button without actually touching the buttons. Pretty cool, right. *Hot tip: Once your machine is up and running, you can reset the machine by holding the preground button (the one with the scooper) down for 10 seconds.

image

Button                 Placement
On/Off                   Far Left
Coffee                   One Cup
Coffee Double        Two Cups
Hot Water              Faucet
Decalcify                Bottom L
Pre-Ground Beans  Bottom R

2.) Hacking Into the Machine

It just takes a few screws to open the machine. Take off the back first and then slide the side panel off. Here are the inside guts of the machine. One of the things you’ll notice is that the area where the Pi will eventually go looks like it was custom made for our hack. It fits in there perfectly.

image

After opening up the DeLonghi we noticed a PIC microcontroller at the heart of the main board. You can read more about this chip on the Microchip website (http://www.microchip.com/wwwproducts/Devices.aspx?dDocName=en010296). We know this chip runs at 5 volts and thus is operating at normal TTL logic levels. That’s good because that’s easy to interface with. We figured the best place to start tapping into the machine was on the ribbon cable that ran from the main board to the front control board.

image

3.) Ribbon Cable

There is a ribbon cable on the main circuit board of the espresso machine that connects to the front control panel. This was our key area to try to interface with the machine since all of the LEDs tell us the state of the machine and it let’s us mimic button presses from a text command rather than pressing the button itself.

image

After some deep probing of the circuits here is what we determined all of the functions of the ribbon cable are for.

image

Pin Description Voltage Waveform
1 Push Button Signal for Coffee Double / Use Pre-Ground Beans 5V Square Wave to PIC Microcontroller. 1.04ms On. 1.04ms Off. Inverted for 2nd button.
2 Push Button Signal for Coffee / Decalcify 5V Square Wave to PIC Microcontroller. 1.04ms On. 1.04ms Off. Inverted for 2nd button.
3 Clock Signal Sinking to GND and rising to 5V 8-bit Pulse to Shift Register
4 Serial In to Shift Register Sinking to GND and rising to 5V 8-bit On/Off Values to Shift Register
5 Latch/Strobe to Shift Register Indicating Data Done Sinking to GND and rising to 5V 1 Pulse to Latch on Shift Register
6 Potentiometer for Coarse / Fine Beans GND to 5V depending on pot setting Varying voltage
7 Potentiometer for Amount of Water Per Cup GND to 5V depending on pot setting Varying voltage
8 GND GND Ground
9 VDD 5V Straight voltage
10 Push Button Signal for On/Off / Hot Water 5V Square Wave to PIC Microcontroller. 1.04ms On. 1.04ms Off. Inverted for 2nd button.
11 Alt GND (Don’t think this is used) -20V Alt GND

4.) The Control Panel Circuit Board

Here is a front and back picture of the control panel circuit board. We had to figure out how this whole thing worked so we could correctly reverse engineer how the signals communicated between the main board and this control panel. There are LEDs, some simple push buttons, a couple potentiometers, a shift register, and two transistors labeled “Q1” and “Q2”. Those ended up being quite important for how the shift register worked. If you look at your finished circuit board, or the one from above you’ll notice some pieces that look exactly the same (the shift register), and you’ll notice that other pieces look completely different (the resistors).

image

Here is the back of the board. It’s hard to trace where the wires go due to the epoxy that Delonghi poured onto the board to protect it from moisture.

image

5.) The Shift Register

The espresso machine sends an 8-bit shift register signal to the control board. This allows the machine to turn on and off the 8 LEDs on the front of the machine by only using 2 wires on the ribbon cable. The circuit board on the front of the machine with the shift register chip is sent a 5V TTL logic signal. On one wire is the clock signal. It sends 8 pulses at a time. The 8 pulses last 24 uS. That’s 3 uS per pulse.At the same time as the clock signal, another wire sends over which register to turn on by sending 5V, or a high state, during the clock pulse. You can see below what that looks like on the oscilloscope. The pulse being sent below occurs when the espresso machine is off. In fact, the machine sends a pulse on register 1 and then register 2 in an oscillating fashion. This generates 2 square waves on the shift register that is used for the buttons. One square wave is used by the On/Off, Coffee, and Coffee Double buttons. The other square wave, which is out of phase with the first square wave, is sent to the other 3 buttons. This way the ribbon cable only needs 3 wires to detect 6 different buttons being pushed.The pulse signal and the clock signal while the machine is off. The clock is yellow. The data is green.The second pulse signal while the machine is off.image

The clock signal comes in every 1.04mS or at 960 Hz. This is important because we need to detect this on the Raspberry Pi GPIO ports. That also means the LEDs blink every 1.04 mS or about 1,000 times each second.

image

Here we have an example of what the shift register 8-bit pulse looks like when a few LED lights are turned on.

image

Here is the shift register that DeLonghi used. Remember, this piece is already included for you in the top section of this blog post.

image

You can view its datasheet at the following URL. Below is also a diagram of the registers that are output from the Serial In data. We deduced that DeLonghi is not using any of the strobing or output enabling available on the shift register. They are simply using Q1 thru Q8 to control how the LEDs are turned on and how to generate the in phase and out of phase square wave for the round trip through the push buttons back to the main board.

http://www.st.com/internet/com/TECHNICAL_RESOURCES/TECHNICAL_LITERATURE/DATASHEET/CD00000323.pdf

image

There are two transistors on the control panel that say “Q1” and “Q2”. That was a hint as to how they were controlling 8 LEDs and generating two square waves from only 8 registers. They essentially use up Q1 and Q2 registers to control if a transistor is on or not. That means they sort of created two planes on the panel. One plane can have Q3 thru Q8 controlling stuff. The other plane gets Q3 thru Q8 again for a brand new set of controls. That gives them 12 switches from an 8 bit register. That does mean they are blinking the LED lights on and off constantly, but humans can’t tell because the blinking is so fast, i.e. 1,000 times per second.

Here is what we determined the shift register is controlling.

Q1

Q2

Q3

Q4

Q5

Q6

Q7

Q8

Coffee LED

High

High

Coffee Double LED

 

High

 

High

Hot Water LED

 

High

 

High

Out of Water LED

High

High

 

Tray Needs Emptied LED

High

High

Warning LED

High

 

High

Decalcify LED

High

High

Use Pre-Ground Beans LED

High

High

This is great. Now that we know what the pulses mean, we can feed them into our Raspberry Pi GPIO port and write

This is great. Now that we know what the pulses mean, we can feed them into our Raspberry Pi GPIO port and write some code to interpret the pulses. That way we’ll always know what our machine is doing and can send back informational text messages to the user when things happen. For example, if you text in a coffee order we can see if the machine is out of water. We can text you back letting you know it needs water first and then vend your coffee.

6.) How the Push Buttons Work

Our hope was that we could read the clock, serial, and latch signals directly into the Raspi. That would have made our circuit way simpler. After some testing, we could never get the Raspi to trigger an interrupt on a GPIO port from a 3uS square wave. That was very disappointing. Our trick was to just use the exact same shift register that the espresso machine was using on it’s front control panel. We knew the latch was only thrown every 1.04ms. Our hope, although we didn’t know, was that this was slow enough for the GPIO ports to catch it. Fortunately, we were right.

We rigged up the shift register and then connected each of Q1 through Q8 to it’s own GPIO ports. We did get worried we would run out of GPIO ports using this approach, but fortunately we had 2 of them left unused when everything was done.

The nice part about the shift register was that we had a bit less code we’d have to write to interpret the clock and data signals. So, at least we got some benefit out of having to use more components.

7.) The Board Layout

Delonghi used a very cool technique to detect what buttons were being pressed. They let the shift register generate a nice square wave at the front control panel. Then that wave is sent back to the main board along Pin 1, 2, or 10 to represent which button was pushed. Refer to the table earlier to see which Pin represents which button.

The shape of the square wave is below. You can see that when the button is pushed the square wave is being sent. When it’s not pushed there is no voltage or waveform on the Pin. So, all we really need to do is mimic this square wave being sent into the main board and we should have our way of mimicking a button push.

One thing you may notice here is that the GND state of the espresso machine is a bit funky in the oscilloscope. It wavers a lot. If we had been able to connect the GND from our oscilloscope probe this problem would be solved, but we found the espresso machine would shut off the moment we connected GND to the probe. So we had to analyze it without connecting ground.

This inability to connect ground of the Raspberyy Pi together with the espresso machine caused us to have to design our circuit board in a way where the grounds were isolated. This required use of some optocouplers which you’ll see more on later.

image

8.) Our Schematic Design

Here is our final board layout. We’re pretty happy with it. It allowed us to connect it directly into the ribbon connector on the main board of the espresso machine, while still fitting the Raspi directly onto it and connecting the ribbon cable to the front of the machine.  We used Advanced Circuits to get a barebones PCB whipped up. We used BatchPCB to get the final boards made. We did do a lot of prototyping on our Shapeoko CNC machine with the TinyG as our controller.

schematicblack

After a lot of prototyping, here is the final design of the schematic. The general arrangement is to have two sides of the schematic. The left side is the Raspberry Pi side. This side is a 3.3v powered circuit to not blow the GPIO ports. It is electrically isolated from the espresso machine via optocouplers. The right side is for the espresso machine. That is a 5V powered circuit off the espresso machine’s power supply. There aren’t that many components being powered on that side so it should be safe to pull a few more mA from the espresso board.

 

schematic4

9.) The Optocouplers

The optocouplers were our approach to solve three issues. 1. To electrically isolate the espresso machine from the Raspberry Pi circuit to ensure we didn’t blow anything on the espresso machine. 2. It was to solve the fact that whenever we touched the GND on the espresso machine, we’d shut it off. We never quite figured out why, so we just avoided the problem. 3.  To solve the conversion of the 5V system to a 3.3V system that was compliant with the Raspberry Pi.

We found that if we used a standard cheap optocoupler that we didn’t have enough response rate to transmit the clock signal, serial data, and latch signal. So, we ended up using some pretty new speedy optocouplers from Fairchild Semiconductor. They are rated at a 44ns rise time. Most standard optocouplers have a 2uS rise time. So there’s a huge difference in the ones we’re using. The clock and data signal runs every 3uS, so you can see why standard optos would never work.

*Hot Tip: Before you solder the optocouplers to the board, it helps to pinch in the legs slightly. This ensures that all 8 of them make contact with the pads on the board.

imageimage

The really cool thing about optos is that you’re using light as part of your circuit. That’s a very cool concept. Instead of using electrons through copper, you are guiding data down a light path. It’s amazing that such an otherwise simple task of text-enabling an espresso machine would require the need to steer light around.

Initially we tried to just feed our optos on the espresso side directly from the clock signal, serial data, and latch signal. Well, that failed miserably. Those signals have no real power behind them. So, we had to drop in a transistor to each of those signals. Then the signal could just toggle on and off an NPN transistor with a tiny bit of power. This meant we had to put in place some pull-up resistors as well to feed the opto on the espresso side. The extra benefit to this approach is that it nicely and correctly inverted our signal. That was good because the optocoupler itself inverts the signal. So with the two inversions, we got the correct output on the Raspi side.

The hardest part of driving the optos was finding the correct resistor size to place just in front of the base on the transistor. You would think that all three of the signals on the espresso machine would need the same resistor size, especially if they’re all being generated from the PIC controller. However, we found that 100k was correct for the serial in and the latch, but 20k was the correct size for the clock signal. We’re still not sure why, but that gave us the best waveforms on our oscilloscope.

Here’s a link to the datasheet. http://www.fairchildsemi.com/ds/6N/6N137.pdf

On the Raspi side we had to also create some pull-up resistors because all the opto does is yank you to a ground state. We missed this in our first design because we incorrectly assumed we’d be given a logic state based on Vcc. After looking deeper at the schematic it was quite obvious that’s not the case. You can see two little NPN transistors inside that package.

10.) Powering The Pi

To power the Raspberry Pi inside your machine you can either plug it in externally, or do the stylish thing and power it through the espresso machine. To do this, you solder one of the plug-ins of the external power supply to one of the internal power supply cords, and the other plug-in to the second internal power supply cord. The order doesn’t matter. Just remember that these wires can’t touch, as that would cause a short circuit. The internal black cords are the thick black cords you see at the bottom of the image and the external power supply looks like it should be plugged into the wall.

raspiblog

If you’ve made it this far in the blog, thank you! We did our best to be as thorough as possible so that completing this hack was easier for you than it was for us. If you find yourself stumped and you need some help, please send an email to support@zipwhip.com with the word “textspresso build” in the subject line. We’ll do our best to get back to you as soon as possible.

Happy Hacking!

Manual Control of a Servo on the Arduino for the Zipwhip TextSpresso Machine


Control a servo without using the Arduino servo library.

The Arduino has a great servo library, but we found while making our TextSpresso machine that the servo library wouldn’t play nice with our stepper motors. We happily connected our stepper motors up to pins 2 through 9 because we were using a stepper motor shield. Then we connected the servos to much higher pin numbers like 26 and 27. To our dismay whenever we sent commands to the steppers, it would cause massive gyrations on the servos.

This had us investigate what was going on and to do that we had to dig under the covers of the servo library. Not many newbies know about the timer capabilities of the Arduino. The servo library hides us from the gory details. Well, it turns out that the servo library relies on a timer callback to get the precision control of the servos. This callback means the Arduino can’t be processing any other code at the exact moment the callback occurs. If the Arduino is busy, then all hell breaks lose on the exact timing of pulses being sent to the servo.

Servos expect a 20 millisecond (ms) pulse. To set the servo at 0 degrees, you send it a high voltage for the 1st ms of the 20 ms pulse. To set the servo at 180 degrees, you send it a high voltage for the 1st 2 ms of the 20 ms pulse. Pretty easy actually. Anywhere in between 1.0 ms and and 2.0 ms gives you different degrees between 0 and 180.

We wrote the sample code below to help you manually control your servo. It just takes a millisecond value and sends it to the servo and then pauses for the remainder of the pulse. I’m actually not sure why the Arduino IDE doesn’t give a manual example like this because it’s so easy to do. We couldn’t find any sample code on the Internet either for “manual servo control on the Arduino”. We hope you find this helpful.

// Manual servo control 
// Copyright 2012 by Zipwhip. 
// You are free to use and modify this code in your own software.

#define SERVO_PIN         39  // Any pin on the Arduino or Mega will work.

void setup()
{
  pinMode(SERVO_PIN, OUTPUT);

}

int lenMicroSecondsOfPeriod = 20 * 1000; // 20 milliseconds (ms)
int lenMicroSecondsOfPulse = 1.8 * 1000; // 1.0 ms is 0 degrees

void loop()
{

 // Servos work by sending a 20 ms pulse.  
 // 1.0 ms at the start of the pulse will turn the servo to the 0 degree position
 // 1.5 ms at the start of the pulse will turn the servo to the 90 degree position 
 // 2.0 ms at the start of the pulse will turn the servo to the 180 degree position 
 // Turn voltage high to start the period and pulse
 digitalWrite(SERVO_PIN, HIGH);

 // Delay for the length of the pulse
 delayMicroseconds(lenMicroSecondsOfPulse);

 // Turn the voltage low for the remainder of the pulse
 digitalWrite(SERVO_PIN, LOW);

 // Delay this loop for the remainder of the period so we don't
 // send the next signal too soon or too late
 delayMicroseconds(lenMicroSecondsOfPeriod - lenMicroSecondsOfPulse); 

}

Manually Sweeping the Servo from 0 to 180 Degrees

Taking the example above further and trying to mimic the Arduino sample sweep code for a servo we produced the test code below. This let us fully test our manual control of the servo. We found that for our HiTec HS-422 servo that the 0 degree position was at about a 0.5 ms pulse and that the full 180 degrees was around a 2.2 ms pulse. You may find something different for your servo but you can adjust the control variables in the code below until you’re happy.

We also found that a pulse length of about 25 ms worked better for our servo. The standard is 20 ms so I’m not surprised we found something a bit longer than the expected pulse worked better. When we went shorter on the pulse like 18 ms we found the servo acted really weird. That’s probably because the voltage was getting applied prior to the servo finishing it’s measurements and that would throw off pulse lengths overall.

// Manually Sweeping the Servo from 0 to 180 Degrees
// Copyright 2012 by Zipwhip.
// You are free to use and modify this code in your own software.

#define SERVO_PIN         39  // 26 or 39. Any pin on the Arduino or Mega will work.

int lenMicroSecondsOfPeriod = 25 * 1000;       // 25 milliseconds (ms). found much better smoothness at 25, not 20 ms.
int lenMicroSecondsOfPulse = 0;                // used in the while loop below
int lenMicroSecondsOfPulseStart = 0.5 * 1000;  // 0 degrees
int lenMicroSecondsOfPulseEnd = 2.2 * 1000;    // 180 degrees
int lenMicroSecondsOfPulseStep = 0.01 * 1000;   // .1 millisecond. That's 200 increments b/w 1.0 and 2.0

void setup()
{
  pinMode(SERVO_PIN, OUTPUT);

  // Setup our start point for our main loop
  lenMicroSecondsOfPulse = lenMicroSecondsOfPulseStart + lenMicroSecondsOfPulseStep;

}

void loop()
{

 // Servos work by sending a 20 ms pulse.
 // 1.0 ms at the start of the pulse will turn the servo to the 0 degree position
 // 1.5 ms at the start of the pulse will turn the servo to the 90 degree position
 // 2.0 ms at the start of the pulse will turn the servo to the 180 degree position

 // Do a while loop starting at our start pulse and incrementing each time thru the loop
 // Stop when we reach our final end point
 while (lenMicroSecondsOfPulse = lenMicroSecondsOfPulseStart)
 {
   // Turn voltage high to start the period and pulse
   digitalWrite(SERVO_PIN, HIGH);

   // Delay for the length of the pulse
   delayMicroseconds(lenMicroSecondsOfPulse);

   // Turn the voltage low for the remainder of the pulse
   digitalWrite(SERVO_PIN, LOW);

   // Delay this loop for the remainder of the period so we don't
   // send the next signal too soon or too late
   delayMicroseconds(lenMicroSecondsOfPeriod - lenMicroSecondsOfPulse); 

   // Increment our pulse
   lenMicroSecondsOfPulse += lenMicroSecondsOfPulseStep;

 }

 // Now reverse the step so we go in the opposite direction
 lenMicroSecondsOfPulseStep = lenMicroSecondsOfPulseStep * -1;
 lenMicroSecondsOfPulse += lenMicroSecondsOfPulseStep;

 // delay for a few seconds and do it all again
 delay(2 * 1000);

}

Zipwhip Registration Process

 

 

We thought it was important to demonstrate the Zipwhip registration process. This video covers it all. We know that nobody likes wasting their time with lengthy sign-ups. That’s why ours is so simple. Like the rest of our services we hope you find it easy and uncomplicated.

After watching this video you should be whipping out text messages in no time. It’s cloud texting, pure and simple.

 

Zipwhip Desktop App Installation (PC)

 

 

For today’s blog we take you through the installation process for our desktop application (PC). Towards the end of the video you’ll get to see cloud texting in action. Software developer Greg Mace is the movie star for the day, while social media manager Kelsey Klevenberg took care of the filming and voice work. If you have any questions about our service please send us an email to info@zipwhip.com. We are also active on facebook (facebook.com/zipwhip), and twitter (@zipwhipinc). Thanks for watching and we hope to hear from you soon.

Zipwhip Desktop App (Mac)

 

 

For all of you Mac users that also use an Android phone, this post is for you. Zipwhip developer Jed Hoffman takes you through the installation of our desktop app. The installation time for the app in this video took about a minute, so you should be ready to text across your devices in no time.

Zipwhip New User Sign Up Flag

 

 

At Zipwhip we like to keep it fun and nerdy. So, we decided to do something a bit whimsical every time we get a new sign up on our site. We purchased some hardware, put in a weekend of work, and now we have a flag hanging on the wall in the office that automatically goes up every time we get a new sign up on our site!

The flag is attached to a servo motor and an Arduino over ethernet . Each time we get the sign up event inside our cloud infrastructure, we send an HTTP command to the embedded web server running on the Arduino. The Arduino then sends the appropriate commands to the servo and UP POPS THE FLAG!

It’s a ton of fun to see something visual happen each time we get a new user.

The one concern is that we may have to build in a queue to the software that sends the signal to the Arduino. This is because we get quite a few sign ups. So much so, that we will get a lag between when the flag actually moves, and the exact time the sign up occurred. If the queues fill up too much or the lag gets too long, we will likely just reduce the size of our flag and make a wall of flags all moving independently.

Want to make your own? Below is a screenshot of (a.) the code inside the Arduino IDE (b.) the board diagram and (c.) the Arduino code to make it happen.

Here’s our bill of materials:

  • 1 Yard of Orange Fabric. 100% Cotton. Joanne Fabrics. $2.
  • 1 Bottle of Liquid Stitch. Joanne Fabrics. $6.
  • 1 Wooden Dowel. Joanne Fabrics. $2.
  • 1 Avery Light Fabric Inkjet Transfer Pack. Staples. $20.
  • 1 Arduino. SparkFun.com. $29.
  • 1 Arduino Ethernet Shield. SparkFun.com. $40.
  • 1 Savox 400 oz-in SC-1256TG High Torque Titanium Gear Standard Digital Coreless Servo. http://www.savoxusa.com $80.
  • 1 Power Adapter. 6V DC 2 Amp. Lynxmotion.com.
  • 1 40’ Ethernet cable. Newegg.com. $2.
  • 1 Ikea Akurum Harlig White Door as Mount. Ikea. $5.
  • 8 Wood Screws. #10 size for servo. #2 size for Arduino. $5.

This is a screenshot of how you use the Arduino IDE to write your code. The free IDE is available at http://www.arduino.cc.

This is a Fritzing diagram of the Arduino main board and the Arduino Ethernet Shield combined with the servo wiring. The hardest part of getting this schematic to work is getting enough power to the servo without sending it through the Arduino control pins.

And finally here is the full code to run your own flag. We painstakingly perfected the code so you know it’s ready to go for your own use.

________________________________________________________________________________________________________

/*

Web Server

A simple web server that shows the value of the analog input pins.
using an Arduino Wiznet Ethernet shield.

Circuit:
* Ethernet shield attached to pins 10, 11, 12, 13
* Analog inputs attached to pins A0 through A5 (optional)

*/

// Sweep
// by BARRAGAN <http://barraganstudio.com&gt;
// This example code is in the public domain.

// Zipwhip Sign Up Flag Codebase
// This code lets the Arduino operate a webserver. Whenever any
// request comes in to the server, it raises the flag. It’s quite
// simple. If too many requests are going to come in, the server is
// single-threaded so other requests have to wait. The sending process
// may want to instituate it’s own queuing system to solve for this.

#include <SPI.h>
#include <Ethernet.h>
#include <Servo.h>

Servo myservo; // create servo object to control a servo
// a maximum of eight servo objects can be created

int pos = 0; // variable to store the servo position
int startPos = 154;

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = { 0xDA, 0xAD, 0xBA, 0xEA, 0xFE, 0xED };
IPAddress ip(192,168,1, 252);

// Initialize the Ethernet server library
// with the IP address and port you want to use
// (port 80 is default for HTTP):
EthernetServer server(80);
EthernetClient client;

void setup()
{

//Serial.begin(9600); // open the serial port at 9600 bps:

// start the Ethernet connection and the server:
Ethernet.begin(mac, ip);
server.begin();

// servo code
myservo.attach(3); // attaches the servo on pin 9 to the servo object
myservo.write(startPos);

// initialize the digital pin as an output.
// Pin 13 has an LED connected on most Arduino boards:
pinMode(13, OUTPUT);
}

void loop()
{
// listen for incoming clients
client = server.available();

if (client) {
// an http request ends with a blank line
boolean currentLineIsBlank = true;
while (client.connected()) {
if (client.available()) {
char c = client.read();
//Serial.print(c);

// if you’ve gotten to the end of the line (received a newline
// character) and the line is blank, the http request has ended,
// so you can send a reply
if (c == ‘\n’ && currentLineIsBlank) {
// send a standard http response header
client.println(“HTTP/1.1 200 OK”);
client.println(“Content-Type: text/html”);
client.println();

// output that we are raising the flag
client.println(“Zipwhip Sign Up Flag Going Up<br />Degrees we will rotate to:<br />”);
raiseFlag();
client.println(“Done<br />”);
//delay(100);
//client.stop();

//raiseFlag();

break;
}
if (c == ‘\n’) {
// you’re starting a new line
currentLineIsBlank = true;
}
else if (c != ‘\r’) {
// you’ve gotten a character on the current line
currentLineIsBlank = false;
}
}
}
// give the web browser time to receive the data
delay(10);
// close the connection:
client.stop();
}
}

void raiseFlag() {

digitalWrite(13, HIGH); // set the LED on

myservo.attach(3); // attaches the servo on pin 9 to the servo object

// servo code
//myservo.attach(3); // attaches the servo on pin 9 to the servo object
//delay(100);
/* -10 (not possible on Sarvox Servo)
* 0
* 30
* 60
* 90 180
* 160
*/

// flag is all the way down. move it up. (may move to only 45 degrees down)
for(pos = startPos; pos>=30; pos-=1) // goes from 180 degrees to 0 degrees
{
client.print(pos);
client.print(” “);
//client.flush();
myservo.write(pos); // tell servo to go to position in variable ‘pos’
//delayMicroseconds(300);
delay(20); // waits 15ms for the servo to reach the position
}
client.println(“Done going forward.<br />”);

// Let’s pause by doing same positioning, but keep writing the same number
for(int ctr = 0; ctr < 60; ctr++)
{
myservo.write(pos);
delay(15);
}

// move flag all the way pointing down again
for(pos = 30; pos <= startPos; pos += 1) // goes from 0 degrees to 180 degrees
{ // in steps of 1 degree
client.print(pos);
client.print(” “);
//client.flush();
myservo.write(pos); // tell servo to go to position in variable ‘pos’
//delayMicroseconds(300);
delay(20); // waits 15ms for the servo to reach the position
}
client.println(“Done going backward.<br />”);
//myservo.detach();
delay(50);

myservo.detach(); // attaches the servo on pin 9 to the servo object

digitalWrite(13, LOW); // set the LED off

}

Tutorial: Using the Zipwhip API in Java to Popup Your Text Messages in a Bubble Window

I wrote a couple of posts about sending and receiving text messages via the Zipwhip API. Those posts culminated in console output showing you the results of sending or receiving. In this post I’m going to take things further and actually have a slick looking bubble popup on your desktop showing you your text message as it comes in to the Zipwhip Cloud in real-time. The results will look something like below.

image

Watch the video below to check out how cool the bubble looks as it fades in and out.

image

We are going to create a new Eclipse project from scratch. Let’s call it ZipwhipBubble. Go into Eclipse and start a new project.

image

Call the project ZipwhipBubble.

image

The source tab can be left with the defaults.

image

On the Libraries tab make sure to add the 3 JARs that we used in the previous posts including Log4j, SLF4j, and of course the most important library—the Zipwhip API JAR. The easiest way to do this is to just download the zip file below of this entire project.

ZipwhipBubble.zip 1.1 MB

The contents of the zip file are as follows:

image

After having downloaded the above zip file, you can add the libraries from the JARs folder into the Libraries tab. Your tab should look like the screenshot below.

image

Click Finish. You should now have a new project like the window below.

image

You need to add your first class to the project.

image

Give the class the name “Main” and give it a package like “com.yourcompany.zw”. Of course set yourcompany to your company.

image

You will see a code window that’s mostly empty like below.

image

Now paste the following code into your window so you get a full class ready to go without you having to do any of the hard work because I did it all for you. You can also download the full Zip file of this project from the top of this posting that contains all of the JARs, source code, and images for the project.

Main.java 4.8 KB

 1: package com.yourcompany.zw;
 2:
 3: import org.apache.log4j.BasicConfigurator;
 4: import org.apache.log4j.Level;
 5: import org.apache.log4j.Logger;
 6:
 7: import com.zipwhip.signals.dto.Message;
 8: import com.zipwhip.api.DefaultZipwhipSubscriptionClient;
 9: import com.zipwhip.api.HttpConnection;
 10: import com.zipwhip.api.signals.JsonSocketSignalClient;
 11: import com.zipwhip.signals.Signal;
 12: import com.zipwhip.signals.SignalObserver;
 13:
 14: public class Main {
 15:
 16:     /**
 17:  * @param args
 18:  */
 19:     public static void main(String[] args) {
 20:         // Setup logging
 21:         final Logger log = Logger.getLogger(Main.class);
 22:         log.setLevel(Level.DEBUG);
 23:         BasicConfigurator.configure();
 24:
 25:         HttpConnection connection = new HttpConnection();
 26:         connection.setDebug(true);
 27:
 28:         String mobileNumber = "3135551234";
 29:         String password = "mypassword";
 30:
 31:         try {
 32:             // This method will send a login request to the Zipwhip network and
 33:             // if succcessful you will get a sesionKey set in your connection object
 34:             // Watch out that you don't run "requestLogin()" too much because if you 
 35:             // create more than 50 sessionKeys within 1 day you will no longer be able 
 36:             // to get a key for 24 hours.
 37:             connection.requestLogin(mobileNumber, password);
 38:             //connection.setSessionKey("2ecfd63-4c57-4aa6-64a0-b0f120a1677a:1");
 39:             connection.apiVersion = "/";
 40:             log.info("Successfully authenticated. Your sessionKey is:" + connection.getSessionKey());
 41:
 42:         } catch (Exception e) {
 43:             log.fatal("Failed to authenticate and get sessionKey to Zipwhip network.");
 44:             log.fatal(e);
 45:             return;
 46:         }
 47:
 48:         DefaultZipwhipSubscriptionClient zipwhipSubscrClient;
 49:         JsonSocketSignalClient signalClient;
 50:
 51:         // This will create a new client object that allows you to perform
 52:         // other tasks against the Zipwhip network once you have created 
 53:         // an authenticated connection, i.e. have a sessionKey to communicate
 54:         // to the Zipwhip network over.
 55:         zipwhipSubscrClient = new DefaultZipwhipSubscriptionClient(connection);
 56:         log.info("Just created our Default Client. Uses HTTP to connect to Zipwhip network.");
 57:
 58:         // We will create our socket connection as well. You still need an HTTP connection because
 59:         // the Zipwhip API uses HTTP calls to authenticate the socket connection.
 60:         signalClient = new JsonSocketSignalClient(zipwhipSubscrClient);
 61:         log.info("Just created our Socket always-connected client. Uses TCP/IP sockets to connect to Zipwhip network.");
 62:
 63:         signalClient.addSignalObserver(new SignalObserver() {
 64:             @Override
 65:             public void notifySignalReceived(Signal signal) {
 66:                 log.debug("Signal received with uri " + signal.uri);
 67:
 68:                 switch (SignalUri.toSignalUri(signal.uri)) {
 69:                 case SIGNAL_MESSAGE_RECEIVE:
 70:                     // We got a message. Let's show it.
 71:                     // The Message object is contained in the signal.content object
 72:                     // but you need to cast it.
 73:                     Message msg = (Message)signal.content;
 74:                     showIncomingMessageAlert(msg);
 75:                     break;
 76:                 case SIGNAL_CONVERSATION_CHANGE:
 77:                     // Do nothing for now
 78:                     break;
 79:                 default:
 80:                     // Do nothing if we don't know the signal
 81:                     break;
 82:                 }
 83:             }
 84:
 85:             @Override
 86:             public void notifySignalProviderEvent(boolean isConnected, String message, long frameCount) {
 87:                 log.debug("Reporting SignalProvider event: isConnected " + isConnected + ", message: " + message + ", frames: " + frameCount);
 88:             }
 89:         });
 90:
 91:         signalClient.connect(connection.getSessionKey());
 92:
 93:         log.info("Socket test will keep running on socket thread. Thanks for using Zipwhip.");
 94:     }
 95:
 96:     public static void showIncomingMessageAlert(Message message) {
 97:         Bubble bubble = new Bubble(message.sourceAddress, message.body, "http://cloudtext.letsbobsled.com");
 98:     }
 99: }
 100:
 101: enum SignalUri
 102: {
 103:     /*
 104:  * /signal/message/progress
 105:  * /signal/messageProgress/messageProgress
 106:  * /signal/message/send
 107:  * /signal/message/receive
 108:  * /signal/message/read
 109:  * /signal/message/delete
 110:  * /signal/conversation/change
 111:  */
 112:     SIGNAL_MESSAGE_RECEIVE,
 113:     SIGNAL_MESSAGE_PROGRESS,
 114:     SIGNAL_MESSAGE_READ,
 115:     SIGNAL_MESSAGE_DELETE,
 116:     SIGNAL_MESSAGEPROGRESS_MESSAGEPROGRESS,
 117:     SIGNAL_CONVERSATION_CHANGE,
 118:     SIGNAL_CONTACT_NEW,
 119:     SIGNAL_CONTACT_SAVE,
 120:     SIGNAL_CONTACT_DELETE,
 121:     NOVALUE;
 122:
 123:     public static SignalUri toSignalUri(String str)
 124:     {
 125:         // We are going to use Java's valueOf method, so
 126:         // we need to cleanup the URI string first
 127:         // Get rid of first slash
 128:         String str2 = str.substring(1, str.length());
 129:         // convert slashes to underscores
 130:         str2 = str2.replaceAll("/", "_");
 131:         // go all upper case
 132:         str2 = str2.toUpperCase();
 133:
 134:         try {
 135:             return valueOf(str2);
 136:         }
 137:         catch (Exception ex) {
 138:             System.out.println("Found no match for SignalURI:" + str);
 139:             return NOVALUE;
 140:         }
 141:     }
 142: }

You will see that once you paste this code in you will have one error. You need to get the Bubble class that I created for this project.

image

The Bubble class takes care of all of the details of displaying a nice looking bubble on your desktop. All you have to do is create a bubble and pass it the mobile number, the text message, and a redirection URL to actually reply to the message. There are numerous web apps on the Internet that use the Zipwhip cloud so it is up to you to pick the URL that is appropriate.

Let’s go ahead and add our Bubble class to the project. Right click on the “com.yourcompany.zw” package name and choose New –> Class from the menu.

image

Call the class Bubble.

image

You will get a nice raw class like below.

image

Go ahead and paste in all of my hard work from my Bubble class. There’s a good deal of code in this class. The code is below. You can also download the file if you want. It’s below or it’s in the main Zip file linked to earlier in this posting.

Bubble.java 29.4 KB

 1: package com.yourcompany.zw;
 2:
 3: import java.awt.AlphaComposite;
 4: import java.awt.BorderLayout;
 5: import java.awt.Color;
 6: import java.awt.Container;
 7: import java.awt.Dimension;
 8: import java.awt.Font;
 9: import java.awt.GradientPaint;
 10: import java.awt.Graphics;
 11: import java.awt.Graphics2D;
 12: import java.awt.GraphicsConfiguration;
 13: import java.awt.Point;
 14: import java.awt.Toolkit;
 15: import java.awt.Transparency;
 16: import java.awt.event.ActionEvent;
 17: import java.awt.event.ActionListener;
 18: import java.awt.event.MouseAdapter;
 19: import java.awt.event.MouseEvent;
 20: import java.awt.event.MouseListener;
 21: import java.awt.event.MouseMotionAdapter;
 22: import java.awt.event.MouseMotionListener;
 23: import java.awt.geom.AffineTransform;
 24: import java.awt.geom.Rectangle2D;
 25: import java.awt.image.BufferedImage;
 26: import java.io.File;
 27: import java.io.IOException;
 28: import java.net.URI;
 29: import java.net.URISyntaxException;
 30: import java.net.URL;
 31: import java.util.ArrayList;
 32: import java.util.Arrays;
 33: import java.util.HashMap;
 34: import java.util.HashSet;
 35: import java.util.List;
 36: import java.util.Map;
 37: import java.util.Set;
 38: import java.util.regex.Pattern;
 39:
 40: import javax.imageio.ImageIO;
 41: import javax.swing.ImageIcon;
 42: import javax.swing.JButton;
 43: import javax.swing.JComponent;
 44: import javax.swing.JDialog;
 45: import javax.swing.JFrame;
 46: import javax.swing.JPanel;
 47: import javax.swing.Timer;
 48: import javax.swing.UIManager;
 49: import javax.swing.text.AbstractDocument;
 50:
 51: public class Bubble extends JDialog {
 52:
 53:     // this code is static to manage the number of windows that are open
 54:     protected static int numOpen;
 55:     protected static int getOpen() { return numOpen; }
 56:     protected static void windowOpen() { numOpen++; }
 57:     protected static void windowClose() { numOpen--; }
 58:
 59:     private int X=0;
 60:     private int Y=0;
 61:     private String fromMobileNumber;
 62:     private String message;
 63:     private String replyUrl;
 64:     private boolean toFade = true;
 65:     private float currOpacity;
 66:     private Timer fadeInTimer;
 67:     private Timer fadeOutTimer;
 68:
 69:     javax.swing.JTextPane jTextPaneTxtMsg;
 70:     AbstractDocument doc;
 71:
 72:     public Bubble(String fromMobileNumber, String message, String replyUrl) {
 73:         this(fromMobileNumber, message, replyUrl, true);
 74:     }
 75:
 76:     public Bubble(String fromMobileNumber, String message, String replyUrl, boolean toFade) {
 77:
 78:         // initialize properties
 79:         setFromMobileNumber(fromMobileNumber);
 80:         setMessage(message);
 81:         setReplyUrl(replyUrl);
 82:
 83:         // initialize the UI of the bubble
 84:         init();
 85:
 86:         // set the location of where this bubble will be shown
 87:         Dimension ourDim = Toolkit.getDefaultToolkit().getScreenSize();
 88:         this.setLocation(
 89:                 (int)ourDim.getWidth() - this.getWidth() - 10,
 90:                 0 + ((this.getHeight() + -20) * getOpen()));
 91:
 92:         // increment how many bubbles are open
 93:         windowOpen();
 94:
 95:         // ok, show it finally
 96:         this.setVisible(true);
 97:     }
 98:
 99:     // Getters/setters
 100:     public String getFromMobileNumber() {
 101:         return StringUtil.format(fromMobileNumber, "(###) ###-####");
 102:     }
 103:
 104:     public void setFromMobileNumber(String fromMobileNumber) {
 105:         this.fromMobileNumber = fromMobileNumber;
 106:     }
 107:
 108:     public String getMessage() {
 109:         return message;
 110:     }
 111:
 112:     public void setMessage(String message) {
 113:         this.message = message;
 114:     }
 115:
 116:     public String getReplyUrl() {
 117:         return replyUrl;
 118:     }
 119:
 120:     public void setReplyUrl(String replyUrl) {
 121:         this.replyUrl = replyUrl;
 122:     }
 123:
 124:     public boolean isToFade() {
 125:         return toFade;
 126:     }
 127:     public void setToFade(boolean toFade) {
 128:         this.toFade = toFade;
 129:     }
 130:
 131:     // Initialize the GUI
 132:     public void init() {
 133:         try {
 134:
 135:             // When JFrame or JDialog
 136:             this.setUndecorated(true);
 137:             this.setModal(false);
 138:             this.setFocusableWindowState(false);
 139:             this.setAlwaysOnTop(true);
 140:
 141:             try {
 142:                 UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
 143:             } catch (Exception evt) {}
 144:
 145:             // Get bg image
 146:             final BufferedImage backgroundImg = ImageIO.read(getClass().getResource("/resources/riser_shd.png"));
 147:
 148:             // Set icon & title
 149:             final BufferedImage z = ImageIO.read(getClass().getResource("/resources/z.png"));
 150:             ((java.awt.Frame)this.getOwner()).setIconImage(z);
 151:             this.setTitle(this.getFromMobileNumber() + ": " + this.getMessage());
 152:
 153:             // Set layout
 154:             this.setLayout(new BorderLayout());
 155:             JPanel mainPanel = new JPanel(new BorderLayout()) {
 156:
 157:                 private static final long serialVersionUID = 1L;
 158:
 159:                 // The paintComponent override let's us make the entire
 160:                 // window transparent on the desktop so we get a cool effect
 161:                 @Override
 162:                 protected void paintComponent(Graphics g) {
 163:                     Graphics2D g2d = (Graphics2D) g.create();
 164:
 165:                     // code from
 166:                     // http://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html
 167:                     int width = backgroundImg.getWidth();
 168:                     int height = backgroundImg.getHeight();
 169:                     GraphicsConfiguration gc = g2d.getDeviceConfiguration();
 170:                     BufferedImage img = gc.createCompatibleImage(
 171:                         width,
 172:                         height,
 173:                         Transparency.TRANSLUCENT);
 174:                     Graphics2D g2 = img.createGraphics();
 175:                     g2.setComposite(AlphaComposite.Src);
 176:                     g2.drawImage(backgroundImg, 0, 0, null);
 177:                     g2.dispose();
 178:                     g2d.drawImage(img, 0, 0, this);
 179:                     g2d.dispose();
 180:                 }
 181:             };
 182:
 183:             // Setup the window
 184:             final JDialog wnd = this;
 185:
 186:             // This is the phone number and textarea message panel
 187:             javax.swing.JPanel jPanelAll = new javax.swing.JPanel();
 188:             javax.swing.JLabel jLabelMobileNum = new javax.swing.JLabel();
 189:             this.jTextPaneTxtMsg = new javax.swing.JTextPane();
 190:             JButton jButtonClose = new javax.swing.JButton();
 191:             JButton jButtonReply = new javax.swing.JButton();
 192:
 193:             jPanelAll.setOpaque(false);
 194:             jPanelAll.setDoubleBuffered(false);
 195:             jPanelAll.setName("jPanelAll"); // NOI18N
 196:
 197:             jLabelMobileNum.setFont( new Font(null, Font.BOLD, 14));
 198:             jLabelMobileNum.setForeground(new Color(67, 74, 84));
 199:             jLabelMobileNum.setText(getFromMobileNumber());
 200:             jLabelMobileNum.setName("jLabelMobileNum"); // NOI18N
 201:
 202:             jTextPaneTxtMsg.setBorder(null);
 203:             jTextPaneTxtMsg.setEditable(false);
 204:             jTextPaneTxtMsg.setFont( new Font(null, Font.PLAIN, 12));
 205:             jTextPaneTxtMsg.setOpaque(false);
 206:             jTextPaneTxtMsg.setDoubleBuffered(false);
 207:             jTextPaneTxtMsg.setText(getMessage());
 208:             jTextPaneTxtMsg.setName("jTextPane1"); // NOI18N
 209:             jTextPaneTxtMsg.setCaretPosition(0);
 210:
 211:             javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(jTextPaneTxtMsg);
 212:             jScrollPane1.getViewport().setOpaque(false);
 213:             jScrollPane1.getViewport().setDoubleBuffered(false);
 214:
 215:             jScrollPane1.setBorder(null);
 216:             jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
 217:             jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
 218:             jScrollPane1.setOpaque(false);
 219:             jScrollPane1.setDoubleBuffered(false);
 220:             jScrollPane1.setWheelScrollingEnabled(true);
 221:
 222:             BufferedImage riserSpriteBtnImg = ImageIO.read(getClass().getResource("/resources/risergang.png"));
 223:
 224:             // Define the close button
 225:             jButtonClose.setIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(1,1,14,14)));
 226:             jButtonClose.setRolloverIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(16,1,14,14)));
 227:             jButtonClose.setPressedIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(31,1,14,14)));
 228:             jButtonClose.setBorder(null);
 229:             jButtonClose.setBorderPainted(false);
 230:             jButtonClose.setContentAreaFilled(false);
 231:             jButtonClose.addActionListener(new ActionListener() {
 232:                 @Override
 233:                 public void actionPerformed(ActionEvent e) {
 234:                     wnd.setVisible(false);
 235:                 }
 236:             });
 237:
 238:             // Define the reply button
 239:             jButtonReply.setIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,16,58,30)));
 240:             jButtonReply.setRolloverIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,47,58,30)));
 241:             jButtonReply.setPressedIcon(new ImageIcon(riserSpriteBtnImg.getSubimage(0,78,58,30)));
 242:             jButtonReply.setBorder(null);
 243:             jButtonReply.setBorderPainted(false);
 244:             jButtonReply.setContentAreaFilled(false);
 245:             jButtonReply.addActionListener(new ActionListener() {
 246:                 @Override
 247:                 public void actionPerformed(ActionEvent e) {
 248:                     openUri(getReplyUrl());
 249:                     wnd.setVisible(false);
 250:                 }
 251:             });
 252:
 253:             // Do the layout positioning using horiz/vert groups 
 254:             javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanelAll);
 255:             jPanelAll.setLayout(jPanel2Layout);
 256:
 257:             jPanel2Layout.setHorizontalGroup(
 258:                 jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 259:                 .addGroup(jPanel2Layout.createSequentialGroup()
 260:                     .addGap(32)
 261:                     .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 262:                         .addComponent(jLabelMobileNum)
 263:                         .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 150, javax.swing.GroupLayout.PREFERRED_SIZE)
 264:                     )
 265:                     .addContainerGap(73, Short.MAX_VALUE))
 266:                 .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 267:                     .addGroup(jPanel2Layout.createSequentialGroup()
 268:                         .addGap(169)
 269:                         .addComponent(jButtonClose, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
 270:                         .addContainerGap(261, Short.MAX_VALUE)))
 271:                 .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 272:                     .addGroup(jPanel2Layout.createSequentialGroup()
 273:                         .addGap(128)
 274:                         .addComponent(jButtonReply, javax.swing.GroupLayout.PREFERRED_SIZE, 58, javax.swing.GroupLayout.PREFERRED_SIZE)
 275:                         .addContainerGap(251, Short.MAX_VALUE)))
 276:             );
 277:
 278:             jPanel2Layout.setVerticalGroup(
 279:                 jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 280:                 .addGroup(jPanel2Layout.createSequentialGroup()
 281:                     .addGap(32)
 282:                     .addComponent(jLabelMobileNum)
 283:                     .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
 284:                     .addComponent(false, jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)
 285:                     .addContainerGap(64, Short.MAX_VALUE))
 286:                 .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 287:                     .addGroup(jPanel2Layout.createSequentialGroup()
 288:                         .addGap(28)
 289:                         .addComponent(jButtonClose, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)
 290:                         .addContainerGap(300, Short.MAX_VALUE)))
 291:                 .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
 292:                     .addGroup(jPanel2Layout.createSequentialGroup()
 293:                         .addGap(168)
 294:                         .addComponent(jButtonReply, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE)
 295:                         .addContainerGap(146, Short.MAX_VALUE)))
 296:             );
 297:
 298:             // End phone/message panel
 299:
 300:             // Setup final main paenl
 301:             mainPanel.add(jPanelAll, BorderLayout.WEST);
 302:             mainPanel.setDoubleBuffered(false);
 303:             mainPanel.setOpaque(true);
 304:             this.add(mainPanel, BorderLayout.CENTER);
 305:
 306:             this.setAlwaysOnTop(true);
 307:             this.setSize(216, 234);
 308:             this.setLocationRelativeTo(null);
 309:             com.sun.awt.AWTUtilities.setWindowOpaque(this, false);
 310:
 311:             // Watch mouse movements and clicks to allow dragging of window
 312:             addMouseListener(new MouseAdapter()
 313:             {
 314:                 public void mousePressed(MouseEvent e)
 315:                 {
 316:                     // Check for double-click
 317:                     if (e.getClickCount() >= 2) {
 318:                         // Open the website to let them reply
 319:                         openUri(getReplyUrl());
 320:                         setVisible(false);
 321:                     } else {
 322:                         // Do drag operation
 323:                         X=e.getX();
 324:                         Y=e.getY();
 325:                     }
 326:                 }
 327:
 328:             });
 329:
 330:             addMouseMotionListener(new MouseMotionAdapter()
 331:             {
 332:                 public void mouseDragged(MouseEvent e)
 333:                 {
 334:                     setLocation(getLocation().x+(e.getX()-X),getLocation().y+(e.getY()-Y));
 335:                 }
 336:             });
 337:
 338:         } catch (IOException e) {
 339:             e.printStackTrace();
 340:         }
 341:     }
 342:
 343:     @Override
 344:     public void setVisible(boolean b) {
 345:
 346:         // Handle fading in or fading out
 347:
 348:         // If setvisible is true
 349:         if (b) {
 350:
 351:             // See if we need to fade in
 352:             if (this.toFade) {
 353:                 // mark the popup with 0% opacity
 354:                 this.currOpacity = 0;
 355:                 com.sun.awt.AWTUtilities.setWindowOpacity(this, 0.0f);
 356:             }
 357:
 358:             super.setVisible(b);
 359:
 360:             final JDialog popupWindow = this;
 361:
 362:             if (this.toFade) {
 363:                 // start fading in
 364:                 this.fadeInTimer = new Timer(50, new ActionListener() {
 365:                     public void actionPerformed(ActionEvent e) {
 366:                         currOpacity += 10;
 367:                         if (currOpacity <= 100) {
 368:                             com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
 369:                                     currOpacity / 100.0f);
 370:                             // workaround bug 6670649 - should call
 371:                             // popupWindow.repaint() but that will not repaint the
 372:                             // panel
 373:                             popupWindow.getContentPane().repaint();
 374:                         } else {
 375:                             currOpacity = 100;
 376:                             fadeInTimer.stop();
 377:                         }
 378:                     }
 379:                 });
 380:                 this.fadeInTimer.setRepeats(true);
 381:                 this.fadeInTimer.start();
 382:             }
 383:
 384:         } else {
 385:
 386:             // If setvisible is false
 387:
 388:             // Handle fading out, if they want a fade
 389:             if (this.toFade) {
 390:
 391:                 // cancel fade-in if it's running.
 392:                 if (this.fadeInTimer.isRunning())
 393:                     this.fadeInTimer.stop();
 394:
 395:                 final Bubble popupWindow = this;
 396:
 397:                 // start fading out
 398:                 this.fadeOutTimer = new Timer(50, new ActionListener() {
 399:                     public void actionPerformed(ActionEvent e) {
 400:                         currOpacity -= 10;
 401:                         if (currOpacity >= 0) {
 402:                             com.sun.awt.AWTUtilities.setWindowOpacity(popupWindow,
 403:                                     currOpacity / 100.0f);
 404:                             // workaround bug 6670649 - should call
 405:                             // popupWindow.repaint() but that will not repaint the
 406:                             // panel
 407:                             popupWindow.getContentPane().repaint();
 408:                         } else {
 409:                             fadeOutTimer.stop();
 410:                             popupWindow.setToFade(false);
 411:                             popupWindow.setVisible(false);
 412:                             currOpacity = 0;
 413:                         }
 414:                     }
 415:                 });
 416:                 this.fadeOutTimer.setRepeats(true);
 417:                 this.fadeOutTimer.start();
 418:
 419:             } else {
 420:
 421:                 // setVisible is being set to false and we're not in fadeout mode,
 422:                 // so let's let the super handle
 423:                 // it cuz we don't want to interfere if there's no fading going on
 424:                 windowClose();
 425:                 super.setVisible(false);
 426:                 this.removeAll();
 427:                 this.dispose();
 428:
 429:             }
 430:         }
 431:     }
 432:
 433:     public void openUri(String url) {
 434:
 435:         if( !java.awt.Desktop.isDesktopSupported() ) {
 436:
 437:             System.err.println( "Desktop is not supported (fatal)" );
 438:             return;
 439:         }
 440:
 441:         java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
 442:
 443:         if( !desktop.isSupported( java.awt.Desktop.Action.BROWSE ) ) {
 444:
 445:             System.err.println( "Desktop doesn't support the browse action (fatal)" );
 446:             return;
 447:         }
 448:
 449:         try {
 450:
 451:             java.net.URI uri = new java.net.URI( url );
 452:             desktop.browse( uri );
 453:         }
 454:         catch ( Exception e ) {
 455:
 456:             System.err.println( e.getMessage() );
 457:         }
 458:
 459:     }
 460:
 461: }
 462:
 463: class MoveMouseListener implements MouseListener, MouseMotionListener {
 464:     JComponent target;
 465:     Point start_drag;
 466:     Point start_loc;
 467:
 468:     public MoveMouseListener(JComponent target) {
 469:         this.target = target;
 470:     }
 471:
 472:     public static JFrame getFrame(Container target) {
 473:         if (target instanceof JFrame) {
 474:             return (JFrame) target;
 475:         }
 476:         return getFrame(target.getParent());
 477:     }
 478:
 479:     Point getScreenLocation(MouseEvent e) {
 480:         Point cursor = e.getPoint();
 481:         Point target_location = this.target.getLocationOnScreen();
 482:         return new Point((int) (target_location.getX() + cursor.getX()),
 483:                 (int) (target_location.getY() + cursor.getY()));
 484:     }
 485:
 486:     public void mouseClicked(MouseEvent e) {
 487:     }
 488:
 489:     public void mouseEntered(MouseEvent e) {
 490:     }
 491:
 492:     public void mouseExited(MouseEvent e) {
 493:     }
 494:
 495:     public void mousePressed(MouseEvent e) {
 496:         this.start_drag = this.getScreenLocation(e);
 497:         this.start_loc = this.getFrame(this.target).getLocation();
 498:     }
 499:
 500:     public void mouseReleased(MouseEvent e) {
 501:     }
 502:
 503:     public void mouseDragged(MouseEvent e) {
 504:         Point current = this.getScreenLocation(e);
 505:         Point offset = new Point((int) current.getX() - (int) start_drag.getX(),
 506:                 (int) current.getY() - (int) start_drag.getY());
 507:         JFrame frame = this.getFrame(target);
 508:         Point new_location = new Point(
 509:                 (int) (this.start_loc.getX() + offset.getX()), (int) (this.start_loc
 510:                         .getY() + offset.getY()));
 511:         frame.setLocation(new_location);
 512:     }
 513:
 514:     public void mouseMoved(MouseEvent e) {
 515:     }
 516: }
 517:
 518: class StringUtil {
 519:
 520:     //private static final Logger logger = LoggerFactory.getLogger(StringUtil.class);
 521:
 522:     public static final String EMPTY_STRING = "";
 523:     public static final int MAX_MOBILE_NUMBER_DIGITS = 16; // Finland being the longest we could find 99500-1-202-444-1212, plus a buffer...
 524:     public static final List<Character> VALID_NUMBERS;
 525:     public static final List<Character> VALID_SPECIAL_CHARACTERS;
 526:
 527:     static {
 528:         VALID_NUMBERS = Arrays.asList(new Character[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' });
 529:         VALID_SPECIAL_CHARACTERS = Arrays.asList(new Character[]{'!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '`', '[', ']', '\\', '{', '}', '|', '<', '>', '?', ',', '.', '/', ':', ';', '\'', '"', '+', '~', '*', '.'});
 530:     }
 531:
 532:     /**
 533:  * Strips all characters that are not numbers (0 - 9) and returns a new
 534:  * string. Returns and empty string if the mobile number is null or empty.
 535:  * 
 536:  * @param mobileNumber - mobile number string to parse
 537:  * @return String - parsed mobile number
 538:  */
 539:     public static String safeCleanMobileNumber(String mobileNumber) {
 540:
 541:         //logger.debug("getting clean for " + mobileNumber);
 542:
 543:         if (isNullOrEmpty(mobileNumber)) {
 544:             //logger.debug("was nullOrEmpty ");
 545:             return null;
 546:         }
 547:
 548:         StringBuilder cleanMobileNumber = new StringBuilder();
 549:         for (int i = 0; i < mobileNumber.length(); i++) {
 550:             if (VALID_NUMBERS.contains(mobileNumber.charAt(i))) {
 551:                 cleanMobileNumber.append(mobileNumber.charAt(i));
 552:             }
 553:         }
 554:
 555:         // remove the first (1) at the beginning of the to match default us
 556:         // numbers
 557:         // 10 digits
 558:         if (cleanMobileNumber.length() > 10 && startsWith(cleanMobileNumber.toString(), "1")) {
 559:             //logger.debug("clean 1 " + cleanMobileNumber.substring(1));
 560:             return cleanMobileNumber.substring(1);
 561:         }
 562:         if (cleanMobileNumber.length() > 10 && startsWith(cleanMobileNumber.toString(), "+1")) {
 563:             //logger.debug("clean 2 " + cleanMobileNumber.substring(1));
 564:             return cleanMobileNumber.substring(2);
 565:         }
 566:
 567:         //logger.debug("clean " + cleanMobileNumber.toString());
 568:         return cleanMobileNumber.toString();
 569:     }
 570:
 571:     /**
 572:  * Same as safeCleanMobileNumber except for devices
 573:  * in which case the device number is removed first
 574:  *
 575:  * @param mobileNumber - mobile number string to parse
 576:  * @return String - parsed mobile number
 577:  */
 578:     public static String safeCleanMobileNumberRemoveDevice(String mobileNumber) {
 579:
 580:         if (startsWith(mobileNumber, "device:/")) {
 581:
 582:             int index = mobileNumber.lastIndexOf('/');
 583:
 584:             // If the last index of '/' is > than the first
 585:             if (index > 7) {
 586:                 mobileNumber = mobileNumber.substring(0, index);
 587:             }
 588:         }
 589:
 590:         return safeCleanMobileNumber(mobileNumber);
 591:     }
 592:
 593:     public static boolean isValidEmail(String email){
 594:         // see http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/
 595:         Pattern pattern ;
 596:         java.util.regex.Matcher matcher;
 597:         final String EMAIL_PATTERN = "^[\\w\\-]+(\\.[\\w\\-]+)*@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,4}$";
 598:
 599:         pattern = Pattern.compile(EMAIL_PATTERN);
 600:         matcher = pattern.matcher(email);
 601:
 602:         return matcher.matches();
 603:     }
 604:
 605:
 606:     /**
 607:  * The length is valid if it is between 3 and 6 or over
 608:  * 10 and up to and including 20
 609:  * 3-6 length means short codes
 610:  * 10 length 000-000-0000
 611:  * 10+ means international
 612:  *
 613:  * @param mobileNumber
 614:  * @return
 615:  */
 616:     public static boolean isValidLengthMobileNumber(String mobileNumber) {
 617:
 618:         if (isNullOrEmpty(mobileNumber))  {
 619:             return false;
 620:         }
 621:
 622:         int numberLength = mobileNumber.length();
 623:
 624:         // Wrong if under 3 and between 7 and 9 digits long or longer than 20
 625:         if (numberLength < 3 || numberLength == 7 || numberLength == 8 || numberLength == 9 || numberLength > MAX_MOBILE_NUMBER_DIGITS) {
 626:             return false;
 627:         }
 628:         // Correct is 3 to 6 for short codes and 10 to 20 for long international
 629:         // numbers, including a buffer
 630:         return true;
 631:     }
 632:
 633:     public static boolean startsWith(String string1, String toFind) {
 634:         if (string1 == null && toFind == null){
 635:             // null contains null.
 636:             return true;
 637:         } else if (string1 == null){
 638:             return false;
 639:         }
 640:
 641:         if (StringUtil.equalsIgnoreCase(string1, toFind)){
 642:             return true;
 643:         }
 644:
 645:         return string1.toLowerCase().startsWith(toFind.toLowerCase());
 646:     }
 647:
 648:     public static boolean endsWith(String source, String toFind) {
 649:         if (source == null) {
 650:             return false;
 651:         }
 652:
 653:         return source.endsWith(toFind);
 654:     }
 655:
 656:     public static String defaultValue(String string, String defaultValue) {
 657:         if (StringUtil.isNullOrEmpty(string)){
 658:             return defaultValue;
 659:         }
 660:         return string;
 661:     }
 662:
 663:     public static final class Schema {
 664:
 665:         public static final String TEL = "tel";
 666:     }
 667:
 668:     /**
 669:  * Takes a string representing the display name and returns and array with
 670:  * first name and last name.
 671:  * 
 672:  * Returns null if null or empty input
 673:  * 
 674:  * @param displayName
 675:  * @return String[]
 676:  */
 677:     public static String[] splitDisplayName(String displayName) {
 678:         if (displayName == null || displayName.length() < 1) {
 679:             return null;
 680:         }
 681:
 682:         String[] names = displayName.split(" ");
 683:
 684:         return names;
 685:     }
 686:
 687:     /**
 688:  * Takes a string representing a list of mobile numbers as
 689:  * 5555551212, 5556667878 or
 690:  * 5555551212; 5556667878
 691:  *
 692:  * Returns null if null or empty input
 693:  *
 694:  * @param sourceNumber
 695:  * @return List<String>
 696:  */
 697:     public static List<String> splitMobileNumbers(String sourceNumber) {
 698:
 699:         ArrayList<String> result = new ArrayList<String>();
 700:         int last = 0;
 701:
 702:         for (int i = 0; i < sourceNumber.length(); i++) {
 703:             if (sourceNumber.charAt(i) == ';' || sourceNumber.charAt(i) == ',') {
 704:                 String number = sourceNumber.substring(last, i).trim();
 705:                 result.add(number);
 706:                 last = i + 1;
 707:             }
 708:         }
 709:         result.add(sourceNumber.substring(last).trim());
 710:         return result;
 711:     }
 712:
 713:     /**
 714:  * Format the mobile number
 715:  * 
 716:  * @param mobileNumber
 717:  * @param format
 718:  * - ###-###-####
 719:  * @return
 720:  */
 721:     public static String format(String mobileNumber, String format) {
 722:         if (mobileNumber == null || mobileNumber.length() < 1 || format == null || format.length() < 1) {
 723:             return mobileNumber;
 724:         }
 725:
 726:         int numberCount = 0;
 727:         for (int i = 0; i < format.length(); i++) {
 728:             if (format.charAt(i) == '#') {
 729:                 numberCount++;
 730:             }
 731:         }
 732:         String number = safeCleanMobileNumber(mobileNumber);
 733:         if (numberCount != number.length()) {
 734:             return mobileNumber;
 735:         }
 736:
 737:         List<Character> numberChars = new ArrayList<Character>();
 738:         char[] chars = new char[format.length()];
 739:         int count = 0;
 740:         for (int i = 0; i < format.length(); i++) {
 741:             if (format.charAt(i) == '#') {
 742:                 numberChars.add(number.charAt(count));
 743:                 chars[i] = number.charAt(count);
 744:                 count++;
 745:             } else {
 746:                 numberChars.add(format.charAt(i));
 747:                 chars[i] = format.charAt(i);
 748:             }
 749:         }
 750:
 751:         return new String(chars);
 752:     }
 753:
 754:     public static String stringArrayToDelimittedString(String[] arrayString, String delimiter) {
 755:         return stringArrayToDelimittedString(arrayString, delimiter, null);
 756:     }
 757:
 758:     public static String stringArrayToDelimittedString(String[] arrayString, String delimiter, String format) {
 759:
 760:         if (arrayString == null || delimiter == null) {
 761:             return null;
 762:         }
 763:
 764:         StringBuilder delimittedString = new StringBuilder();
 765:         if (format == null || format.length() < 1) {
 766:             for (String number : arrayString) {
 767:                 delimittedString.append(number).append(delimiter);
 768:             }
 769:         } else {
 770:             for (String number : arrayString) {
 771:                 number = StringUtil.format(number, format);
 772:                 delimittedString.append(number).append(delimiter);
 773:             }
 774:         }
 775:
 776:         return delimittedString.toString();
 777:     }
 778:
 779:     /**
 780:  * Takes a delimited values string and returns is as a set
 781:  * 
 782:  * @param string
 783:  * @param delimiter
 784:  * @return Set<String>
 785:  */
 786:     public static Set<String> stringToSet(String string, String delimiter) {
 787:         if (isNullOrEmpty(string) || isNullOrEmpty(delimiter)) {
 788:             return null;
 789:         }
 790:
 791:         String[] toArray = string.split(delimiter);
 792:         Set<String> toSet = null;
 793:         if (toArray != null && toArray.length > 0) {
 794:             toSet = new HashSet<String>();
 795:             for (String value : toArray) {
 796:                 if (!isNullOrEmpty(value)) toSet.add(value);
 797:             }
 798:         }
 799:         return toSet;
 800:     }
 801:
 802:     /**
 803:  * Return true if the string is null. Trims the string and checks if it is
 804:  * an empty string
 805:  * 
 806:  * @param string
 807:  * @return
 808:  */
 809:     public static boolean isNullOrEmpty(String string) {
 810:         return (string == null || string.trim().length() < 1);
 811:     }
 812:
 813:     public static boolean isNullOrEmpty(String... strings) {
 814:         for (String string : strings) {
 815:             if (isNullOrEmpty(string)) {
 816:                 return true;
 817:             }
 818:         }
 819:         return false;
 820:     }
 821:
 822:     public static boolean exists(String string) {
 823:         return !isNullOrEmpty(string);
 824:     }
 825:
 826:     public static boolean equals(String string1, String string2){
 827:         if (string1 == string2)
 828:             return true; // covers both null, or both same instance
 829:         if (string1 == null){
 830:             return false; // covers 1 null, other not.
 831:         }
 832:
 833:         return (string1.equals(string2)); // covers equals
 834:     }
 835:
 836:     public static boolean equalsIgnoreCase(String string, String type) {
 837:         boolean oneEmpty = isNullOrEmpty(string);
 838:         boolean otherEmpty = isNullOrEmpty(type);
 839:         if (oneEmpty && otherEmpty) {
 840:             return true;
 841:         }
 842:         if (oneEmpty || otherEmpty) {
 843:             return false;
 844:         }
 845:
 846:         return string.equalsIgnoreCase(type);
 847:     }
 848:
 849:     public static boolean isIntegerParsable(String toCheck){
 850:         if (toCheck == null) return false;
 851:         try {
 852:             Integer.parseInt(toCheck);
 853:             return true;
 854:         } catch (NumberFormatException e){
 855:             return false;
 856:         }
 857:     }
 858:
 859:     public static boolean isLongParsable(String toCheck){
 860:         if (toCheck == null) return false;
 861:         try {
 862:             Long.parseLong(toCheck);
 863:             return true;
 864:         } catch (NumberFormatException e){
 865:             return false;
 866:         }
 867:     }
 868:
 869:     public static String[] convert(Object... parameters){
 870:         String[] args = new String[parameters.length];
 871:
 872:         int idx = 0;
 873:         for(Object object : parameters){
 874:             args[idx] = String.valueOf(object);
 875:             idx ++;
 876:         }
 877:
 878:         return args;
 879:     }
 880:
 881:     public static String join(Object... parts) {
 882:         StringBuilder sb = new StringBuilder();
 883:         for(Object part : parts){
 884:             if (part == null){
 885:                 continue;
 886:             }
 887:             sb.append(String.valueOf(part));
 888:         }
 889:         return sb.toString();
 890:     }
 891:
 892:     /**
 893:  * Check if the last character in the string matches the input character and
 894:  * removes. If the match fails, we return the string as it is.
 895:  * 
 896:  * @param inputString
 897:  * @param c
 898:  * @return string
 899:  */
 900:     public static String removeLast(String inputString, char c) {
 901:
 902:         if (isNullOrEmpty(inputString)) {
 903:             return inputString;
 904:         }
 905:
 906:         if (inputString.charAt(inputString.length() - 1) == c) {
 907:             return inputString.substring(0, inputString.length() - 1);
 908:         }
 909:
 910:         return inputString;
 911:     }
 912:
 913:     public static String stripStringNull(String param) {
 914:         return (StringUtil.isNullOrEmpty(param) || "null".equalsIgnoreCase(param)) ? StringUtil.EMPTY_STRING : param;
 915:     }
 916:
 917:     /**
 918:  * 
 919:  * @param contents
 920:  * @param key
 921:  * @param value
 922:  * @return
 923:  */
 924:     public static String convertPatterns(String contents, Map<String, String> keyVals) {
 925:
 926:         if (contents == null) {
 927:             throw new NullPointerException("Cannot convert null pattern");
 928:         }
 929:
 930:         for(Map.Entry<String, String> entry : keyVals.entrySet()) {
 931:             contents = contents.replaceAll(entry.getKey(), entry.getValue());
 932:         }
 933:
 934:         return contents;
 935:     }
 936:
 937:     public static String convertPatterns(
 938:         final String contents,
 939:         final String hostnamePattern,
 940:         final String string
 941:     ) {
 942:
 943:         final Map<String, String> keyVals = new HashMap<String, String>();
 944:
 945:         return convertPatterns(contents, keyVals);
 946:
 947:     }
 948:
 949: }

Your Eclipse window package explorer should have two files in it now: Bubble.java and Main.java.

image

Now, you can go ahead and run your code but you will get errors because you don’t have the pictures required by the Bubble.java class to render out the nice looking bubble. So, you need to go create a resources folder and place the pictures inside it.

image

Then paste in the 3 pictures into the folder. These pictures are also in the main zip file for the entire project at the start of this posting. Or you can download them here.

resources.zip 23 KB

image

Ok, now go to your Main.java file and run the program. The best way to do this is to move your cursor into the static void main() method and right-click. Choose Run As –> Java Application. This will execute the static void main() and actually run your app. You will see a lot of output in the console window. This is the Zipwhip API giving you lots of debugging feedback.

image

Now, here’s the big moment. Go ahead and send yourself a text message to the phone you are logged in as. You will get a slick popup window fading in on your desktop in the upper right corner. You can double-click the bubble to jump to the configured URL to reply to your message. You can also hit the reply button or close the bubble.

image

The key extra chunk of code for this example vs. the previous posting is that we are now listening to signals and parsing them so we can perform a switch. I created an ENUM of some of the standard signals from the Zipwhip API so that I could perform a switch since Java still does not let you do a switch on strings.

Here is the signal observer code and the switch statement.

 1: signalClient.addSignalObserver(new SignalObserver() {
 2:     @Override
 3:     public void notifySignalReceived(Signal signal) {
 4:         log.debug("Signal received with uri " + signal.uri);
 5:
 6:         switch (SignalUri.toSignalUri(signal.uri)) {
 7:         case SIGNAL_MESSAGE_RECEIVE:
 8:             // We got a message. Let's show it.
 9:             // The Message object is contained in the signal.content object
 10:             // but you need to cast it.
 11:             Message msg = (Message)signal.content;
 12:             showIncomingMessageAlert(msg);
 13:             break;
 14:         case SIGNAL_CONVERSATION_CHANGE:
 15:             // Do nothing for now
 16:             break;
 17:         default:
 18:             // Do nothing if we don't know the signal
 19:             break;
 20:         }
 21:     }
 22:
 23:  });

You will need to also have the class for the Enum. I got lazy and just placed it into the Main.java file. Technically you should likely place this in its own class file.

 1: enum SignalUri
 2: {
 3:     /*
 4:  * /signal/message/progress
 5:  * /signal/messageProgress/messageProgress
 6:  * /signal/message/send
 7:  * /signal/message/receive
 8:  * /signal/message/read
 9:  * /signal/message/delete
 10:  * /signal/conversation/change
 11:  */
 12:     SIGNAL_MESSAGE_RECEIVE,
 13:     SIGNAL_MESSAGE_PROGRESS,
 14:     SIGNAL_MESSAGE_READ,
 15:     SIGNAL_MESSAGE_DELETE,
 16:     SIGNAL_MESSAGEPROGRESS_MESSAGEPROGRESS,
 17:     SIGNAL_CONVERSATION_CHANGE,
 18:     SIGNAL_CONTACT_NEW,
 19:     SIGNAL_CONTACT_SAVE,
 20:     SIGNAL_CONTACT_DELETE,
 21:     NOVALUE;
 22:
 23:     public static SignalUri toSignalUri(String str)
 24:     {
 25:         // We are going to use Java's valueOf method, so
 26:         // we need to cleanup the URI string first
 27:         // Get rid of first slash
 28:         String str2 = str.substring(1, str.length());
 29:         // convert slashes to underscores
 30:         str2 = str2.replaceAll("/", "_");
 31:         // go all upper case
 32:         str2 = str2.toUpperCase();
 33:
 34:         try {
 35:             return valueOf(str2);
 36:         }
 37:         catch (Exception ex) {
 38:             System.out.println("Found no match for SignalURI:" + str);
 39:             return NOVALUE;
 40:         }
 41:     }
 42: }

This posting is about the Zipwhip API so I don’t want to get into the details of the cool looking bubble being generated from the Bubble() class, but it’s worth noting that we are using some of the features only available in later versions of Java that let you create transparent windows. We are doing some fade in/out tricks. We are manually adding mouse listeners to let you drag the window around. We are doing some sprite tricks with getSubImage() so we can repurpose some web PNGs as well. It was quite a lot of work to get that Bubble class setup, but it was well worth it because you can run this app non-stop and actually start using it to get real-time popups.

image

If you want to try some tricks with the Bubble to see if there are things you like better than the settings I picked, one of the things I recommend is to allow the window focus to be set. If you do this, you will be able to select the text from the message to copy it to the clipboard. Go into the code and change the this.setFocusableWindowState() line:

image

Notice that you can now select text, hit Ctrl-C to copy it to the clipboard, hit tab to choose Reply, hit the spacebar to choose the button, etc. The reason I didn’t pick allowing focus is that if a bubble pops in while you’re doing something like writing an email, I don’t think it’s a good idea to steal the focus. The user could consider that rude. However, it’s up to you how you want to setup your app.

image imageimage

Ok, thanks for reading the tutorial. Enjoy playing with your popup bubbles on your desktop when your friends text you on your phone.

Tutorial: Using the Zipwhip API in Java to Receive Text Messages via Sockets

I wrote a post about two weeks ago about using the Zipwhip API in Java to send a text message. This post will complete the picture by showing you how to receive a text message via the Zipwhip API. This is a little bit more involved than the previous post. Sending a message simply requires your code to zip off a message via the API which uses HTTP. Receiving, however, requires your code to bind to Zipwhip’s socket server and stay bound in to wait for the moment a text message actually arrives. This means your app must always be running to receive the text message.

As a background, there are a few ways to receive text messages from the Zipwhip network:

  1. Short polling
    You can query Zipwhip’s session/update API once every 10 seconds, or however often, and you will be told what signals you missed such as new messages, messages being marked as read, new contacts, etc. This means you are not getting messages in real-time, however this is a super simplistic method of getting updates or events. This is especially useful in Javascript apps that can’t create sockets.
  2. IP Socket-based push
    You can bind into a socket and get real-time signals from the Zipwhip network. This means that when a text message hits your phone, it will be immediately (within milliseconds) hitting your app. This requires a bit more complexity in your app, but it’s very worthwhile for the best experience.
  3. Post to a URL
    This method allows you to tell the Zipwhip network to post a copy of all of your signals to a URL. This is a very cool feature that Zipwhip gives you, but you must be running your own server at a public address for Zipwhip to be able to reach it. If you have to move your server around, you have to unregister the URL and reregister the URL you want posted to. This solution would not likely be used by consumer apps due to the configuration overhead.

This tutorial will just describe how to do IP Socket-based push because, in my opinion, it’s one of the coolest aspects of the Zipwhip network and the most powerful for the consumer or general developer. It lets you connect from any location at any time, or as many locations as you want, and get all of your signals delivered. So, if I want to run an app on my laptop, my desktop, my tablet, my Ubuntu box, and my Google TV, to get my text messages I can simply bind in via a socket and get copies of texts popping up everywhere I could ever want them. Because the Zipwhip network propagates things such as text messages being marked as read, especially back to my phone, I don’t have to mark a text as read on all of my computers like the way Skype makes me mark messages as read everywhere or the way Outlook makes me expire my envelope in the system tray on all of my machines.

Let’s get started. We need to fire up Eclipse where we’ll make a new Java project from scratch.

image_thumb

image_thumb1

Call the project ZipwhipAPISocketExample. (BTW, you can zoom in on the screenshots by clicking them.)

image

Go to the Libraries tab and make sure you have the ZipwhipAPI Jar added as well as the Log4j Jar. You can get these in your list by clicking on “Add External JARs…”. You will need to download the ZipwhipAPI from here if you don’t already have it. If you don’t have Log4j you can search for it or head over to the Apache Log4j project here. You will also need slf4j which is a small utility that abstracts Log4j to be used in different environments. These log dependencies are just a result of the ZipwhipAPI relying on them. They’re nice to use in your own projects too if you want to.

Zipwhip API Jar (820KB)

If you want to also download the dependencies you can grab the files below. Because these are popular files, you may already have these.

Log4j Jar (ZipwhipAPI.jar depends on this Jar)

Slf4j Jar (ZipwhipAPI.jar depends on this Jar)

image

Hit Finish on the dialog above. You should now have a new project called “ZipwhipAPISocketExample” and a clean Eclipse window with an empty project.

image

Create a new Java class so that we have a file to create our entry point in.

image

Create a package name like com.yourcompany.zwsocket. Of course swap in your name or your company’s name in place of what I typed. Then call the Java class ExampleMain or whatever name you prefer. Make sure to check off to create the “void main()” method so we have an entry point to our example.

image

Click Finish. You should see a new Java class in your editor.

image

Now, replace all of the text with the source code below. You can download it here, or you can cut & paste it.

 1: package com.yourcompany.zwsocket;
 2:
 3: import com.zipwhip.api.DefaultZipwhipSubscriptionClient;
 4: import com.zipwhip.api.HttpConnection;
 5: import com.zipwhip.api.signals.JsonSocketSignalClient;
 6: import com.zipwhip.signals.Signal;
 7: import com.zipwhip.signals.SignalObserver;
 8:
 9: public class ExampleMain {
 10:
 11:     public static void main(String[] args) {
 12:
 13:         // Create our connection object for the Zipwhip API.
 14:         // This object gets an authenticated HTTP connection to the 
 15:         // Zipwhip network via a login which gets you a sessionKey.
 16:         HttpConnection connection = new HttpConnection();
 17:
 18:         String mobileNumber = "3135551234";
 19:         String password = "mypassword";
 20:
 21:         System.out.println(String.format(
 22:             "Logging into Zipwhip network with mobile:%s, pass:%s",
 23:             mobileNumber, password));
 24:
 25:         try {
 26:             // This method will send a login request to the Zipwhip network and
 27:             // if successful you will get a sesionKey set in your connection object
 28:             // Watch out that you don't run "requestLogin()" too much because if you 
 29:             // create more than 50 sessionKeys within 1 day you will no longer be able 
 30:             // to get a key for 24 hours.
 31:             connection.requestLogin(mobileNumber, password);
 32:             // You can alternately just set your sessionKey from the last time you
 33:             // did a requestLogin(). Just store the sessionKey and use that. It
 34:             // is good for 30 days. Uncomment the line below and set your sessionKey.
 35:             // connection.setSessionKey("2e08a863-4c57-7af6-92c0-b0a120a16778:32423");
 36:
 37:             System.out.println("Got sessionKey:" + connection.getSessionKey());
 38:
 39:             // Set the version to "/" so we use non-signed web service calls
 40:             connection.apiVersion = "/";
 41:
 42:         } catch (Exception e) {
 43:             System.out.println("Failed to authenticate and get sessionKey to Zipwhip network.");
 44:             return;
 45:         }
 46:
 47:         // Zipwhip's HTTP web services client library. It uses the connection object for
 48:         // an authenticated transport.
 49:         DefaultZipwhipSubscriptionClient zipwhipSubscrClient;
 50:
 51:         // Zipwhip's Socket client.
 52:         JsonSocketSignalClient signalClient;
 53:
 54:         // This will create a high level client object that allows you to perform
 55:         // the full suite of tasks against the Zipwhip network such as send texts,
 56:         // create contacts, create groups, etc.
 57:         zipwhipSubscrClient = new DefaultZipwhipSubscriptionClient(connection);
 58:
 59:         // We will create our TCP/IP socket connection as well. You still need an 
 60:         // HTTP client because the Zipwhip API uses HTTP calls in concert with the 
 61:         // socket because the socket only pushes signals to you. If you want to
 62:         // send in a command, you do it over the DefaultZipwhipSubscriptionClient 
 63:         // HTTP client.
 64:         signalClient = new JsonSocketSignalClient(zipwhipSubscrClient);
 65:
 66:         // We need to add our callback methods to the socket client so that
 67:         // we can act upon incoming signals.
 68:         signalClient.addSignalObserver(new SignalObserver() {
 69:
 70:             // This method is called when a push socket signal is received
 71:             @Override
 72:             public void notifySignalReceived(Signal signal) {
 73:                 //log.debug("Signal received with uri " + signal.uri);
 74:                 System.out.println("Signal received with uri " + signal.uri);
 75:                 System.out.println("\t" + signal.rawContent);
 76:             }
 77:
 78:             @Override
 79:             public void notifySignalProviderEvent(boolean isConnected, String message, long frameCount) {
 80:                 System.out.println("Socket status. isConnected:" + isConnected +
 81:                     ", msg:" + message + ", frame:" + frameCount);
 82:             }
 83:         });
 84:
 85:         // Let's finally call the connect method to actually bind in over TCP/IP
 86:         signalClient.connect(connection.getSessionKey());
 87:
 88:         System.out.println("Done with socket test void main(), however socket thread will keep running listening for signals until you kill the process. Thanks for using Zipwhip.");
 89:
 90:     }
 91:
 92: }

After you get the source code into Eclipse, your window should look like below. Remember, you can zoom by clicking the image.

image

You need to set your mobile number and password before you can run the file. If you don’t have a login to the Zipwhip network, you can get one via your carrier if your carrier is a Zipwhip partner.

image

Go ahead and run the code. Just right-click anywhere in the code and choose Run As –> Java Application.

image

When the code runs, you should see output in the Console.

image

The output in the Console window will be similar to the text below. Don’t worry, I changed the phone numbers and other important details in the output like the sessionKey because those are sensitive items of data.

 1: Logging into Zipwhip network with mobile:3134447002, pass:****
 2: log4j:WARN No appenders could be found for logger (com.zipwhip.api.HttpConnection).
 3: log4j:WARN Please initialize the log4j system properly.
 4: Got sessionKey:64abcd3d-9ab6-45ed-aa78-91e4a262169:1
 5: Done with socket test void main(), however socket thread will keep running listening for signals until you kill the process. Thanks for using Zipwhip.
 6: Socket status. isConnected:false, msg:Connecting..., frame:0
 7: Socket status. isConnected:true, msg:Connection Established - Negotiating, frame:0
 8: Socket status. isConnected:true, msg:Connection Established, frame:1
 9: Socket status. isConnected:true, msg:Connection Established, frame:1
 10: Socket status. isConnected:true, msg:Connection Established, frame:2
 11: Socket status. isConnected:true, msg:Connection Established, frame:3
 12: Signal received with uri /signal/message/receive
 13:     {"id":"11218401","content":{"to":"","body":"hey, what's up duder?","bodySize":21,"visible":true,"transmissionState":{"enumType":"com.zipwhip.outgoing.TransmissionState","name":"QUEUED"},"type":"MO","metaDataId":0,"dtoParentId":1,"scheduledDate":null,"thread":"","carrier":"Tmo","deviceId":1,"openMarketMessageId":"","lastName":"Phone","class":"com.zipwhip.website.data.dto.Message","isParent":false,"lastUpdated":"2011-07-03T22:49:17-07:00","loc":"","messageConsoleLog":"Message created on Sun Jul 03 22:49:17 PDT 2011. Setting status code to 4 by default","deleted":false,"contactId":2782629,"uuid":"7e397eb-01ca-4f19-9166-b8d431c52cf","isInFinalState":false,"statusDesc":"","cc":"","subject":"","encoded":true,"expectDeliveryReceipt":false,"transferedToCarrierReceipt":null,"version":1,"statusCode":4,"id":118401,"fingerprint":"29091579","parentId":0,"phoneKey":"samSGH-T729","smartForwarded":false,"fromName":null,"isSelf":false,"firstName":"Sidekick","sourceAddress":"2063984565","deliveryReceipt":null,"dishedToOpenMarket":null,"errorState":false,"creatorId":0,"advertisement":null,"bcc":"","fwd":"","contactDeviceId":1,"smartForwardingCandidate":false,"destAddress":"3134447333","DCSId":"not parsed at the moment","latlong":"","new":false,"address":"ptn:/2063982344","dateCreated":"2011-07-03T22:49:27-07:00","UDH":"","carbonedMessageId":-1,"channel":" ","isRead":false},"scope":"device","reason":null,"tag":null,"event":"receive","class":"com.zipwhip.signals.Signal","uuid":"787cfec2-d094-4e6d-9b59-d8f55486d","type":"message","uri":"/signal/message/receive"}
 14: Socket status. isConnected:true, msg:Connection Established, frame:4
 15: Signal received with uri /signal/conversation/change
 16:     {"id":"8312","content":{"lastContactFirstName":"Sidekick","lastContactLastName":"Phone","lastContactDeviceId":0,"unreadCount":2,"bcc":"","lastUpdated":"2011-07-03T22:49:18-07:00","class":"com.zipwhip.website.data.dto.Conversation","deviceAddress":"device:/3134441232/0","lastNonDeletedMessageDate":"2011-07-03T22:49:27-07:00","deleted":false,"lastContactId":27629,"lastMessageDate":"2011-07-03T22:49:27-07:00","dtoParentId":1,"version":336,"lastContactMobileNumber":"2063981234","id":81312,"fingerprint":"2906579","new":false,"lastMessageBody":"hey, what's up duder?","address":"ptn:/2063983244","dateCreated":"2011-03-17T22:14:24-07:00","cc":"","deviceId":1},"scope":"device","reason":null,"tag":null,"event":"change","class":"com.zipwhip.signals.Signal","uuid":"787cc2-d094-4e6d-9b59-d8f56d","type":"conversation","uri":"/signal/conversation/change"}
 17:

Of course to get the output like that above, you have to send a text message to the phone that you logged in under. Once the text message hits that phone, assuming you have Zipwhip correctly installed on the phone, you will get a text message hitting your application instantaneously. You will see a signal called “/signal/message/receive” popping up in your console window.

That’s it. I hope you’ve enjoyed working with the Zipwhip API using sockets to receive real-time incoming text messages to your phone! Enjoy.

Tutorial: Using the Zipwhip API in Java

I’m new to the Zipwhip API so I figured I would share my experience sending out my first text message using the Zipwhip system. To follow along with this article you will need to get your hands on the Zipwhip API as a JAR file. It’s available here for download:

Zipwhip API Jar File (170KB) ZipwhipAPI.jar

Release Date: 6/18/2011

First, open up Eclipse and make a new Java project.

image

image

Call the project ZipwhipAPIExample. (BTW, you can zoom in on the screenshots by clicking them.)

image

You can leave the Source tab alone, however you will want to add the ZipwhipAPI Jar to the Libraries tab.

image

Make sure you download the ZipwhipAPI Jar from the link at the start of this article. Then, add it to the Libraries list by choosing “Add External JARs” and finding the file on your local computer. Make sure to add the Log4J Jar file as well since the ZipwhipAPI is dependant upon it. You will get compile errors if you don’t have Log4J somewhere in the build path.

image

You should now have a new project in your Package Explorer. You are ready to go.

image

Make a new Java class file by right-clicking the src folder and choosing “Class”.

image

Call your class “Example” after you’ve entered a package name of something like “com.yourcompany.zipwhipapi”. Make sure to also create a public static void main() method so you have an entry point for your Java app.

image

After you hit “Finish” you should see something similar to the following generated code.

image

Now, we’ll help you out with the code you should place into the Example.java file in order to send off your first text message using the Zipwhip API. Make sure you set your own mobile number and password in the code. If you don’t know what your password is, or you have never signed up for Zipwhip, you need to do it via your carrier website if your carrier supports Zipwhip. Zipwhip has contracts with most major U.S. carriers so please visit your carrier site or call your carrier to find out how to gain access.

Download source code below Example.java

 1: package com.yourcompany.zipwhipapi;
 2:
 3: import com.zipwhip.api.DefaultZipwhipClient;
 4: import com.zipwhip.api.HttpConnection;
 5: import com.zipwhip.api.ZipwhipClient;
 6:
 7: public class Example {
 8:
 9:     public static void main(String[] args) {
 10:
 11:         HttpConnection connection = new HttpConnection();
 12:
 13:         String mobileNumber = "3135551212";
 14:         String password = "mypassword";
 15:
 16:         try {
 17:             connection.requestLogin(mobileNumber, password);
 18:             ZipwhipClient zipwhipClient = new DefaultZipwhipClient(connection);
 19:
 20:             // Ok, go ahead and send your message
 21:             zipwhipClient.sendMessage("3134440002", "This is our first test SMS message.");
 22:
 23:             System.out.println("Done sending message");
 24:
 25:         } catch (Exception e) {
 26:             // TODO Auto-generated catch block
 27:             e.printStackTrace();
 28:         }
 29:     }
 30: }

This is what your Eclipse window should look like when all of the code is done and ready to go.

image

That’s it. Have fun sending off text messages via Zipwhip’s spectacularly simple API. If you want to get fancy, try to use some of the other methods in the API that let you create contacts, create groups, receive messages back, let you setup a URL to be posted to when an SMS comes in, etc.

To learn how to receive text messages via the Zipwhip API you can read my next posting.

Follow

Get every new post delivered to your Inbox.

Join 29 other followers