This commit is contained in:
M McWilliams 2018-01-30 04:10:23 -05:00
commit 0a28c84ef9
29 changed files with 1095 additions and 1381 deletions

1
app/.gitignore vendored
View File

@ -1,4 +1,3 @@
data/cfg.json
node_modules/*
logs/*
data/transfer*.json

View File

@ -1,3 +1,6 @@
#!/bin/bash
electron-packager . mcopy --overwrite --asar=true --platform=linux --arch=x64 --icon=assets/icons/icon.png --prune=true --out=../dist
#package app
./node_modules/.bin/electron-packager . mcopy --overwrite --asar=true --platform=linux --arch=x64 --icon=assets/icons/icon.png --prune=true --out=../dist
#build a .deb installer
./node_modules/.bin/electron-installer-debian --src ../dist/mcopy-linux-x64/ --dest ../dist/installers/ --arch amd64

View File

@ -1,3 +1,9 @@
#!/bin/bash
electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/icon.icns --prune=true --out=../dist
./node_modules/.bin/electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/icon.icns --prune=true --out=../dist
#build dmg for mac install
mkdir ../dist/installers
./node_modules/.bin/electron-installer-dmg ../dist/mcopy-darwin-x64 mcopy --out=../dist/installers --icon=assets/icons/icon.icns Path to the icon file that will be the app icon in the DMG window.
# --icon-size=<px> How big to make the icon for the app in the DMG. [Default: `80`].
# --background=<path> Path to a PNG image to use as the background of the DMG.
#--overwrite Overwrite any existing DMG.

View File

@ -1,3 +1,5 @@
#!/bin/bash
electron-packager . mcopy --overwrite --asar=true --platform=win32 --arch=x64 --icon=assets/icons/win/icon.ico --prune=true --out=../dist --version-string.CompanyName="sixteenmillimeter.com" --version-string.FileDescription="Open Source Optical Printer Platform" --version-string.ProductName="mcopy"
./node_modules/.bin/electron-packager . mcopy --overwrite --asar=true --platform=win32 --arch=x64 --icon=assets/icons/win/icon.ico --prune=true --out=../dist --version-string.CompanyName="sixteenmillimeter.com" --version-string.FileDescription="Open Source Optical Printer Platform" --version-string.ProductName="mcopy"
mkdir ../dist/installers

View File

@ -545,6 +545,7 @@ button:focus {
.cmd {
width: 240px;
text-align: center;
margin: 0 auto;
}
.cmd i {
float: left;
@ -589,6 +590,58 @@ button:focus {
::-webkit-scrollbar-thumb:window-inactive {
background: rgba(0, 0, 0, 0.05);
}
#settings > div {
width: 300px;
margin: 0 auto;
}
#settings > div > div {
width: 360px;
}
#settings input[type=text],
#settings select {
display: block;
border-radius: 5px;
border: 2px solid #fff;
text-align: center;
background: transparent;
color: #fff;
padding: 8px 0;
font-size: 12px;
font-weight: 400;
display: inline-block;
padding: 6px 12px;
font-size: 21px;
min-width: 300px;
}
#settings input[type=text] span,
#settings select span {
display: block;
font-size: 16px;
font-weight: 200;
}
#settings input[type=text]:active,
#settings select:active,
#settings input[type=text] .active,
#settings select .active {
background: #fff;
color: #272b30;
outline: none;
}
#settings input[type=text]:focus,
#settings select:focus {
outline: none;
}
#settings button {
margin-top: -1px;
float: right;
}
#settings input[type=radio] {
float: right;
margin-right: 20px;
}
#settings .spacer {
margin-top: 10px;
}
#log {
position: fixed;
width: 100%;

46
app/data/cfg.json Normal file
View File

@ -0,0 +1,46 @@
{
"version" : "2.0.0",
"ext_port" : 1111,
"arduino" : {
"baud" : 57600,
"board" : "uno",
"serialDelay" : 20,
"sequenceDelay" : 100,
"cam" : {
"time" : 750,
"delay" : 50,
"momentary" : 300
},
"proj" : {
"time" : 1300,
"delay" : 50,
"momentary" : 300
},
"black" : {
"before" : 250,
"after" : 250
},
"cmd" : {
"debug" : "d",
"connect": "i",
"light" : "l",
"camera" : "c",
"projector" : "p",
"black" : "b",
"cam_forward" : "e",
"cam_backward" : "f",
"proj_forward" : "g",
"proj_backward" : "h",
"proj_identifier" : "j",
"cam_identifier" : "k",
"mcopy_identifier" : "m",
"cam_timed" : "n",
"proj_identifier" : "j",
"cam_identifier" : "k",
"light_identifier" : "o",
"proj_light_identifier" : "q",
"proj_cam_light_identifier" : "r",
"proj_cam_identifier" : "s"
}
}
}

View File

@ -11,7 +11,7 @@
<link href="./css/monokai.css" rel="stylesheet">
<link href="./css/app.css" rel="stylesheet">
</head>
<body onload="init();">
<body onload="init();" style="background:#272b30;">
<nav id="toolbar"></nav>
<div id="screens">
<div id="sequencer" class="screen" style="display: block;">
@ -213,24 +213,38 @@
</footer>
</div>
<div id="settings" class="screen">
<h4>Devices</h4>
<select id="devices">
</select>
<button title="Refresh devices"><i class="fa fa-refresh"></i></button>
<h4>Projector</h4>
<select id="projector_device">
</select>
<h4>Camera</h4>
<select id="camera_device">
</select>
<input type="text" id="intval" name="intval" placeholder="INTVAL3 URL"/>
<h4>Light</h4>
<select id="light_device">
</select>
<div>
<div>
<h4>Devices</h4>
<select id="devices">
<option>Not Set</option>
</select>
<button title="Refresh devices"><i class="fa fa-refresh"></i></button>
</div>
<div>
<h4>Projector</h4>
<select id="projector_device">
<option>Not Set</option>
</select>
</div>
<div>
<h4>Camera</h4>
<select id="camera_device">
<option>Not Set</option>
</select>
<input type="radio" name="camera_type" value="arduino" checked="checked" />
</div>
<div class="spacer">
<input type="text" id="intval" name="intval" placeholder="INTVAL3 URL"/>
<input type="radio" name="camera_type" value="intval" />
</div>
<div>
<h4>Light</h4>
<select id="light_device">
<option>Not Set</option>
</select>
</div>
</div>
</div>
</div>
<div id="overlay" onclick="gui.overlay(false);gui.spinner(false);"></div>

View File

@ -3,4 +3,3 @@
npm install -g gulp electron
npm install
./node_modules/.bin/electron-rebuild
mkdir logs

View File

@ -6,6 +6,7 @@
@import "./seq.less";
@import "./cmd.less";
@import "./scroll.less";
@import "./settings.less";
#log{
position: fixed;

View File

@ -8,6 +8,7 @@
.cmd{
width: 240px;
text-align: center;
margin: 0 auto;
i{
float: left;
margin-right: 5px;

27
app/less/settings.less Normal file
View File

@ -0,0 +1,27 @@
#settings{
> div{
width: 300px;
margin: 0 auto;
}
> div > div{
width: 360px;
}
input[type=text], select{
.button();
display: inline-block;
padding: 6px 12px;
font-size: 21px;
min-width: 300px;
}
button{
margin-top: -1px;
float: right;
}
input[type=radio]{
float: right;
margin-right: 20px;
}
.spacer{
margin-top: 10px;
}
}

View File

@ -1,7 +1,11 @@
'use strict'
const SerialPort = require('serialport')
const Readline = SerialPort.parsers.Readline
const exec = require('child_process').exec
const parser = new Readline('')
const newlineRe = new RegExp('\n', 'g')
const returnRe = new RegExp('\r', 'g')
let eventEmitter
const mcopy = {}
@ -109,8 +113,8 @@ mcopy.arduino.connect = function (serial, device, confirm, callback) {
mcopy.arduino.alias[serial] = device;
mcopy.arduino.serial[device] = new SerialPort(mcopy.arduino.path[serial], {
autoOpen : false,
baudrate: mcopy.cfg.arduino.baud,
parser: SerialPort.parsers.readline("\n")
baudRate: mcopy.cfg.arduino.baud,
parser: parser
});
mcopy.arduino.serial[device].open(error => {
if (error) {
@ -120,13 +124,15 @@ mcopy.arduino.connect = function (serial, device, confirm, callback) {
console.log(`Opened connection with ${mcopy.arduino.path[serial]} as ${serial}`);
if (!confirm) {
mcopy.arduino.serial[device].on('data', data => {
data = data.replace('\r', '')
mcopy.arduino.end(data)
let d = data.toString('utf8')
d = d.replace(newlineRe, '').replace(returnRe, '')
mcopy.arduino.end(d)
})
} else {
mcopy.arduino.serial[device].on('data', data => {
data = data.replace('\r', '')
mcopy.arduino.confirmEnd(data)
let d = data.toString('utf8')
d = d.replace(newlineRe, '').replace(returnRe, '')
mcopy.arduino.confirmEnd(d)
})
}
if (callback) {
@ -138,6 +144,7 @@ mcopy.arduino.connect = function (serial, device, confirm, callback) {
mcopy.arduino.confirmExec = {};
mcopy.arduino.confirmEnd = function (data) {
//console.dir(data)
if (data === mcopy.cfg.arduino.cmd.connect
|| data === mcopy.cfg.arduino.cmd.proj_identifier
|| data === mcopy.cfg.arduino.cmd.cam_identifier

View File

@ -1,9 +1,12 @@
'use strict'
const os = require('os')
const path = require('os')
const sqlite3 = require('sqlite3')
const squel = require('squel')
const PATH = path.join(os.homedir(), '.mcopy/mcopy.db')
const actionTable = `CREATE TABLE IF NOT EXISTS actions (
time INTEGER PRIMARY KEY,
type TEXT,
@ -11,6 +14,21 @@ const actionTable = `CREATE TABLE IF NOT EXISTS actions (
counter INTEGER,
light TEXT,
dir INTEGER,
sequence INTEGER
sequence INTEGER,
device TEXT
);`
);`
var checkDir = function () {
const dir = path.join(os.homedir(), '.mcopy/')
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
}
class DB {
constructor () {
}
}
module.exports = DB

View File

@ -11,7 +11,7 @@ class Intval {
const timeStart = +new Date()
const baseUrl = devices[device]
const url = `${baseUrl}/frame`
console.log(url)
//console.log(url)
req(url, (err, res, body) => {
let ms = (+new Date()) - timeStart
if (err) {
@ -24,7 +24,7 @@ class Intval {
const timeStart = +new Date()
const baseUrl = devices[device]
const url = `${baseUrl}/dir?dir=${dir}`
console.log(url)
//console.log(url)
req(url, (err, res, body) => {
let ms = (+new Date()) - timeStart
if (err) {
@ -38,7 +38,7 @@ class Intval {
const timeStart = +new Date()
const baseUrl = devices[device]
const url = `${baseUrl}/exposure?exposure=${exposure}`
console.log(url)
//console.log(url)
req(url, (err, res, body) => {
let ms = (+new Date()) - timeStart
if (err) {

View File

@ -9,63 +9,67 @@ let proj
let light
class Server {
constructor (mcopy) {
constructor (camera, projector, light) {
restify = require('restify')
os = require('os')
app = express()
app = restify.createServer({
name: 'mcopy-server',
version: '2.0.0'
})
this.ip = this.getIp()
this.getIp()
app.get('/', function (req, res) {
/*app.get('/', function (req, res) {
mcopy.mobile.log('Device connected');
res.send(fs.readFileSync('tmpl/mcopy_index.html', 'utf8'));
})
app.get('/js/mcopy_mobile.js', function (req, res) {
res.send(fs.readFileSync('js/mcopy_mobile.js', 'utf8'));
});
app.get('/js/mcopy_mobile.js', function (req, res) {
res.send(fs.readFileSync('js/mcopy_mobile.js', 'utf8'));
});
app.get('/js/jquery.js', function (req, res) {
res.send(fs.readFileSync('js/jquery.js', 'utf8'));
});
app.get('/cmd/:cmd', function (req, res) {
var cmd,
success = function (res) {
var obj = {
success: true,
cmd : cmd,
cam : mcopy.state.camera,
proj : mcopy.state.projector
app.get('/js/jquery.js', function (req, res) {
res.send(fs.readFileSync('js/jquery.js', 'utf8'));
});
app.get('/cmd/:cmd', function (req, res) {
var cmd,
success = function (res) {
var obj = {
success: true,
cmd : cmd,
cam : mcopy.state.camera,
proj : mcopy.state.projector
}
res.json(obj);
};
if (typeof req.params.cmd !== 'undefined') {
mcopy.log('Receiving command from mobile: ' + req.params.cmd);
cmd = req.params.cmd;
if (cmd === 'CF'){
mcopy.cmd.cam_forward(success);
} else if (cmd === 'CB') {
mcopy.cmd.cam_backward(success);
} else if (cmd === 'PF') {
mcopy.cmd.proj_forward(success);
} else if (cmd === 'PB') {
mcopy.cmd.proj_backward(success);
} else {
mcopy.mobile.fail(res, 'Command ' + cmd + ' not found');
}
res.json(obj);
};
if (typeof req.params.cmd !== 'undefined') {
mcopy.log('Receiving command from mobile: ' + req.params.cmd);
cmd = req.params.cmd;
if (cmd === 'CF'){
mcopy.cmd.cam_forward(success);
} else if (cmd === 'CB') {
mcopy.cmd.cam_backward(success);
} else if (cmd === 'PF') {
mcopy.cmd.proj_forward(success);
} else if (cmd === 'PB') {
mcopy.cmd.proj_backward(success);
} else {
mcopy.mobile.fail(res, 'Command ' + cmd + ' not found');
mcopy.mobile.fail(res, 'No command provided');
}
} else {
mcopy.mobile.fail(res, 'No command provided');
}
});
app.get('/state', function (req, res) {
res.json({
cam: mcopy.state.camera,
proj: mcopy.state.projector
});
});
var http = require('http');
http.createServer(app).listen(mcopy.cfg.ext_port);
app.get('/state', function (req, res) {
res.json({
cam: mcopy.state.camera,
proj: mcopy.state.projector
});
});*/
}
end () {
app.close()
app = null
}
}

View File

@ -1,5 +1,3 @@
'use strict'
const os = require('os');
const path = require('path');
const fs = require('fs');
@ -11,42 +9,61 @@ settings.state = {
port : 1111,
enabled : true
},
devices : [],
camera : {},
projector : {},
light : {}
}
settings.checkDir = function () {
const dir = path.join(os.homedir(), '.mcopy/')
'use strict'
const dir = path.join(os.homedir(), '.mcopy/');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
}
settings.save = function () {
'use strict'
const str = JSON.stringify(settings.state, null, '\t');
settings.checkDir()
settings.checkDir();
fs.writeFile(settings.file, str, 'utf8', (err) => {
if (err) console.error(err);
})
}
settings.update = function (key, val) {
settings.state[key] = val
'use strict'
settings.state[key] = val;
}
settings.get = function (key) {
return settings.state[key]
'use strict'
return settings.state[key];
}
settings.all = function () {
'use strict'
return settings.state;
}
settings.restore = function () {
let str
settings.checkDir()
'use strict'
let str;
settings.checkDir();
if (fs.existsSync(settings.file)) {
str = fs.readFileSync(settings.file, 'utf8')
settings.state = JSON.parse(str)
settings.state = JSON.parse(str);
} else {
settings.save()
settings.save();
}
}
settings.reset = function () {
'use strict'
if (fs.existsSync(settings.file)) {
fs.unlinkSync(settings.file);
}
settings.restore();
};
module.exports = settings

View File

@ -3,9 +3,11 @@ var cmd = {};
cmd.proj_forward = function (callback) {
'use strict';
var res = function (ms) {
$('#cmd_proj_forward').removeClass('active');
gui.updateState();
if (callback) { callback(ms); }
};
$('#cmd_proj_forward').addClass('active');
if (!mcopy.state.projector.direction) {
proj.set(true, function (ms) {
setTimeout(function () {
@ -21,9 +23,11 @@ cmd.proj_forward = function (callback) {
cmd.proj_backward = function (callback) {
'use strict';
var res = function (ms) {
$('#cmd_proj_backward').removeClass('active');
gui.updateState();
if (callback) { callback(ms); }
};
$('#cmd_proj_backward').addClass('active');
if (mcopy.state.projector.direction) {
proj.set(false, function (ms) {
setTimeout(function () {
@ -38,15 +42,18 @@ cmd.proj_backward = function (callback) {
};
cmd.cam_forward = function (rgb, callback) {
'use strict';
var off = [0, 0, 0];
var res = function (ms) {
gui.updateState();
setTimeout(function () {
light.display([0,0,0]);
light.set([0, 0, 0], function () {
light.display(off);
light.set(off, function () {
$('#cmd_cam_forward').removeClass('active');
if (callback) { callback(ms); }
});
}, mcopy.cfg.arduino.serialDelay);
};
$('#cmd_cam_forward').addClass('active');
if (!mcopy.state.camera.direction) {
cam.set(true, function () {
setTimeout( function () {
@ -70,17 +77,43 @@ cmd.cam_forward = function (rgb, callback) {
cmd.black_forward = function (callback) {
'use strict';
var off = [0, 0, 0];
cmd.cam_forward(off, callback);
var res = function (ms) {
$('#cmd_black_forward').removeClass('active');
gui.updateState();
};
$('#cmd_black_forward').addClass('active');
if (!mcopy.state.camera.direction) {
cam.set(true, function () {
setTimeout( function () {
light.display(off);
light.set(off, function () {
setTimeout( function () {
cam.move(res);
}, mcopy.cfg.arduino.serialDelay);
});
}, mcopy.cfg.arduino.serialDelay);
});
} else {
light.display(off);
light.set(off, function () {
setTimeout(function () {
cam.move(res);
}, mcopy.cfg.arduino.serialDelay);
});
}
};
cmd.cam_backward = function (rgb, callback) {
'use strict';
var off = [0, 0, 0];
var res = function (ms) {
gui.updateState();
light.display([0,0,0]);
light.set([0, 0, 0], function () {
light.display(off);
light.set(off, function () {
$('#cmd_cam_backward').removeClass('active');
if (callback) { callback(ms); }
});
};
$('#cmd_cam_backward').addClass('active');
if (mcopy.state.camera.direction) {
cam.set(false, function () {
setTimeout(function () {
@ -102,7 +135,28 @@ cmd.cam_backward = function (rgb, callback) {
cmd.black_backward = function (callback) {
'use strict';
var off = [0, 0, 0];
cmd.cam_backward(off, callback);
var res = function (ms) {
$('#cmd_black_backward').removeClass('active');
gui.updateState();
};
$('#cmd_black_backward').addClass('active');
if (mcopy.state.camera.direction) {
cam.set(false, function () {
setTimeout(function () {
light.display(off);
light.set(off, function () {
cam.move(res);
});
}, mcopy.cfg.arduino.serialDelay);
});
} else {
setTimeout(function () {
light.display(off);
light.set(off, function () {
cam.move(res);
});
}, mcopy.cfg.arduino.serialDelay);
}
};
module.exports = cmd;

View File

@ -8,22 +8,22 @@ devices.init = function () {
};
devices.listen = function () {
'use strict';
let opt
ipcRenderer.on('ready', function (event, arg) {
opt = $('<option>')
opt.value = arg.camera
opt.text = arg.camera
$('#camera_device').empty()
$('#camera_device').append(opt)
console.dir(arg)
devices.ready();
return event.returnValue = true;
});
ipcRenderer.on('ready', devices.ready);
};
devices.ready = function () {
devices.ready = function (event, arg) {
'use strict';
let opt;
gui.spinner(false);
gui.overlay(false);
for (let i in arg) {
opt = $('<option>');
opt.val(arg[i]);
opt.text(arg[i]);
$(`#${i}_device`).empty();
$(`#${i}_device`).append(opt);
}
return event.returnValue = true;
};
module.exports = devices;

View File

@ -31,9 +31,8 @@ let log = {}
//console.log(process.version)
//cfg is now hardcoded, should only be modified by developers
//settings is now the source of user editable variables
mcopy.cfg = require('./data/cfg.json')
mcopy.settings = {}
var enumerateDevices = function (err, devices) {
if (err) {
@ -61,6 +60,7 @@ var distinguishDevice = function (device, callback) {
return console.error(err)
}
log.info(`Verified ${device} as mcopy device`, 'SERIAL', true, true)
setTimeout(function () {
arduino.distinguish(distinguishCb);
}, 1000);
@ -69,6 +69,8 @@ var distinguishDevice = function (device, callback) {
if (err) {
return console.error(err)
}
rememberDevice(device, type)
log.info(`Determined ${device} to be ${type}`, 'SERIAL', true, true)
if (callback) { callback(err, type); }
}
@ -155,6 +157,8 @@ var distinguishDevices = function (devices) {
}
})
}
console.dir(mcopy.settings)
console.dir(checklist)
checklist = devices.map(device => {
return next => {
@ -182,6 +186,24 @@ var distinguishDevices = function (devices) {
})
};
var rememberDevice = function (device, type) {
let deviceEntry
let match = mcopy.settings.devices.filter(dev => {
if (dev.arduino && dev.arduino === device) {
return dev
}
})
if (match.length === 0) {
deviceEntry = {
arduino : device,
type : type
}
mcopy.settings.devices.push(deviceEntry)
settings.update('devices', mcopy.settings.devices)
settings.save()
}
}
var devicesReady = function (projector, camera, light) {
mainWindow.webContents.send('ready', {camera: camera, projector: projector, light: light })
settings.update('camera', { arduino : camera })
@ -291,7 +313,6 @@ cam.state = {
}
cam.init = function () {
cam.listen()
cam.intval = new Intval('camera', '192.168.1.224')
}
cam.set = function (dir, id) {
let cmd
@ -304,14 +325,27 @@ cam.set = function (dir, id) {
arduino.send('camera', cmd, (ms) => {
cam.end(cmd, id, ms)
})
/*
intval.setDir('camera', dir, (ms) => {
cam.end(cmd, id, ms)
})
*/
}
cam.move = function (frame, id) {
let cmd = mcopy.cfg.arduino.cmd.camera
/*arduino.send('camera', cmd, (ms) => {
arduino.send('camera', cmd, (ms) => {
cam.end(cmd, id, ms)
})*/
cam.intval.move('camera', (ms) => {
})
/*
intval.move('camera', (ms) => {
cam.end(cmd, id, ms)
})
*/
}
cam.exposure = function (exposure, id) {
intval.setDir('camera', exposure, (ms) => {
cam.end(cmd, id, ms)
})
}
@ -414,8 +448,6 @@ transfer.listen = function () {
}
var init = function () {
settings.restore()
createWindow()
//createMenu()
@ -429,7 +461,10 @@ var init = function () {
arduino = require('./lib/arduino')(mcopy.cfg, ee)
mscript = require('./lib/mscript')
settings.restore()
mcopy.settings = settings.all()
setTimeout( () => {
arduino.enumerate(enumerateDevices)

1333
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,11 +35,11 @@
},
"dependencies": {
"async": "^2.6.0",
"electron": "^1.7.10",
"humanize-duration": "^3.10.0",
"electron": "^1.8.2-beta.4",
"humanize-duration": "^3.12.1",
"moment": "^2.17.1",
"node-notifier": "^4.6.1",
"serialport": "^4.0.7",
"node-notifier": "^5.1.2",
"serialport": "^6.0.4",
"sqlite3": "^3.1.13",
"uuid": "^3.0.1",
"winston": "^2.3.0"

View File

@ -1,5 +1,7 @@
volatile char cmd_char = 'z';
const char cmd_id = 'i';
void setup() {
Serial.begin(57600);
Serial.flush();
@ -17,7 +19,7 @@ void loop() {
}
void cmd (char val) {
if (val == 'i') {
Serial.println("i");//End of action
if (val == cmd_id) {
Serial.println(cmd_id);//End of action
}
}

View File

@ -0,0 +1,225 @@
//MCOPY firmware.
//Projector + Light
//
// LIGHT WIRING
// ARDUINO PIXIE
// GND -----> -
// PIN 3 -----> in
//
// POWER SUPPLY 5V PIXIE
// GND -----> -
// +5VDC -----> +
/*
PROJECTOR WIRING
----------------------------------------------------
Microswitch (use INPUT_PULLUP!!)
GND-----\ | \-----PIN
----------------------------------------------------
*/
//LIGHT HEADERS
#include "SoftwareSerial.h"
#include "Adafruit_Pixie.h"
#define NUMPIXELS 1 // Number of Pixies in the strip
#define PIXIEPIN 6 // Pin number for SoftwareSerial output
SoftwareSerial pixieSerial(-1, PIXIEPIN);
Adafruit_Pixie light = Adafruit_Pixie(NUMPIXELS, &pixieSerial);
//PROJECTOR HEADERS
boolean debug_state = false;
//LIGHT VARIABLES
String color = "000,000,000";
volatile int commaR = 0;
volatile int commaG = 0;
String strR = "000";
String strG = "000";
String strB = "000";
volatile int r = 0;
volatile int g = 0;
volatile int b = 0;
unsigned long light_time;
//PROJECTOR VARIABLES
//const int proj_time = {{proj.time}};
//const int proj_delay = {{proj.delay}};
const int proj_fwd_pin = 8;
const int proj_bwd_pin = 9;
volatile boolean proj_running = false;
const int proj_micro_pin = 4;
volatile int proj_micro_raw;
boolean proj_dir = true;
//APP
unsigned long now; //to be compared to stored values every loop
const char cmd_light = 'l';
const char cmd_projector = 'p';
const char cmd_proj_forward = 'g';
const char cmd_proj_backward = 'h';
const char cmd_mcopy_identifier = 'm';
//for just proj
//const char cmd_proj_identifier = 'j';
//for proj + light
const char cmd_proj_identifier = 'q';
const char cmd_debug = 'd';
const char cmd_connect = 'i';
volatile char cmd_char = 'z';
const int serialDelay = 5;
void setup() {
Serial.begin(57600);
Serial.flush();
Serial.setTimeout(serialDelay);
pixieSerial.begin(115200); // Pixie REQUIRES this baud rate
light.setPixelColor(0, 0, 0, 0);
light.show();
pinMode(proj_micro_pin, INPUT_PULLUP);
pinMode(proj_fwd_pin, OUTPUT);
pinMode(proj_bwd_pin, OUTPUT);
}
void loop() {
if (Serial.available()) {
/* read the most recent byte */
cmd_char = (char)Serial.read();
}
if (cmd_char != 'z') {
cmd(cmd_char);
cmd_char = 'z';
}
now = millis();
if (proj_running) {
proj_reading();
}
//send light signal to pixie every second
if (now - light_time >= 1000) {
light.setPixelColor(0, r, g, b);
light.show();
light_time = now;
}
}
void cmd (char val) {
if (val == cmd_debug) {
debug();
} else if (val == cmd_connect) {
connect();
} else if (val == cmd_mcopy_identifier) {
identify();
} else if (val == cmd_projector) {
proj_start();
} else if (val == cmd_proj_forward) {
proj_direction(true);
} else if (val == cmd_proj_backward) {
proj_direction(false);
} else if (val == cmd_light) {
light_set();
}
}
void debug () {
debug_state = true;
Serial.println(cmd_debug);
log("debugging enabled");
}
void connect () {
Serial.println(cmd_connect);
log("connect()");
}
void identify () {
Serial.println(cmd_proj_identifier);
log("identify()");
}
void light_set () {
while (Serial.available() == 0) {
//Wait for color string
}
color = Serial.readString();
//Serial.println(color);
commaR = color.indexOf(','); //comma trailing R
commaG = color.indexOf(',', commaR + 1);
strR = color.substring(0, commaR);
strG = color.substring(commaR + 1, commaG);
strB = color.substring(commaG + 1);
r = strR.toInt();
g = strG.toInt();
b = strB.toInt();
light.setPixelColor(0, r, g, b);
light.show();
Serial.println(cmd_light);//confirm light change
log(color);
}
void proj_start () {
if (proj_dir) {
digitalWrite(proj_fwd_pin, HIGH);
digitalWrite(proj_bwd_pin, LOW);
} else {
digitalWrite(proj_bwd_pin, HIGH);
digitalWrite(proj_fwd_pin, LOW);
}
proj_running = true;
delay(500); // Let bump pass out of microswitch
//delay(1300); //TEMPORARY DELAY FOR TESTING TIMING
}
void proj_reading () {
proj_micro_raw = digitalRead(proj_micro_pin);
if (proj_micro_raw == 1) {
//do nothing
} else if (proj_micro_raw == 0) {
proj_stop();
}
//delay(1); //needed?
}
void proj_stop () {
digitalWrite(proj_bwd_pin, LOW);
digitalWrite(proj_fwd_pin, LOW);
proj_running = false;
Serial.println(cmd_projector);
log("projector()");
}
void proj_direction (boolean state) {
proj_dir = state;
if (state) {
Serial.println(cmd_proj_forward);
log("proj_direction -> true");
} else {
Serial.println(cmd_proj_backward);
log("proj_direction -> false");
}
//delay(50); //delay after direction change to account for slippage of the belt
}
void log (String msg) {
if (debug_state) {
Serial.println(msg);
}
}

View File

@ -1,10 +1,12 @@
/*
Wiring
HOLD OFF FOR NOW
For "MONITOR" pins with INPUT_PULLUP resistors:
GND-----\ | \-----PIN
No additional resistors/caps needed.
--Note: not needed in prototype
CAMERA + CAMERA_DIR and PROJECTOR + PROJECTOR_DIR:
Wire directly to corresponding relay pins.
@ -14,27 +16,25 @@
boolean debug_state = false;
unsigned long now; //to be compared to stored values every loop
//unsigned long now; //to be compared to stored values every loop
//CAMERA CONSTANTS
const int CAMERA = 2;
const int CAMERA_DIR = 3;
const int CAMERA_MONITOR = 4;
unsigned long cam_momentary_end;
const long cam_moment = 300;
const int CAMERA_FWD = 3;
const int CAMERA_BWD = 4;
const int CAMERA_MOMENT = 200;
const int CAMERA_FRAME = 800;
//CAMERA VARIABLES
boolean cam_dir = true;
boolean cam_running = false;
boolean cam_momentary = false;
//PROJECTOR CONSTANTS
const int PROJECTOR = 8;
const int PROJECTOR_DIR = 9;
const int PROJECTOR_MONITOR = 10;
unsigned long proj_momentary_end;
const long proj_moment = 300;
const int PROJECTOR_FWD = 9;
const int PROJECTOR_BWD = 10;
const int PROJECTOR_MOMENT = 200;
const int PROJECTOR_FRAME = 800;
//PROJECTOR VARIABLES
boolean proj_dir = true;
boolean proj_running = false;
boolean proj_momentary = false;
//PROJECTOR COMMANDS
const char cmd_projector = 'p';
@ -59,13 +59,13 @@ const int serialDelay = 5;
void setup () {
Serial.begin(57600);
Serial.flush();
Serial.setTimeout(serialDelay);
//Serial.setTimeout(serialDelay);
pins();
}
void loop () {
now = millis();
//now = millis();
if (Serial.available()) {
/* read the most recent byte */
cmd_char = (char)Serial.read();
@ -74,14 +74,11 @@ void loop () {
cmd(cmd_char);
cmd_char = 'z';
}
if (cam_running) {
monitorCam();
}
if (proj_running) {
monitorProj();
}
/*delay(2000);
cam_start();
delay(2000);
proj_start();*/
}
void pins () {
@ -89,18 +86,20 @@ void pins () {
pinMode(CAMERA, OUTPUT);
pinMode(PROJECTOR, OUTPUT);
pinMode(CAMERA_DIR, OUTPUT);
pinMode(PROJECTOR_DIR, OUTPUT);
//PULLUP
pinMode(CAMERA_MONITOR, INPUT_PULLUP);
pinMode(PROJECTOR_MONITOR, INPUT_PULLUP);
pinMode(CAMERA_FWD, OUTPUT);
pinMode(CAMERA_BWD, OUTPUT);
pinMode(PROJECTOR_FWD, OUTPUT);
pinMode(PROJECTOR_BWD, OUTPUT);
//SET LOW
digitalWrite(CAMERA, LOW);
digitalWrite(CAMERA_DIR, LOW);
digitalWrite(PROJECTOR, LOW);
digitalWrite(PROJECTOR_DIR, LOW);
digitalWrite(CAMERA_FWD, HIGH);
digitalWrite(CAMERA_BWD, LOW);
digitalWrite(PROJECTOR_FWD, HIGH);
digitalWrite(PROJECTOR_BWD, LOW);
}
void cmd (char val) {
@ -125,42 +124,6 @@ void cmd (char val) {
}
}
void monitorCam () {
int position = digitalRead(CAMERA_MONITOR);
if (cam_momentary && now >= cam_momentary_end) {
digitalWrite(CAMERA, LOW);
cam_momentary = false;
}
if (!cam_momentary) {
if (position == LOW) {
cam_stop();
}
}
}
void monitorProj () {
int position = digitalRead(PROJECTOR_MONITOR);
if (proj_momentary && now >= proj_momentary_end) {
digitalWrite(PROJECTOR, LOW);
proj_momentary = false;
}
if (!proj_momentary) {
//If internam microswitch is set to LOW?
if (position == LOW) {
proj_stop();
}
}
}
void setDir (int pin, boolean dir) {
if (dir) {
digitalWrite(pin, HIGH);
} else {
digitalWrite(pin, LOW);
}
}
void debug () {
debug_state = true;
Serial.println(cmd_debug);
@ -177,39 +140,52 @@ void identify () {
log("identify()");
}
void setDir (int pin, boolean dir) {
if (!dir) {
digitalWrite(pin, HIGH);
} else {
digitalWrite(pin, LOW);
}
}
void proj_start () {
digitalWrite(PROJECTOR, HIGH);
proj_running = true;
proj_momentary = true;
proj_momentary_end = now + proj_moment;
delay(PROJECTOR_MOMENT);
digitalWrite(PROJECTOR, LOW);
delay(PROJECTOR_FRAME);
proj_stop();
}
void cam_start () {
digitalWrite(CAMERA, HIGH);
cam_running = true;
cam_momentary = true;
cam_momentary_end = now + cam_moment;
delay(CAMERA_MOMENT);
digitalWrite(CAMERA, LOW);
delay(CAMERA_FRAME);
cam_stop();
}
void proj_stop () {
proj_running = false;
Serial.println(cmd_projector);
log("projector()");
}
void cam_stop () {
cam_running = false;
Serial.println(cmd_camera);
log("camera()");
}
void proj_direction (boolean state) {
proj_dir = state;
setDir(PROJECTOR_DIR, state);
if (state) {
digitalWrite(PROJECTOR_BWD, LOW);
delay(10);
digitalWrite(PROJECTOR_FWD, HIGH);
Serial.println(cmd_proj_forward);
log("proj_direction -> true");
} else {
digitalWrite(PROJECTOR_FWD, LOW);
delay(10);
digitalWrite(PROJECTOR_BWD, HIGH);
Serial.println(cmd_proj_backward);
log("proj_direction -> false");
}
@ -217,11 +193,16 @@ void proj_direction (boolean state) {
void cam_direction (boolean state) {
cam_dir = state;
setDir(CAMERA_DIR, state);
if (state) {
digitalWrite(CAMERA_BWD, LOW);
delay(10);
digitalWrite(CAMERA_FWD, HIGH);
Serial.println(cmd_cam_forward);
log("cam_direction -> true");
} else {
digitalWrite(CAMERA_FWD, LOW);
delay(10);
digitalWrite(CAMERA_BWD, HIGH);
Serial.println(cmd_cam_backward);
log("cam_direction -> false");
}
@ -231,4 +212,5 @@ void log (String msg) {
if (debug_state) {
Serial.println(msg);
}
}
}

34
scad/jk.scad Normal file
View File

@ -0,0 +1,34 @@
/*
JK modules
Directional switch connector =
Philmore
Mobile Connector
Panel Mount 3 Pin Male/Female
No. 61-623
.625" Diameter
*/
module jk_male_connector_mount () {
$fn = 200;
D = 17;
OD = 28;
H = 25;
difference () {
union () {
cylinder(r = OD / 2, h = H, center = true);
//cube([12, 48, 3], center = true);
}
//main void
translate([0, 0, -3]) cylinder(r = (OD / 2) - 3, h = H, center = true);
//connector void
cylinder(r = D / 2, h = H * 2, center = true);
}
}
rotate([180, 0, 0]) jk_male_connector_mount();

9
scad/lens.scad Normal file
View File

@ -0,0 +1,9 @@
$fn=200;
module lens_cap_back () {
difference() {
cylinder(r = 36 / 2, h = 9, center = true);
translate([0, 0, 2]) cylinder(r = 33 / 2, h = 9, center = true);
}
}
lens_cap_back();

View File

@ -48,7 +48,7 @@ module pixie_mount () {
}
//pins for mounting pixie
translate ([0, -3, 0]) rotate([90, 0, 0]) {
translate ([0, -0.5, 0]) rotate([90, 0, 0]) {
translate([W/2, 0, -2]) cylinder(r = INNER_D / 2, h = Z + 1, center = true);
translate([-W/2, 0, -2]) cylinder(r = INNER_D / 2, h = Z + 1, center = true);
@ -76,14 +76,20 @@ module pixie_mount () {
//outer shell surrounding pixie
translate ([0, -1 + LENS_OFFSET, -6]) {
difference () {
translate([0, 0, 6]) rounded_cube([W + 16, 36, 32], d = 6, center = true);
translate([0, 0, 6]) rounded_cube([W + 20, 40, 32], d = 6, center = true);
translate([0, 0, 6]) rounded_cube([(W + 16) - 4, 36 - 4, 32 + 1], d = 4, center = true);
translate([0, -8, 8]) rotate([90, 0, 0]) cylinder(r = 10, h = 5, center = true);//circular void
translate([0, -50, 0]) cube([100, 100, 100], center = true); //half
//inner tab for centering
translate([0, 0, 6]) cube([W + 16, 10, 32 - 10], center = true);
//cylinder void for bolt
translate([0, 3, 6]) rotate([0, 90, 0]) cylinder(r = 1, h = 40 + 1, center = true, $fn = 60);
//cut in half
translate([0, -50, 0]) cube([100, 100, 100], center = true);
//wires
translate([0, 0, -3]) cube([10, 40, 5], center = true);
translate([-5, 0, -5.5]) cube([2, 40, 10], center = true);
}
}
}
@ -93,29 +99,50 @@ module diffuser_mount () {
W = 0.8 * 25.4;
translate ([0, 0, -6]) {
difference () {
translate([0, 0, 6]) rounded_cube([W + 16, 36, 32], d = 6, center = true);
translate([0, 0, 6]) rounded_cube([W + 20, 40, 32], d = 6, center = true);
translate([0, 0, 6]) rounded_cube([(W + 16) - 4, 36 - 4, 32 + 1], d = 4, center = true);
translate([0, -8, 8]) rotate([90, 0, 0]) cylinder(r = 10, h = 5, center = true);//circular void
translate([0, 50, 0]) cube([100, 100, 100], center = true); //half
//circular void
translate([0, -8, 8]) rotate([90, 0, 0]) cylinder(r = 10, h = 5, center = true);
//cylinder void for bolt
translate([0, 3, 6]) rotate([0, 90, 0]) cylinder(r = 1, h = 40 + 1, center = true, $fn = 60);
//cut in half
translate([0, 50, 0]) {
difference () {
cube([100, 100, 100], center = true);
translate([0, -50, 6]) cube([W + 16, 10, 32 - 10], center = true);
}
}
translate ([0, -18, 6]) rotate([90, 90, 0]) cylinder(r = 30 / 2, h = 20, center = true);
//void for attachment
//translate([20, -8.5, 6]) cube([8, 8, 8], center = true);
}
translate([22, -8.5, 6]) light_diffuser_notch();
}
translate ([0, -15.5, 0]) rotate([90, 90, 0]) {
difference () {
cylinder(r = 30 / 2, h = 5, center = true);
cylinder(r = 28 / 2, h = 5 + 1, center = true);
cylinder(r = 30 / 2, h = 9, center = true);
cylinder(r = 28 / 2, h = 9 + 1, center = true);
}
}
}
module light_diffuser_notch () {
difference () {
cube([6, 12, 8], center = true);
translate([-1, 0, 7]) rotate([0, -20, 0]) cube([8, 12 + 1, 8], center = true);
translate([-1, 0, -7]) rotate([0, 20, 0]) cube([8, 12 + 1, 8], center = true);
}
}
module diffuser_spacer () {
$fn = 100;
LEN = 10;
LEN = 15;
THICKNESS = 3;
difference () {
cylinder(r = 27.9 / 2, h = LEN, center = true);
cylinder(r = 24 / 2, h = LEN + 1, center = true);
translate([0, 0, (LEN / 2) - (2/2) ]) cylinder(r = 26 / 2, h = 2, center = true);
cylinder(r = 23 / 2, h = LEN + 1, center = true);
translate([0, 0, (LEN / 2) - (THICKNESS / 2)]) cylinder(r = 26 / 2, h = THICKNESS, center = true);
}
}
@ -156,24 +183,49 @@ module light_body () {
}
module light_body35 () {
$fn = 60;
W = 0.8 * 25.4;
L = 0.78 * 25.4;
Z = 12;
FAN_W = 35;
FAN_Z = 10;
translate([0, 0, 5]) difference () {
rounded_cube([W + 20 + 6, 40 + 6, 13], d = 6, center = true);
rounded_cube([W + 20, 40, 13 + 1], d = 6, center = true);
translate([0, -15, 20 - 1.5]) rotate([90, 0, 0]) cylinder(r = 31 / 2, h = 30, center = true);
}
translate([0, 0, 0]) difference() {
rounded_cube([W + 20 + 6, 40 + 6, FAN_Z], d = 6, center = true);
rounded_cube([FAN_W + 0.2, FAN_W + 0.2, FAN_Z + 1], d = 4, center = true);
}
}
module light_vent_top () {
$fn = 60;
difference () {
rounded_cube([36+4, 36+4, 10], d = 6, center = true);
rounded_cube([36, 36, 10 + 1], d = 6, center = true);
$fn = 60;
W = 0.8 * 25.4;
L = 0.78 * 25.4;
Z = 12;
translate([0, 0, -1.5]) difference () {
rounded_cube([W + 20 + 6, 40 + 6, 13], d = 6, center = true);
rounded_cube([W + 20, 40, 13 + 1], d = 6, center = true);
translate([0, -15, -20 + 1.5]) rotate([90, 0, 0]) cylinder(r = 31 / 2, h = 30, center = true);
}
translate([0, 0, 1]) difference() {
rounded_cube([36+2, 36+2, 8], d = 6, center = true);
rounded_cube([W + 20 + 1, 40 + 1, 8], d = 6, center = true);
for (i = [0:5]) {
translate([i * 6, 0, 0]) rotate([0, -40, 0]) cube([4, 36 + 3, 12], center = true);
translate([(i + 1) * -6, 0, 0]) rotate([0, -40, 0]) cube([4, 36 + 3, 12], center = true);
}
//translate([50, 0, 0]) cube([100, 100, 100], center = true);
}
}
module fan () {
module fan (SIZE = 50) {
$fn = 60;
FAN_W = 50;
FAN_W = SIZE;
FAN_Z = 10;
SCREW_D = 4;
SCREW_INNER = 6;
@ -185,4 +237,92 @@ module fan () {
translate([(FAN_W - SCREW_INNER)/2, -(FAN_W - SCREW_INNER)/2, 0]) cylinder(r = SCREW_D/2, h = FAN_Z + 1, center = true);
cylinder(r = (FAN_W - SCREW_INNER) /2, h = FAN_Z + 1, center = true);
}
}
module flashlight_mount () {
$fn = 100;
FLASHLIGHT_D = 25.3;
ROD_D = 12.6;
difference () {
rotate([0, 90, 0]) cylinder(r = FLASHLIGHT_D / 2 + 3, h = ROD_D, center = true);
rotate([0, 90, 0]) cylinder(r = FLASHLIGHT_D / 2, h = ROD_D + .1, center = true);
}
translate([0, -35, 0]) rotate([90, 0, 0]) {
difference () {
cylinder(r = ROD_D / 2, h = 43, center = true);
translate([-10, 0, 24.5]) cube([15, 15, 15], center = true);
}
}
translate([0, -46.4, 0]) {
difference() {
rotate ([90, 90, 0]) cylinder(r = (ROD_D / 2) + 2, h = 3, center = true);
translate([50 + (ROD_D / 2), 0, 0]) cube([100, 100, 100], center = true);
translate([-50 - (ROD_D / 2), 0, 0]) cube([100, 100, 100], center = true);
}
}
}
module flashlight_mount_cap (DEBUG = false) {
$fn = 120;
ROD_D = 12.6;
translate([0, 0, 0]) rotate([90, 0, 0]) {
difference () {
union () {
translate([0, 0, 0]) cylinder(r = 9, h = 7, center = true);
translate([-6, 0, 0]) cube([12, 18, 7], center = true);
translate([-10.75, 0, -3]) cube([2.5, 18, 11], center = true);
}
difference () {
translate([0, 0, -(7 - 4.5) / 2 - .1]) cylinder(r = ROD_D / 2, h = 4.5, center = true);
translate([-10, 0, 0]) cube([15, 15, 15], center = true);
}
if (DEBUG) {
translate([0, 50, 0]) cube([100, 100, 100], center = true);
}
}
}
}
module impromptu_mount () {
$fn = 100;
ROD_D = 12.6;
translate([0, -35, 0]) rotate([90, 0, 0]) {
difference () {
union() {
cylinder(r = ROD_D / 2, h = 43, center = true);
translate([0, 0, -5]) cylinder(r = (ROD_D / 2) + 2, h = 30, center = true);
}
//notch for cap
translate([-10, 0, 24.5]) cube([15, 15, 15], center = true);
translate([0, 0, -22.25]) cube([45, 45, 15], center = true);
translate([0, 0, -12.5 - 0.5]) scale([4, 1, 1]) rotate([0, -90, 90]) light_diffuser_notch();
}
difference () {
translate([0, 0, -12.25 + 1]) cube([40, 12, 7], center = true);
translate([0, 0, -12.5 - 0.5]) scale([4, 1, 1]) rotate([0, -90, 90]) light_diffuser_notch();
}
}
translate([0, -46.4, 0]) {
difference() {
rotate ([90, 90, 0]) cylinder(r = (ROD_D / 2) + 2, h = 3, center = true);
translate([50 + (ROD_D / 2), 0, 0]) cube([100, 100, 100], center = true);
translate([-50 - (ROD_D / 2), 0, 0]) cube([100, 100, 100], center = true);
}
}
}
module light_fresnel (D = 24, BASE = 3, RINGS = 4, d) {
$fn = 200;
STEP = D / RINGS;
cylinder(r = D / 2, h = BASE, center = true);
translate([0, 0, 4]) for (i = [0 : RINGS]) {
cylinder(r1 = (D - (STEP * (i + 1))) / 2, r2 = (D - (STEP * i)) / 2, h = 2, center = true);
}
}

View File

@ -3,11 +3,17 @@ include <./connectors.scad>;
include <./light.scad>;
include <./motor.scad>;
//rotate([90, 0, 0]) color("red") adafruit_pixie();
//translate([0, 6, 0]) pixie_mount();
//translate([0, 0, -50]) color("red") fan();
color("green") diffuser_mount();
//translate([0, 0, -51.2]) light_body();
//translate([0, 2.5, 0]) rotate([90, 0, 0]) color("red") adafruit_pixie();
//translate([0, 6, 0]) color("blue") pixie_mount();
//translate([0, 0, -50]) color("red") fan(35);
//color("green") diffuser_mount();
//translate([0, 0, -51.2]) light_body35();
//translate([0, 0, 20]) light_vent_top();
//translate ([0, -20, 0]) rotate([90, 90, 0]) color("red") diffuser_insert();
//translate ([0, -20, 0]) rotate([90, 90, 0]) color("red") diffuser_spacer();
//translate ([0, -10, 0]) rotate([90, 90, 0]) color("red") diffuser_spacer();
//rotate([0, 0, 90]) flashlight_mount();
//translate([0, -8.5, 0]) rotate([0, 0, 90]) impromptu_mount();
//translate([30, -8.5, 0]) rotate([90, 0, 90]) flashlight_mount_cap();
//translate([0, -9, 0]) cube([15, 15, 15], center = true);
light_fresnel();

View File

@ -489,9 +489,9 @@ module flashlight_mount_cap (DEBUG = false) {
translate([0, 0, 0]) rotate([90, 0, 0]) {
difference () {
union () {
translate([0, 0, 0]) cylinder(r = 9, h = 7, center = true);
translate([-6, 0, 0]) cube([12, 18, 7], center = true);
translate([-10.75, 0, -3]) cube([2.5, 18, 11], center = true);
translate([0, 0, 0]) cylinder(r = 11, h = 7, center = true);
translate([-6, 0, 0]) cube([12, 22, 7], center = true);
translate([-11.5, 0, -3]) cube([4, 22, 13], center = true);
}
difference () {
translate([0, 0, -(7 - 4.5) / 2 - .1]) cylinder(r = ROD_D / 2, h = 4.5, center = true);
@ -520,4 +520,5 @@ module bolex_stand () {
}
//translate([0, 0, 40]) cube([35, 150, 30], center = true);
}
}
}