MyRaspberryAndMe

Tinkering with Raspberry (and other things)

AM03127 LED marquee + Arduino + Bluetooth = RaspberryPi remote control

2 Comments

After some days of soldering, testing and coding I am now able to control my LED marquee remotely via bluetooth from my Raspberry Pi. Using an Arduino Pro Mini (5V model) and the HC-05 module mentioned in my last blog post, I was able to mount all components inside the sign’s housing.

Some features of the bluetooth enabled display:

  • send messages to display (up to two messages with a length of 360 characters each are possible)
  • select display mode (message – time – off)
  • set time from and to Real Time Clock
  • set display intervals
  • increase/decrease speed of marquee

The sourcecode consists of the Arduino sketch to control the sign and a Python class that encapsulates the communication and message handling to the LED sign. As always, the sources will be available in my GitHub repository (direct link to sources). In this post I will describe the hardware and software developed to accomplish this. Some soldering skills may be required…

I am going to start with the hardware part as I needed to add some electronics between the LED sign and the Arduino. Some useful information about the internals of the LED sign is in my blog post about the “teardown“, so I won’t go into all the details here. The second half of this blog post will cover the software and an example.

The Hardware

As I had discovered, the LED sign uses an Atmel8 processor. So it was really easy to find the connecting points needed to communicate directly with the processor on the LED sign. Moreover, the complete circuit is running with 5 Volts, so the Arduino can be powered from the LED sign. Here is an annotated view of the PCB’s back showing where some connections need to be made:

led_pins_ctrl

The connectors marked as DWN, SEL, UP and ENTER are used by the remote control the sign came with. When a button is pressed, the remote control simply shorts the pin to ground. So that should be easy to simulate with an Arduino. The RXD pin is where the processor receives serial data. The communication settings are fixed to 9600 bps, 1 parity bit, 0 stop bits.

On first thought this should be really simple to implement. Connect to one of the digital pins of  the Arduino and set to LOW if a button press should be sent. Unfortunately things are not that simple. While doing some measurements I discovered that there is a 5V signal on all remote control pins (DWN, SEL, UP, ENTER) when the sign is powered. This means that these pins use the internal pull-up resistors of the Atmel8 processor to have a defined state. Pressing the remote will pull down the pin to ground and this change is detected. So directly connecting an Arduino pin that is set to OUTPUT will destroy the Arduino.

What is the use of the pull-up resistor? Have a look at the following image:

pullupsexplained

On the left is a switch that is connected to ground and to an (imaginary) microprocessor pin. When the switch is not pressed the microprocessor pin is in an undefined state (neither HIGH nor LOW). If the processor is reading the pin the result can not be predicted, you will get a HIGH or a LOW randomly.

On the right is the same circuit with a pull-up resistor. When the switch is open, the pin is connected to VCC through the resistor. This is usually a 10k Ohm or higher resistor to limit the current flowing. Remember Ohm’s Law: U = R * I. So with 5 Volts and 10000 Ohms the current is 0.5 mA. If this resistor is not soldered on the PCB but manufactured inside the processor it is called an “internal pull-up resistor”.

For us tinkering with the circuit this has some limitations. We can’t connect anything directly to the pins. We do need to use some kind of circuit that “isolates” the Arduinos’ pins from the 5V on the sign’s processor. This could be done with an opto-coupler or using a transistor as a switch. As I had some transistors lying around, I opted to use them. The following image shows the schematic of my “control-board” (click on the image for a bigger version):

The tranistors are NPN types, so when the voltage at the base gets higher than that of the emitter (that is: a current flows between base and emitter), the transistor closes the connection between collector and emitter. The collector is connected to the pin of the processor and as the emitter is connected to ground, the pin is set to LOW (ground).
The resistors are there to limit the current flowing. At the transistor’s base I used 10k (R1, R5 and R6), which is pretty much a default value to use. Between emitter and ground I first had a 10K resistor too, but that didn’t trigger the pin. So I went with 1k resistors (R2, R3 and R4) which means that the current flowing when shorting the pin to ground is limited to 5 mA.  We are simulating a button press here, so this current will flow for less than a second. This means no problems with heat.

