Merge branch 'master' of ssh://git.sixteenmillimeter.com/16mm/contact_printer

This commit is contained in:
mmcwilliams 2025-02-14 00:31:38 -05:00
commit b927db828c
96 changed files with 866759 additions and 557973 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
*.DS_Store *.DS_Store
bin/

3
.gitmodules vendored
View File

@ -1,9 +1,12 @@
[submodule "scad/common"] [submodule "scad/common"]
path = scad/common path = scad/common
url = https://git.sixteenmillimeter.com/modules/common.git url = https://git.sixteenmillimeter.com/modules/common.git
ignore = dirty
[submodule "scad/takeup"] [submodule "scad/takeup"]
path = scad/takeup path = scad/takeup
url = https://git.sixteenmillimeter.com/modules/takeup.git url = https://git.sixteenmillimeter.com/modules/takeup.git
ignore = dirty
[submodule "scad/sprocketed_roller"] [submodule "scad/sprocketed_roller"]
path = scad/sprocketed_roller path = scad/sprocketed_roller
url = https://git.sixteenmillimeter.com/modules/sprocketed_roller.git url = https://git.sixteenmillimeter.com/modules/sprocketed_roller.git
ignore = dirty

View File

@ -1,17 +1,67 @@
# contact printer # contact printer
A desktop 16mm contact printer made for CNC/laser cutting and 3D printing A desktop 16mm contact printer and film transport platform made using 3D printing and aluminum extrusion.
## [Project Home: git.sixteenmillimeter.com/16mm/contact_printer](https://git.sixteenmillimeter.com/16mm/contact_printer)
## Acknowledgements
Developed with support from [Filmwerkplaats at WORM](https://worm.org/spaces/filmwerkplaats/) and [SPECTRAL](http://www.spectral-cinematics.eu/).
Special thanks to [Esther Urlus](https://estherurlus.hotglue.me/), [Hrvoje Spudić](https://hrvojespudic.net/) and [Nan Wang](https://nanwang.org/).
Magnetic clutch design inspired by Clyde Shaffer's [Shaffer Linear Processor](https://clydeshaffer.com/slp/).
--- ---
## [Project Home: git.sixteenmillimeter.com/16mm/contact_printer](https://git.sixteenmillimeter.com/16mm/contact_printer) ## Description
This contact printer is designed to make 16mm prints using affordable electronics and 3D printing.
It is *not* intended as a replacement for professional printers like the Bell & Howell Model C.
This project is for DIY filmmakers, independent artists and small film labs to have a machine that can produce reasonable-quality work prints with a small footprint on short lengths of film easily.
---
![contact printer render](./img/contact_printer.png)
---
# Examples
[Contact](https://vimeo.com/flashfra/contact?share=git) the first print made on the prototype during a residency at [Filmwerkplaats](https://filmwerkplaats.org/).
Soundtrack applied by the opt_snd soundtrack recorder system by [Hrvoje Spudić](https://hrvojespudic.net/optical-sound-2).
# Bill of Materials
Below are estimates based on per-unit costs found online.
Actual price of a full build may be higher.
<!-- bom -->
| Part | Qty | Cost (USD) | Minumum |
|-------------------------------------|------|------------|---------------------------------------------|
| M4 hex bolt 40mm | 4 | $2.44 | [10 for $6.09](https://amzn.to/4ikpYL8) |
| M3 sliding t slot nut | 25 | $1.43 | [105 for $5.99](https://amzn.to/48GRrSU) |
| M3 hex cap bolt 8mm | 25 | $2.25 | [100 for $8.99](https://amzn.to/3YEvWNB) |
| M3 hex cap bolt 6mm | 8 | $0.59 | [100 for $7.26](https://amzn.to/3AwiZxo) |
| M3 hex cap bolt 12mm | 1 | $0.09 | [100 for $8.36](https://amzn.to/48CGa5Y) |
| L298N Motor driver module | 1 | $2.88 | [4 for $11.49](https://amzn.to/4ellssy) |
| ESP32 GPIO breakout board | 1 | $6.00 | [2 for $11.99](https://amzn.to/3UFjpbO) |
| ESP32 Dev board | 1 | $6.67 | [3 for $19.99](https://amzn.to/3NXCvGj) |
| 608-RS Ball Bearing | 1 | $0.18 | [100 for $17.79](https://amzn.to/4fKxDA7) |
| 250RPM DC geared motor | 2 | $29.98 | [1 for $14.99](https://amzn.to/3NWkcRL) |
| 2020 Aluminum extrusion mm | 1880 | $11.41 | [12200 for $73.99](https://amzn.to/418OicC) |
| 100RPM DC geared motor with encoder | 1 | $16.19 | [1 for $16.19](https://amzn.to/3UF707G) |
|---------|------|--------|---------|
| TOTAL | 1950 | $80.11 | $203.12 |
<!-- /bom -->
### Mirrors ### Mirrors
* [github.com/sixteenmillimeter/contact_printer](https://github.com/sixteenmillimeter/contact_printer) * [github.com/sixteenmillimeter/contact_printer](https://github.com/sixteenmillimeter/contact_printer)
* [gitlab.com/16mm/contact_printer](https://gitlab.com/16mm/contact_printer) * [gitlab.com/16mm/contact_printer](https://gitlab.com/16mm/contact_printer)
---
![contact printer render](https://github.com/sixteenmillimeter/contact_printer/blob/master/img/contact_printer?raw=true)
![contact printer render - bottom](https://github.com/sixteenmillimeter/contact_printer/blob/master/img/contact_printer_2?raw=true)

1
VERSION.txt Normal file
View File

@ -0,0 +1 @@
0.2.2

2
dev/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.env

35
dev/README.md Normal file
View File

@ -0,0 +1,35 @@
# contact printer dev server
The purpose of this project is to host a dev server that development branches of the contact printer can post reports to after running.
This can be used for profiling the operation with different settings, hardware and conditions.
Reports will log data as it is needed without a specific goal from the outset, largely looking at speed of motors over time compared to PWM and can be used to improve performance or documentation.
The server has no frontend and simply accepts a post from the ESP32 running the contact printer and inserts the data into the SQLite database.
## Installing
Install the node dependencies for running the server.
```bash
npm install --omit=dev
```
If you would like to develop the server, install the complete set of dependencies.
```bash
npm install
```
## Running
```bash
node dist
```
## Developing
If you make changes to the Typescript in source, recompile the server code.
```bash
npm run compile
```

1
dev/default.env Normal file
View File

@ -0,0 +1 @@
PORT=9999

32
dev/dist/index.js vendored Normal file
View File

@ -0,0 +1,32 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const express_1 = __importDefault(require("express"));
const body_parser_1 = __importDefault(require("body-parser"));
const log_1 = require("./log");
const PORT = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 9999;
let db;
const app = (0, express_1.default)();
const log = (0, log_1.createLog)('server');
app.use(body_parser_1.default.json());
app.use(body_parser_1.default.urlencoded({ extended: true }));
async function setup() {
//db = new Database('./data.sqlite');
}
async function report(req, res, next) {
//
log.info(`Added record`);
res.json({ success: true });
}
app.post('/', report);
async function main() {
await setup();
app.listen(PORT, async () => {
log.info(`contact_printer_dev_server running on port ${PORT}`);
});
}
main();
//# sourceMappingURL=index.js.map

1
dev/dist/index.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAK9B,8DAAqC;AAGrC,+BAAiC;AAGjC,MAAM,IAAI,GAAY,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAE5G,IAAI,EAAa,CAAC;AAClB,MAAM,GAAG,GAAa,IAAA,iBAAO,GAAE,CAAC;AAChC,MAAM,GAAG,GAAY,IAAA,eAAS,EAAC,QAAQ,CAAC,CAAC;AAEzC,GAAG,CAAC,GAAG,CAAC,qBAAU,CAAC,IAAI,EAAE,CAAC,CAAC;AAC3B,GAAG,CAAC,GAAG,CAAC,qBAAU,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAGnD,KAAK,UAAU,KAAK;IACnB,qCAAqC;AACtC,CAAC;AAED,KAAK,UAAU,MAAM,CAAE,GAAa,EAAE,GAAa,EAAE,IAAmB;IACvE,EAAE;IACF,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAG,IAAI,EAAE,CAAC,CAAC;AAC9B,CAAC;AAED,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;AAGtB,KAAK,UAAU,IAAI;IAClB,MAAM,KAAK,EAAE,CAAC;IACd,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;QAC3B,GAAG,CAAC,IAAI,CAAC,8CAA8C,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC"}

50
dev/dist/log.js vendored Normal file
View File

@ -0,0 +1,50 @@
'use strict';
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLog = void 0;
/** @module log */
/** Wrapper for winston that tags streams and optionally writes files with a simple interface. */
/** Module now also supports optional papertrail integration, other services to follow */
const winston_1 = require("winston");
const { SPLAT } = require('triple-beam');
const { isObject } = require('lodash');
const APP_NAME = process.env.APP_NAME || 'default';
let winstonPapertrail;
function formatObject(param) {
if (isObject(param)) {
return JSON.stringify(param);
}
return param;
}
const all = (0, winston_1.format)((info) => {
const splat = info[SPLAT] || [];
const message = formatObject(info.message);
const rest = splat.map(formatObject).join(' ');
info.message = `${message} ${rest}`;
return info;
});
const myFormat = winston_1.format.printf(({ level, message, label, timestamp }) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});
/**
* Returns a winston logger configured to service
*
* @param {string} label Label appearing on logger
* @param {string} filename Optional file to write log to
*
* @returns {object} Winston logger
*/
function createLog(label, filename = null) {
const tports = [new (winston_1.transports.Console)()];
const fmat = winston_1.format.combine(all(), winston_1.format.label({ label }), winston_1.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), winston_1.format.colorize(), myFormat);
let papertrailOpts;
if (filename !== null) {
tports.push(new (winston_1.transports.File)({ filename }));
}
return (0, winston_1.createLogger)({
format: fmat,
transports: tports
});
}
exports.createLog = createLog;
module.exports = { createLog };
//# sourceMappingURL=log.js.map

1
dev/dist/log.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;;;AAEZ,kBAAkB;AAClB,iGAAiG;AACjG,yFAAyF;AAEzF,qCAA2D;AAC3D,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;AACzC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvC,MAAM,QAAQ,GAAY,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,SAAS,CAAC;AAE5D,IAAI,iBAAiB,CAAC;AAEtB,SAAS,YAAY,CAAE,KAAW;IAChC,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,GAAG,GAAG,IAAA,gBAAM,EAAC,CAAC,IAAU,EAAE,EAAE;IAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,CAAC,OAAO,GAAG,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,gBAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAQ,EAAE,EAAE;IAC5E,OAAO,GAAG,SAAS,KAAK,KAAK,KAAK,KAAK,KAAK,OAAO,EAAE,CAAC;AACxD,CAAC,CAAC,CAAC;AAEH;;;;;;;EAOE;AACF,SAAgB,SAAS,CAAE,KAAc,EAAE,WAAoB,IAAI;IAC/D,MAAM,MAAM,GAAW,CAAE,IAAI,CAAC,oBAAU,CAAC,OAAO,CAAC,EAAE,CAAE,CAAC;IACtD,MAAM,IAAI,GAAS,gBAAM,CAAC,OAAO,CAC7B,GAAG,EAAE,EACL,gBAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,EACvB,gBAAM,CAAC,SAAS,CAAC,EAAC,MAAM,EAAE,yBAAyB,EAAC,CAAC,EACrD,gBAAM,CAAC,QAAQ,EAAE,EACjB,QAAQ,CACX,CAAC;IACF,IAAI,cAAoB,CAAC;IAEzB,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CAAE,IAAI,CAAC,oBAAU,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAE,CAAC;IACvD,CAAC;IAED,OAAO,IAAA,sBAAY,EAAC;QAChB,MAAM,EAAG,IAAI;QACb,UAAU,EAAG,MAAM;KACtB,CAAC,CAAC;AACP,CAAC;AAnBD,8BAmBC;AAED,MAAM,CAAC,OAAO,GAAG,EAAE,SAAS,EAAE,CAAC"}

4682
dev/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
dev/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "contact_printer_dev_server",
"version": "1.0.0",
"description": "Server for receiving data from contact printer for profiling performance.",
"main": "dist/index.js",
"scripts": {
"compile": "./node_modules/.bin/tsc -p tsconfig.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git.sixteenmillimeter.com/16mm/contact_printer.git"
},
"author": "mmcwilliams",
"license": "MIT",
"devDependencies": {
"@types/express": "^4.17.21",
"@types/multer": "^1.4.11",
"@types/node": "^20.11.20",
"@types/uuid": "^9.0.8",
"@types/winston": "^2.4.4",
"typescript": "^5.3.3"
},
"dependencies": {
"body-parser": "^1.20.2",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"lodash": "^4.17.21",
"multer": "^1.4.5-lts.1",
"sqlite3": "^5.1.7",
"triple-beam": "^1.4.1",
"uuid": "^9.0.1"
}
}

4
dev/sql/setup.sql Normal file
View File

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS queue (
id TEXT(36) PRIMARY KEY,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

43
dev/src/index.ts Normal file
View File

@ -0,0 +1,43 @@
import 'dotenv/config';
import express from 'express';
import { Express, Request, Response, NextFunction } from 'express'
import fs from 'fs/promises';
import { join } from 'path';
import { Database } from 'sqlite3';
import bodyParser from 'body-parser';
import multer from 'multer';
import { v4 as uuid } from 'uuid';
import { createLog } from './log'
import type { Logger } from 'winston';
const PORT : number = typeof process.env['PORT'] !== 'undefined' ? parseInt(process.env['PORT'], 10) : 9999;
let db : Database;
const app : Express = express();
const log : Logger = createLog('server');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
async function setup () {
//db = new Database('./data.sqlite');
}
async function report (req : Request, res: Response, next : NextFunction) {
//
log.info(`Added record`);
res.json({ success : true });
}
app.post('/', report);
async function main () {
await setup();
app.listen(PORT, async () => {
log.info(`contact_printer_dev_server running on port ${PORT}`);
});
}
main();

63
dev/src/log.ts Normal file
View File

@ -0,0 +1,63 @@
'use strict'
/** @module log */
/** Wrapper for winston that tags streams and optionally writes files with a simple interface. */
/** Module now also supports optional papertrail integration, other services to follow */
import { format, transports, createLogger } from 'winston';
const { SPLAT } = require('triple-beam');
const { isObject } = require('lodash');
const APP_NAME : string = process.env.APP_NAME || 'default';
let winstonPapertrail;
function formatObject (param : any) {
if (isObject(param)) {
return JSON.stringify(param);
}
return param;
}
const all = format((info : any) => {
const splat = info[SPLAT] || [];
const message = formatObject(info.message);
const rest = splat.map(formatObject).join(' ');
info.message = `${message} ${rest}`;
return info;
});
const myFormat = format.printf(({ level, message, label, timestamp } : any) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});
/**
* Returns a winston logger configured to service
*
* @param {string} label Label appearing on logger
* @param {string} filename Optional file to write log to
*
* @returns {object} Winston logger
*/
export function createLog (label : string, filename : string = null) {
const tports : any[] = [ new (transports.Console)() ];
const fmat : any = format.combine(
all(),
format.label({ label }),
format.timestamp({format: 'YYYY-MM-DD HH:mm:ss.SSS'}),
format.colorize(),
myFormat,
);
let papertrailOpts : any;
if (filename !== null) {
tports.push( new (transports.File)({ filename }) );
}
return createLogger({
format : fmat,
transports : tports
});
}
module.exports = { createLog };

19
dev/tsconfig.json Executable file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "ES2020",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"removeComments" : false,
"baseUrl" : "dist",
"outDir": "./dist/",
"rootDir" : "./src/",
"paths" : {
}
},
"exclude" : [
"./dist"
]
}

View File

@ -0,0 +1,22 @@
module,quantity,part,part_id,description
contact_printer,1040,2020 Aluminum extrusion mm,N/A,Sides and central frame 4x 260mm
contact_printer,840,2020 Aluminum extrusion mm,N/A,Top and bottom frame 2x 420mm
electronics_panel,4,M3 hex cap bolt 6mm,N/A,Attach the GPIO breakout board to the panel
electronics_panel,1,ESP32 GPIO breakout board,N/A,To make the ESP32 dev board easier to wire
electronics_panel,6,M3 sliding t slot nut,N/A,Attach the frame to the electronics_panel
electronics_panel,6,M3 hex cap bolt 8mm,N/A,Attach the electronics_panel to the frame
electronics_panel,1,L298N Motor driver module,N/A,Control the 3 motors using 2 channels
electronics_panel,1,ESP32 Dev board,N/A,Control the contact_printer
sprocketed_roller_invert_solid,1,608-RS Ball Bearing,608-RS,Reduces wobble in the rollers spin
sprocketed_roller_invert_solid,1,M3 hex cap bolt 12mm,N/A,Attaches the sprocketed_roller to the geared motor
takeup_panel_stock,6,M3 sliding t slot nut,N/A,Attach the frame to the takeup_panel_stock
takeup_panel_stock,6,M3 hex cap bolt 8mm,N/A,Attach the takeup_panel_stock to the frame
takeup_panel_stock,1,250RPM DC geared motor,JSX40-370,Drive the takeup of the stock pathway
takeup_panel_picture,7,M3 sliding t slot nut,N/A,Attach the frame to the takeup_panel_picture
takeup_panel_picture,7,M3 hex cap bolt 8mm,N/A,Attach the takeup_panel_picture to the frame
takeup_panel_picture,1,250RPM DC geared motor,JSX40-370,Drive the takeup of the picture pathway
panel,4,M4 hex bolt 40mm,N/A,Attach the lamp to the panel
panel,1,100RPM DC geared motor with encoder,N/A,Drive the sprocketed_roller
panel,6,M3 sliding t slot nut,N/A,Attach aluminum extrusions to panel
panel,6,M3 hex cap bolt 8mm,N/A,Attach panel to aluminum extrusions
panel,4,M3 hex cap bolt 6mm,N/A,Attach encoder motor to panel
1 module quantity part part_id description
2 contact_printer 1040 2020 Aluminum extrusion mm N/A Sides and central frame 4x 260mm
3 contact_printer 840 2020 Aluminum extrusion mm N/A Top and bottom frame 2x 420mm
4 electronics_panel 4 M3 hex cap bolt 6mm N/A Attach the GPIO breakout board to the panel
5 electronics_panel 1 ESP32 GPIO breakout board N/A To make the ESP32 dev board easier to wire
6 electronics_panel 6 M3 sliding t slot nut N/A Attach the frame to the electronics_panel
7 electronics_panel 6 M3 hex cap bolt 8mm N/A Attach the electronics_panel to the frame
8 electronics_panel 1 L298N Motor driver module N/A Control the 3 motors using 2 channels
9 electronics_panel 1 ESP32 Dev board N/A Control the contact_printer
10 sprocketed_roller_invert_solid 1 608-RS Ball Bearing 608-RS Reduces wobble in the rollers spin
11 sprocketed_roller_invert_solid 1 M3 hex cap bolt 12mm N/A Attaches the sprocketed_roller to the geared motor
12 takeup_panel_stock 6 M3 sliding t slot nut N/A Attach the frame to the takeup_panel_stock
13 takeup_panel_stock 6 M3 hex cap bolt 8mm N/A Attach the takeup_panel_stock to the frame
14 takeup_panel_stock 1 250RPM DC geared motor JSX40-370 Drive the takeup of the stock pathway
15 takeup_panel_picture 7 M3 sliding t slot nut N/A Attach the frame to the takeup_panel_picture
16 takeup_panel_picture 7 M3 hex cap bolt 8mm N/A Attach the takeup_panel_picture to the frame
17 takeup_panel_picture 1 250RPM DC geared motor JSX40-370 Drive the takeup of the picture pathway
18 panel 4 M4 hex bolt 40mm N/A Attach the lamp to the panel
19 panel 1 100RPM DC geared motor with encoder N/A Drive the sprocketed_roller
20 panel 6 M3 sliding t slot nut N/A Attach aluminum extrusions to panel
21 panel 6 M3 hex cap bolt 8mm N/A Attach panel to aluminum extrusions
22 panel 4 M3 hex cap bolt 6mm N/A Attach encoder motor to panel

View File

@ -0,0 +1,14 @@
quantity,part,part_id,price
4,"M4 hex bolt 40mm",N/A,244
25,"M3 sliding t slot nut",N/A,143
25,"M3 hex cap bolt 8mm",N/A,225
8,"M3 hex cap bolt 6mm",N/A,59
1,"M3 hex cap bolt 12mm",N/A,9
1,"L298N Motor driver module",N/A,288
1,"ESP32 GPIO breakout board",N/A,600
1,"ESP32 Dev board",N/A,667
1,"608-RS Ball Bearing",608-RS,18
2,"250RPM DC geared motor",JSX40-370,2998
1880,"2020 Aluminum extrusion mm",N/A,1141
1,"100RPM DC geared motor with encoder",N/A,1619
1950,TOTAL,N/A,8011
1 quantity part part_id price
2 4 M4 hex bolt 40mm N/A 244
3 25 M3 sliding t slot nut N/A 143
4 25 M3 hex cap bolt 8mm N/A 225
5 8 M3 hex cap bolt 6mm N/A 59
6 1 M3 hex cap bolt 12mm N/A 9
7 1 L298N Motor driver module N/A 288
8 1 ESP32 GPIO breakout board N/A 600
9 1 ESP32 Dev board N/A 667
10 1 608-RS Ball Bearing 608-RS 18
11 2 250RPM DC geared motor JSX40-370 2998
12 1880 2020 Aluminum extrusion mm N/A 1141
13 1 100RPM DC geared motor with encoder N/A 1619
14 1950 TOTAL N/A 8011

14
hardware/prices.csv Normal file
View File

@ -0,0 +1,14 @@
part,part_id,price,quantity,url
M3 hex nut,N/A,999,300,https://amzn.to/4hAnwjc
M3 hex cap bolt 6mm,N/A,726,100,https://amzn.to/3AwiZxo
M3 hex cap bolt 8mm,N/A,899,100,https://amzn.to/3YEvWNB
M3 hex cap bolt 12mm,N/A,836,100,https://amzn.to/48CGa5Y
M3 sliding t slot nut,N/A,599,105,https://amzn.to/48GRrSU
2020 Aluminum extrusion mm,N/A,7399,12200,https://amzn.to/418OicC
100RPM DC geared motor with encoder,N/A,1619,1,https://amzn.to/3UF707G
250RPM DC geared motor,JSX40-370,1499,1,https://amzn.to/3NWkcRL
ESP32 Dev board,N/A,1999,3,https://amzn.to/3NXCvGj
L298N Motor driver module,N/A,1149,4,https://amzn.to/4ellssy
ESP32 GPIO breakout board,N/A,1199,2,https://amzn.to/3UFjpbO
M4 hex bolt 40mm,N/A,609,10,https://amzn.to/4ikpYL8
608-RS Ball Bearing,608-RS,1779,100,https://amzn.to/4fKxDA7
1 part part_id price quantity url
2 M3 hex nut N/A 999 300 https://amzn.to/4hAnwjc
3 M3 hex cap bolt 6mm N/A 726 100 https://amzn.to/3AwiZxo
4 M3 hex cap bolt 8mm N/A 899 100 https://amzn.to/3YEvWNB
5 M3 hex cap bolt 12mm N/A 836 100 https://amzn.to/48CGa5Y
6 M3 sliding t slot nut N/A 599 105 https://amzn.to/48GRrSU
7 2020 Aluminum extrusion mm N/A 7399 12200 https://amzn.to/418OicC
8 100RPM DC geared motor with encoder N/A 1619 1 https://amzn.to/3UF707G
9 250RPM DC geared motor JSX40-370 1499 1 https://amzn.to/3NWkcRL
10 ESP32 Dev board N/A 1999 3 https://amzn.to/3NXCvGj
11 L298N Motor driver module N/A 1149 4 https://amzn.to/4ellssy
12 ESP32 GPIO breakout board N/A 1199 2 https://amzn.to/3UFjpbO
13 M4 hex bolt 40mm N/A 609 10 https://amzn.to/4ikpYL8
14 608-RS Ball Bearing 608-RS 1779 100 https://amzn.to/4fKxDA7

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
img/contact_printer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,94 +1,153 @@
#include "ContactPrinter.h" #include "ContactPrinter.h"
ContactPrinter::ContactPrinter () { ContactPrinter::ContactPrinter () {
SetDriveSpeed(drive_speed);
SetSpeedTakeup(takeup_speed);
} }
void ContactPrinter::Setup () { void ContactPrinter::Setup () {
pinMode(drive_pin, OUTPUT);
pinMode(takeup_picture_pin_cw, OUTPUT);
pinMode(takeup_picture_pin_ccw, OUTPUT);
pinMode(takeup_stock_pin_cw, OUTPUT);
pinMode(takeup_stock_pin_ccw, OUTPUT);
digitalWrite(drive_pin, LOW); pinMode(takeup_pin_dir_a, OUTPUT);
digitalWrite(takeup_picture_pin_cw, LOW); pinMode(takeup_pin_dir_b, OUTPUT);
digitalWrite(takeup_picture_pin_ccw, LOW);
digitalWrite(takeup_stock_pin_cw, LOW); pinMode(start_button_pin, INPUT_PULLUP);
digitalWrite(takeup_stock_pin_ccw, LOW);
drive_motor.Setup();
lamp.Setup();
ledcSetup(takeup_pwm_channel, pwm_frequency, pwm_resolution);
Serial.print("Attaching pin ");
Serial.print(takeup_pin_enable);
Serial.print(" to ledc channel ");
Serial.print(takeup_pwm_channel);
Serial.println(" for takeup");
ledcAttachPin(takeup_pin_enable, takeup_pwm_channel);
ledcWrite(takeup_pwm_channel, takeup_pwm_duty_cycle);
digitalWrite(takeup_pin_dir_a, LOW);
digitalWrite(takeup_pin_dir_b, LOW);
SetupTakeup();
SetupDrive();
start_time = millis();
//lamp.On();
} }
void ContactPrinter::Start () { void ContactPrinter::Start () {
RampTakeup(0, takeup_pwm, takeup_ramp_time); Serial.println("Start()");
delay(100); drive_motor.Start();
analogWrite(drive_pin, drive_pwm); StartTakeup();
run_time = timer;
running = true;
} }
void ContactPrinter::Stop () { void ContactPrinter::Stop () {
analogWrite(drive_pin, 0); Serial.println("Stop()");
delay(100); lamp.Off();
RampTakeup(takeup_pwm, 0, takeup_ramp_time); drive_motor.Stop();
StopTakeup();
run_time = timer;
running = false;
} }
void ContactPrinter::SetDirectionTakeup(bool dir) {
takeup_dir = dir;
}
void ContactPrinter::SetSpeedTakeup(float speed) { void ContactPrinter::SetSpeedTakeup(float speed) {
takeup_speed = speed; takeup_speed = speed;
takeup_pwm = round(speed * 255); takeup_pwm_duty_cycle = floor(speed * pwm_maximum);
Serial.print("Set takeup motors PWM = ");
Serial.print(takeup_pwm_duty_cycle);
Serial.print(" / ");
Serial.println(pwm_maximum);
} }
void ContactPrinter::SetSpeedDrive(float speed) { void ContactPrinter::StartTakeup () {
drive_speed = speed; ledcWrite(takeup_pwm_channel, takeup_pwm_duty_cycle);
drive_pwm = round(speed * 255); if (takeup_dir) {
digitalWrite(takeup_pin_dir_a, LOW);
digitalWrite(takeup_pin_dir_b, HIGH);
} else {
digitalWrite(takeup_pin_dir_a, HIGH);
digitalWrite(takeup_pin_dir_b, LOW);
}
} }
void ContactPrinter::SetDirectionStock(bool clockwise) { void ContactPrinter::StopTakeup() {
takeup_stock_cw = clockwise; digitalWrite(takeup_pin_dir_a, LOW);
digitalWrite(takeup_pin_dir_b, LOW);
ledcWrite(takeup_pwm_channel, 0);
} }
void ContactPrinter::SetDirectionPicture(bool clockwise) { void ContactPrinter::SetupTakeup () {
takeup_picture_cw = clockwise; SetDirectionTakeup(true);
SetSpeedTakeup(0.9);
}
void ContactPrinter::SetupDrive() {
//drive_motor.SetSpeed(speed);
//drive_motor.SetPWM(247);
drive_motor.SetLoad(load);
drive_motor.SetFPS(18.0);
} }
//linear //linear
void ContactPrinter::RampTakeup(uint16_t start, uint16_t end, uint16_t time) { void ContactPrinter::RampTakeup(uint16_t start_pwm, uint16_t end_pwm, uint16_t time) {
uint16_t steps = abs(start - end); takeup_ramp_steps = abs(start_pwm - end_pwm);
uint16_t step = round(time / steps); takeup_ramp_step = round(time / takeup_ramp_steps);
uint16_t pwm = start; takeup_pwm_duty_cycle = start_pwm;
uint8_t takeup_picture_pin; takeup_ramp_dir = end_pwm < start_pwm;
uint8_t takeup_stock_pin; takeup_ramp_current_step = 0;
bool dir = end < start; takeup_ramping = true;
if (takeup_picture_cw) { for (uint16_t i = 0; i < takeup_ramp_steps; i++) {
takeup_picture_pin = takeup_picture_pin_cw; if (takeup_pwm_duty_cycle <= 0 || takeup_pwm_duty_cycle >= pwm_maximum) {
analogWrite(takeup_picture_pin_ccw, 0);
} else {
takeup_picture_pin = takeup_picture_pin_ccw;
analogWrite(takeup_picture_pin_cw, 0);
}
if (takeup_stock_cw) {
takeup_stock_pin = takeup_stock_pin_cw;
analogWrite(takeup_stock_pin_ccw, 0);
} else {
takeup_stock_pin = takeup_stock_pin_cw;
analogWrite(takeup_stock_pin_cw, 0);
}
for (uint16_t i = 0; i < steps; i++) {
if (pwm <= 0 || pwm >= 256) {
break; break;
} }
analogWrite(takeup_picture_pin, pwm); ledcWrite(takeup_pwm_channel, takeup_pwm_duty_cycle);
analogWrite(takeup_stock_pin, pwm); delay(takeup_ramp_step);
delay(step); if (takeup_ramp_dir) {
if (dir) { takeup_pwm_duty_cycle++;
pwm++;
} else { } else {
pwm--; takeup_pwm_duty_cycle--;
} }
} }
takeup_ramping = false;
}
void ContactPrinter::ButtonLoop () {
if (!running && timer >= run_time + button_delay && digitalRead(start_button_pin) == LOW) {
Start();
} else if (running && timer >= run_time + button_delay && digitalRead(start_button_pin) == LOW) {
Stop();
}
} }
bool ContactPrinter::IsRunning () { bool ContactPrinter::IsRunning () {
return running; return running;
} }
void ContactPrinter::Loop () {
int32_t frame;
timer = millis();
if (initialized) {
ButtonLoop();
if (running) {
drive_motor.Loop();
frame = drive_motor.GetFrames();
if (!lamp.IsOn() && start_lamp > 0 && frame >= start_lamp) {
lamp.On();
}
if (lamp.IsOn() && stop_lamp > 0 && frame >= stop_lamp) {
lamp.Off();
}
if (stop_after > 0 && frame >= stop_after) {
Stop();
}
}
} else if (timer >= start_time + 100) {
initialized = true;
}
}

View File

@ -2,47 +2,91 @@
#define CONTACT_PRINTER #define CONTACT_PRINTER
#include <Arduino.h> #include <Arduino.h>
#include "DriveMotor.h"
#include "Lamp.h"
#include "WebGUI.h"
class ContactPrinter { class ContactPrinter {
private: private:
//use default drive motor pins
DriveMotor drive_motor;
Lamp lamp;
const uint16_t serial_delay = 5; const uint16_t serial_delay = 5;
const uint16_t baud = 57600; const uint16_t baud = 115200;
const uint8_t drive_pin = 7;
const uint8_t takeup_picture_pin_cw = 8;
const uint8_t takeup_picture_pin_ccw = 9;
const uint8_t takeup_stock_pin_cw = 10;
const uint8_t takeup_stock_pin_ccw = 11;
volatile float drive_speed = 1f; /* PINS */
volatile float takeup_speed = 1f; const uint8_t takeup_pin_enable = 21;
const uint8_t takeup_pin_dir_a = 22;
const uint8_t takeup_pin_dir_b = 23;
volatile uint16_t drive_pwm; const uint8_t start_button_pin = 15;
volatile uint16_t takeup_pwm;
volatile bool takeup_picture_cw = false; /* MOTOR PWM */
volatile bool takeup_picture_ccw = true; const uint32_t pwm_frequency = 5000;
const uint8_t takeup_pwm_channel = 1;
const uint8_t pwm_resolution = 8;
const uint16_t pwm_maximum = 255; //8 = 255, 10 = 1024, 16 = 65535
volatile uint16_t takeup_ramp_time = 500; /* BUTTONS */
const uint16_t button_delay = 500;
/* MEMORY */
volatile long timer = 0;
volatile long start_time = 0;
volatile long run_time = 0;
volatile float drive_speed = 1.0; //CHANGE
volatile float takeup_speed = 1.0; //percentage of max PWM
volatile uint16_t takeup_pwm_duty_cycle = 0;
volatile uint16_t takeup_ramp_steps = 0; //# of steps
volatile uint16_t takeup_ramp_step = 0; //length of step (ms)
volatile boolean takeup_ramp_dir = true; //true = up, false = down
volatile uint16_t takeup_ramp_time = 500; //default ramp time (ms)
volatile long takeup_ramp_start = 0; //time to start ramping
volatile long takeup_ramp_current_step = 0;
volatile long takeup_ramp_next_step_start = 0;
volatile boolean takeup_ramping = false;
volatile uint8_t load = 2; //0 = no load, 1 = single thread, 2 = dual thread
volatile uint32_t start_lamp = 24;
volatile uint32_t stop_lamp = -1;
volatile uint32_t stop_after = -1;
volatile bool takeup_dir = true;
volatile bool initialized = false;
volatile bool running = false; volatile bool running = false;
public: public:
ContactPrinter(); ContactPrinter();
void Setup(); void Setup();
void Loop();
void Start(); void Start();
void Stop(); void Stop();
void SetSpeedTakeup(float speed); void SetSpeedTakeup(float speed); //percent
void SetSpeedDrive(float speed); void SetupDrive();
void SetDirectionStock(bool clockwise); void SetupTakeup();
void SetDirectionPicture(bool clockwise); void SetDirectionTakeup(bool dir);
void StartTakeup();
void StopTakeup();
void EnableTakeup();
void RampTakeup(uint16_t start, uint16_t end, uint16_t time); void RampTakeup(uint16_t start, uint16_t end, uint16_t time);
void RampTakeupLoop();
void ButtonLoop();
bool IsRunning (); bool IsRunning ();
}; };
#endif #endif

View File

@ -0,0 +1,192 @@
#include "DriveMotor.h"
DriveMotor::DriveMotor () {
};
void DriveMotor::Setup () {
pinMode(enable_pin, OUTPUT);
pinMode(forward_pin, OUTPUT);
pinMode(backward_pin, OUTPUT);
pinMode(encoder_a_pin, INPUT);
pinMode(encoder_b_pin, INPUT);
ledcSetup(pwm_channel, pwm_frequency, pwm_resolution);
Serial.print("Attaching pin ");
Serial.print(enable_pin);
Serial.print(" to ledc channel ");
Serial.print(pwm_channel);
Serial.println(" for drive");
ledcAttachPin(enable_pin, pwm_channel);
ledcWrite(pwm_channel, pwm_duty_cycle);
digitalWrite(forward_pin, LOW);
digitalWrite(backward_pin, LOW);
attachInterrupt(digitalPinToInterrupt(encoder_b_pin), ReadEncoder, RISING);
}
void DriveMotor::Start() {
pulses = 0;
fps = 0.0;
fps_max = -1.0;
fps_min = 100000.0;
fps_avg = -1.0;
rpm = 0.0;
rpm_max = -1.0;
rpm_min = 100000.0;
rpm_avg = -1.0;
start_frame = frames;
start_rotation = rotations;
start_time = millis();
ledcWrite(pwm_channel, pwm_duty_cycle);
digitalWrite(forward_pin, HIGH);
digitalWrite(backward_pin, LOW);
}
void DriveMotor::Stop() {;
digitalWrite(forward_pin, LOW);
digitalWrite(backward_pin, LOW);
ledcWrite(pwm_channel, 0);
Report();
}
void DriveMotor::SetLoad(uint8_t loadInt) {
load = loadInt;
}
void DriveMotor::SetSpeed(float speed) {
pwm_duty_cycle = floor(pwm_maximum * speed);
Serial.print("Set drive motor PWM = ");
Serial.print(pwm_duty_cycle);
Serial.print(" / ");
Serial.println(pwm_maximum);
}
void DriveMotor::SetPWM(uint32_t pwm) {
pwm_duty_cycle = pwm;
Serial.print("Set drive motor PWM = ");
Serial.print(pwm_duty_cycle);
Serial.print(" / ");
Serial.println(pwm_maximum);
}
void DriveMotor::SetFPS(float fps) {
target_fps = fps;
Serial.print("FPS = ");
Serial.println(fps);
SetPWM(EstimatePWMFromFPS(fps));
}
int32_t DriveMotor::pulses = 0;
void DriveMotor::ReadEncoder () {
int b = digitalRead(encoder_b_pin);
if (b > 0) {
pulses++;
} else {
pulses--;
}
}
void DriveMotor::Loop () {
timer = millis();
//monitor speed
frames = (int32_t) floor((float) pulses / pulses_per_frame);
if (frames != last_frame) {
last_frame = frames;
fps = CalculateFPS(timer - start_time, frames - start_frame);
if (fps < fps_min) { fps_min = fps; }
if (fps > fps_max) { fps_max = fps; }
if (fps_avg < 0.0) {
fps_avg = fps;
} else {
fps_avg = (fps_avg + fps) / 2.0;
}
Serial.print("Frame: ");
Serial.println(frames);
}
rotations = (int32_t) floor((float) pulses / (float) pulses_per_rotation);
if (rotations != last_rotation) {
last_rotation = rotations;
//correction
frames = rotations * frames_per_rotation;
rpm = CalculateRPM(timer - start_time, rotations - start_rotation);
if (rpm < rpm_min) { rpm_min = rpm; }
if (rpm > rpm_max) { rpm_max = rpm; }
if (rpm_avg < 0.0) {
rpm_avg = rpm;
} else {
rpm_avg = (rpm_avg + rpm) / 2.0;
}
Serial.print("Rotation: ");
Serial.println(rotations);
}
}
void DriveMotor::Report () {
fps = CalculateFPS(timer - start_time, frames - start_frame);
rpm = CalculateRPM(timer - start_time, rotations - start_rotation);
Serial.print("RPM ");
Serial.println(rpm);
Serial.print("RPM avg: ");
Serial.println(rpm_avg);
Serial.print("RPM min: ");
Serial.println(rpm_min);
Serial.print("RPM max: ");
Serial.println(rpm_max);
Serial.print("FPS ");
Serial.println(fps);
Serial.print("FPS avg: ");
Serial.println(fps_avg);
Serial.print("FPS min: ");
Serial.println(fps_min);
Serial.print("FPS max: ");
Serial.println(fps_max);
}
/* Helper methods */
float DriveMotor::CalculateFPS (long time_length, uint32_t frames) {
return 1000.0 / ((float) time_length / (float) frames);
}
float DriveMotor::CalculateRPM (long time_length, uint32_t rotations) {
return 60000.0 / ((float) time_length / (float) rotations);
}
int32_t DriveMotor::GetFrames () {
return frames;
}
int32_t DriveMotor::GetRotations () {
return rotations;
}
float DriveMotor::FloatMap (float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
uint16_t DriveMotor::EstimatePWMFromFPS (float fps) {
float min, max;
float max_pwm = pwm_range[0];
float min_pwm = pwm_range[1];
switch (load) {
case 0 :
max = load_none[0];
min = load_none[1];
break;
case 1 :
max = load_one[0];
min = load_one[1];
break;
case 2 :
max = load_two[0];
min = load_two[1];
break;
}
return (uint16_t) floor(FloatMap(fps, min, max, (float) min_pwm, (float) max_pwm));
}

View File

@ -0,0 +1,95 @@
#ifndef DRIVE_MOTOR
#define DRIVE_MOTOR
#include <Arduino.h>
class DriveMotor {
private:
//defaults are for EPS32 dev board
static const uint8_t enable_pin = 26;
static const uint8_t forward_pin = 27; //Clockwise
static const uint8_t backward_pin = 14; //Counter-clockwise
static const uint8_t encoder_a_pin = 33;
static const uint8_t encoder_b_pin = 25;
const uint32_t pwm_frequency = 5000;
const uint8_t pwm_channel = 0;
const uint8_t pwm_resolution = 8;
const uint16_t pwm_maximum = 255; //8 = 255, 10 = 1024, 16 = 65535
const uint8_t ppr = 11;
const float ratio = 187.0 / 3.0;
const uint32_t pulses_per_rotation = (int) round((float) ppr * ratio);
const uint8_t frames_per_rotation = 18;
const float pulses_per_frame = (float) pulses_per_rotation / (float) frames_per_rotation;
//pwm ranges for mapping to estimated fps
const uint16_t pwm_range[2] = { 255, 210 };
const float load_none[2] = { 25.4, 16.75 };
const float load_one [2] = { 24.8, 16.5 };
const float load_two [2] = { 21.9, 13.4 };
volatile uint8_t load = 2;
volatile uint16_t pwm_duty_cycle = 0;
static int32_t pulses;
//state
volatile long timer = 0;
volatile long start_time = 0;
volatile int32_t start_rotation = 0;
volatile int32_t start_frame = 0;
//measure
volatile float rpm = 0.0;
volatile float rpm_max = -1.0;
volatile float rpm_min = 100000.0;
volatile float rpm_avg = -1.0;
volatile int32_t rotations = 0;
volatile int32_t last_rotation = 0;
volatile float fps = 0.0;
volatile float fps_max = -1.0;
volatile float fps_min = 100000.0;
volatile float fps_avg = -1.0;
volatile int32_t frames = 0;
volatile int32_t last_frame = 0;
//target
volatile float target_fps = 0.0;
volatile float target_rpm = 0.0;
volatile float test[1000];
float CalculateFPS (long time_length, uint32_t frames);
float CalculateRPM (long time_length, uint32_t rotations);
float FloatMap(float x, float in_min, float in_max, float out_min, float out_max);
uint16_t EstimatePWMFromFPS(float);
void Report();
public:
DriveMotor();
void Setup();
void Loop();
void Start();
void Stop();
void SetLoad(uint8_t loadInt);
void SetSpeed(float speed);
void SetPWM(uint32_t pwm);
void SetFPS(float fps);
int32_t GetFrames();
int32_t GetRotations();
protected:
static void ReadEncoder();
};
#endif

View File

@ -0,0 +1,26 @@
#include "Lamp.h"
Lamp::Lamp () {
//
}
void Lamp::Setup() {
pinMode(lamp_pin_a, OUTPUT);
digitalWrite(lamp_pin_a, LOW);
Serial.print("Simple white LED lamp on pin: ");
Serial.println(lamp_pin_a);
}
void Lamp::On () {
digitalWrite(lamp_pin_a, HIGH);
on = true;
}
void Lamp::Off () {
digitalWrite(lamp_pin_a, LOW);
on = false;
}
boolean Lamp::IsOn () {
return on;
}

View File

@ -0,0 +1,21 @@
#ifndef LAMP
#define LAMP
#include <Arduino.h>
class Lamp {
private:
const uint8_t lamp_pin_a = 32;
volatile boolean on = false;
public:
Lamp();
void Setup();
void Loop();
void On();
void Off();
boolean IsOn();
};
#endif

View File

@ -0,0 +1,26 @@
#include "WebGUI.h"
void WebGUI::Setup () {
}
void WebGUI::Loop () {
}
void WebGUI::HandleNotFound() {
String res = "404 Not Found\n\n";
res += "URI: ";
res += server.uri();
res += "\nMethod: ";
res += (server.method() == HTTP_GET) ? "GET" : "POST";
res += "\nArguments: ";
res += server.args();
res += "\n";
for (uint8_t i = 0; i < server.args(); i++) {
res += " " + server.argName(i) + ": " + server.arg(i) + "\n";
}
server.send(404, "text/plain", res);
}

View File

@ -0,0 +1,32 @@
#ifndef WEBGUI
#define WEBGUI
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiAP.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <ArduinoJson.h>
class WebGUI {
private:
const char *AP_SSID = "contact_printer";
const char *DEFAULT_PASSWORD = "contact_printer";
const char *MDNS_NAME = "contact_printer";
//IPAddress wifiIP;
//String IP;
//DynamicJsonDocument postJSON(1024);
WebServer server(80);
protected:
static void HandleNotFound();
public:
WebGUI();
void Setup();
void Loop();
};
#endif

View File

@ -1,4 +1,38 @@
#include "ContactPrinter.h"; #include "ContactPrinter.h";
void setup () {} #define VERSION "0.2.2"
void loop () {}
/**
*
*
* Target Board: ESP32 Dev Kit
*
* Pins
*
* 21 Takeup Picture Enable - set duty rate
* 22 Takeup Direction A - Stock Clockwise, Picture Counter Clockwise
* 23 Takeup Direction B - Stoc k Counter Clockwise, Picture Clockwise
*
* 26 Drive Enable
* 27 Drive Forward (Clockwise)
* 14 Drive Backward (Counter Clockwise)
* 33 Drive Encoder A
* 25 Drive Encoder B
*
* 15 Start Button
*
* 32 Lamp
*
**/
ContactPrinter contact_printer;
void setup () {
Serial.begin(115200);
Serial.print("contact_printer v");
Serial.println(VERSION);
contact_printer.Setup();
}
void loop () {
contact_printer.Loop();
}

View File

@ -6,15 +6,20 @@
*********/ *********/
// Motor A // Motor A
int motor1Pin1 = 27; //int motor1Pin1 = 14;
int motor1Pin2 = 26; //int motor1Pin2 = 27;
int enable1Pin = 14; //int enable1Pin = 26;
// Motor B
int motor1Pin1 = 5;
int motor1Pin2 = 18;
int enable1Pin = 19;
// Setting PWM properties // Setting PWM properties
const int freq = 30000; const int freq = 30000;
const int pwmChannel = 0; const int pwmChannel = 0;
const int resolution = 8; const int resolution = 8;
int dutyCycle = 200; int dutyCycle = 250;
void setup() { void setup() {
// sets the pins as outputs: // sets the pins as outputs:
@ -28,6 +33,8 @@ void setup() {
// attach the channel to the GPIO to be controlled // attach the channel to the GPIO to be controlled
ledcAttachPin(enable1Pin, pwmChannel); ledcAttachPin(enable1Pin, pwmChannel);
ledcWrite(pwmChannel, dutyCycle);
Serial.begin(115200); Serial.begin(115200);
// testing // testing
@ -59,15 +66,4 @@ void loop() {
digitalWrite(motor1Pin2, LOW); digitalWrite(motor1Pin2, LOW);
delay(1000); delay(1000);
// Move DC motor forward with increasing speed
digitalWrite(motor1Pin1, HIGH);
digitalWrite(motor1Pin2, LOW);
while (dutyCycle <= 255){
ledcWrite(pwmChannel, dutyCycle);
Serial.print("Forward with duty cycle: ");
Serial.println(dutyCycle);
dutyCycle = dutyCycle + 5;
delay(500);
}
dutyCycle = 200;
} }

View File

@ -0,0 +1,19 @@
#include <Adafruit_NeoPixel.h>
#define PIN 15
#define NUM 1
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUM, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
Serial.begin(115200);
pixels.begin();
pixels.setBrightness(255);
pixels.fill(0xFF0000);
pixels.show();
}
void loop () {
pixels.show();
}

View File

View File

@ -0,0 +1,31 @@
/** ______________
* | |
* | |
* | TIP120 |
* | |
* | |
* --------------
* || || ||
* || || ||
* || || ||
* || || ||
* 2.2K
* |
* |
* \/
* 8 pin
**/
int signalPin = 8;
void setup () {
pinMode(signalPin, OUTPUT);
}
void loop () {
digitalWrite(signalPin, HIGH);
delay(1000);
digitalWrite(signalPin, LOW);
delay(2000);
}

View File

@ -0,0 +1,170 @@
/***
*
* 100RPM 12VDC Worm Gear Motor w/ encoder
* SKU - JGY-370
* DC12V100RPM - SKU-GS00127-05
*
* Gear ratio: 40:1(?) 60:1(?)
* 11 PPR Encoder - (!)
*
* RedMotor power + (exchange can control rotating and reversing)
* BlackCoding power- negative (3.3-5V) polarity cannot be wrong
* YellowSignal feedback
* GreenSignal feedback
* BlueCoding power + positive(3.3-5V)polarity cannot be wrong
* WhiteMotor power - (exchange can control rotating and
*
***/
#include <util/atomic.h> // For the ATOMIC_BLOCK macro
#define ENCA 2 // YELLOW
#define ENCB 3 // WHITE
#define MOTORA 10
#define MOTORB 11
volatile int32_t posi = 0; // specify posi as volatile
const long maxTime = 100000;
const int32_t maxRotations = 10;
const int ppr = 11;
const float ratio = 187.0 / 3.0;
const int maxPulses = (int) round((float) ppr * ratio);
const int speed = 255;
const int framesPerRotation = 18;
const float pulsesPerFrame = (float) maxPulses / (float) framesPerRotation;
volatile long start = 0;
volatile long total;
volatile bool done = false;
volatile bool stop = false;
volatile int32_t incoming;
volatile float rpm;
volatile float fps;
volatile float fpsMax = -1.0;
volatile float fpsMin = 100000.0;
volatile int32_t rotations = 0;
volatile int32_t lastRotationPosition = 0;
volatile int32_t lastFramePosition = 0;
volatile int32_t frames = 0;
float calculateFPS (long timeLength, int frames) {
return 1000.0 / ((float) timeLength / (float) frames);
}
float calculateRPM (long rotationLength) {
return 60000.0 / (float) (rotationLength);
}
void setup() {
Serial.begin(57600);
Serial.flush();
pinMode(ENCA, INPUT);
pinMode(ENCB, INPUT);
pinMode(MOTORA, OUTPUT);
pinMode(MOTORB, OUTPUT);
attachInterrupt(digitalPinToInterrupt(ENCA), readEncoder,RISING);
Serial.println("Connected");
Serial.print("PPR: ");
Serial.println(ppr);
Serial.print("Ratio: ");
Serial.println(ratio);
Serial.print("Pulses: ");
Serial.println(maxPulses);
Serial.print("Frames per Rotation: ");
Serial.println(framesPerRotation);
Serial.print("Pulses per Frame: ");
Serial.println(pulsesPerFrame);
}
void loop() {
int32_t pos;
// Read the position in an atomic block to avoid a potential
// misread if the interrupt coincides with this code running
// see: https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/volatile/
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
pos = posi;
}
rotations = (int) floor((float) pos / (float) maxPulses);
if (rotations != lastRotationPosition) {
lastRotationPosition = rotations;
frames = rotations * framesPerRotation;
Serial.print("Rotations: ");
Serial.print(rotations);
Serial.print(" = ");
Serial.println(millis() - start);
}
frames = (int) floor((float) pos / pulsesPerFrame);
if (frames != lastFramePosition) {
lastFramePosition = frames;
total = millis() - start;
fps = calculateFPS(total, frames);
if (fps < 10000.0 && fps > fpsMax) {
fpsMax = fps;
}
if (fps < fpsMin) {
fpsMin = fps;
}
//Serial.print("Frames: ");
//Serial.print(frames);
//Serial.print(" = ");
//Serial.println(total);
//Serial.println(fps);
//Serial.print("@");
//Serial.println(pos);
}
if (abs(pos) >= maxPulses * maxRotations) {
stop = true;
}
if (start == -1) {
delay(1000);
analogWrite(MOTORA, speed);
digitalWrite(MOTORB, LOW);
start = millis();
rotations = 0;
} else if (stop && !done) {
digitalWrite(MOTORA, LOW);
digitalWrite(MOTORB, LOW);
//Serial.print("Final: ");
//Serial.println(pos);
Serial.print("Time: ");
total = millis() - start;
Serial.println(total);
rpm = calculateRPM(total) * (float) (rotations + 1);
fps = calculateFPS(total, frames);
Serial.print("RPM: ");
Serial.println(rpm);
Serial.print("FPS: ");
Serial.println(fps);
Serial.print(" Min: ");
Serial.println(fpsMin);
Serial.print(" Max: ");
Serial.println(fpsMax);
Serial.print("Rotations: ");
Serial.println(rotations);
Serial.print("Frames: ");
Serial.println(frames);
done = true;
}
if (Serial.available() > 0) {
incoming = Serial.read();
start = -1;
posi = 0;
done = false;
stop = false;
}
}
void readEncoder(){
int b = digitalRead(ENCB);
if(b > 0){
posi++;
}
else{
posi--;
}
}

View File

@ -1,107 +0,0 @@
/***
*
* 100RPM 12VDC Worm Gear Motor w/ encoder
* SKU - JGY-370
* DC12V100RPM - SKU-GS00127-05
*
* Gear ratio: 40:1(?) 60:1(?)
* 11 PPR Encoder - (!)
*
* RedMotor power + (exchange can control rotating and reversing)
* BlackCoding power- negative (3.3-5V) polarity cannot be wrong
* YellowSignal feedback
* GreenSignal feedback
* BlueCoding power + positive(3.3-5V)polarity cannot be wrong
* WhiteMotor power - (exchange can control rotating and
*
***/
#include <util/atomic.h> // For the ATOMIC_BLOCK macro
#define ENCA 2 // YELLOW
#define ENCB 3 // WHITE
#define MOTORA 10
#define MOTORB 11
volatile int posi = 0; // specify posi as volatile
const long maxTime = 10000;
const int ppr = 11;
const float ratio = 62.0;
const int maxPulses = (int) round((float) ppr * ratio);
const int speed = 40;
volatile long start = 0;
volatile bool done = false;
volatile bool stop = false;
volatile int incoming;
volatile float rpm;
void setup() {
Serial.begin(57600);
Serial.flush();
pinMode(ENCA, INPUT);
pinMode(ENCB, INPUT);
pinMode(MOTORA, OUTPUT);
pinMode(MOTORB, OUTPUT);
attachInterrupt(digitalPinToInterrupt(ENCA), readEncoder,RISING);
Serial.println("Connected");
Serial.print("PPR: ");
Serial.println(ppr);
Serial.print("Ratio: ");
Serial.println(ratio);
Serial.print("Pulses: ");
Serial.println(maxPulses);
}
void loop() {
int pos;
if (Serial.available() > 0) {
incoming = Serial.read();
start = -1;
posi = 0;
done = false;
stop = false;
}
// Read the position in an atomic block to avoid a potential
// misread if the interrupt coincides with this code running
// see: https://www.arduino.cc/reference/en/language/variables/variable-scope-qualifiers/volatile/
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
pos = posi;
}
if (abs(pos) == maxPulses) {
stop = true;
}
if (start == -1) {
delay(1000);
analogWrite(MOTORA, speed);
digitalWrite(MOTORB, LOW);
start = millis();
} else if ((stop || millis() - start >= maxTime) && !done) {
digitalWrite(MOTORA, LOW);
digitalWrite(MOTORB, LOW);
Serial.print("Final: ");
Serial.println(pos);
Serial.print("Time: ");
Serial.println(millis() - start);
rpm = (float) 60000 / (float) (millis() - start);
Serial.print("RPM: ");
Serial.println(rpm);
done = true;
}
}
void readEncoder(){
int b = digitalRead(ENCB);
if(b > 0){
posi++;
}
else{
posi--;
}
}

View File

@ -1,18 +1,22 @@
2020_tslot_insert 2020_tslot_insert
bearing_post_nut bearing_post_nut
blank
corner_foot corner_foot
daylight_spool_insert_reinforced daylight_spool_insert_reinforced
daylight_spool_insert_reinforced_nut daylight_spool_insert_reinforced_nut
electronics_panel
feed_panel_motor_mount feed_panel_motor_mount
feed_panel_picture feed_panel_picture
feed_panel_stock feed_panel_stock
filter_carrier
full_gate full_gate
gate_carrier
gate_holder gate_holder
idle_roller_half_a idle_roller_half_a
idle_roller_half_b idle_roller_half_b
lamp_LEDs
lamp_cover lamp_cover
lamp_dual lamp_dual
lamp_LEDs
lamp_single lamp_single
magnetic_coupling magnetic_coupling
motor_controller_panel motor_controller_panel
@ -22,6 +26,9 @@ slip_coupling
sound_gate sound_gate
sprocketed_roller sprocketed_roller
sprocketed_roller_invert sprocketed_roller_invert
sprocketed_roller_invert_solid
sprocketed_roller_solid
sprocketed_wheel
super_gate super_gate
takeup_panel_picture takeup_panel_picture
takeup_panel_picture_motor_mount takeup_panel_picture_motor_mount

12
notes/LinearRegression.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef LINEAR_REGRESSION
#define LINEAR_REGRESSION
class LinearRegression {
private:
uint32_t i;
public:
LinearRegression();
};
#endif

493
notes/motor_encoder_poc.txt Normal file
View File

@ -0,0 +1,493 @@
Connected
11
Ratio: 62.00
Pulses: 682
Frames per Rotation: 18
Pulses per Frame: 37.89
Connected
PPR: 11
Ratio: 62.00
Pulses: 682
Frames per Rotation: 18
Pulses per Frame: 37.89
Time: 7124
RPM: 92.64
FPS: 39577.78
Rotations: 11
Frames: 180
------------------------
Connected
PPR: 11
Ratio: 62.00
Pulses: 682
Frames per Rotation: 18
Pulses per Frame: 37.89
Connected
PPR: 11
Ratio: 62.00
Pulses: 682
Frames per Rotation: 18
Pulses per Frame: 37.89
Frames: 20 = 0
Frames: 21 = 12
Frames: 22 = 60
Frames: 23 = 107
Frames: 24 = 156
Frames: 25 = 203
Frames: 26 = 250
Frames: 27 = 296
Frames: 28 = 343
Frames: 29 = 389
Frames: 30 = 435
Frames: 31 = 480
Frames: 32 = 526
Frames: 33 = 572
Frames: 34 = 618
Frames: 35 = 664
Frames: 36 = 710
Frames: 37 = 757
Frames: 38 = 804
Frames: 39 = 853
Frames: 40 = 901
Frames: 41 = 950
Frames: 42 = 998
Frames: 43 = 1046
Frames: 44 = 1094
Frames: 45 = 1139
Frames: 46 = 1186
Frames: 47 = 1232
Frames: 48 = 1277
Frames: 49 = 1324
Frames: 50 = 1369
Frames: 51 = 1415
Frames: 52 = 1461
Frames: 53 = 1507
Frames: 54 = 1552
Frames: 55 = 1599
Frames: 56 = 1646
Frames: 57 = 1693
Frames: 58 = 1741
Frames: 59 = 1789
Frames: 60 = 1838
Frames: 61 = 1886
Frames: 62 = 1934
Frames: 63 = 1979
Frames: 64 = 2026
Frames: 65 = 2072
Frames: 66 = 2117
Frames: 67 = 2163
Frames: 68 = 2208
Frames: 69 = 2254
Frames: 70 = 2300
Frames: 71 = 2347
Frames: 72 = 2392
Frames: 73 = 2440
Frames: 74 = 2487
Frames: 75 = 2535
Frames: 76 = 2582
Frames: 77 = 2631
Frames: 78 = 2679
Frames: 79 = 2727
Frames: 80 = 2775
Frames: 81 = 2821
Frames: 82 = 2868
Frames: 83 = 2914
Frames: 84 = 2959
Frames: 85 = 3005
Frames: 86 = 3051
Frames: 87 = 3096
Frames: 88 = 3142
Frames: 89 = 3188
Frames: 90 = 3234
Frames: 91 = 3281
Frames: 92 = 3329
Frames: 93 = 3377
Frames: 94 = 3424
Frames: 95 = 3472
Frames: 96 = 3521
Frames: 97 = 3569
Frames: 98 = 3616
Frames: 99 = 3662
Frames: 100 = 3708
Frames: 101 = 3755
Frames: 102 = 3801
Frames: 103 = 3846
Frames: 104 = 3892
Frames: 105 = 3938
Frames: 106 = 3983
Frames: 107 = 4029
Frames: 108 = 4074
Frames: 109 = 4121
Frames: 110 = 4169
Frames: 111 = 4216
Frames: 112 = 4264
Frames: 113 = 4313
Frames: 114 = 4361
Frames: 115 = 4409
Frames: 116 = 4457
Frames: 117 = 4503
Frames: 118 = 4549
Frames: 119 = 4595
Frames: 120 = 4641
Frames: 121 = 4686
Frames: 122 = 4732
Frames: 123 = 4779
Frames: 124 = 4824
Frames: 125 = 4870
Frames: 126 = 4915
Frames: 127 = 4962
Frames: 128 = 5009
Frames: 129 = 5057
Frames: 130 = 5105
Frames: 131 = 5153
Frames: 132 = 5201
Frames: 133 = 5250
Frames: 134 = 5298
Frames: 135 = 5344
Frames: 136 = 5390
Frames: 137 = 5436
Frames: 138 = 5482
Frames: 139 = 5527
Frames: 140 = 5573
Frames: 141 = 5619
Frames: 142 = 5665
Frames: 143 = 5710
Frames: 144 = 5755
Frames: 145 = 5803
Frames: 146 = 5850
Frames: 147 = 5897
Frames: 148 = 5945
Frames: 149 = 5992
Frames: 150 = 6040
Frames: 151 = 6089
Frames: 152 = 6136
Frames: 153 = 6182
Frames: 154 = 6230
Frames: 155 = 6276
Frames: 156 = 6321
Frames: 157 = 6367
Frames: 158 = 6413
Frames: 159 = 6458
Frames: 160 = 6504
Frames: 161 = 6550
Frames: 162 = 6595
Frames: 163 = 6642
Frames: 164 = 6689
Frames: 165 = 6737
Frames: 166 = 6785
Frames: 167 = 6834
Frames: 168 = 6882
Frames: 169 = 6930
Frames: 170 = 6978
Frames: 171 = 7023
Frames: 172 = 7070
Frames: 173 = 7116
Frames: 174 = 7162
Frames: 175 = 7207
Frames: 176 = 7254
Frames: 177 = 7299
Frames: 178 = 7345
Frames: 179 = 7391
Frames: 180 = 7435
Time: 7436
RPM: 88.76
FPS: 24.21
Min: 24.21
Max: 1750.00
Rotations: 10
Frames: 180
-------------------------------
FPS avg
Connected
PPR: 11
Ratio: 62.00
Pulses: 682
Frames per Rotation: 18
Pulses per Frame: 37.89
Frames: 20 = 20000.00
Frames: 21 = 1500.00
Frames: 22 = 354.84
Frames: 23 = 209.09
Frames: 24 = 151.90
Frames: 25 = 120.77
Frames: 26 = 101.96
Frames: 27 = 89.70
Frames: 28 = 80.46
Frames: 29 = 73.60
Frames: 30 = 68.18
Frames: 31 = 63.92
Frames: 32 = 60.26
Frames: 33 = 57.19
Frames: 34 = 54.57
Frames: 35 = 52.24
Frames: 36 = 50.35
Frames: 37 = 48.49
Frames: 38 = 46.86
Frames: 39 = 45.45
Frames: 40 = 44.15
Frames: 41 = 42.93
Frames: 42 = 41.87
Frames: 43 = 40.87
Frames: 44 = 40.04
Frames: 45 = 39.27
Frames: 46 = 38.56
Frames: 47 = 37.93
Frames: 48 = 37.35
Frames: 49 = 36.81
Frames: 50 = 36.34
Frames: 51 = 35.86
Frames: 52 = 35.42
Frames: 53 = 35.01
Frames: 54 = 34.64
Frames: 55 = 34.25
Frames: 56 = 33.88
Frames: 57 = 33.51
Frames: 58 = 33.16
Frames: 59 = 32.83
Frames: 60 = 32.52
Frames: 61 = 32.21
Frames: 62 = 31.93
Frames: 63 = 31.69
Frames: 64 = 31.45
Frames: 65 = 31.22
Frames: 66 = 31.02
Frames: 67 = 30.83
Frames: 68 = 30.64
Frames: 69 = 30.46
Frames: 70 = 30.29
Frames: 71 = 30.12
Frames: 72 = 29.98
Frames: 73 = 29.81
Frames: 74 = 29.65
Frames: 75 = 29.48
Frames: 76 = 29.33
Frames: 77 = 29.17
Frames: 78 = 29.02
Frames: 79 = 28.87
Frames: 80 = 28.74
Frames: 81 = 28.62
Frames: 82 = 28.50
Frames: 83 = 28.40
Frames: 84 = 28.30
Frames: 85 = 28.20
Frames: 86 = 28.10
Frames: 87 = 28.01
Frames: 88 = 27.91
Frames: 89 = 27.83
Frames: 90 = 27.74
Frames: 91 = 27.66
Frames: 92 = 27.56
Frames: 93 = 27.47
Frames: 94 = 27.38
Frames: 95 = 27.29
Frames: 96 = 27.20
Frames: 97 = 27.10
Frames: 98 = 27.02
Frames: 99 = 26.95
Frames: 100 = 26.88
Frames: 101 = 26.81
Frames: 102 = 26.76
Frames: 103 = 26.70
Frames: 104 = 26.64
Frames: 105 = 26.59
Frames: 106 = 26.53
Frames: 107 = 26.48
Frames: 108 = 26.44
Frames: 109 = 26.37
Frames: 110 = 26.32
Frames: 111 = 26.26
Frames: 112 = 26.20
Frames: 113 = 26.15
Frames: 114 = 26.09
Frames: 115 = 26.03
Frames: 116 = 25.97
Frames: 117 = 25.92
Frames: 118 = 25.88
Frames: 119 = 25.83
Frames: 120 = 25.80
Frames: 121 = 25.76
Frames: 122 = 25.72
Frames: 123 = 25.68
Frames: 124 = 25.65
Frames: 125 = 25.61
Frames: 126 = 25.58
Frames: 127 = 25.54
Frames: 128 = 25.50
Frames: 129 = 25.46
Frames: 130 = 25.42
Frames: 131 = 25.38
Frames: 132 = 25.34
Frames: 133 = 25.29
Frames: 134 = 25.25
Frames: 135 = 25.22
Frames: 136 = 25.19
Frames: 137 = 25.16
Frames: 138 = 25.13
Frames: 139 = 25.10
Frames: 140 = 25.07
Frames: 141 = 25.05
Frames: 142 = 25.02
Frames: 143 = 25.00
Frames: 144 = 24.97
Frames: 145 = 24.95
Frames: 146 = 24.92
Frames: 147 = 24.89
Frames: 148 = 24.86
Frames: 149 = 24.83
Frames: 150 = 24.79
Frames: 151 = 24.76
Frames: 152 = 24.73
Frames: 153 = 24.70
Frames: 154 = 24.68
Frames: 155 = 24.65
Frames: 156 = 24.63
Frames: 157 = 24.61
Frames: 158 = 24.60
Frames: 159 = 24.57
Frames: 160 = 24.55
Frames: 161 = 24.53
Frames: 162 = 24.52
Frames: 163 = 24.50
Frames: 164 = 24.48
Frames: 165 = 24.45
Frames: 166 = 24.43
Frames: 167 = 24.40
Frames: 168 = 24.38
Frames: 169 = 24.36
Frames: 170 = 24.33
Frames: 171 = 24.31
Frames: 172 = 24.29
Frames: 173 = 24.27
Frames: 174 = 24.25
Frames: 175 = 24.24
Frames: 176 = 24.23
Frames: 177 = 24.21
Frames: 178 = 24.20
Frames: 179 = 24.18
Frames: 180 = 24.17
Time: 7448
RPM: 88.61
FPS: 24.17
Min: 24.17
Max: 1500.00
Rotations: 10
Frames: 180
-------------------
switch to 187/3
-------------------
Connected
PPR: 11
Ratio: 62.33
Pulses: 686
Frames per Rotation: 18
Pulses per Frame: 38.11
Rotations: 1 = 0
Rotations: 2 = 470
Rotations: 3 = 1189
Rotations: 4 = 1910
Rotations: 5 = 2631
Rotations: 6 = 3352
Rotations: 7 = 4071
Rotations: 8 = 4791
Rotations: 9 = 5511
Rotations: 10 = 6232
Rotations: 11 = 6950
Rotations: 12 = 7670
Rotations: 13 = 8388
Rotations: 14 = 9109
Rotations: 15 = 9828
Rotations: 16 = 10548
Rotations: 17 = 11267
Rotations: 18 = 11988
Rotations: 19 = 12707
Rotations: 20 = 13427
Rotations: 21 = 14145
Rotations: 22 = 14866
Rotations: 23 = 15585
Rotations: 24 = 16304
Rotations: 25 = 17021
Rotations: 26 = 17742
Rotations: 27 = 18461
Rotations: 28 = 19180
Rotations: 29 = 19898
Rotations: 30 = 20619
Rotations: 31 = 21336
Rotations: 32 = 22053
Rotations: 33 = 22772
Rotations: 34 = 23493
Rotations: 35 = 24212
Rotations: 36 = 24931
Rotations: 37 = 25651
Rotations: 38 = 26371
Rotations: 39 = 27088
Rotations: 40 = 27806
Rotations: 41 = 28527
Rotations: 42 = 29244
Rotations: 43 = 29963
Rotations: 44 = 30681
Rotations: 45 = 31401
Rotations: 46 = 32118
Rotations: 47 = 32837
Rotations: 48 = 33556
Rotations: 49 = 34277
Rotations: 50 = 34995
Rotations: 51 = 35712
Rotations: 52 = 36431
Rotations: 53 = 37149
Rotations: 54 = 37867
Rotations: 55 = 38586
Rotations: 56 = 39306
Rotations: 57 = 40024
Rotations: 58 = 40741
Rotations: 59 = 41461
Rotations: 60 = 42182
Rotations: 61 = 42899
Rotations: 62 = 43617
Rotations: 63 = 44338
Rotations: 64 = 45057
Rotations: 65 = 45775
Rotations: 66 = 46492
Rotations: 67 = 47213
Rotations: 68 = 47934
Rotations: 69 = 48655
Rotations: 70 = 49379
Rotations: 71 = 50100
Rotations: 72 = 50819
Rotations: 73 = 51538
Rotations: 74 = 52258
Rotations: 75 = 52976
Rotations: 76 = 53694
Rotations: 77 = 54414
Rotations: 78 = 55132
Rotations: 79 = 55849
Rotations: 80 = 56568
Rotations: 81 = 57286
Rotations: 82 = 58004
Rotations: 83 = 58723
Rotations: 84 = 59443
Rotations: 85 = 60161
Rotations: 86 = 60879
Rotations: 87 = 61600
Rotations: 88 = 62318
Rotations: 89 = 63035
Rotations: 90 = 63754
Rotations: 91 = 64471
Rotations: 92 = 65187
Rotations: 93 = 65903
Rotations: 94 = 66623
Rotations: 95 = 67340
Rotations: 96 = 68057
Rotations: 97 = 68774
Rotations: 98 = 69492
Rotations: 99 = 70209
Rotations: 100 = 70926
Time: 70927
RPM: 85.44
FPS: 25.38
Min: 25.38
Max: 961.54
Rotations: 100
Frames: 1800

90
notes/residency_report.md Normal file
View File

@ -0,0 +1,90 @@
## Report on Residency at Filmwerkplaats
Matthew McWilliams
01/03/2024
------
### Dates
18/02/2024 to 25/02/2024
### Artist Biography
Matt McWilliams is an artist and inventor working on free, open-source and open-hardware tools for analog filmmakers and photographers.
He works as a software developer in robotics research in the greater Boston area.
His website, [sixteenmillimeter.com](https://sixteenmillimeter.com), hosts various models for 3D printing as well as software and design documents for machines for making analog cinema that are all freely-available to use and modify under [The MIT License](https://opensource.org/license/mit).
### Project Description
The purpose of this project is to develop a free, open-source and open-hardware desktop contact printer for 16mm film to allow artists the ability to make prints of their 16mm films from negatives and other sources that would otherwise be cost and time prohibitive to do at a small scale.
Contact printers are an essential piece of film lab equipment that performs a simple but important service for filmmakers.
By taking two (or more) pieces of film--one of them developed and the other undeveloped--and sandwiching them together at the emulsion (the "contact") a light projected behind the developed film will impart a negative of the image on the undeveloped film.
In the simple case of having a stand of developed negative black and white 16mm film, one can pair it with a piece of undeveloped black and white 16mm print stock and produce a positive image that can be used for projection.
This project aims to leverage advances in 3D printing, cheap-but-reliable geared DC motors and open-platform microcontrollers to build a small, affordable and reproducible contact printer which can be used as-is or adapted and modified to fit the purposes of individuals and groups who are working with particular analog production techniques.
### Relevancy and Quality of the Project
Many artist-run film labs and individual filmmakers who work with small gauge analog film do not have access to large-footprint commercial machines and lack the space and maintenance resources to keep them.
Since information about commercially-developed equipment is guarded, expensive or even lost to time, starting a project from the principles of free, open-source software (FOSS) gives it a better chance to exist in the open where others can freely access it and improve upon its development without the risk of violating patents or copyright.
By designing a desktop-scale contact printer, in the spirit of the Uhler Cine Printer, artists who make short films can utilize it for making tests, work prints and even short release-quality prints without the need to work with large amounts of film at a time.
Filmmakers who work with hand-processed film in a small darkroom can process and then print on a machine not much larger than a laptop.
### Developed Activities
The work completed during the residency addressed practical limitations in the current design and established a list of improvements that will be made in the project.
Issues with the overall tension on the film as it advances across the drive gear and past the lamp head were resolved by adding thin spacers between the takeup and feed spindles and the magnetic clutches which allow them to tension the film without potentially snapping it.
Improving the tension of the film allows the film to be contact printed without the frame lines pulling up or down and improves image stability.
The optimal operating speed of the contact printing process was determined through a series of tests under different loads (no film, one film and two films) and utilizing performance assessment code written as part of the residency.
By testing the drive motor of the contact printer at different drive speeds, I was able to establish minimum and maximum operating speeds that can be predicted and monitored by the software.
18 frames per second was settled on as the speed most likely to be stabilized while transporting both print stock and negative with the "100RPM" motor that was selected for the drive sprocketed roller.
Since a single strip of film, likely the print stock, is capable of running at 24 or 25 fps, this means additional capabilities are possible in future developments; namely that the contact printer could be used with the optical soundtrack recorder being developed in parallel by Hrvoje Spudić.
The 18 frame sprocketed roller, driven stably at 60RPM with two strips of film, can also be run at or above 24fps to record sound to stock in real-time.
Tests with Kodak 3302 black and white print stock and 3383 color print stock established a baseline for exposure that will be used to improve the lamp design.
Currently employing three standard 5mm cool white LED bulbs powered with 5V DC and with 330 Ohm resistance each, we know that a standard 216 diffusion gel and a .6 ND filter will produce a proper gray card density from a LAD test negative on black and white print stock (Kodak 3302).
Similarly, we were able to approximate the exposure and filters required to print from color negative onto color print stock although further testing and development will be needed.
Immediate next tasks are to address the lamp design, user interface issues and motor speed stability.
The lamp should be expanded from the current 3 LED design for increased exposure headroom and from solid color modules to RGB-controllable ones.
This will improve color printing and remove the need for as many physical color filters which ultimately reduce the amount of light needed to make accurate exposures on color print stock.
The lamp should also be made focus-able (the LED "bulb") and the diffusion.
Additionally, larger filters should be accepted, possibly allowing for the Bolex filter holder to be used.
All of this, as well as adding the ability to move the gate closer to the film
Since the behavior of the contact printer can only currently be altered with code changes, a UI beyond a start/stop button must be implemented.
Though the aims of this design are simple, contact printing analog film has many variables and being able to adjust them is one of the main advantages of using DIY projects such as these.
Exposing functionality to artists will make this tool more suitable for release-quality printing, providing the ability to do shot-to-shot timing and A/B roll printing.
Another area of potential improvement is the overall stability of the drive roller and therefore the resulting print.
A bearing of larger diameter or height could improve the rotational stability of the drive roller.
Also a redesign of the gate to put contact against the film could both improve the sharpness of the image and of the selective areas to be printed.
### Artist's Feedback
The amount of knowledge, expertise, capacity for experimentation and encouragement to work was unique and indispensable.
The only thing I would request or suggest is for more time at the residency, though the limitation is entirely with my schedule.
Having the ability to change code, strike print tests and process them without leaving the darkroom made progress possible at a rate previously not available to the project.
Without the support and resources provided by this residency, months or years would have been spent trying to discover what was during those seven days.
### Images
![The assembled contact printer with 30m daylight spools and 120m takeup reels.](../img/IMG_8295.jpg)
![Black and white print from negative with soundtrack.](../img/IMG_8298.jpg)
![Color print of LAD test film.](../img/IMG_8313.jpg)
![Overscan of 16mm black and white print with soundtrack.](../img/screen_cap_with_sound.png)
-----
![](../img/EN_FundedbytheEU_RGB_POS.png)

@ -1 +1 @@
Subproject commit b499c2810117b5ca57014d5cb4219cf68b4c5c0f Subproject commit e2eeb27f173d739a174c0d147bcb62a16859e2d9

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
include <../contact_printer.scad>;
PART="blank";
translate([0, -30, 4]) sprocketed_roller_invert_solid();
lamp_single([0, 10, 0 + 1]);
lamp_single_assembly([0, 10, 0 + 1]);

77
scad/filmless.scad Normal file
View File

@ -0,0 +1,77 @@
//OpenSCAD representation of small gauge film formats
//Website: https://git.sixteenmillimeter.com/16mm/filmless.git
module rounded_cube (cube_arr = [1, 1, 1], d = 0, center = false) {
off_x = 0;
off_y = 0;
r = d/2;
union () {
cube([cube_arr[0] - d, cube_arr[1], cube_arr[2]], center = center);
cube([cube_arr[0], cube_arr[1] - d, cube_arr[2]], center = center);
translate ([1 * (cube_arr[0] / 2) - r , 1 * (cube_arr[1] / 2)- r, 0]) cylinder(r = r, h = cube_arr[2], center = center);
translate ([-1 * (cube_arr[0] / 2) + r, -1 * (cube_arr[1] / 2) + r, 0]) cylinder(r = r, h = cube_arr[2], center = center);
translate ([1 * (cube_arr[0] / 2) - r, -1 * (cube_arr[1] / 2) + r, 0]) cylinder(r = r, h = cube_arr[2], center = center);
translate ([-1 * (cube_arr[0] / 2) + r, 1 * (cube_arr[1] / 2)- r, 0]) cylinder(r = r, h = cube_arr[2], center = center);
}
}
module 16mm_perf () {
$fn = 10;
PERF_W = 1.829;
PERF_H = 1.27;
rounded_cube([PERF_W, PERF_H, 2], d = .5, center = true);
}
module 16mm_film (frames = 10, double = false, long = true) {
THICKNESS = 0.0047 * 25.4;
WIDTH = 16;
SPACING_LONG = 7.62; //long pitch (projection)
SPACING_SHORT = 7.605; //short pitch
PERF_OFFSET = (1.829 / 2) + .85;
difference () {
if (long) {
cube([WIDTH, SPACING_LONG * frames, THICKNESS], center = true);
} else {
cube([WIDTH, SPACING_SHORT * frames, THICKNESS], center = true);
}
if (long) {
OFFSET = (SPACING_LONG * frames) / 2;
for (i = [0 : frames]) {
translate([8 - PERF_OFFSET, -OFFSET + SPACING_LONG * i, 0]) 16mm_perf();
}
if (double) {
for (i = [0 : frames]) {
translate([-8 + PERF_OFFSET, -OFFSET + SPACING_LONG * i, 0]) 16mm_perf();
}
}
} else {
OFFSET = (SPACING_SHORT * frames) / 2;
for (i = [0 : frames]) {
translate([8 - PERF_OFFSET, -OFFSET + SPACING_LONG * i, 0]) 16mm_perf();
}
if (double) {
for (i = [0 : frames]) {
translate([-8 + PERF_OFFSET, -OFFSET + SPACING_LONG * i, 0]) 16mm_perf();
}
}
}
}
}
module film_sheet () {
STRIPS = 12;
FRAMES = 33;
PERFS = "single";
PITCH = "long";
projection() for (i = [0:STRIPS - 1]) {
translate([16.01 * i, 0, 0]) {
16mm_film(FRAMES, PERFS == "double", PITCH == "long");
}
}
echo("STRIP LENGTH", (PITCH == "long" ? 7.62 : 7.605) * FRAMES, "mm");
echo("PAGE WIDTH", 16 * STRIPS, "mm");
echo("PITCH", PITCH);
echo("PERFS", PERFS);
}
//film_sheet();

View File

@ -1,4 +1,4 @@
/*
AT = 25.4 * 0.22; AT = 25.4 * 0.22;
@ -685,7 +685,7 @@ module sprocketed_roller_gear_cap () {
} }
translate([0, 0, -30]) contact_printer_roller(); translate([0, 0, -30]) contact_printer_roller();
} }
}*/ }

View File

@ -1,4 +1,4 @@
include <./common/common.scad> include <../common/common.scad>
in = 25.4; in = 25.4;
$fn = 100; $fn = 100;

View File

@ -1,7 +1,7 @@
$fn = 80; $fn = 80;
include <./common/common.scad>; include <../common/common.scad>;
include <./16mm_sprocketed_roller_var.scad>; include <../16mm_sprocketed_roller_var.scad>;
BRACE_L = 24; BRACE_L = 24;
PLATE_L = 47; PLATE_L = 47;

@ -1 +1 @@
Subproject commit 4482a471bb357e93e9cabe9efaecba0370a3a0bd Subproject commit e108b53ced5fd86dd283baaa6c911964d54ecf40

@ -1 +1 @@
Subproject commit 74133c800b4a4267cd3bf82b567beb0121bfb7d0 Subproject commit dbb3eca85a46c0474c7e68e7febf43db508b53a8

78
scripts/bom.sh Normal file
View File

@ -0,0 +1,78 @@
#!/bin/bash
if [[ "${1}" == "" ]]; then
echo "Please use a .scad file as first argument"
exit 1
fi
FILENAME=$(basename "${1}" | tr '[:upper:]' '[:lower:]')
EXTENSION="${FILENAME##*.}"
if [[ "${EXTENSION}" != "scad" ]]; then
echo "Please use a .scad file as first argument, not .${EXTENSION}"
exit 2
fi
if [[ ! -f "${1}" ]]; then
echo "File ${1} does not exist"
exit 3
fi
mkdir -p hardware
NAME="${FILENAME%.*}"
DESTINATION="./hardware/${NAME}_BOM.csv"
TOTAL="./hardware/${NAME}_BOM_total.csv"
touch "${DESTINATION}"
touch "${TOTAL}"
DESTINATION=$(realpath "${DESTINATION}")
TOTAL=$(realpath "${TOTAL}")
PRICES=$(realpath "./hardware/prices.csv")
MODULE=""
echo "module,quantity,part,part_id,description" > "${DESTINATION}"
tac "${1}" | while read line; do
module=$(echo "${line}" | grep 'module ' | grep '(' | grep ')')
if [[ "${module}" != "" ]]; then
MODULE=$(echo "${module}" | xargs | awk '{print $2}' | awk -F'{' '{print $1}')
fi
bom=$(echo "${line}" | grep '//' | grep 'BOM' | awk -F'BOM:' '{print $2}'| xargs)
if [[ "${bom}" != "" ]]; then
QUANTITY=$(echo "${bom}" | awk -F',' '{print $1}' | xargs)
PART=$(echo "${bom}" | awk -F',' '{print $2}' | xargs)
ID=$(echo "${bom}" | awk -F',' '{print $3}' | xargs)
DESCRIPTION=$(echo "${bom}" | awk -F',' '{print $4}' | xargs)
echo "[${MODULE}] ${QUANTITY}x ${PART} (${ID})"
echo "${MODULE},${QUANTITY},${PART},${ID},${DESCRIPTION}" >> "${DESTINATION}"
fi
done
echo "quantity,part,part_id,price" > "${TOTAL}"
sqlite3 :memory: -cmd '.mode csv' -cmd ".import ${DESTINATION} bom" -cmd ".import ${PRICES} prices"\
'SELECT SUM(quantity),part,part_id, CAST( CEIL( CAST(SUM(quantity) AS FLOAT) * (SELECT CAST(prices.price AS FLOAT) / CAST(prices.quantity AS FLOAT) FROM prices WHERE prices.part = bom.part LIMIT 1) ) AS INTEGER) as price FROM bom GROUP BY part ORDER BY part DESC;' >> "${TOTAL}"
sqlite3 :memory: -cmd '.mode csv' -cmd ".import ${TOTAL} bom" -cmd ".import ${PRICES} prices" -cmd '.mode markdown' \
"SELECT part as Part, quantity as Qty, \
printf('$%.2f', CAST(price AS FLOAT) / 100) as 'Cost (USD)', \
printf( '[%s for $%.2f](%s)', (SELECT prices.quantity FROM prices WHERE prices.part = bom.part), (SELECT CAST(prices.price AS FLOAT) / 100 FROM prices WHERE prices.part = bom.part), (SELECT prices.url FROM prices WHERE prices.part = bom.part)) as 'Minumum' \
FROM bom ORDER BY part DESC;"
sqlite3 :memory: -cmd '.mode csv' -cmd ".import ${TOTAL} bom" -cmd ".import ${PRICES} prices" -cmd '.mode markdown' \
"SELECT 'TOTAL', SUM(quantity) AS qty, \
printf('$%.2f', CAST(SUM(price) AS FLOAT) / 100) as total, \
printf('$%.2f', ( SELECT CAST( SUM(price) AS FLOAT) / 100 FROM prices WHERE prices.part IN ( SELECT bom.part FROM bom ) ) ) as min \
FROM bom;" | grep -v 'qty'
sqlite3 :memory: -cmd '.mode csv' -cmd ".import ${TOTAL} bom"\
"SELECT SUM(quantity), 'TOTAL', 'N/A', SUM(price) FROM bom;" | tr -d '"' >> "${TOTAL}"
NONEFOUND=$(sqlite3 :memory: -cmd '.mode csv' -cmd ".import ${DESTINATION} bom" -cmd ".import ${PRICES} prices" -cmd '.mode column' -cmd '.headers off' \
'SELECT DISTINCT part FROM bom WHERE part NOT IN (SELECT part FROM prices) ORDER BY part;')
if [[ "${NONEFOUND}" != "" ]]; then
echo "No price found for the following parts:"
echo "${NONEFOUND}"
fi

14
scripts/compile.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/bash
set -e
FQBN=esp32:esp32:esp32 #ESP32
INO="./ino/contact_printer"
INOFILE="${INO}/contact_printer.ino"
OUTPUT="./bin"
mkdir -p "${OUTPUT}"
#esp32
arduino-cli compile --fqbn ${FQBN} --output-dir "${OUTPUT}" "${INO}" || echo 'Compile failed' && exit 1

11
scripts/img.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
WIDTH=2400
HEIGHT=2000
SCHEME=DeepOcean
IMG=img/contact_printer.png
# setting PART equal to a non-existent module will render the debug layout
openscad -o ${IMG} --enable manifold --colorscheme ${SCHEME} --imgsize ${WIDTH},${HEIGHT} -D "PART=\"DEBUGxxxxxxx\"" scad/contact_printer.scad
convert ${IMG} -resize 1200x1000 -gravity center -extent 1000x700 ${IMG}

4
scripts/report.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
cd ./notes
pandoc --wrap=preserve ./residency_report.md -o ./residency_report.pdf

View File

@ -12,8 +12,10 @@ cat scad/contact_printer.scad | grep "PART ==" | awk -F'==' '{print $2}' | awk -
while read m; do while read m; do
echo "Rendering $m..." echo "Rendering $m..."
openscad -o "stl/contact_printer_$m.stl" -D "PART=\"$m\"" scad/contact_printer.scad openscad --export-format=asciistl --enable manifold -o "stl/contact_printer_$m.stl" -D "PART=\"$m\"" scad/contact_printer.scad
python scad/common/c14n_stl.py "stl/contact_printer_$m.stl" python scad/common/c14n_stl.py "stl/contact_printer_$m.stl"
done < models.txt done < models.txt
bash scripts/bom.sh "./scad/contact_printer.scad"
#run client tests? #run client tests?

7
scripts/submodules.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
echo "Updating all git submodules"
git submodule update --recursive scad/common
git submodule update --recursive scad/takeup
git submodule update --recursive scad/sprocketed_roller

29
scripts/version.sh Normal file
View File

@ -0,0 +1,29 @@
#! /bin/bash
VERSION_FILE="./VERSION.txt"
SOURCE_FILE="./ino/contact_printer/contact_printer.ino"
CURRENT=`cat "${VERSION_FILE}"`
DATESTR=`date +"%Y%m%d"`
IFS="."
read -ra VERSION <<< "${CURRENT}"
IFS=" "
if [[ "${1}" == "major" ]]; then
let "VERSION[0]=${VERSION[0]}+1"
let "VERSION[1]=0"
let "VERSION[2]=0"
elif [[ "${1}" == "minor" ]]; then
let "VERSION[1]=${VERSION[1]}+1"
let "VERSION[2]=0"
else
let "VERSION[2]=${VERSION[2]}+1"
fi
V="${VERSION[0]}.${VERSION[1]}.${VERSION[2]}"
#echo "{ \"version\" : \"$V\", \"bin\" : \"/bin/contact_printer.bin\", \"date\" : $DATESTR }" > ./ota.json
VERSION_UPDATE=`sed "s/.*define VERSION.*/ #define VERSION \"${V}\"/" "${SOURCE_FILE}"`
echo "${VERSION_UPDATE}" > "${SOURCE_FILE}"
echo $V > "${VERSION_FILE}"
echo $V

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff