Pixel hardware and software
This commit is contained in:
45
software/README
Normal file
45
software/README
Normal file
@@ -0,0 +1,45 @@
|
||||
Software for the pixel grid.
|
||||
|
||||
== pixel.py ==
|
||||
|
||||
This software will turn an 8x8 image or animated image (as read by the PIL
|
||||
library) into servo commands for a pixel grid.
|
||||
|
||||
Usage:
|
||||
pixel.py [-h] [-t] [-v] [-c] [-d delay] <files>
|
||||
|
||||
-h Display usage string
|
||||
-t Test mode
|
||||
This sets all pixels to 0, then to 255, then back to 0.
|
||||
-v View mode
|
||||
Outputs to the screen only. Does not try to open any devices.
|
||||
-c Calibration mode
|
||||
Sets all outputs to 1500us. Useful for assembling the motors.
|
||||
-d n Sets inter-image delay to n seconds
|
||||
|
||||
This command will display all files in sequence, with a specified delay in
|
||||
between each image. Animated formats will be played once through, frame by
|
||||
frame, with a fixed inter-frame delay.
|
||||
|
||||
|
||||
maestro.py is from:
|
||||
https://github.com/FRC4564/Maestro
|
||||
|
||||
With the addition of the ability to specify the baud rate when opening the
|
||||
serial port.
|
||||
|
||||
|
||||
== maketext.py ==
|
||||
|
||||
maketext.py is a helper program that will generate a scrolltext as an animated
|
||||
image file.
|
||||
|
||||
Usage:
|
||||
maketext.py text file.gif
|
||||
|
||||
Note that text is a single argument and should be quoted appropriately.
|
||||
|
||||
It requires ImageMagick to be installed. It will also look for a font in
|
||||
Beeb/Beeb.ttf. That font is available from:
|
||||
|
||||
https://fontstruct.com/fontstructions/show/63444/beeb
|
||||
169
software/maestro.py
Normal file
169
software/maestro.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import serial
|
||||
from sys import version_info
|
||||
|
||||
PY2 = version_info[0] == 2 #Running Python 2.x?
|
||||
|
||||
#
|
||||
#---------------------------
|
||||
# Maestro Servo Controller
|
||||
#---------------------------
|
||||
#
|
||||
# Support for the Pololu Maestro line of servo controllers
|
||||
#
|
||||
# Steven Jacobs -- Aug 2013
|
||||
# https://github.com/FRC4564/Maestro/
|
||||
#
|
||||
# These functions provide access to many of the Maestro's capabilities using the
|
||||
# Pololu serial protocol
|
||||
#
|
||||
class Controller:
|
||||
# When connected via USB, the Maestro creates two virtual serial ports
|
||||
# /dev/ttyACM0 for commands and /dev/ttyACM1 for communications.
|
||||
# Be sure the Maestro is configured for "USB Dual Port" serial mode.
|
||||
# "USB Chained Mode" may work as well, but hasn't been tested.
|
||||
#
|
||||
# Pololu protocol allows for multiple Maestros to be connected to a single
|
||||
# serial port. Each connected device is then indexed by number.
|
||||
# This device number defaults to 0x0C (or 12 in decimal), which this module
|
||||
# assumes. If two or more controllers are connected to different serial
|
||||
# ports, or you are using a Windows OS, you can provide the tty port. For
|
||||
# example, '/dev/ttyACM2' or for Windows, something like 'COM3'.
|
||||
def __init__(self,ttyStr='/dev/ttyACM0',device=0x0c,baud=115200):
|
||||
# Open the command port
|
||||
self.usb = serial.Serial(ttyStr,baudrate=baud)
|
||||
# Command lead-in and device number are sent for each Pololu serial command.
|
||||
self.PololuCmd = chr(0xaa) + chr(device)
|
||||
# Track target position for each servo. The function isMoving() will
|
||||
# use the Target vs Current servo position to determine if movement is
|
||||
# occuring. Upto 24 servos on a Maestro, (0-23). Targets start at 0.
|
||||
self.Targets = [0] * 24
|
||||
# Servo minimum and maximum targets can be restricted to protect components.
|
||||
self.Mins = [0] * 24
|
||||
self.Maxs = [0] * 24
|
||||
|
||||
# Cleanup by closing USB serial port
|
||||
def close(self):
|
||||
self.usb.close()
|
||||
|
||||
# Send a Pololu command out the serial port
|
||||
def sendCmd(self, cmd):
|
||||
cmdStr = self.PololuCmd + cmd
|
||||
if PY2:
|
||||
self.usb.write(cmdStr)
|
||||
else:
|
||||
self.usb.write(bytes(cmdStr,'latin-1'))
|
||||
|
||||
# Set channels min and max value range. Use this as a safety to protect
|
||||
# from accidentally moving outside known safe parameters. A setting of 0
|
||||
# allows unrestricted movement.
|
||||
#
|
||||
# ***Note that the Maestro itself is configured to limit the range of servo travel
|
||||
# which has precedence over these values. Use the Maestro Control Center to configure
|
||||
# ranges that are saved to the controller. Use setRange for software controllable ranges.
|
||||
def setRange(self, chan, min, max):
|
||||
self.Mins[chan] = min
|
||||
self.Maxs[chan] = max
|
||||
|
||||
# Return Minimum channel range value
|
||||
def getMin(self, chan):
|
||||
return self.Mins[chan]
|
||||
|
||||
# Return Maximum channel range value
|
||||
def getMax(self, chan):
|
||||
return self.Maxs[chan]
|
||||
|
||||
# Set channel to a specified target value. Servo will begin moving based
|
||||
# on Speed and Acceleration parameters previously set.
|
||||
# Target values will be constrained within Min and Max range, if set.
|
||||
# For servos, target represents the pulse width in of quarter-microseconds
|
||||
# Servo center is at 1500 microseconds, or 6000 quarter-microseconds
|
||||
# Typcially valid servo range is 3000 to 9000 quarter-microseconds
|
||||
# If channel is configured for digital output, values < 6000 = Low ouput
|
||||
def setTarget(self, chan, target):
|
||||
# if Min is defined and Target is below, force to Min
|
||||
if self.Mins[chan] > 0 and target < self.Mins[chan]:
|
||||
target = self.Mins[chan]
|
||||
# if Max is defined and Target is above, force to Max
|
||||
if self.Maxs[chan] > 0 and target > self.Maxs[chan]:
|
||||
target = self.Maxs[chan]
|
||||
#
|
||||
lsb = target & 0x7f #7 bits for least significant byte
|
||||
msb = (target >> 7) & 0x7f #shift 7 and take next 7 bits for msb
|
||||
cmd = chr(0x04) + chr(chan) + chr(lsb) + chr(msb)
|
||||
self.sendCmd(cmd)
|
||||
# Record Target value
|
||||
self.Targets[chan] = target
|
||||
|
||||
# Set speed of channel
|
||||
# Speed is measured as 0.25microseconds/10milliseconds
|
||||
# For the standard 1ms pulse width change to move a servo between extremes, a speed
|
||||
# of 1 will take 1 minute, and a speed of 60 would take 1 second.
|
||||
# Speed of 0 is unrestricted.
|
||||
def setSpeed(self, chan, speed):
|
||||
lsb = speed & 0x7f #7 bits for least significant byte
|
||||
msb = (speed >> 7) & 0x7f #shift 7 and take next 7 bits for msb
|
||||
cmd = chr(0x07) + chr(chan) + chr(lsb) + chr(msb)
|
||||
self.sendCmd(cmd)
|
||||
|
||||
# Set acceleration of channel
|
||||
# This provide soft starts and finishes when servo moves to target position.
|
||||
# Valid values are from 0 to 255. 0=unrestricted, 1 is slowest start.
|
||||
# A value of 1 will take the servo about 3s to move between 1ms to 2ms range.
|
||||
def setAccel(self, chan, accel):
|
||||
lsb = accel & 0x7f #7 bits for least significant byte
|
||||
msb = (accel >> 7) & 0x7f #shift 7 and take next 7 bits for msb
|
||||
cmd = chr(0x09) + chr(chan) + chr(lsb) + chr(msb)
|
||||
self.sendCmd(cmd)
|
||||
|
||||
# Get the current position of the device on the specified channel
|
||||
# The result is returned in a measure of quarter-microseconds, which mirrors
|
||||
# the Target parameter of setTarget.
|
||||
# This is not reading the true servo position, but the last target position sent
|
||||
# to the servo. If the Speed is set to below the top speed of the servo, then
|
||||
# the position result will align well with the acutal servo position, assuming
|
||||
# it is not stalled or slowed.
|
||||
def getPosition(self, chan):
|
||||
cmd = chr(0x10) + chr(chan)
|
||||
self.sendCmd(cmd)
|
||||
lsb = ord(self.usb.read())
|
||||
msb = ord(self.usb.read())
|
||||
return (msb << 8) + lsb
|
||||
|
||||
# Test to see if a servo has reached the set target position. This only provides
|
||||
# useful results if the Speed parameter is set slower than the maximum speed of
|
||||
# the servo. Servo range must be defined first using setRange. See setRange comment.
|
||||
#
|
||||
# ***Note if target position goes outside of Maestro's allowable range for the
|
||||
# channel, then the target can never be reached, so it will appear to always be
|
||||
# moving to the target.
|
||||
def isMoving(self, chan):
|
||||
if self.Targets[chan] > 0:
|
||||
if self.getPosition(chan) != self.Targets[chan]:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Have all servo outputs reached their targets? This is useful only if Speed and/or
|
||||
# Acceleration have been set on one or more of the channels. Returns True or False.
|
||||
# Not available with Micro Maestro.
|
||||
def getMovingState(self):
|
||||
cmd = chr(0x13)
|
||||
self.sendCmd(cmd)
|
||||
if self.usb.read() == chr(0):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# Run a Maestro Script subroutine in the currently active script. Scripts can
|
||||
# have multiple subroutines, which get numbered sequentially from 0 on up. Code your
|
||||
# Maestro subroutine to either infinitely loop, or just end (return is not valid).
|
||||
def runScriptSub(self, subNumber):
|
||||
cmd = chr(0x27) + chr(subNumber)
|
||||
# can pass a param with command 0x28
|
||||
# cmd = chr(0x28) + chr(subNumber) + chr(lsb) + chr(msb)
|
||||
self.sendCmd(cmd)
|
||||
|
||||
# Stop the current Maestro Script
|
||||
def stopScript(self):
|
||||
cmd = chr(0x24)
|
||||
self.sendCmd(cmd)
|
||||
|
||||
32
software/maketext.py
Executable file
32
software/maketext.py
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from PIL import Image, ImageFont, ImageDraw
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
directory = "output"
|
||||
text = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
|
||||
fnt = ImageFont.truetype('Beeb/Beeb.ttf', 8)
|
||||
|
||||
(pixels, height) = fnt.getsize(text)
|
||||
|
||||
# Lead-in plus lead-out
|
||||
frames = pixels + 8 + 8
|
||||
|
||||
image = Image.new('L', (frames, 8), 0)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
draw.text((8, 0), text, 255, fnt)
|
||||
|
||||
os.mkdir(directory)
|
||||
|
||||
for frame in range(0,frames):
|
||||
img = image.crop((frame, 0, frame+8, 8))
|
||||
img.save("%s/%04d.png" % (directory, frame))
|
||||
|
||||
subprocess.call(["convert", "%s/*.png" % directory, outfile])
|
||||
shutil.rmtree(directory)
|
||||
261
software/pixel.py
Executable file
261
software/pixel.py
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import time
|
||||
import maestro
|
||||
from PIL import Image
|
||||
from PIL import ImageSequence
|
||||
import sys, getopt
|
||||
|
||||
default_min = 7232
|
||||
default_max = 4032
|
||||
|
||||
def create_display(s1, s2, s3):
|
||||
pixels = [
|
||||
[
|
||||
[s3, 8, 7232, 4032],
|
||||
[s3, 9],
|
||||
[s3, 10],
|
||||
[s3, 11],
|
||||
[s3, 12],
|
||||
[s3, 13],
|
||||
[s3, 14],
|
||||
[s3, 15],
|
||||
], [
|
||||
[s3, 0],
|
||||
[s3, 1],
|
||||
[s3, 2],
|
||||
[s3, 3],
|
||||
[s3, 4],
|
||||
[s3, 5],
|
||||
[s3, 6],
|
||||
[s3, 7],
|
||||
], [
|
||||
[s2, 16],
|
||||
[s2, 17],
|
||||
[s2, 18],
|
||||
[s2, 19],
|
||||
[s2, 20],
|
||||
[s2, 21],
|
||||
[s2, 22],
|
||||
[s2, 23],
|
||||
], [
|
||||
[s2, 8],
|
||||
[s2, 9],
|
||||
[s2, 10],
|
||||
[s2, 11],
|
||||
[s2, 12],
|
||||
[s2, 13],
|
||||
[s2, 14],
|
||||
[s2, 15],
|
||||
], [
|
||||
[s2, 0],
|
||||
[s2, 1],
|
||||
[s2, 2],
|
||||
[s2, 3],
|
||||
[s2, 4],
|
||||
[s2, 5],
|
||||
[s2, 6],
|
||||
[s2, 7],
|
||||
], [
|
||||
[s1, 16],
|
||||
[s1, 17],
|
||||
[s1, 18],
|
||||
[s1, 19],
|
||||
[s1, 20],
|
||||
[s1, 21],
|
||||
[s1, 22],
|
||||
[s1, 23],
|
||||
], [
|
||||
[s1, 8],
|
||||
[s1, 9],
|
||||
[s1, 10],
|
||||
[s1, 11],
|
||||
[s1, 12],
|
||||
[s1, 13],
|
||||
[s1, 14],
|
||||
[s1, 15],
|
||||
], [
|
||||
[s1, 0],
|
||||
[s1, 1],
|
||||
[s1, 2],
|
||||
[s1, 3],
|
||||
[s1, 4],
|
||||
[s1, 5],
|
||||
[s1, 6],
|
||||
[s1, 7],
|
||||
]
|
||||
]
|
||||
return display(pixels)
|
||||
|
||||
class pixel:
|
||||
def __init__(self, servo, id, min=default_min, max=default_max):
|
||||
self.servo = servo
|
||||
self.id = id
|
||||
self.value = 0
|
||||
self.dirty = True
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.output()
|
||||
|
||||
def set_min(self, value):
|
||||
self.min = value
|
||||
|
||||
def set_max(self, value):
|
||||
self.max = value
|
||||
|
||||
def set(self, value):
|
||||
if value < 0:
|
||||
value = 0
|
||||
if value > 255:
|
||||
value = 255
|
||||
if self.value != value:
|
||||
self.dirty = True
|
||||
self.value = value
|
||||
|
||||
def get(self):
|
||||
return self.value
|
||||
|
||||
def output(self):
|
||||
if self.dirty:
|
||||
value = ((self.max-self.min) * self.value / 255) + self.min
|
||||
#print('('+repr(self.id)+') '+repr(value))
|
||||
if self.servo:
|
||||
self.servo.setTarget(self.id, int(round(value)))
|
||||
self.dirty = False
|
||||
|
||||
def cal(self):
|
||||
self.servo.setTarget(self.id, 1500*4)
|
||||
|
||||
class display:
|
||||
def __init__(self, params):
|
||||
self.p = []
|
||||
for param_row in params:
|
||||
row = []
|
||||
for args in param_row:
|
||||
row.append(pixel(*args))
|
||||
self.p.append(row)
|
||||
|
||||
def init(self, x, y, pixel):
|
||||
self.p[y][x] = pixel
|
||||
|
||||
def set(self, x, y, value):
|
||||
self.p[y][x].set(value)
|
||||
|
||||
def draw(self, im):
|
||||
for y, row in enumerate(self.p):
|
||||
for x, pixel in enumerate(row):
|
||||
pixel.set(im.getpixel((x, y)))
|
||||
|
||||
def output(self):
|
||||
for row in self.p:
|
||||
for pixel in row:
|
||||
if pixel != None:
|
||||
pixel.output()
|
||||
|
||||
def cal(self):
|
||||
for row in self.p:
|
||||
for pixel in row:
|
||||
if pixel != None:
|
||||
pixel.cal()
|
||||
|
||||
def print(self):
|
||||
for row in self.p:
|
||||
s = ""
|
||||
for pixel in row:
|
||||
if pixel != None:
|
||||
if pixel.get() > 127:
|
||||
s += "X"
|
||||
else:
|
||||
s += " "
|
||||
print(s)
|
||||
print("")
|
||||
|
||||
|
||||
def slideshow(d, images, delay):
|
||||
for image in images:
|
||||
show(d, image)
|
||||
time.sleep(delay)
|
||||
|
||||
def show(d, image):
|
||||
global viewmode
|
||||
print("Displaying " + repr(image))
|
||||
im = Image.open(image)
|
||||
print(repr(im.info))
|
||||
for frame in ImageSequence.Iterator(im):
|
||||
im8 = frame.convert("L")
|
||||
print(repr(frame.info))
|
||||
if viewmode:
|
||||
im8.show()
|
||||
else:
|
||||
d.draw(im8)
|
||||
d.output()
|
||||
d.print()
|
||||
time.sleep(0.2)
|
||||
|
||||
def test(d, value):
|
||||
print("Outputting "+repr(value))
|
||||
for y in range(0,8):
|
||||
for x in range(0,8):
|
||||
d.set(x, y, value)
|
||||
d.output()
|
||||
|
||||
baudrate = 115200
|
||||
|
||||
usage = "'pixel.py [-h] [-t] [-v] [-c] [-d delay] <files>'"
|
||||
|
||||
def main(argv):
|
||||
global viewmode
|
||||
|
||||
delay = 10
|
||||
testmode = False
|
||||
viewmode = False
|
||||
calmode = False
|
||||
try:
|
||||
opts, args = getopt.getopt(argv, "htvcd:", ["delay="])
|
||||
except getopt.GetoptError:
|
||||
print(usage)
|
||||
sys.exit(2)
|
||||
for opt, arg in opts:
|
||||
if opt in ('-h', "--help"):
|
||||
print(usage)
|
||||
sys.exit()
|
||||
elif opt in ("-d", "--delay"):
|
||||
delay = arg
|
||||
elif opt in ("-t", "--test"):
|
||||
testmode = True
|
||||
elif opt in ("-v", "--view"):
|
||||
viewmode = True
|
||||
elif opt in ("-c", "--cal"):
|
||||
calmode = True
|
||||
d = None
|
||||
if not viewmode:
|
||||
try:
|
||||
s1 = maestro.Controller(device=0x0c, baud=baudrate)
|
||||
s2 = maestro.Controller(device=0x0d, baud=baudrate)
|
||||
s3 = maestro.Controller(device=0x0e, baud=baudrate)
|
||||
except:
|
||||
s1 = None
|
||||
s2 = None
|
||||
s3 = None
|
||||
d = create_display(s1, s2, s3)
|
||||
if calmode:
|
||||
d.cal()
|
||||
elif testmode:
|
||||
test(d, 0)
|
||||
time.sleep(1)
|
||||
test(d, 255)
|
||||
time.sleep(1)
|
||||
test(d, 0)
|
||||
else:
|
||||
slideshow(d, args, delay)
|
||||
|
||||
if not viewmode:
|
||||
s3.close
|
||||
s2.close
|
||||
s1.close
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user