On the left are two connectors. The top one goes to the LED sign and supplies 5V and Ground to the circuit. Remember that the TX-line of the Arduino is connected to the RX-line of the LED sign. The bottom connector goes to the HC-05 bluetooth module (this must be 5V tolerant!), again the TX and RX lines are crossed (TX-BT -> RX-Arduinio etc.).

I added an RTC module with the famous DS1307 chip on it to my board. This module needs to be connected to 5V and ground and has two connections called SDA and SCL. On the Arduino Uno and Mini Pro you need to connect: A4 -> SDA and A5 -> SCL, then use the wire library in your sketch. The prototype breadboard looked a little messy:

prototype_light

But the complete circuit actually fits onto a small piece of perfboard. I did the layout on a piece of paper (good cognitive training by the way). I added a jumper between the 5V from the sign and the power supply to the PCB. This makes it possible to reprogram the Arduino while connected to the sign. Just open the jumper first. Take a look at the PCB (click image for bigger version and a scan from my PCB-design notebook for fun):

The Arduino Software

The sources for this projects can be found on my GitHub repository. Here is an overview what the files do:

  • Ledcom.py: a Python class encapsulating all communication with the Arduino controller
  • ledcomtest.py: an example file showing the usage of the LEDcom class
  • ledcom2.ino: the actual sketch needed on the Arduino microcontroller. Tested with Arduino Pro Mini and Arduino Uno
  • ledutils.h/.cpp: a very lightweight utility class for communicating with the DS1307 RTC

I can’t post the complete code here, because the files are around 500 lines long. So I will just post some snippets here and discuss the general functionality and functions.

General functionality

On boot the time is read from the RTC and written to the display. Once booting and setup has finished, the message “Ready” is displayed. If no messages were sent to the LED display it will toggle its display every minute between the time and the “Ready” message.
As soon as messages are sent to the file, the messages will be displayed. Each message is displayed for a default time of 2 minutes, then the next message is displayed. Every 15 minutes the time is displayed for one minute.

The time intervals for refreshing message and time display can be set freely. Internally the sketch simply compares the minutes read from the RTC to its interval settings. If the difference between last refresh and the actual minutes is greater than the interval, the message is toggled.
If the display is set to display the time at frequent intervals, this simply happens if the actual minutes can be divided by the interval value without residue (actualMinutes%intervalMinutes == 0).

The Code

For a starter here is the list of supported commands:

[up]        increase speed of marquee (triggers UP-pin)
[down]      decrease speed of marquee (triggers DOWN-pin)
[dtime]     switch to display the time/date (triggers SEL-pin until time is displayed)
[dmsg]      switch to display saved messages (triggers SEL-pin)
[doff]      switch display off. There will be no refreshs until controller sends
             [dtime] or [dmsg] again
[wtime]     write time. Gets actual time from RTC and updates internal clock of LED sign
[notime]    do not display time between messages
[yestime]   enable time display between messages

The following commands need parameters:
[data]#                 announce new message data. # iis an integer between 1 and 2
                        message data must be sent immediately after this command
[sdefint]##             redefine message display interval to ## minutes
[stimediv]##            redefine divisor for time display to ## minutes 
                           (calculation is minutes%## == 0)
[srtc]################  set RTC time. Format of parameter string is (in time 
                                                           struct notation):
                          %y%w%m%d%H%M%S
                        Attention: DS1307 starts with Sunday=1 to Saturday=7 
                             so controller must recalculate
                        Day of Week (%w)

Let’s start with the Arduino sketch. If you did take a look at it, you may have noticed that I declared every single variable globally outside any functions. This is to prevent (or better minimize) memory fragmentation. All variables are declared, have their reserved space in memory and are used over and over again. If I had declared some of the variables inside a function they would be discarded on leaving the function. The memory they used would be free again. Here comes fragmentation, because the memory is freed but there is no guarantee that the available memory is “in one piece”.

