Browse Source

Pixel hardware and software

master
Gavan Fantom 6 years ago
commit
5e72f0a55d
  1. 39
      README
  2. 17530
      STL/Pixel-guide-4.stl
  3. 3138
      STL/Pixel-motor-holder-3.stl
  4. 677896
      STL/Pixel-screw-case-screw-3.stl
  5. 382986
      STL/Pixel-screw-composite-3.stl
  6. 5462
      STL/horn-holder-2.stl
  7. 5742
      STL/nail-driver-larger4.stl
  8. 5742
      STL/nail-receiver-larger4.stl
  9. 361
      scad/Pixel-screw.scad
  10. 37
      scad/README
  11. 58
      scad/horn-holder.scad
  12. 54
      scad/nail-test.scad
  13. 45
      software/README
  14. 169
      software/maestro.py
  15. 32
      software/maketext.py
  16. 261
      software/pixel.py

39
README

@ -0,0 +1,39 @@
To make the pixel, 3D print all 7 parts.
The pixel insert (screw) should be in one colour. The case and guide should
be in a contrasting colour. The small parts can be in any colour, and are
only visible from the back.
Print one pixel before trying to scale up. Some of these objects are difficult
to print and will require a well set up 3D printer.
I printed the large parts on an Original Prusa i3 MK2S and the small parts
on a Creality CR-10. The printing time was of the order of two weeks plus
failures and spares.
There is some redesigning that would be helpful before scaling up.
The pixel also requires 2 nails and a Tower Pro SG90 servo motor.
Driving the system requires a Pololu Mini Maestro 24-channel USB Servo
Controller for each 24 servos.
https://www.pololu.com/product/1356
Also required is a suitable 5 volt power supply. I used a Mean Well RS-150-5
for each servo controller. The worst case current of the servo is around an
amp, and while the average current will never reach anywhere near that, I sized
the power supplies accordingly so that they should never shut down due to
transients. The PCB trace on the servo controller is unlikely to be able to
handle that much current for long.
Note that the pixel grid will need external mechanical support. I used parts
from a couple of monitor stands, but any sturdy vertical pole on either side
should suffice. Black cable ties will then hold it steady.
Assembly should be done carefully. Most parts are push-fit, but that does
rely on 3D printing tolerances. You may need to slightly adapt some of the
sizes in the source files to your printer, or be careful about how much
you file or cut away the rim of the first layer.
Prototyping with a single pixel is essential.

17530
STL/Pixel-guide-4.stl

File diff suppressed because it is too large Load Diff

3138
STL/Pixel-motor-holder-3.stl

File diff suppressed because it is too large Load Diff

677896
STL/Pixel-screw-case-screw-3.stl

File diff suppressed because it is too large Load Diff

382986
STL/Pixel-screw-composite-3.stl

File diff suppressed because it is too large Load Diff

5462
STL/horn-holder-2.stl

File diff suppressed because it is too large Load Diff

5742
STL/nail-driver-larger4.stl

File diff suppressed because it is too large Load Diff

5742
STL/nail-receiver-larger4.stl

File diff suppressed because it is too large Load Diff

361
scad/Pixel-screw.scad

