After soldering the circuit, I was really relieved that everything worked perfectly. The first thing I did was to test if I was able to identify the Propeller chip with the programming software and load a program in the EEPROM memory. Once done, I soldered the 3 MOSFET LED drivers and fired a 3.3V signal on their gates. The LEDs of the disco ball turned on as normal. I was a bit stressed with the last part, driving the 120V motor with the Triac. I tested the connections on the breadboard before doing the PCB, but there is always a chance that something is not correct. I closed my eyes when I plugged the 120V in... no explosion or blue fume, phew :)
Software
Originally, I wrote a command interpreter in Python that compile a sequence file and send the compiled sequence to the microcontroller on the PCB. The compiled sequence was stored in the unused part of the boot EEPROM and the main program purpose was to run the loaded sequence. The Python code is shown below :
# -*- coding: utf-8 -*-
"""
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
(at your option) 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/>.
"""
import sys, re
#Pyserial needed to connect to the printed circuit board
try:
import serial
except:
sys.exit(COM_LIBRARY_UNDEFINED)
#This constant value needs to fit with the one in the microcontroller, it won't work otherwise
MAX_COMMANDS = 1024
#Error codes, ease the debugging process and script interpreting troubleshooting
OK = 0 #Everything is OK
COM_UNDEFINED = 1 #COM Port undefined on the first line
COM_ERROR = 2 #Unable to connect to the board
SYNTAX_ERROR = 3 #Incorrect command syntax
COLOR_ERROR = 4 #bad color range (0-100)
DELAY_ERROR = 5 #bad delay range
LABEL_NOT_FOUND_ERROR = 6 #unable to find loop label
LABEL_DUPLICATE_ERROR = 10 #loop lable duplicate
SCRIPT_NOT_FOUND = 7 #wrong script path
COM_LIBRARY_UNDEFINED = 8 #pyserial not installed on the machine
ARGS_ERROR = 9 #need path as command line argument
#we need the disco light sequence script path as first argument
try:
script_path = sys.argv[1]
except:
sys.exit(ARGS_ERROR)
#read the script and put it in a list
try:
script_file = open(script_path)
raw_script = script_file.readlines()
script_file.close()
except:
sys.exit(SCRIPT_NOT_FOUND)
#only keep the important stuff (remove comments)
def strip(line):
if line[0] == '[':
index = line.find(']')
if index==-1:
return
else:
return line[1:index].lower()
#list comprehension to execute the above function on all the lines
cleaned_script = [strip(line) for line in raw_script if strip(line)]
print "Clean script:\n"+str(cleaned_script)
#find all the labels
labels = {}
index = -1 #Strip the COM Port line
for command in cleaned_script:
index += 1
if command[0] == '.':
labels[command[1:]] = index
#look for duplicates
unique_labels = tuple(set(labels))
if tuple(labels) != unique_labels:
sys.exit(LABEL_DUPLICATE_ERROR)
print "Labels:\n"+str(labels)
#regex patterns to parse commands
pat_com = re.compile("(com\d+)")
pat_col = re.compile("([rgb]):(\d+)")
pat_del = re.compile("\$(\d+)")
pat_jmp = re.compile("jmp\.(.+)?(\d+)")
pat_jmpINF = re.compile("jmp\.(.+)")
pat_lbl = re.compile("\.(.+)")
#get the serial COM Port and set the link speed
m = pat_com.match(cleaned_script[0])
if not m:
sys.exit(COM_UNDEFINED)
else:
com_port = m.group(1)
print "Port: "+str(com_port)
try:
BAUDRATE = 115200
ser = serial.Serial(com_port,BAUDRATE)
except:
sys.exit(COM_ERROR)
#The next lines parse all the commands and generate
# a list of parsed command in an easy to use way
commands = []
for command in cleaned_script[1:]:
m = pat_col.match(command)
if m:
color = m.group(1)
brightness = m.group(2)
if int(brightness) > 100:
print brightness
sys.exit(COLOR_ERROR)
commands.append(("COLOR",color,brightness))
continue
m = pat_del.match(command)
if m:
delay = m.group(1)
if int(delay) > 100000:
sys.exit(DELAY_ERROR)
delay = int(delay)
b0 = delay >> 0 & 0b11111111
b1 = delay >> 8 & 0b11111111
b2 = delay >> 16 & 0b11111111
b3 = delay >> 24 & 0b11111111
commands.append(("DELAY",b0,b1,b2,b3))
continue
m = pat_jmp.match(command)
if m:
label = m.group(1)
label = label[:-1] # Strip the question mark
times = m.group(2)
if not label in labels:
sys.exit(LABEL_NOT_FOUND_ERROR)
commands.append(("REPEAT",labels[label],times))
continue
m = pat_jmpINF.match(command)
if m:
label = m.group(1)
if not label in labels:
sys.exit(LABEL_NOT_FOUND_ERROR)
commands.append(("LOOP FOREVER",labels[label]))
continue
m = pat_lbl.match(command)
if m:
label = m.group(1)
commands.append(("LABEL",labels[label]))
continue
if command == "stop":
commands.append(("MOTOR",False))
continue
if command == "start":
commands.append(("MOTOR",True))
continue
sys.exit(SYNTAX_ERROR)
print "Commands:\n"+str(commands)
#Patch for the ser.write : we want to send
# the decimal number as a byte
commands_sent = 0
def write(num):
global commands_sent
commands_sent += 1
if num != 0:
pass#print num
ser.write(chr(int(num)))
#Special read : read the bytes as string and when there is a newline, it marks the end of string
#Return all the strings in a array (tuple) stop reading when null character received chr(0)
def readLineFeed():
data = []
buffer = []
while(True):
value = ser.read()
if value == "\n": #LineFeed
data.append("".join(buffer))
buffer = [] #Flush the buffer
elif value == chr(0):
break
else:
buffer.append(value)
return tuple(data)
def readINF():
while(True):
value = ser.read()
print value
#After the commands are parsed, we send
# serial commands to the controller PCB
for command in commands:
if command[0] == "COLOR":
if command[1] == 'r':
write(1)
write(command[2]) #brightness
elif command[1] == 'g':
write(2)
write(command[2]) #brightness
elif command[1] == 'b':
write(3)
write(command[2]) #brightness
elif command[0] == "MOTOR":
if command[1]:
write(4) #Start
else:
write(5) #Stop
elif command[0] == "DELAY":
write(6)
write(command[1]) #delay b0
write(command[2]) #delay b1
write(command[3]) #delay b2
write(command[4]) #delay b3
elif command[0] == "LABEL":
write(7)
write(command[1])
elif command[0] == "REPEAT":
write(8)
write(command[1]) #label
write(command[2]) #times
elif command[0] == "LOOP FOREVER":
write(9)
write(command[1]) #label
if commands_sent != MAX_COMMANDS:
print "Before looping: "+str(commands_sent)
to_max = MAX_COMMANDS-commands_sent
for index in range(to_max):
write('0')
print "After looping: "+str(commands_sent)
#Print the data echoed (debug)
for data in readLineFeed():
print(data)
#Everything should be fine if we get there
ser.close()
print "Script OK!\n"
sys.exit(OK)
In the end, my friend and I decided to control the board with a Raspberry Pi, by using a serial port on each side. The interface is really simple : you have commands to dim the 3 LEDs from 0 to 100% and a command to turn the motor on and off. The reason for the Raspberry Pi is that with an internet connected device, we can easily make a simple webpage to control the board with our mobile phones.
I love Python, so for the web framework on the Pi I decided to use Flask. For the HTML code, I used jQuery Mobile for a good page rendering on mobile devices. There is an excellent jQuery Mobile tutorial on the w3schools website. To make the page interactive, I used a bit of AJAX with jQuery. I really should do a little tutorial about all that in another article. It is really cool to be able to control the IO of the Pi with a webpage! Pictures of the webpage:
Here is a YouTube video showing the Disco Ball in action
Finally, I did not gave too much explanation about the software side of the project (webpage source and microcontroller programs)... I might just be a little bit lazy :) If someone is interested, I can send my source code. Thanks for reading!
Hey cool. Would like to see a video of the final result in action! Was also wondering if you needed to snub the triac - I've had problems with them sticking on in the past.
ReplyDeleteI did not need any snubbing components for the triac. I tested it first on a breadboard and it worked well. A snubber circuit is used when the load is strongly inductive, meaning that there is a phase angle between the voltage and current. In this situation, the triac could trigger itself because there is still voltage across it even if there is no more current. I'll try to update the post with a video :)
DeleteThis comment has been removed by the author.
ReplyDelete