Sounds weird, here’s an example: Suppose you have 100 bytes of memory. You declare two variables that need 30 bytes each. This leaves you 40 bytes in memory. Now one of this variables is discarded, giving you 30 bytes back. This does not necessarily mean that you have now one chunk of 70 bytes free, you rather have two blocks of memory free: 40+30 bytes. If you now need to declare a variable that is 50 bytes long there is a problem, because you have no block big enough to hold 50 bytes. That’s memory fragmentation.

The biggest problem for communicating with the display is to keep track of the current  mode or state. Has the command been completely sent or not? How many data bytes are sent with the command etc. This problem can be solved by doing something the computer science guys call a “Finite State Machine”. In short: you have a flag set to the current state and let your program act according to the state or set a new state. The Arduino sketch “knows” three states: COM_WAIT, COM_START and COM_DONE. Have a look at this diagram:

statemachine

The program starts in COM_WAIT state. In this state all incoming data is parsed as a command and (if valid) executed. If the command “[data]” is received, the state switches to COM_START. The “[data]” command is followed by exactly one byte holding the number of messages to be received. Actually only the values 1 and 2 are allowed, as the Arduino is at its limits here. A variable for counting the received messages is reset.
In COM_START all incoming data is interpreted as part of a text message. All bytes read until the end signal “[” is received are appended to the message buffer. If the “end message” signal is received the count of messages is increased by one. If the count reaches the announced number of messages, the state changes to COM_DONE. If the announced number of messages is not reached, incoming data will still be treated as message data.
In COM_DONE the interpretation of incoming data as message data ends. In fact, COM_DONE immediately sets the state back to COM_WAIT.

In COM_WAIT commands are handled by a number of if / if else statements. All commands end with “]”, so we read the serial port until “]” is transmitted. Then we start checking if the command is known. Here’s a snippet of the actual code doing this (it checks if the up-command has been received and if yes, triggers the UP pin):