@ -0,0 +1,361 @@
include <MCAD/screw.scad>
screw_length = 12;
shaft1_length = 5;
shaft2_length = 5;
screw_inner_radius = 1;
shaft1_radius = 3;
shaft2_radius = 2;
screw_radius = 20;
screw_pitch = 60;
fins = 6;
taper = 1;
groove_fraction = 0.85;
groove_width = 2;
groove_depth = 0; //0.5;
groove_taper_ratio = 0.3;
screw_travel_in = 10;
screw_travel_out = 10;
leadscrew_pitch = 60;
leadscrew_length = 30;
leadscrew_width = 2.5;
leadscrew_inner = 5;
leadscrew_outer = 7.5;
leadscrew_endcap = 2;
leadscrew_spacer = 5; // aka cone
leadscrew_starts = 4;
leadscrew_start = screw_length+leadscrew_spacer+shaft1_length;
leadscrew_width_shrink = 1;
leadscrew_inner_shrink = 0.5;
leadscrew_outer_shrink = 1;
clearance = 1;
shaft_clearance = 0.8;
body_thickness = 1;
// Overlap between the guide and the body
guide_depth = 2;
//body_length = 26;
//body_length = 30; // shaft_length+screw_length;
body_length = screw_length + screw_travel_in + shaft1_length + leadscrew_spacer + guide_depth;
body_inner_radius = screw_radius + clearance;
body_outer_radius = body_inner_radius + body_thickness;
pipe_inner_radius = 1;
pipe_hole_radius = 4;
hole_taper = 1;
body_screw_length = screw_pitch/fins;
slot_width = 1;
slot_start = shaft2_length - 3;
insert_height_inner = 4;
insert_height_outer = 6;
insert_width = 2;
insert_clearance = 0.1;
insert_block_thickness = 2 + insert_clearance;
insert_length = 20; // body_length - guide_depth;
guide_lid_depth = 2;
guide_radius = 5/2;
guide_clearance = 0; // 0.1;
guide_extra = 6;
guide_middle_radius = 8;
guide_notch_width = 4;
guide_notch_depth = 2;
guide_start_position = body_length - guide_depth;
guide_rotation = 360 * (guide_start_position - leadscrew_start) / leadscrew_pitch;
support_width = 8;
support_height = 4;
//support_length = 40;
support_radius = 14;
support_position = support_radius + support_height/2 + 1;
support_top_width = 4;
support_top_length = 5;
support_clearance = 0; //0.1;
driver_inner_radius = guide_radius;
driver_outer_radius = driver_inner_radius+guide_extra;
driver_slot_clearance = 0.5;
driver_slot_length = 25;
driver_end_cap = 2; // Later this will be an attachment for a servo horn
motor_x = 12;
motor_y = 22.5;
motor_mount_depth = 5;
motor_x_thickness = 2;
motor_y_thickness = 2;
motor_screw_diameter = 1.5;
motor_screw_housing = 3;
motor_screw_position = 14;
motor_shaft_position = 6 - motor_y/2;
motor_to_horn_end = 13;
motor_mount_to_horn_end = motor_to_horn_end - motor_mount_depth;
support_spare = 1;
shaft2_overlap = 2;
nails_length = 10;
horn_shaft_length = 5;
horn_shaft_overlap = 2;
horn_thickness = 2.5;
support_length = leadscrew_length + shaft2_length - shaft2_overlap + nails_length + horn_shaft_length - horn_shaft_overlap + horn_thickness + motor_mount_to_horn_end + support_spare;
delta = 0.1;
debug_travel = 0;
render_screw = 1;
render_pipe_screw = 0;
render_guide = 0;
render_motor_mount = 0;
// These are obsolete
render_pipe = 0;
render_pipe_alternate = 0;
render_driver = 0;
translate_guide = render_pipe_screw?(guide_start_position):0;
translate_motor_mount = render_guide?(translate_guide+support_length+guide_depth+guide_lid_depth):0;
rotate([0, 0, -360*debug_travel/leadscrew_pitch])
translate([0, 0, debug_travel])
if (render_screw) {
// Visible screw
augers(screw_pitch, screw_length, screw_radius, screw_inner_radius, taper, groove_fraction, groove_width, groove_depth, 0, groove_taper_ratio, fins);
// shaft 1
translate([0, 0, screw_length])
translate([0, 0, -delta])
cylinder(h=shaft1_length+2*delta, r=shaft1_radius, $fn=36);
// shaft 2
translate([0, 0, screw_length+shaft1_length+leadscrew_spacer+leadscrew_length])
difference() {
translate([0, 0, -delta])
cylinder(h=shaft2_length+delta, r=shaft2_radius, $fn=36);
translate([-shaft2_radius*1.5, -slot_width/2, slot_start])
cube([shaft2_radius*3,slot_width, shaft2_length-slot_start+delta]);
}
// cone
translate([0, 0, screw_length+shaft1_length])
cylinder(h=leadscrew_spacer, r1=shaft1_radius, r2=sqrt(leadscrew_outer*leadscrew_outer + leadscrew_width*leadscrew_width), $fn=36);
// leadscrew
translate([0, 0, leadscrew_start])
difference() {
leadscrew(leadscrew_pitch, leadscrew_length, leadscrew_outer, leadscrew_width, leadscrew_inner, leadscrew_starts);
translate([0, 0, -leadscrew_pitch])
intersection() {
leadscrew(leadscrew_pitch, leadscrew_length+2*leadscrew_pitch, leadscrew_outer-leadscrew_outer_shrink, leadscrew_width-leadscrew_width_shrink, leadscrew_inner-leadscrew_inner_shrink, leadscrew_starts);
translate([0, 0, 0])
cylinder(h=leadscrew_length-leadscrew_endcap, r=leadscrew_outer*2);
}
}
}
/* width is doubled within this module */
module leadscrew(pitch, length, outer, width, inner, n=4)
{
augers(pitch, length, outer, width, 1, n=n);
cylinder(h=length, r=inner);
}
module augers(pitch, length, outside_radius, inner_radius, taper_ratio = 0.25, ,groove_fraction = 0, groove_width = 0, groove1_depth = 0, groove2_depth = 0, groove_taper_ratio = 1, n = 1)
{
for (i = [0:n-1]) {
rotate([0, 0, i*360/n]) auger_plus(pitch, length, outside_radius, inner_radius, taper_ratio, groove_fraction, groove_width, groove1_depth, groove2_depth, groove_taper_ratio);
}
}
module auger_plus(pitch, length, outside_radius, inner_radius, taper_ratio = 0.25, groove_fraction = 0, groove_width = 0, groove1_depth = 0, groove2_depth = 0, groove_taper_ratio = 1) {
groove_radius = groove_fraction*(outside_radius-inner_radius) + inner_radius;
groove_inner_radius = max(0, groove_radius - groove_width/2);
groove_outer_radius = min(outside_radius, groove_radius + groove_width/2);
groove_taper = (groove_outer_radius - groove_inner_radius) * (1- groove_taper_ratio) / 2;
groove_mid_inner_radius = groove_inner_radius + groove_taper;
groove_mid_outer_radius = groove_outer_radius - groove_taper;
union(){
helix(pitch, length)
polygon(points=[
[0,inner_radius],
[groove_inner_radius, inner_radius * (1 + groove_inner_radius * (taper_ratio - 1) / outside_radius)],
[groove_mid_inner_radius, inner_radius * (1 + groove_mid_inner_radius * (taper_ratio - 1) / outside_radius) + groove1_depth],
[groove_mid_outer_radius, inner_radius * (1 + groove_mid_outer_radius * (taper_ratio - 1) / outside_radius) + groove1_depth],
[groove_outer_radius, inner_radius * (1 + groove_outer_radius * (taper_ratio - 1) / outside_radius)],
[outside_radius,(inner_radius * taper_ratio)],
[outside_radius,(inner_radius * -1 * taper_ratio)],
[groove_outer_radius, -inner_radius * (1 + groove_outer_radius * (taper_ratio - 1) / outside_radius)],
[groove_mid_outer_radius, -inner_radius * (1 + groove_mid_outer_radius * (taper_ratio - 1) / outside_radius) - groove2_depth],
[groove_mid_inner_radius, -inner_radius * (1 + groove_mid_inner_radius * (taper_ratio - 1) / outside_radius) - groove2_depth],
[groove_inner_radius, -inner_radius * (1 + groove_inner_radius * (taper_ratio - 1) / outside_radius)],
[0,(-1 * inner_radius)]],
paths=[[0,1,2,3,4,5,6,7,8,9,10,11]]);
cylinder(h=length, r=inner_radius);
}
}
if (render_pipe) {
translate([render_screw?50:0, 0, 0])
difference() {
cylinder(h=body_length, r=body_outer_radius);
translate([0, 0, body_screw_length]) cylinder(h=body_length - body_screw_length + delta, r=body_inner_radius);
translate([0, 0, -delta]) augers(screw_pitch, body_screw_length+2*delta, screw_radius+clearance, screw_inner_radius+shaft_clearance, hole_taper, groove_fraction, groove_width, 0, -groove_depth, groove_taper_ratio, fins);
}
}
if (render_pipe_alternate) {
translate([render_screw?50:0, 0, 0])
scale([1.1,1.1,1])
difference() {
cylinder(h=body_length, r=body_outer_radius);
translate([0, 0, body_screw_length]) cylinder(h=body_length - body_screw_length + delta, r=body_inner_radius);
translate([0, 0, -delta]) augers(screw_pitch, body_screw_length+2*delta, screw_radius, screw_inner_radius, hole_taper, groove_fraction, groove_width, 0, -groove_depth, groove_taper_ratio, fins);
}
}
module matrix_insert(c = 0) {
translate([0, 0, -c])
linear_extrude(height=insert_length+2*c) {
polygon(points=[
[body_outer_radius, insert_height_inner/2+c],
[body_outer_radius+insert_width+2*c, insert_height_outer/2+c],
[body_outer_radius+insert_width+2*c, -insert_height_outer/2-c],
[body_outer_radius, -insert_height_inner/2-c]
]);
}
translate([body_outer_radius, 0, insert_length/2])
cube([2*body_thickness, insert_height_inner+2*c, insert_length], center=true);
}
module matrix_block() {
difference() {
translate([-body_inner_radius-insert_width-body_thickness-2*insert_clearance, -insert_height_outer/2-insert_block_thickness])
cube([body_thickness+insert_width+2*insert_clearance, insert_height_outer+2*insert_block_thickness, insert_length]);
translate([-2*body_outer_radius-insert_width-2*insert_clearance, 0, 0])
matrix_insert(insert_clearance);
}
}
if (render_pipe_screw) {
difference() {
rotate([0, 0, -180/fins])
augers(screw_pitch, body_screw_length, screw_radius+clearance, pipe_inner_radius, hole_taper, groove_fraction, groove_width, 0, -groove_depth, groove_taper_ratio, fins);
translate([0, 0, -delta])
cylinder(h=body_length + 2*delta, r=pipe_hole_radius);
translate([0, 0, -delta]) augers(screw_pitch, body_screw_length+2*delta, screw_radius+clearance, screw_inner_radius+shaft_clearance, hole_taper, groove_fraction, groove_width, 0, -groove_depth, groove_taper_ratio, fins);
}
difference() {
cylinder(h=body_length, r=body_outer_radius);
translate([0, 0, -delta])
cylinder(h=body_length + 2*delta, r=body_inner_radius);
translate([0, 0, body_length-guide_notch_depth/2])
cube([body_outer_radius*3, guide_notch_width+guide_clearance, guide_notch_depth+delta], center=true);
translate([0, 0, body_length-guide_notch_depth/2])
cube([guide_notch_width+guide_clearance, body_outer_radius*3, guide_notch_depth+delta], center=true);
}
matrix_insert();
rotate([0, 0, 90]) matrix_insert();
matrix_block();
rotate([0, 0, 90]) matrix_block();
}
if (render_guide) {
translate([0, 0, translate_guide]) {
difference() {
union() {
// Bottom plate
cylinder(h=guide_depth+delta, r=body_inner_radius-guide_clearance);
// Lugs
intersection() {
cylinder(h=guide_depth+delta, r=body_outer_radius);
union() {
translate([0, 0, guide_notch_depth/2])
cube([body_outer_radius*3, guide_notch_width, guide_notch_depth+delta], center=true);
translate([0, 0, guide_notch_depth/2])
cube([guide_notch_width, body_outer_radius*3, guide_notch_depth+delta], center=true);
}
}
// Top plate
translate([0, 0, guide_depth])
cylinder(h=guide_lid_depth, r=body_outer_radius);
// Screw casing
translate([0, 0, guide_depth + guide_lid_depth - delta])
cylinder(h=guide_extra + delta, r=guide_middle_radius);
}
// Hole
//translate([0, 0, -delta])
// cylinder(h=guide_depth+guide_lid_depth+guide_extra+2*delta, r=guide_radius, $fn=36);
scale([1.1, 1.1, 1])
translate([0, 0, -leadscrew_pitch])
rotate([0, 0, -guide_rotation])
leadscrew(leadscrew_pitch, leadscrew_length+leadscrew_pitch*2, leadscrew_outer, leadscrew_width, leadscrew_inner, leadscrew_starts);
}
// Support
translate([0, -support_position, guide_depth + guide_lid_depth]) {
translate([0, 0, support_length/2+delta/2])
cube([support_width, support_height, support_length+delta], center=true);
translate([0, 0, support_length+support_top_length/2-delta/2])
cube([support_top_width, support_height, support_top_length+delta], center=true);
}
}
}
if (render_driver) {
difference() {
cylinder(h = driver_slot_length + driver_end_cap, r=driver_outer_radius, $fn=36);
translate([0, 0, driver_end_cap])
cylinder(h = driver_slot_length + delta, r=driver_inner_radius, $fn=36);
}
cubex = (driver_inner_radius+driver_outer_radius);
cubey = slot_width - driver_slot_clearance;
translate([-cubex/2, -cubey/2, driver_end_cap-delta])
cube([cubex, cubey, driver_slot_length+delta]);
}
translate([0, 0, translate_motor_mount])
if (render_motor_mount) {
translate([0, -motor_shaft_position, motor_mount_depth/2]) {
difference() {
union() {
cube([motor_x + 2*motor_x_thickness, motor_y + 2*motor_y_thickness, motor_mount_depth], center=true);
translate([0, motor_screw_position, 0])
cylinder(h=motor_mount_depth, d=motor_screw_housing, $fn=36, center=true);
translate([0, -motor_screw_position, 0])
cylinder(h=motor_mount_depth, d=motor_screw_housing, $fn=36, center=true);
translate([0, (motor_screw_position + motor_y/2 + delta)/2, 0])
cube([motor_screw_housing, motor_screw_position - motor_y/2 - delta, motor_mount_depth], center=true);
translate([0, -(motor_screw_position + motor_y/2 + delta)/2, 0])
cube([motor_screw_housing, motor_screw_position - motor_y/2 - delta, motor_mount_depth], center=true);
support_extra = (support_width - support_top_width)/2;
cube_len = support_extra+support_position+support_height/2-motor_shaft_position-motor_y/2-motor_y_thickness+delta;
translate([0, -cube_len/2 - motor_y/2 - motor_y_thickness + delta/2, 0])
cube([support_width, cube_len, motor_mount_depth], center=true);
}
cube([motor_x, motor_y, motor_mount_depth+2*delta], center=true);
translate([0, motor_screw_position, 0])
cylinder(h=motor_mount_depth+2*delta, d=motor_screw_diameter, $fn=36, center=true);
translate([0, -motor_screw_position, 0])
cylinder(h=motor_mount_depth+2*delta, d=motor_screw_diameter, $fn=36, center=true);
translate([0, motor_shaft_position-support_position, 0])
cube([support_top_width+2*support_clearance, support_height+2*support_clearance, motor_mount_depth+2*delta], center=true);
}
}
}

37
scad/README

@ -0,0 +1,37 @@
== Pixel-screw.scad ==
This can generate four parts. Set each of the following to 1 in turn:
render_screw = 1;
render_pipe_screw = 0;
render_guide = 0;
render_motor_mount = 0;
Note that they can be combined to see how the model fits together.
The screw should be printed in a one colour (I chose yellow) and the pipe screw
and the guide should be printed in a contrasting colour (I chose black).
The colour of the motor mount and the other components isn't visible from the
front, and so only matters to the aesthetics from the back. (I chose blue)
== nail-test.scad ==
Set the following to 0 and to 1 to generate the two halves of this part.
nail = 0;
The difference is that one has a narrower diameter in the holes, so that a nail
can be banged into each hole. I used 40x1.6mm steel panel pins.
== horn-holder.scad ==
This fits the straight arm from a Tower Pro SG90 servo.
== Notes ==
The mechanism for attaching the nail system to the rest of the parts is
considerably weaker than the rest of the system, and is prone to breakage.
This really needs redesigning. A square hole/insert would probably make
more sense here.

58
scad/horn-holder.scad

@ -0,0 +1,58 @@
thickness = 2;
centre_d = 7;
middle_d = 5.5;
outer_d = 4;
length = 32;
$fn = 36;
rim = 2;
rim_thickness = 2;
clearance = 0.1;
shaft_radius = 2;
shaft_length = 5;
groove_width = 1;
groove_length = 3;
outer_centre = length/2 - outer_d/2;
screw_d = 2.4;
screw_depth = 1;
delta = 0.1;
module horn(thickness, length, centre_d, middle_d, outer_d) {
cylinder(h=thickness, d=centre_d);
translate([-outer_centre, 0, 0])
cylinder(h=thickness, d=outer_d);
translate([outer_centre, 0, 0])
cylinder(h=thickness, d=outer_d);
linear_extrude(height = thickness)
polygon(points=[
[0, -middle_d/2],
[-outer_centre, -outer_d/2],
[-outer_centre, outer_d/2],
[0, middle_d/2],
[outer_centre, outer_d/2],
[outer_centre, -outer_d/2]
]);
}
difference() {
horn(thickness+rim, length+rim, centre_d+rim, middle_d+rim, outer_d+rim);
translate([0, 0, -delta])
horn(thickness+delta, length + clearance, centre_d + clearance, middle_d + clearance, outer_d + clearance);
translate([0, 0, thickness-delta])
cylinder(h=screw_depth+delta, d=screw_d);
}
difference() {
translate([0, 0, thickness + rim - delta])
cylinder(h=delta + shaft_length, r = shaft_radius);
translate([-shaft_radius*3/2, -groove_width/2, thickness + rim + shaft_length - groove_length])
cube([shaft_radius*3, groove_width, groove_length+delta]);
}