void loop()
{
  // if there is data available begin parsing according to comState
  while(mySerial.available())
  {
    // this is the main parsing loop. in COM_WAIT the Arduino will
    // wait for commands to be sent
    // every command is checked in its own if-statement
    if(comState == COM_WAIT)
    {
      bytesRcvd = mySerial.readBytesUntil(']',message,9);
      message[bytesRcvd] = 0; // terminate char array
      if( strcmp(message,"[up") ==0 )
      {
        triggerPin(pinUP);
        message[0] = 0;
        signalReady();
        break;
      }
      else if(...

Because I am reusing all variables I am terminating the variable message with the “0” value. Message is in fact an array of chars (char[]) and “0” signals to the processor that the message content ends here. One weird thing you might have recognized is that I am comparing the received command without the closing bracket (“[command” instead of “[command]”). That’s simply because we are reading until “[” is received. So we never will get the closing bracket here.

Things get a little more weird with commands that are followed by parameters like the “[sdefint]”. The software for simplicity is not tolerant on missing parameters. “[sdefint]” must always be followed by two bytes. Here’s the code:

      else if(strcmp(message,"[sdefint") ==0 )
      {
        message[0] = 0;
        // next two chars are new time
        message[0] = mySerial.read();
        message[1] = mySerial.read();
        message[2] = 0;
        defaultInterval = atoi(message);
        message[0] = 0;
        signalReady();
        break;
      }

After the command has been received the code is reading exactly two more bytes, interprets them as an integer and sets the interval to this value. Then the message is “reset” by terminating the very first character with “0”.

Error handling

This is a tricky one, as the Arduino has limited memory and as it is really not meant to handle large char arrays I had to reduce error handling to the absolute limit. So in fact I am just ignoring anything unknown and send a “refused” message back. I do not handle the wrong number of parameters (see above [sdefint] example), as sending these messages is encapsulated by my Python class and this simply should not happen…

A little bit of error handling is implemented, however. There’s a total of three answers the Arduino is sending back: Ready, Error and Unknown. The Python class is handling the Ready-message, everything else needs to be handled by an implementation of one’s own software.

This is what gets sent back:

Condition ----- Message
  Ready   -----  [go]
  Error   -----  [error]
  Unknown -----  [refused]

The Python Class

The python code defines a class that encapsulates all functionality in methods that can be called from other Python programs. Furthermore it defines some “constants” to make the calls and the code more readable. E.g you do not need to send “<MA>” for fastest display speed, you just use the predefined value called “SPEED_FASTEST”. It also handles “Umlauts” which is important to us Germans. Here is the code of the Python file for testing communication:

#!/usr/bin/python
# -*- encoding: utf-8 -*-

"""
      example usage of Ledcom class for AM03127 LED signs
      Copyright (C) 2013  Thomas Henkel (mypiandme@gmail.com)
"""

from Ledcom import Ledcom as LED
import time

"""
    instantiate a LED object
    parameters:     the ID of the sign
                    port to communicate through
"""
sign = LED(1, "/dev/rfcomm0")
time.sleep(2)

"""
    append message to object
    no transmission takes place yet!
"""
sign.appendMessage("This is message one")
sign.appendMessage("This is the second message")

"""
    transmit one single message
    parameters:     speed - speed for message display. See constants in class
                    index - which message to send (1 or 2)
                    tries - number of transmission attempts before aborting
    return value:   True/False
"""
sign.transmitMessage(LED.SPEED_NORMAL, 1, 5)
time.sleep(2)

"""
    transmit all messages
    parameters:     speed
                    tries
"""
sign.transmitAllMessages(LED.SPEED_NORMAL, 5)
time.sleep(2)

# increase marquee speed
sign.increaseSpeed()
time.sleep(2)

# toggle to display time
sign.displayTime()
time.sleep(2)

# set RTC clock from system time
sign.setRTC()
time.sleep(2)

After every command sent I am adding a 2 second pause just because the LED display needs time to handle the commands. If multiple commands are sent to fast consecutively the sign will act erroneous.

The final thing

Here’s a picture how I assembled the whole thing. The display, after being reassembled, looks just like before.

inside_pcb

What next?

With the general communication working without any errors it is completely up to you what to do with the display. I created a private twitter account and, using the very cool Twitter Python library from Mike Verdone, I am reading the tweets I subscribed to and display them.

The idea is based on the “Druckuck” project from German magazine “Hardware Hacks” (Vol 4/2013), where they use the Twitter API and a thermal printer to have a Cuckoo Clock printing tweets.

Every 15 minutes the Twitter timeline is re-read and uploaded to the sign. My example runs as a daemon using the Daemon class “daemon.py” from Sander Marechal (http://www.jejik.com). Here is the sourcecode:

#!/usr/bin/python

"""
      ledtweeter - reads private twitter feed and displays them
      on the LED display.
      Copyright (C) 2013 mypiandme@gmail.com

      This code was inspired by and is using the twitter reading parts from
      the "Druckuck" project by German magazine "Hardware Hacks", 4/2013
         Links: http://www.heise.de/hardware-hacks/
                http://www.heise.de/hardware-hacks/inhalt/2013/4/22/

      This program is free software: you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
      the Free Software Foundation, either version 3 of the License, or
      any later version.

      This program is distributed in the hope that it will be useful,
      but WITHOUT ANY WARRANTY; without even the implied warranty of
      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      GNU General Public License for more details.

      You should have received a copy of the GNU General Public License
      along with this program.  If not, see <http://www.gnu.org/licenses/>.

      Packages/classes needed:
      daemon.py from Sander Marechal (http://www.jejik.com)
                http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/
      Minimalist Twitter API for Python (http://mike.verdone.ca/twitter/)
                this library is MIT licensed

      I created a Twitter account especially for the LED display and immediately set
      everything to private. You then need to register an application under
      https://dev.twitter.com/apps. Fill out the fields and on the page "OAuth Settings"
      click "Create my access token". This should give you all credentials you need.
"""

import sys, time, telnetlib, serial
import logging
from daemon import Daemon
from Ledcom import Ledcom as LED
from twitter import *
reload(sys)
sys.setdefaultencoding("utf-8")

class LedTweeter(Daemon):
   def run(self):
      try:
        # config logging
        logging.basicConfig(filename='/tmp/ledtweeter.log', filemode='w', level=logging.INFO, format='%(asctime)s %(message)s')
        logging.info('Starting leedtweeter')
        # initialize some things
        mySign = LED(1, "/dev/rfcomm0")
        logging.info("Started LED sign")
        mySign.setMessageSwitchInterval(2)
        time.sleep(2)
        mySign.setRTC()
        time.sleep(2)
        mySign.refreshTime()
        messageBuffer = []
        messageLine = ""
        charCount = 0
        maxCount = 380
        sleeptime = 600

        # initialize Twitter API
        CONSUMER_KEY="your_consumer_key_here"
        CONSUMER_SECRET="your_consumer_secret_here"
        OAUTH_TOKEN="your_access_token_here"
        OAUTH_SECRET="your_access_token_secret_here"
        t = Twitter(auth=OAuth(OAUTH_TOKEN, OAUTH_SECRET, CONSUMER_KEY, CONSUMER_SECRET))

        while True:
            sleeptime = 900 # everything was OK, next read in 15 minutes
            # do everything here

            home_timeline = t.statuses.home_timeline(count=40)
            tweetCount = 0
            bufferCount = 0
            # print "Length of Timeline is %i"%len(home_timeline)
            while len(messageLine) < maxCount:
                #print "Processing tweet %i"%tweetCount
                temp = home_timeline[tweetCount]['text']
                tweetCount += 1
                if not temp.startswith("RT"):
                    # not retweeted, so process
                    split = temp.split("http:/")
                    msg = split[0]
                    if len(messageLine)+len(msg) >= maxCount:
                        messageBuffer.append(messageLine)
                        messageLine = msg + "-- "
                        bufferCount += 1
                    else:
                        messageLine += msg + "-- "
                if len(messageLine) >= maxCount:
                    messageBuffer.append(messageLine)
                    messageLine = ""
                    bufferCount += 1
                if bufferCount > 1:
                    break
                if tweetCount > len(home_timeline)-1:
                    break
            logging.info("Processed %d Tweets"%tweetCount)

            mySign.appendMessage(messageBuffer[0])
            mySign.appendMessage(messageBuffer[1])
            logging.info("Appended messages")
            mySign.transmitAllMessages(LED.SPEED_FAST, 5)
            logging.info("Transmitted MEssages")
            # clear messageBuffer
            del messageBuffer[:]
            time.sleep(sleeptime)

      except serial.SerialException as se:
        logging.error("Communication Error: " + str(se))
        # there is no such serial port. Can't handle this. Exit.
        sys.exit("Serial Port Error: " + str(se))
      except Exception, e:
        logging.error("Other fatal Error: " + str(e))
        #print "Error: " +str(e)
        sys.exit("Fatal Error. Reason: " + str(e))

if __name__ == "__main__":
   daemon = LedTweeter('/tmp/ledtweeter.pid', '/dev/null', '/dev/null', '/dev/null')
   if len(sys.argv) == 2:
      if 'start' == sys.argv[1]:
         daemon.start()
      elif 'stop' == sys.argv[1]:
         daemon.stop()
      elif 'restart' == sys.argv[1]:
         daemon.restart()
      else:
         print "Unknown command"
         sys.exit(2)
      sys.exit(0)
   else:
      print "usage: %s start|stop|restart" % sys.argv[0]
      sys.exit(2)

And of course Bluetooth gives us the ability to, let’s say, control the display from an Android phone…

One word of warning though: All communication between the controller (Raspberry Pi) and the Arduino is not encrypted. So it would not be a good idea to update the display with, let’s say incoming mails or something like that. Of course, if you have no one around you in 50 meters radius you could do it. Bluetooth just doesn’t work over those distances with the HC-05 module.

Advertisements

2 thoughts on “AM03127 LED marquee + Arduino + Bluetooth = RaspberryPi remote control

  1. Pingback: Pi-Hicle final – motor-control and autonomous driving | MyRaspberryAndMe

  2. Hi there! Well done! and quite interesting too. Thanks for sharing your knowledge.
    I was also involved a with a led sign having to be controlled from and arduino and a RaspPi… and finally succeeded 😉
    Cheers,
    JuanMi