54
scad/nail-test.scad

@ -0,0 +1,54 @@
delta = 0.1;
xo = 10;
height = 10;
bar_thickness = 2;
nail = 0;
d_inner_nail = 1.8;
d_outer_nail = 6;
d_inner_hole = 2.2;
d_outer_hole = 6;
d_inner = nail ? d_inner_nail : d_inner_hole;
d_outer = nail ? d_outer_nail : d_outer_hole;
d_centre = 7;
d_joint = 4.55;
w_joint = 0.7;
difference() {
union() {
translate([-xo, -d_outer/2, 0])
cube([xo*2, d_outer, bar_thickness]);
cylinder(h=bar_thickness, d=d_centre, $fn=36);
translate([-xo, 0, 0])
nail_outer();
translate([xo, 0, 0])
nail_outer();
}
translate([-xo, 0, 0])
nail_hole(d_inner);
translate([xo, 0, 0])
nail_hole(d_inner);
nail_hole(d_joint);
}
joint_bar();
module nail_outer() {
cylinder(h=height, d=d_outer, $fn=36);
}
module nail_hole(diameter) {
translate([0, 0, -delta])
cylinder(h=height+2*delta, d=diameter, $fn=36);
}
module joint_bar() {
translate([-d_joint, -w_joint/2, 0])
cube([d_joint*2, w_joint, bar_thickness]);
}

45
software/README

@ -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

@ -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

@ -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

@ -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:])
Loading…
Cancel
Save