Push dev work to master #2

Merged
sixteenmillimeter merged 416 commits from dev into master 2018-07-19 15:29:02 +00:00
85 changed files with 27795 additions and 619 deletions

6
.gitignore vendored
View File

@ -1 +1,5 @@
node_modules
node_modules
run_dev.sh
state
*/.DS_Store
.DS_Store

View File

@ -1,2 +1,55 @@
# intval3
# INTVAL3
### What is this?
INTVAL3 is an open source intervalometer for the Bolex 16mm camera. The goal of the project is to create a cheap-to-make intervalometer that can be used to automate time-lapse or animation on the Bolex using mobile, web or physical controls.
This is the third incarnation of the INTVAL project, this time utilizing the [Raspberry Pi Zero W](https://www.raspberrypi.org/products/raspberry-pi-zero-w/) for Wifi and Bluetooth control. Earlier versions, the [INTVAL](https://github.com/sixteenmillimeter/INTVAL) and [INTVAL2](https://github.com/sixteenmillimeter/intval2) were Arduino-based. The original INTVAL used a solenoid (!!!) to hammer a camera release cable, while the second attempt was a proving ground for the motor-and-key hardware used in this version.
The [INTVAL2](https://github.com/sixteenmillimeter/intval2) project should be used if you prefer a simpler, physical interface approach.
### Components
* [Firmware](#firmware) for the Raspberry Pi Zero W running [Node.js](https://nodejs.org) on Raspian
* [Mobile app](#mobile) for controlling device using [Cordova](https://cordova.apache.org/) + [Bleno](https://github.com/sandeepmistry/bleno)
* [Web app](#web) for controlling device using [Restify](http://restify.com/)
* Hardware files, parts models for 3D printing, laser cutting and CNC
* PCB design for a Raspberry Pi Zero W Bonnet
* [Parts list](#parts-list)
<a name="firmware"></a>
## Firmware
The firmware of the INTVAL3 is a node.js application running on the Raspian OS intended for installation on the Raspberry Pi Zero W.
<a name="mobile"></a>
## Mobile App
The INTVAL3 mobile app controls the intervalometer over Bluetooth. It can be used to configure the settings on the intervalometer such as exposure length, delay between frames and the direction of the film. The app can also be used to trigger individual frames, as well as start and stop sequences. As an experimental feature, film exposure settings can be determined with the camera on a mobile device.
<a name="web"></a>
## Web App
As a function of the firmware, there is an embedded web application that is hosted on the INTVAL3. When connected to a wifi network (via the mobile app) users are able to control the intervalometer from a browser. Users are also able to trigger functions and change settings on the intervalometer firmware from the command line by using cURL or wget, so actions can be scripted and automated from an external machine.
<a name="hardware"></a>
## Hardware
All of the non-electronic hardware is generated from OpenSCAD scripts and built into either STL files for 3D printing or DXF files for laser cutting or CNCing.
Electronics designs are available in the form of a Fritzing file, a wiring diagram and a mask image that can be used to fabricate a board from a blank PCB. One of the easiest ways to
<a name="parts-list"></a>
## PARTS
1. Raspberry Pi Zero W - [[Adafruit](https://www.adafruit.com/product/3400)] [[Sparkfun](https://www.sparkfun.com/products/14277)]
2. L298N Breakout Board - ?
3. 120RPM 12VDC Motor - ?
4. Microswitch w/ Roller - [[Adafruit](https://www.adafruit.com/product/819)]
5. L7805 5V Regulator - [[Adafruit](https://www.adafruit.com/product/2164)] [Sparkfun](https://www.sparkfun.com/products/107)]
6. (Optional) Proto Bonnet - [[Adafruit](https://www.adafruit.com/product/3203)]

5
app/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
*.DS_Store
platforms/*
plugins/*
node_modules/*
build.json

30
app/Readme.md Normal file
View File

@ -0,0 +1,30 @@
# INTVAL3
## Mobile App
The INTVAL3 mobile app is built using the Cordova framework for cross-platform deployment to iOS and Android.
## Requirements
* node.js
* npm
* Cordova
* XCode (for iOS) and/or
* Android Studio (for Android)
## Installation
All of the required plugins can be installed directly by executing the `install.sh` script on a system which supports bash. This script will use the `cordova` application to install the Cordova plugins. Cordova also supports the npm package.json format, so plugins may be alternately installed simply by running a `npm install` command from within the `app` directory.
## Building
Once all dependencies and plugins are installed, you can build the INTVAL3 app by running
```cordova build ios```
or
```cordova build android```
This generates the application source code in the `platforms` directory, under either the `ios` or `android` directory depending on your build target. The app can be built and run on your device by going to the project file and opening it in your IDE, either XCode or Android Studio. Alternately it can be run on your device using the `cordova run ios` or `cordova run android` commands.

48
app/config.xml Normal file
View File

@ -0,0 +1,48 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="com.sixteenmillimeter.intval3" version="1.0.3" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>INTVAL3</name>
<description>
Mobile control app for the INTVAL intervalometer for Bolex 16mm cameras
</description>
<author email="hi@mmcwilliams.com" href="https://sixteenmillimeter.com">
M McWilliams
</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<platform name="android">
<allow-intent href="market:*" />
<icon density="mdpi" src="res/icon/android/mdpi.png" />
<icon density="hdpi" src="res/icon/android/hdpi.png" />
<icon density="xhdpi" src="res/icon/android/xhdpi.png" />
<icon density="xxhdpi" src="res/icon/android/xxhdpi.png" />
<icon density="xxxhdpi" src="res/icon/android/xxxhdpi.png" />
</platform>
<platform name="ios">
<allow-intent href="itms:*" />
<allow-intent href="itms-apps:*" />
<icon height="180" src="res/icon/ios/icon-60@3x.png" width="180" />
<icon height="120" src="res/icon/ios/icon-60@2x.png" width="120" />
<icon height="60" src="res/icon/ios/icon-60.png" width="60" />
<splash src="res/screen/ios/Default@2x~universal~anyany.png" />
</platform>
<preference name="DisallowOverscroll" value="true" />
<preference name="StatusBarBackgroundColor" value="#212121" />
<preference name="CameraUsesGeolocation" value="false" />
<plugin name="cordova-plugin-whitelist" spec="1" />
<plugin name="cordova-plugin-device" spec="^1.1.7" />
<plugin name="cordova-plugin-dialogs" spec="^1.3.4" />
<plugin name="cordova-plugin-statusbar" spec="^2.3.0" />
<plugin name="cordova-plugin-splashscreen" spec="~4.1.0" />
<plugin name="cordova-plugin-camera-with-exif" spec="^1.2.2" />
<plugin name="cordova-plugin-ble-central" spec="^1.1.4">
<variable name="BLUETOOTH_USAGE_DESCRIPTION" value="INTVAL3 intervalometer controls" />
</plugin>
<engine name="android" spec="^6.4.0" />
<engine name="ios" spec="^4.5.4" />
</widget>

23
app/hooks/README.md Normal file
View File

@ -0,0 +1,23 @@
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
-->
# Cordova Hooks
Cordova Hooks represent special scripts which could be added by application and plugin developers or even by your own build system to customize cordova commands. See Hooks Guide for more details: http://cordova.apache.org/docs/en/edge/guide_appdev_hooks_index.md.html#Hooks%20Guide.

31
app/hooks/icons.sh Normal file
View File

@ -0,0 +1,31 @@
#!/bin/sh
base=$1
#convert "$base" -resize '29x29' -unsharp 1x4 "res/icon/ios/Icon-Small.png"
#convert "$base" -resize '40x40' -unsharp 1x4 "res/icon/ios/Icon-Small-40.png"
#convert "$base" -resize '50x50' -unsharp 1x4 "res/icon/ios/Icon-Small-50.png"
#convert "$base" -resize '57x57' -unsharp 1x4 "res/icon/ios/Icon.png"
#convert "$base" -resize '58x58' -unsharp 1x4 "res/icon/ios/Icon-Small@2x.png"
convert "$base" -resize '60x60' -unsharp 1x4 "res/icon/ios/icon-60.png"
#convert "$base" -resize '72x72' -unsharp 1x4 "res/icon/ios/Icon-72.png"
#convert "$base" -resize '76x76' -unsharp 1x4 "res/icon/ios/Icon-76.png"
#convert "$base" -resize '80x80' -unsharp 1x4 "res/icon/ios/Icon-Small-40@2x.png"
#convert "$base" -resize '100x100' -unsharp 1x4 "res/icon/ios/Icon-Small-50@2x.png"
#convert "$base" -resize '114x114' -unsharp 1x4 "res/icon/ios/Icon@2x.png"
convert "$base" -resize '120x120' -unsharp 1x4 "res/icon/ios/icon-60@2x.png"
#convert "$base" -resize '144x144' -unsharp 1x4 "res/icon/ios/Icon-72@2x.png"
#convert "$base" -resize '152x152' -unsharp 1x4 "res/icon/ios/Icon-76@2x.png"
convert "$base" -resize '180x180' -unsharp 1x4 "res/icon/ios/icon-60@3x.png"
#convert "$base" -resize '512x512' -unsharp 1x4 "res/icon/ios/iTunesArtwork"
#convert "$base" -resize '1024x1024' -unsharp 1x4 "res/icon/ios/iTunesArtwork@2x"
#convert "$base" -resize '36x36' -unsharp 1x4 "res/icon/android/Icon-ldpi.png"
convert "$base" -resize '48x48' -unsharp 1x4 "res/icon/android/mdpi.png"
convert "$base" -resize '72x72' -unsharp 1x4 "res/icon/android/hdpi.png"
convert "$base" -resize '96x96' -unsharp 1x4 "res/icon/android/xhdpi.png"
convert "$base" -resize '144x144' -unsharp 1x4 "res/icon/android/xxhdpi.png"
convert "$base" -resize '192x192' -unsharp 1x4 "res/icon/android/xxxhdpi.png"
cd res/icon/ios/
find -type f -name "*.png" -exec optipng -o7 {} \;
cd ../android/
find -type f -name "*.png" -exec optipng -o7 {} \;

14
app/hooks/screen.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/sh
base=$1
c="convert $1 -gravity center"
# iPhone
$c -resize 320x480 "res/screen/ios/Default~iphone.png"
$c -resize 640x960 "res/screen/ios/Default@2x~iphone.png"
$c -resize 640x1136 "res/screen/ios/Default-568h@2x~iphone.png"
$c -resize 320x426 "res/screen/android/splash-portrait-ldpi.png"
$c -resize 320x470 "res/screen/android/splash-portrait-mdpi.png"
$c -resize 480x640 "res/screen/android/splash-portrait-hdpi.png"
$c -resize 720x960 "res/screen/android/splash-portrait-xhdpi.png"

10
app/install.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
npm install
cordova platform add ios
cordova platform add android
cordova plugin add cordova-plugin-device
cordova plugin add cordova-plugin-dialogs
cordova plugin add cordova-plugin-ble-central --variable BLUETOOTH_USAGE_DESCRIPTION="INTVAL3 intervalometer controls"
cordova plugin add cordova-plugin-statusbar
cordova plugin add cordova-plugin-camera-with-exif

570
app/package-lock.json generated Normal file
View File

@ -0,0 +1,570 @@
{
"name": "com.sixteenmillimeter.intval3",
"version": "1.0.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"cordova-android": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-6.4.0.tgz",
"integrity": "sha1-VK6NpXKKjX5e/MYXLT3MoXvH/n0=",
"requires": {
"android-versions": "1.2.1",
"cordova-common": "2.1.0",
"elementtree": "0.1.6",
"nopt": "3.0.6",
"properties-parser": "0.2.3",
"q": "1.5.0",
"shelljs": "0.5.3"
},
"dependencies": {
"abbrev": {
"version": "1.1.0",
"bundled": true
},
"android-versions": {
"version": "1.2.1",
"bundled": true
},
"ansi": {
"version": "0.3.1",
"bundled": true
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
},
"base64-js": {
"version": "0.0.8",
"bundled": true
},
"big-integer": {
"version": "1.6.25",
"bundled": true
},
"bplist-parser": {
"version": "0.1.1",
"bundled": true,
"requires": {
"big-integer": "1.6.25"
}
},
"brace-expansion": {
"version": "1.1.8",
"bundled": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"bundled": true
},
"cordova-common": {
"version": "2.1.0",
"bundled": true,
"requires": {
"ansi": "0.3.1",
"bplist-parser": "0.1.1",
"cordova-registry-mapper": "1.1.15",
"elementtree": "0.1.6",
"glob": "5.0.15",
"minimatch": "3.0.4",
"osenv": "0.1.4",
"plist": "1.2.0",
"q": "1.5.0",
"semver": "5.4.1",
"shelljs": "0.5.3",
"underscore": "1.8.3",
"unorm": "1.4.1"
}
},
"cordova-registry-mapper": {
"version": "1.1.15",
"bundled": true
},
"elementtree": {
"version": "0.1.6",
"bundled": true,
"requires": {
"sax": "0.3.5"
}
},
"glob": {
"version": "5.0.15",
"bundled": true,
"requires": {
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true
},
"lodash": {
"version": "3.10.1",
"bundled": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"requires": {
"brace-expansion": "1.1.8"
}
},
"nopt": {
"version": "3.0.6",
"bundled": true,
"requires": {
"abbrev": "1.1.0"
}
},
"once": {
"version": "1.4.0",
"bundled": true,
"requires": {
"wrappy": "1.0.2"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true
},
"osenv": {
"version": "0.1.4",
"bundled": true,
"requires": {
"os-homedir": "1.0.2",
"os-tmpdir": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true
},
"plist": {
"version": "1.2.0",
"bundled": true,
"requires": {
"base64-js": "0.0.8",
"util-deprecate": "1.0.2",
"xmlbuilder": "4.0.0",
"xmldom": "0.1.27"
}
},
"properties-parser": {
"version": "0.2.3",
"bundled": true
},
"q": {
"version": "1.5.0",
"bundled": true
},
"sax": {
"version": "0.3.5",
"bundled": true
},
"semver": {
"version": "5.4.1",
"bundled": true
},
"shelljs": {
"version": "0.5.3",
"bundled": true
},
"underscore": {
"version": "1.8.3",
"bundled": true
},
"unorm": {
"version": "1.4.1",
"bundled": true
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true
},
"wrappy": {
"version": "1.0.2",
"bundled": true
},
"xmlbuilder": {
"version": "4.0.0",
"bundled": true,
"requires": {
"lodash": "3.10.1"
}
},
"xmldom": {
"version": "0.1.27",
"bundled": true
}
}
},
"cordova-ios": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-4.5.4.tgz",
"integrity": "sha1-yAZIBYlyloVw3BXalzFP+S0H3+c=",
"requires": {
"cordova-common": "2.1.0",
"ios-sim": "6.1.2",
"nopt": "3.0.6",
"plist": "1.2.0",
"q": "1.5.1",
"shelljs": "0.5.3",
"xcode": "0.9.3",
"xml-escape": "1.1.0"
},
"dependencies": {
"abbrev": {
"version": "1.1.1",
"bundled": true
},
"ansi": {
"version": "0.3.1",
"bundled": true
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
},
"base64-js": {
"version": "0.0.8",
"bundled": true
},
"big-integer": {
"version": "1.6.25",
"bundled": true
},
"bplist-creator": {
"version": "0.0.7",
"bundled": true,
"requires": {
"stream-buffers": "2.2.0"
}
},
"bplist-parser": {
"version": "0.1.1",
"bundled": true,
"requires": {
"big-integer": "1.6.25"
}
},
"brace-expansion": {
"version": "1.1.8",
"bundled": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
}
},
"concat-map": {
"version": "0.0.1",
"bundled": true
},
"cordova-common": {
"version": "2.1.0",
"bundled": true,
"requires": {
"ansi": "0.3.1",
"bplist-parser": "0.1.1",
"cordova-registry-mapper": "1.1.15",
"elementtree": "0.1.6",
"glob": "5.0.15",
"minimatch": "3.0.4",
"osenv": "0.1.4",
"plist": "1.2.0",
"q": "1.5.1",
"semver": "5.4.1",
"shelljs": "0.5.3",
"underscore": "1.8.3",
"unorm": "1.4.1"
}
},
"cordova-registry-mapper": {
"version": "1.1.15",
"bundled": true
},
"elementtree": {
"version": "0.1.6",
"bundled": true,
"requires": {
"sax": "0.3.5"
}
},
"glob": {
"version": "5.0.15",
"bundled": true,
"requires": {
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true
},
"ios-sim": {
"version": "6.1.2",
"bundled": true,
"requires": {
"bplist-parser": "0.0.6",
"nopt": "1.0.9",
"plist": "1.2.0",
"simctl": "1.1.1"
},
"dependencies": {
"bplist-parser": {
"version": "0.0.6",
"bundled": true
},
"nopt": {
"version": "1.0.9",
"bundled": true,
"requires": {
"abbrev": "1.1.1"
}
}
}
},
"lodash": {
"version": "3.10.1",
"bundled": true
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"requires": {
"brace-expansion": "1.1.8"
}
},
"nopt": {
"version": "3.0.6",
"bundled": true,
"requires": {
"abbrev": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"bundled": true,
"requires": {
"wrappy": "1.0.2"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true
},
"osenv": {
"version": "0.1.4",
"bundled": true,
"requires": {
"os-homedir": "1.0.2",
"os-tmpdir": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true
},
"pegjs": {
"version": "0.10.0",
"bundled": true
},
"plist": {
"version": "1.2.0",
"bundled": true,
"requires": {
"base64-js": "0.0.8",
"util-deprecate": "1.0.2",
"xmlbuilder": "4.0.0",
"xmldom": "0.1.27"
}
},
"q": {
"version": "1.5.1",
"bundled": true
},
"sax": {
"version": "0.3.5",
"bundled": true
},
"semver": {
"version": "5.4.1",
"bundled": true
},
"shelljs": {
"version": "0.5.3",
"bundled": true
},
"simctl": {
"version": "1.1.1",
"bundled": true,
"requires": {
"shelljs": "0.2.6",
"tail": "0.4.0"
},
"dependencies": {
"shelljs": {
"version": "0.2.6",
"bundled": true
}
}
},
"simple-plist": {
"version": "0.2.1",
"bundled": true,
"requires": {
"bplist-creator": "0.0.7",
"bplist-parser": "0.1.1",
"plist": "2.0.1"
},
"dependencies": {
"base64-js": {
"version": "1.1.2",
"bundled": true
},
"plist": {
"version": "2.0.1",
"bundled": true,
"requires": {
"base64-js": "1.1.2",
"xmlbuilder": "8.2.2",
"xmldom": "0.1.27"
}
},
"xmlbuilder": {
"version": "8.2.2",
"bundled": true
}
}
},
"stream-buffers": {
"version": "2.2.0",
"bundled": true
},
"tail": {
"version": "0.4.0",
"bundled": true
},
"underscore": {
"version": "1.8.3",
"bundled": true
},
"unorm": {
"version": "1.4.1",
"bundled": true
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true
},
"uuid": {
"version": "3.0.1",
"bundled": true
},
"wrappy": {
"version": "1.0.2",
"bundled": true
},
"xcode": {
"version": "0.9.3",
"bundled": true,
"requires": {
"pegjs": "0.10.0",
"simple-plist": "0.2.1",
"uuid": "3.0.1"
}
},
"xml-escape": {
"version": "1.1.0",
"bundled": true
},
"xmlbuilder": {
"version": "4.0.0",
"bundled": true,
"requires": {
"lodash": "3.10.1"
}
},
"xmldom": {
"version": "0.1.27",
"bundled": true
}
}
},
"cordova-plugin-ble-central": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/cordova-plugin-ble-central/-/cordova-plugin-ble-central-1.1.4.tgz",
"integrity": "sha1-rZA2mnla1wChuf3WbhnnzkSnicM="
},
"cordova-plugin-camera-with-exif": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cordova-plugin-camera-with-exif/-/cordova-plugin-camera-with-exif-1.2.2.tgz",
"integrity": "sha1-/kxfHWgga6QoOqtNM4MrVSwil3A="
},
"cordova-plugin-compat": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-compat/-/cordova-plugin-compat-1.2.0.tgz",
"integrity": "sha1-C8ZXVyduvZIMASzpIOJ0F3V2Nz4="
},
"cordova-plugin-device": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-1.1.7.tgz",
"integrity": "sha1-/JQRG+aTJijGaGiTjd89yCyfv+Y="
},
"cordova-plugin-dialogs": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/cordova-plugin-dialogs/-/cordova-plugin-dialogs-1.3.4.tgz",
"integrity": "sha1-XMlm7nyZsvW1s934SQAmKLDacVc="
},
"cordova-plugin-splashscreen": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-4.1.0.tgz",
"integrity": "sha1-gQKKt2Q+YVWT0n8q0CRFYR8ZRrY="
},
"cordova-plugin-statusbar": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-2.4.0.tgz",
"integrity": "sha1-JOspc3ldEPbxrjIC90+Ix9mQzyA="
},
"cordova-plugin-whitelist": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz",
"integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q="
}
}
}

41
app/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "com.sixteenmillimeter.intval3",
"displayName": "INTVAL3",
"version": "1.0.3",
"description": "Mobile control app for the INTVAL intervalometer for Bolex 16mm cameras",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "M McWilliams",
"license": "MIT",
"dependencies": {
"cordova-android": "^6.4.0",
"cordova-ios": "^4.5.4",
"cordova-plugin-ble-central": "^1.1.4",
"cordova-plugin-camera-with-exif": "^1.2.2",
"cordova-plugin-compat": "^1.2.0",
"cordova-plugin-device": "^1.1.7",
"cordova-plugin-dialogs": "^1.3.4",
"cordova-plugin-splashscreen": "^4.1.0",
"cordova-plugin-statusbar": "^2.4.0",
"cordova-plugin-whitelist": "^1.3.3"
},
"cordova": {
"plugins": {
"cordova-plugin-whitelist": {},
"cordova-plugin-device": {},
"cordova-plugin-dialogs": {},
"cordova-plugin-statusbar": {},
"cordova-plugin-ble-central": {
"BLUETOOTH_USAGE_DESCRIPTION": "INTVAL intervalometer controls"
},
"cordova-plugin-splashscreen": {},
"cordova-plugin-camera-with-exif": {}
},
"platforms": [
"android",
"ios"
]
}
}

29
app/res/README.md Normal file
View File

@ -0,0 +1,29 @@
<!--
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
-->
Note that these image resources are not copied into a project when a project
is created with the CLI. Although there are default image resources in a
newly-created project, those come from the platform-specific project template,
which can generally be found in the platform's `template` directory. Until
icon and splashscreen support is added to the CLI, these image resources
aren't used directly.
See https://issues.apache.org/jira/browse/CB-5145

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

3
app/res/icon/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/Icon-60.png
/Icon-60@2x.png
/Icon-60@3x.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -1,11 +1,181 @@
<!doctype html>
<html>
<head>
<title>intval 3</title>
<title>INTVAL3</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<meta name="viewport" content="width = 320, initial-scale = 1.0, user-scalable = no, minimal-ui, viewport-fit=cover">
<!--<link rel="stylesheet" href="static/css/codemirror.css" />
<link rel="stylesheet" href="static/css/monokai.css" />-->
<link rel="stylesheet" href="static/css/index.css" />
</head>
<body>
<form method="/frame">
</form>
<div id="overlay">
<div id="spinner"></div>
<div id="msg"></div>
</div>
<div id="app" class="page selected">
<h2>INTVAL3</h2>
<div>
<div class="label">Counter</div>
<input type="number" id="counter" onclick="setCounter();" value="0" step="1" readonly />
</div>
<div>
<div class="label">Direction</div>
<span id="bwdLabel">BACKWARD</span>
<span id="fwdLabel" class="selected">FORWARD</span>
<label class="switch">
<input type="checkbox" id="dir" onclick="setDir();">
<span class="slider round"></span>
</label>
</div>
<div>
<div class="label">Exposure <span id="str">1/5</span></div>
<input type="number" id="exposure" value="630" min="0" onchange="setExposure();" />
<select id="scale" onchange="setExposureScale();">
<option value="ms" selected>ms</option>
<option value="sec">sec</option>
<option value="min">min</option>
<option value="hour">hour</option>
</select>
</div>
<div>
<div class="label">Delay</div>
<input type="number" id="delay" value="0" min="0" step="1" onchange="setDelay();" />
<select id="delayScale" onchange="setDelayScale();">
<option value="ms" selected>ms</option>
<option value="sec">sec</option>
<option value="min">min</option>
<option value="hour">hour</option>
</select>
</div>
<div>
<button id="seq" onclick="sequence();">START SEQUENCE</button>
</div>
<div>
<button id="frame" onclick="frame();">+1 FRAME</button>
</div>
</div>
<div id="settings" class="page">
<div class="ble">
<h2>BLUETOOTH</h2>
<select id="bluetooth">
<option>N/A</option>
</select>
<button id="disconnect" onclick="mobile.ble.disconnect();">DISCONNECT</button>
<button id="scan" class="active" onclick="mobile.ble.scan();">SCAN FOR DEVICE</button>
</div>
<div class="ble">
<h2>WIFI</h2>
<div id="ip">
Local IP: null
</div>
<div>
<select id="available" class="" onchange="mobile.editWifi();">
<option>N/A</option>
</select>
</div>
<div>
<input type="password" id="password" class="" placeholder="Wifi Password" />
</div>
<button id="wifi" class="" onclick="mobile.setWifi();">CONNECT</button>
<button id="wifiRefresh" class="" onclick="mobile.getWifi();">REFRESH WIFI</button>
</div>
<div>
<button id="reset" onclick="reset();">RESET</button>
<button id="restart" onclick="restart();">RESTART</button>
<button id="update" onclick="update();">UPDATE</button>
</div>
</div>
<!--<div id="mscript" class="page">
<h2>MSCRIPT</h2>
<textarea id="mscript_editor"></textarea>
<button id="compile">COMPILE</button>
<button id="mscript_seq">START SEQUENCE</button>
</div>-->
<div id="camera" class="page">
<h2>CAMERA</h2>
<div class="clearfix">
<div class="setting">
<div class="label">ISO</div>
<input type="number" class="iso" placeholder="100" value="100" onchange="mobile.refreshExposure();">
</div>
<div class="setting">
<div class="label">F-stop</div>
<input type="number" class="fstop" placeholder="5.6" value="5.6" onchange="mobile.refreshExposure();" />
</div>
<div class="setting">
<div class="label">Rex-o-fader</div>
<select class="angle">
<option value="133" selected>0 (Normal)</option>
<option value="66">1 Stop</option>
<option value="33">2 Stop</option>
</select>
</div>
</div>
<button id="cameraBtn" class="ble" onclick="mobile.getCamera();">
<i class="cameraIcon"></i>
</button>
<div class="clearfix ble">
<div id="camera_exposure">
<h3>PHONE</h3>
<div>
<label for="cam_exp">EXP</label>
<input readonly id="cam_exp" type="text" />
</div>
<div>
<label for="cam_f">F</label>
<input readonly id="cam_f" type="text" />
</div>
<div>
<label for="cam_iso">ISO</label>
<input readonly id="cam_iso" type="text" />
</div>
<div>
<label>COMP</label>
</div>
</div>
<div id="bolex_exposure">
<h3>BOLEX</h3>
<div>
<label id="bol_exp_diff"></label>
<input readonly id="bol_exp" type="text" />
</div>
<div>
<label id="bol_f_diff"></label>
<input readonly id="bol_f" type="text" />
</div>
<div>
<label id="bol_iso_diff"></label>
<input readonly id="bol_iso" type="text" />
</div>
<div>
<label><span class="pos">+0.8</span></label>
</div>
</div>
</div>
</div>
<footer>
<div id="settingsIcon" onclick="settingsPage();" class="icon">
<div> </div>
</div>
<div id="appIcon" onclick="appPage();" class="icon selected">
<div></div>
</div>
<!--<div id="mscriptIcon" onclick="mscriptPage();" class="icon">
<div></div>
</div>-->
<div id="cameraIcon" onclick="cameraPage();" class="icon">
<div class="cameraIcon"></div>
</div>
</footer>
<script src="cordova.js"></script>
<script src="static/js/spin.min.js"></script>
<!--<script src="static/js/codemirror.js"></script>-->
<script src="static/js/intval.core.js"></script>
<script src="static/js/intval.web.js"></script>
<script src="static/js/intval.mobile.js"></script>
<!--<script src="static/js/intval.mscript.js"></script>-->
<script src="static/js/index.js"></script>
</body>
</html>

View File

@ -0,0 +1,346 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

View File

@ -0,0 +1,430 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
* {
-webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adjust last value opacity 0 to 1.0 */
}
body {
-webkit-touch-callout: none; /* prevent callout to copy image, etc when tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */
}
html,body{
background: #212121;
height: 100%;
padding: 0;
margin: 0;
}
html,body,button,h2,label,input{
color: #fff;
font-family: 'Arial Neue', Helvetical, Arial, sans-serif;
}
.clearfix::after {
content: "";
clear: both;
display: table;
}
body.mobile{
padding-top: 5px;
}
.page{
padding: 5px 10% 0 10%;
display: none;
}
.page.selected{
display: block;
}
.ble{
display: none;
}
.ble.active{
display: block;
}
h2{
font-size: 18px;
text-align: center;
font-weight: normal;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: block;
width: 60px;
height: 34px;
margin: 0 auto;
}
/* Hide default HTML checkbox */
.switch input {display:none;}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #20ce45;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #f32121;
}
input:focus + .slider {
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
input[type=number],
input[type=text],
input[type=password],
select{
width: 100%;
border: 1px solid #fff;
border-radius: 5px;
color: #fff;
font-size: 18px;
-webkit-appearance: none;
background: transparent;
box-shadow: none;
outline: none;
margin: 5px 0;
padding: 5px 10px;
box-sizing: border-box;
}
option{
color: #212121;
}
button{
width: 100%;
border: 1px solid #fff;
border-radius: 5px;
color: #fff;
font-size: 18px;
background: #363636;
-webkit-appearance: none;
box-shadow: none;
outline: none;
margin: 5px 0;
padding: 5px 0;
text-align: center;
}
button:focus,
button.focus{
background-color: #20ce45;
border-color: #20ce45;
color: #212121;
font-weight: bold;
}
#fwdLabel,#bwdLabel{
margin-top: 8px;
color: #666;
}
#fwdLabel.selected,
#bwdLabel.selected{
color: #fff;
}
#fwdLabel.selected{
text-shadow: 0px 0px 4px #20ce45;
}
#bwdLabel.selected{
text-shadow: 0px 0px 4px #f32121;
}
#fwdLabel{
float: left;
}
#bwdLabel{
position: absolute;
right: 10%;
}
.label{
/*text-align: center;*/
color: #666;
margin-top: 6px;
margin-bottom: 9px;
}
/* MAIN */
#app{
}
#app > h2{
font-weight: bold;
}
#exposure,
#delay{
width: 70%;
display: inline-block;
}
#scale,
#delayScale{
width: 25%;
display: inline-block;
float: right;
}
#str{
color: #fff;
}
#counter{
text-align: center;
}
#frame{
padding: 20px 0;
font-size: 24px;
}
#settingsIcon > div{
background: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDU0IDU0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1NCA1NDsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMnB4IiBoZWlnaHQ9IjMycHgiPgo8Zz4KCTxwYXRoIGQ9Ik01MS4yMiwyMWgtNS4wNTJjLTAuODEyLDAtMS40ODEtMC40NDctMS43OTItMS4xOTdzLTAuMTUzLTEuNTQsMC40Mi0yLjExNGwzLjU3Mi0zLjU3MSAgIGMwLjUyNS0wLjUyNSwwLjgxNC0xLjIyNCwwLjgxNC0xLjk2NmMwLTAuNzQzLTAuMjg5LTEuNDQxLTAuODE0LTEuOTY3bC00LjU1My00LjU1M2MtMS4wNS0xLjA1LTIuODgxLTEuMDUyLTMuOTMzLDBsLTMuNTcxLDMuNTcxICAgYy0wLjU3NCwwLjU3My0xLjM2NiwwLjczMy0yLjExNCwwLjQyMUMzMy40NDcsOS4zMTMsMzMsOC42NDQsMzMsNy44MzJWMi43OEMzMywxLjI0NywzMS43NTMsMCwzMC4yMiwwSDIzLjc4ICAgQzIyLjI0NywwLDIxLDEuMjQ3LDIxLDIuNzh2NS4wNTJjMCwwLjgxMi0wLjQ0NywxLjQ4MS0xLjE5NywxLjc5MmMtMC43NDgsMC4zMTMtMS41NCwwLjE1Mi0yLjExNC0wLjQyMWwtMy41NzEtMy41NzEgICBjLTEuMDUyLTEuMDUyLTIuODgzLTEuMDUtMy45MzMsMGwtNC41NTMsNC41NTNjLTAuNTI1LDAuNTI1LTAuODE0LDEuMjI0LTAuODE0LDEuOTY3YzAsMC43NDIsMC4yODksMS40NCwwLjgxNCwxLjk2NmwzLjU3MiwzLjU3MSAgIGMwLjU3MywwLjU3NCwwLjczLDEuMzY0LDAuNDIsMi4xMTRTOC42NDQsMjEsNy44MzIsMjFIMi43OEMxLjI0NywyMSwwLDIyLjI0NywwLDIzLjc4djYuNDM5QzAsMzEuNzUzLDEuMjQ3LDMzLDIuNzgsMzNoNS4wNTIgICBjMC44MTIsMCwxLjQ4MSwwLjQ0NywxLjc5MiwxLjE5N3MwLjE1MywxLjU0LTAuNDIsMi4xMTRsLTMuNTcyLDMuNTcxYy0wLjUyNSwwLjUyNS0wLjgxNCwxLjIyNC0wLjgxNCwxLjk2NiAgIGMwLDAuNzQzLDAuMjg5LDEuNDQxLDAuODE0LDEuOTY3bDQuNTUzLDQuNTUzYzEuMDUxLDEuMDUxLDIuODgxLDEuMDUzLDMuOTMzLDBsMy41NzEtMy41NzJjMC41NzQtMC41NzMsMS4zNjMtMC43MzEsMi4xMTQtMC40MiAgIGMwLjc1LDAuMzExLDEuMTk3LDAuOTgsMS4xOTcsMS43OTJ2NS4wNTJjMCwxLjUzMywxLjI0NywyLjc4LDIuNzgsMi43OGg2LjQzOWMxLjUzMywwLDIuNzgtMS4yNDcsMi43OC0yLjc4di01LjA1MiAgIGMwLTAuODEyLDAuNDQ3LTEuNDgxLDEuMTk3LTEuNzkyYzAuNzUxLTAuMzEyLDEuNTQtMC4xNTMsMi4xMTQsMC40MmwzLjU3MSwzLjU3MmMxLjA1MiwxLjA1MiwyLjg4MywxLjA1LDMuOTMzLDBsNC41NTMtNC41NTMgICBjMC41MjUtMC41MjUsMC44MTQtMS4yMjQsMC44MTQtMS45NjdjMC0wLjc0Mi0wLjI4OS0xLjQ0LTAuODE0LTEuOTY2bC0zLjU3Mi0zLjU3MWMtMC41NzMtMC41NzQtMC43My0xLjM2NC0wLjQyLTIuMTE0ICAgUzQ1LjM1NiwzMyw0Ni4xNjgsMzNoNS4wNTJjMS41MzMsMCwyLjc4LTEuMjQ3LDIuNzgtMi43OFYyMy43OEM1NCwyMi4yNDcsNTIuNzUzLDIxLDUxLjIyLDIxeiBNNTIsMzAuMjIgICBDNTIsMzAuNjUsNTEuNjUsMzEsNTEuMjIsMzFoLTUuMDUyYy0xLjYyNCwwLTMuMDE5LDAuOTMyLTMuNjQsMi40MzJjLTAuNjIyLDEuNS0wLjI5NSwzLjE0NiwwLjg1NCw0LjI5NGwzLjU3MiwzLjU3MSAgIGMwLjMwNSwwLjMwNSwwLjMwNSwwLjgsMCwxLjEwNGwtNC41NTMsNC41NTNjLTAuMzA0LDAuMzA0LTAuNzk5LDAuMzA2LTEuMTA0LDBsLTMuNTcxLTMuNTcyYy0xLjE0OS0xLjE0OS0yLjc5NC0xLjQ3NC00LjI5NC0wLjg1NCAgIGMtMS41LDAuNjIxLTIuNDMyLDIuMDE2LTIuNDMyLDMuNjR2NS4wNTJDMzEsNTEuNjUsMzAuNjUsNTIsMzAuMjIsNTJIMjMuNzhDMjMuMzUsNTIsMjMsNTEuNjUsMjMsNTEuMjJ2LTUuMDUyICAgYzAtMS42MjQtMC45MzItMy4wMTktMi40MzItMy42NGMtMC41MDMtMC4yMDktMS4wMjEtMC4zMTEtMS41MzMtMC4zMTFjLTEuMDE0LDAtMS45OTcsMC40LTIuNzYxLDEuMTY0bC0zLjU3MSwzLjU3MiAgIGMtMC4zMDYsMC4zMDYtMC44MDEsMC4zMDQtMS4xMDQsMGwtNC41NTMtNC41NTNjLTAuMzA1LTAuMzA1LTAuMzA1LTAuOCwwLTEuMTA0bDMuNTcyLTMuNTcxYzEuMTQ4LTEuMTQ4LDEuNDc2LTIuNzk0LDAuODU0LTQuMjk0ICAgQzEwLjg1MSwzMS45MzIsOS40NTYsMzEsNy44MzIsMzFIMi43OEMyLjM1LDMxLDIsMzAuNjUsMiwzMC4yMlYyMy43OEMyLDIzLjM1LDIuMzUsMjMsMi43OCwyM2g1LjA1MiAgIGMxLjYyNCwwLDMuMDE5LTAuOTMyLDMuNjQtMi40MzJjMC42MjItMS41LDAuMjk1LTMuMTQ2LTAuODU0LTQuMjk0bC0zLjU3Mi0zLjU3MWMtMC4zMDUtMC4zMDUtMC4zMDUtMC44LDAtMS4xMDRsNC41NTMtNC41NTMgICBjMC4zMDQtMC4zMDUsMC43OTktMC4zMDUsMS4xMDQsMGwzLjU3MSwzLjU3MWMxLjE0NywxLjE0NywyLjc5MiwxLjQ3Niw0LjI5NCwwLjg1NEMyMi4wNjgsMTAuODUxLDIzLDkuNDU2LDIzLDcuODMyVjIuNzggICBDMjMsMi4zNSwyMy4zNSwyLDIzLjc4LDJoNi40MzlDMzAuNjUsMiwzMSwyLjM1LDMxLDIuNzh2NS4wNTJjMCwxLjYyNCwwLjkzMiwzLjAxOSwyLjQzMiwzLjY0ICAgYzEuNTAyLDAuNjIyLDMuMTQ2LDAuMjk0LDQuMjk0LTAuODU0bDMuNTcxLTMuNTcxYzAuMzA2LTAuMzA1LDAuODAxLTAuMzA1LDEuMTA0LDBsNC41NTMsNC41NTNjMC4zMDUsMC4zMDUsMC4zMDUsMC44LDAsMS4xMDQgICBsLTMuNTcyLDMuNTcxYy0xLjE0OCwxLjE0OC0xLjQ3NiwyLjc5NC0wLjg1NCw0LjI5NGMwLjYyMSwxLjUsMi4wMTYsMi40MzIsMy42NCwyLjQzMmg1LjA1MkM1MS42NSwyMyw1MiwyMy4zNSw1MiwyMy43OFYzMC4yMnoiIGZpbGw9IiNGRkZGRkYiLz4KCTxwYXRoIGQ9Ik0yNywxOGMtNC45NjMsMC05LDQuMDM3LTksOXM0LjAzNyw5LDksOXM5LTQuMDM3LDktOVMzMS45NjMsMTgsMjcsMTh6IE0yNywzNGMtMy44NTksMC03LTMuMTQxLTctN3MzLjE0MS03LDctNyAgIHM3LDMuMTQxLDcsN1MzMC44NTksMzQsMjcsMzR6IiBmaWxsPSIjRkZGRkZGIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==) no-repeat;
}
#mscriptIcon > div{
background: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjMycHgiIGhlaWdodD0iMzJweCIgdmlld0JveD0iMCAwIDUyMi40NjggNTIyLjQ2OSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTIyLjQ2OCA1MjIuNDY5OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPGc+CgkJPHBhdGggZD0iTTMyNS43NjIsNzAuNTEzbC0xNy43MDYtNC44NTRjLTIuMjc5LTAuNzYtNC41MjQtMC41MjEtNi43MDcsMC43MTVjLTIuMTksMS4yMzctMy42NjksMy4wOTQtNC40MjksNS41NjhMMTkwLjQyNiw0NDAuNTMgICAgYy0wLjc2LDIuNDc1LTAuNTIyLDQuODA5LDAuNzE1LDYuOTk1YzEuMjM3LDIuMTksMy4wOSwzLjY2NSw1LjU2OCw0LjQyNWwxNy43MDEsNC44NTZjMi4yODQsMC43NjYsNC41MjEsMC41MjYsNi43MS0wLjcxMiAgICBjMi4xOS0xLjI0MywzLjY2Ni0zLjA5NCw0LjQyNS01LjU2NEwzMzIuMDQyLDgxLjkzNmMwLjc1OS0yLjQ3NCwwLjUyMy00LjgwOC0wLjcxNi02Ljk5OSAgICBDMzMwLjA4OCw3Mi43NDcsMzI4LjIzNyw3MS4yNzIsMzI1Ljc2Miw3MC41MTN6IiBmaWxsPSIjRkZGRkZGIi8+CgkJPHBhdGggZD0iTTE2Ni4xNjcsMTQyLjQ2NWMwLTIuNDc0LTAuOTUzLTQuNjY1LTIuODU2LTYuNTY3bC0xNC4yNzctMTQuMjc2Yy0xLjkwMy0xLjkwMy00LjA5My0yLjg1Ny02LjU2Ny0yLjg1NyAgICBzLTQuNjY1LDAuOTU1LTYuNTY3LDIuODU3TDIuODU2LDI1NC42NjZDMC45NSwyNTYuNTY5LDAsMjU4Ljc1OSwwLDI2MS4yMzNjMCwyLjQ3NCwwLjk1Myw0LjY2NCwyLjg1Niw2LjU2NmwxMzMuMDQzLDEzMy4wNDQgICAgYzEuOTAyLDEuOTA2LDQuMDg5LDIuODU0LDYuNTY3LDIuODU0czQuNjY1LTAuOTUxLDYuNTY3LTIuODU0bDE0LjI3Ny0xNC4yNjhjMS45MDMtMS45MDIsMi44NTYtNC4wOTMsMi44NTYtNi41NyAgICBjMC0yLjQ3MS0wLjk1My00LjY2MS0yLjg1Ni02LjU2M0w1MS4xMDcsMjYxLjIzM2wxMTIuMjA0LTExMi4yMDFDMTY1LjIxNywxNDcuMTMsMTY2LjE2NywxNDQuOTM5LDE2Ni4xNjcsMTQyLjQ2NXoiIGZpbGw9IiNGRkZGRkYiLz4KCQk8cGF0aCBkPSJNNTE5LjYxNCwyNTQuNjYzTDM4Ni41NjcsMTIxLjYxOWMtMS45MDItMS45MDItNC4wOTMtMi44NTctNi41NjMtMi44NTdjLTIuNDc4LDAtNC42NjEsMC45NTUtNi41NywyLjg1N2wtMTQuMjcxLDE0LjI3NSAgICBjLTEuOTAyLDEuOTAzLTIuODUxLDQuMDktMi44NTEsNi41NjdzMC45NDgsNC42NjUsMi44NTEsNi41NjdsMTEyLjIwNiwxMTIuMjA0TDM1OS4xNjMsMzczLjQ0MiAgICBjLTEuOTAyLDEuOTAyLTIuODUxLDQuMDkzLTIuODUxLDYuNTYzYzAsMi40NzgsMC45NDgsNC42NjgsMi44NTEsNi41N2wxNC4yNzEsMTQuMjY4YzEuOTA5LDEuOTA2LDQuMDkzLDIuODU0LDYuNTcsMi44NTQgICAgYzIuNDcxLDAsNC42NjEtMC45NTEsNi41NjMtMi44NTRMNTE5LjYxNCwyNjcuOGMxLjkwMy0xLjkwMiwyLjg1NC00LjA5NiwyLjg1NC02LjU3ICAgIEM1MjIuNDY4LDI1OC43NTUsNTIxLjUxNywyNTYuNTY1LDUxOS42MTQsMjU0LjY2M3oiIGZpbGw9IiNGRkZGRkYiLz4KCTwvZz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K) no-repeat;
}
#appIcon > div{
background: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTkuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDYwIDYwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA2MCA2MDsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHdpZHRoPSIzMnB4IiBoZWlnaHQ9IjMycHgiPgo8Zz4KCTxwYXRoIGQ9Ik00NS41NjMsMjkuMTc0bC0yMi0xNWMtMC4zMDctMC4yMDgtMC43MDMtMC4yMzEtMS4wMzEtMC4wNThDMjIuMjA1LDE0LjI4OSwyMiwxNC42MjksMjIsMTV2MzAgICBjMCwwLjM3MSwwLjIwNSwwLjcxMSwwLjUzMywwLjg4NEMyMi42NzksNDUuOTYyLDIyLjg0LDQ2LDIzLDQ2YzAuMTk3LDAsMC4zOTQtMC4wNTksMC41NjMtMC4xNzRsMjItMTUgICBDNDUuODM2LDMwLjY0LDQ2LDMwLjMzMSw0NiwzMFM0NS44MzYsMjkuMzYsNDUuNTYzLDI5LjE3NHogTTI0LDQzLjEwN1YxNi44OTNMNDMuMjI1LDMwTDI0LDQzLjEwN3oiIGZpbGw9IiNGRkZGRkYiLz4KCTxwYXRoIGQ9Ik0zMCwwQzEzLjQ1OCwwLDAsMTMuNDU4LDAsMzBzMTMuNDU4LDMwLDMwLDMwczMwLTEzLjQ1OCwzMC0zMFM0Ni41NDIsMCwzMCwweiBNMzAsNThDMTQuNTYxLDU4LDIsNDUuNDM5LDIsMzAgICBTMTQuNTYxLDIsMzAsMnMyOCwxMi41NjEsMjgsMjhTNDUuNDM5LDU4LDMwLDU4eiIgZmlsbD0iI0ZGRkZGRiIvPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=) no-repeat;
}
.cameraIcon{
background: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDEwMCAxMDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIiB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4Ij4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNNTAsNDBjLTguMjg1LDAtMTUsNi43MTgtMTUsMTVjMCw4LjI4NSw2LjcxNSwxNSwxNSwxNWM4LjI4MywwLDE1LTYuNzE1LDE1LTE1ICAgIEM2NSw0Ni43MTgsNTguMjgzLDQwLDUwLDQweiBNOTAsMjVINzhjLTEuNjUsMC0zLjQyOC0xLjI4LTMuOTQ5LTIuODQ2bC0zLjEwMi05LjMwOUM3MC40MjYsMTEuMjgsNjguNjUsMTAsNjcsMTBIMzMgICAgYy0xLjY1LDAtMy40MjgsMS4yOC0zLjk0OSwyLjg0NmwtMy4xMDIsOS4zMDlDMjUuNDI2LDIzLjcyLDIzLjY1LDI1LDIyLDI1SDEwQzQuNSwyNSwwLDI5LjUsMCwzNXY0NWMwLDUuNSw0LjUsMTAsMTAsMTBoODAgICAgYzUuNSwwLDEwLTQuNSwxMC0xMFYzNUMxMDAsMjkuNSw5NS41LDI1LDkwLDI1eiBNNTAsODBjLTEzLjgwNywwLTI1LTExLjE5My0yNS0yNWMwLTEzLjgwNiwxMS4xOTMtMjUsMjUtMjUgICAgYzEzLjgwNSwwLDI1LDExLjE5NCwyNSwyNUM3NSw2OC44MDcsNjMuODA1LDgwLDUwLDgweiBNODYuNSw0MS45OTNjLTEuOTMyLDAtMy41LTEuNTY2LTMuNS0zLjVjMC0xLjkzMiwxLjU2OC0zLjUsMy41LTMuNSAgICBjMS45MzQsMCwzLjUsMS41NjgsMy41LDMuNUM5MCw0MC40MjcsODguNDMzLDQxLjk5Myw4Ni41LDQxLjk5M3oiIGZpbGw9IiNGRkZGRkYiLz4KCTwvZz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K) no-repeat;
}
button i {
display: block;
width: 33px;
height: 33px;
margin: 10px auto;
}
footer{
display: block;
width: 100%;
height: 50px;
position: fixed;
bottom: 0;
border-top: 1px solid rgba(255, 255, 255, 0.3);
}
footer .icon {
width: 33.33%;
/*width: 50%;*/
height: 50px;
float: left;
box-sizing: border-box;
border-right: 1px solid rgba(255, 255, 255, 0.3);
}
body.mobile footer .icon{
/*width: 25%;*/
width: 33.33%;
}
footer .icon:last-child{
border-right: 0;
}
.icon > div{
display: block;
line-height: 34px;
height: 33px;
width: 33px;
opacity: 0.5;
color: #fff;
margin: 10px auto 0;
}
.icon.selected > div{
opacity: 1.0;
}
footer > div.selected{
background: rgba(255, 255, 255, 0.1);
}
.CodeMirror{
font-size: 18px;
}
#compile{
margin-top: 20px;
}
#seq{
margin-top: 40px;
padding: 10px 0;
}
#overlay{
position: fixed;
z-index: 2001;
top: 0;
left: 0;
bottom: 0;
right: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
}
#overlay.active{
display: block;
}
#spinner{
display: none;
margin-top: 200px;
}
#spinner.active{
display: block;
}
#disconnect,#scan{
display: none;
}
#disconnect.active,
#scan.active{
display: block;
}
#available.active{
border-color: #20ce45;
}
#ip{
color: #666;
}
#ip span{
color: #20ce45;
}
#password,#wifi,#ip{
display: none;
}
#password.active,
#ip.active,
#wifi.active{
display: block;
}
.indicator {
color: red;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid black;
}
.setting{
width: 50%;
float: left;
}
.setting input,
.setting select{
width: 90%;
}
.setting .label{
margin-bottom: 3px;
}
#cameraBtn{
margin-top: 20px;
}
#camera_exposure,
#bolex_exposure{
width: 50%;
float: left;
}
#camera_exposure input,
#bolex_exposure input{
margin: 12px auto;
width: 69%;
display: block;
border-color: #666;
}
#camera_exposure h3,
#bolex_exposure h3{
text-align: center;
font-weight: normal;
}
#camera_exposure div label,
#bolex_exposure div label{
color: #666;
position: absolute;
margin-top: 8px;
text-align: center;
width: 100%;
margin-left: -50%;
}
span.neg{
color: #f32121;;
}
span.pos{
color: #20ce45;
}
body.mobile footer{
display: block;
}
#reset{
margin-top: 60px;
}
#msg{
display: none;
width: 200px;
height: 44px;
position: fixed;
left: 50%;
margin-left: -100px;
margin-top: 45px;
color: #fff;
text-align: center;
text-shadow: 1px 1px 0px #000;
}
#msg.active{
display: block;
}

View File

@ -0,0 +1,36 @@
/* Based on Sublime Text's Monokai theme */
.cm-s-monokai.CodeMirror { background: #272822; color: #f8f8f2; }
.cm-s-monokai div.CodeMirror-selected { background: #49483E; }
.cm-s-monokai .CodeMirror-line::selection, .cm-s-monokai .CodeMirror-line > span::selection, .cm-s-monokai .CodeMirror-line > span > span::selection { background: rgba(73, 72, 62, .99); }
.cm-s-monokai .CodeMirror-line::-moz-selection, .cm-s-monokai .CodeMirror-line > span::-moz-selection, .cm-s-monokai .CodeMirror-line > span > span::-moz-selection { background: rgba(73, 72, 62, .99); }
.cm-s-monokai .CodeMirror-gutters { background: #272822; border-right: 0px; }
.cm-s-monokai .CodeMirror-guttermarker { color: white; }
.cm-s-monokai .CodeMirror-guttermarker-subtle { color: #d0d0d0; }
.cm-s-monokai .CodeMirror-linenumber { color: #d0d0d0; }
.cm-s-monokai .CodeMirror-cursor { border-left: 1px solid #f8f8f0; }
.cm-s-monokai span.cm-comment { color: #75715e; }
.cm-s-monokai span.cm-atom { color: #ae81ff; }
.cm-s-monokai span.cm-number { color: #ae81ff; }
.cm-s-monokai span.cm-property, .cm-s-monokai span.cm-attribute { color: #a6e22e; }
.cm-s-monokai span.cm-keyword { color: #f92672; }
.cm-s-monokai span.cm-builtin { color: #66d9ef; }
.cm-s-monokai span.cm-string { color: #e6db74; }
.cm-s-monokai span.cm-variable { color: #f8f8f2; }
.cm-s-monokai span.cm-variable-2 { color: #9effff; }
.cm-s-monokai span.cm-variable-3, .cm-s-monokai span.cm-type { color: #66d9ef; }
.cm-s-monokai span.cm-def { color: #fd971f; }
.cm-s-monokai span.cm-bracket { color: #f8f8f2; }
.cm-s-monokai span.cm-tag { color: #f92672; }
.cm-s-monokai span.cm-header { color: #ae81ff; }
.cm-s-monokai span.cm-link { color: #ae81ff; }
.cm-s-monokai span.cm-error { background: #f92672; color: #f8f8f0; }
.cm-s-monokai .CodeMirror-activeline-background { background: #373831; }
.cm-s-monokai .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}

View File

@ -0,0 +1,436 @@
/*!
* QUnit 2.5.0
* https://qunitjs.com/
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2018-01-10T02:56Z
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header (excluding toolbar) */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699A4;
background-color: #0D3349;
font-size: 1.5em;
line-height: 1em;
font-weight: 400;
border-radius: 5px 5px 0 0;
}
#qunit-header a {
text-decoration: none;
color: #C2CCD1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #FFF;
}
#qunit-banner {
height: 5px;
}
#qunit-filteredTest {
padding: 0.5em 1em 0.5em 1em;
color: #366097;
background-color: #F4FF77;
}
#qunit-userAgent {
padding: 0.5em 1em 0.5em 1em;
color: #FFF;
background-color: #2B81AF;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
/** Toolbar */
#qunit-testrunner-toolbar {
padding: 0.5em 1em 0.5em 1em;
color: #5E740B;
background-color: #EEE;
}
#qunit-testrunner-toolbar .clearfix {
height: 0;
clear: both;
}
#qunit-testrunner-toolbar label {
display: inline-block;
}
#qunit-testrunner-toolbar input[type=checkbox],
#qunit-testrunner-toolbar input[type=radio] {
margin: 3px;
vertical-align: -2px;
}
#qunit-testrunner-toolbar input[type=text] {
box-sizing: border-box;
height: 1.6em;
}
.qunit-url-config,
.qunit-filter,
#qunit-modulefilter {
display: inline-block;
line-height: 2.1em;
}
.qunit-filter,
#qunit-modulefilter {
float: right;
position: relative;
margin-left: 1em;
}
.qunit-url-config label {
margin-right: 0.5em;
}
#qunit-modulefilter-search {
box-sizing: border-box;
width: 400px;
}
#qunit-modulefilter-search-container:after {
position: absolute;
right: 0.3em;
content: "\25bc";
color: black;
}
#qunit-modulefilter-dropdown {
/* align with #qunit-modulefilter-search */
box-sizing: border-box;
width: 400px;
position: absolute;
right: 0;
top: 50%;
margin-top: 0.8em;
border: 1px solid #D3D3D3;
border-top: none;
border-radius: 0 0 .25em .25em;
color: #000;
background-color: #F5F5F5;
z-index: 99;
}
#qunit-modulefilter-dropdown a {
color: inherit;
text-decoration: none;
}
#qunit-modulefilter-dropdown .clickable.checked {
font-weight: bold;
color: #000;
background-color: #D2E0E6;
}
#qunit-modulefilter-dropdown .clickable:hover {
color: #FFF;
background-color: #0D3349;
}
#qunit-modulefilter-actions {
display: block;
overflow: auto;
/* align with #qunit-modulefilter-dropdown-list */
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * {
box-sizing: border-box;
max-height: 2.8em;
display: block;
padding: 0.4em;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button {
float: right;
font: inherit;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child {
/* insert padding to align with checkbox margins */
padding-left: 3px;
}
#qunit-modulefilter-dropdown-list {
max-height: 200px;
overflow-y: auto;
margin: 0;
border-top: 2px groove threedhighlight;
padding: 0.4em 0 0;
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown-list li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#qunit-modulefilter-dropdown-list .clickable {
display: block;
padding-left: 0.15em;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 1em 0.4em 1em;
border-bottom: 1px solid #FFF;
list-style-position: inside;
}
#qunit-tests > li {
display: none;
}
#qunit-tests li.running,
#qunit-tests li.pass,
#qunit-tests li.fail,
#qunit-tests li.skipped,
#qunit-tests li.aborted {
display: list-item;
}
#qunit-tests.hidepass {
position: relative;
}
#qunit-tests.hidepass li.running,
#qunit-tests.hidepass li.pass:not(.todo) {
visibility: hidden;
position: absolute;
width: 0;
height: 0;
padding: 0;
border: 0;
margin: 0;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li.skipped strong {
cursor: default;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
text-decoration: none;
}
#qunit-tests li p a {
padding: 0.25em;
color: #6B6464;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #FFF;
border-radius: 5px;
}
.qunit-source {
margin: 0.6em 0 0.3em;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: 0.2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 0.5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
color: #374E0C;
background-color: #E0F2BE;
text-decoration: none;
}
#qunit-tests ins {
color: #500;
background-color: #FFCACA;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: #000; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #FFF;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3C510C;
background-color: #FFF;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #FFF;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
}
#qunit-tests .fail { color: #000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: #008000; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/*** Aborted tests */
#qunit-tests .aborted { color: #000; background-color: orange; }
/*** Skipped tests */
#qunit-tests .skipped {
background-color: #EBECE9;
}
#qunit-tests .qunit-todo-label,
#qunit-tests .qunit-skipped-label {
background-color: #F4FF77;
display: inline-block;
font-style: normal;
color: #366097;
line-height: 1.8em;
padding: 0 0.5em;
margin: -0.4em 0.4em -0.4em 0;
}
#qunit-tests .qunit-todo-label {
background-color: #EEE;
}
/** Result */
#qunit-testresult {
color: #2B81AF;
background-color: #D2E0E6;
border-bottom: 1px solid #FFF;
}
#qunit-testresult .clearfix {
height: 0;
clear: both;
}
#qunit-testresult .module-name {
font-weight: 700;
}
#qunit-testresult-display {
padding: 0.5em 1em 0.5em 1em;
width: 85%;
float:left;
}
#qunit-testresult-controls {
padding: 0.5em 1em 0.5em 1em;
width: 10%;
float:left;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var app = {
// Application Constructor
initialize: function() {
document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
document.addEventListener('resume', this.onDeviceResume.bind(this), false);
document.addEventListener('DOMContentLoaded', event => {
if (typeof cordova === 'undefined') {
init();
web.init();
getState();
}
})
},
// deviceready Event Handler
//
// Bind any cordova events here. Common events are:
// 'pause', 'resume', etc.
onDeviceReady: function() {
init();
mobile.init();
},
onDeviceResume : function () {
getState();
}
};
app.initialize();

View File

@ -0,0 +1,318 @@
'use strict';
const BOLEX = {
angle : 133,
prism : 0.8,
iso : 100,
fstop : 5.6,
expected : 630
};
const STATE = {
dir : true,
exposure : 630, //always ms
delay : 0,
scale : 'ms',
delayScale : 'ms',
counter : 0,
sequence : false
};
//functions
window.frame = null;
window.getState = null;
window.setDir = null;
window.setExposure = null;
window.setDelay = null;
window.setCounter = null;
window.sequence = null;
window.reset = null;
window.restart = null;
window.update = null;
//ms
var shutter = function (exposure) {
let fraction = BOLEX.expected / 1000;
let speed;
let corrected;
let str;
if (exposure > BOLEX.expected) {
//if exposure is explicitly set
fraction = exposure / 1000;
speed = fraction;
} else {
speed = fraction * (BOLEX.angle / 360);
}
corrected = speed * BOLEX.prism;
if (corrected < 1.0) {
//less than a second
str = '1/' + Math.round(Math.pow(corrected, -1)) + ' sec';
} else if (corrected >= 1.0 && corrected < 60) {
//greater than a second, less than a minute
str = '' + (Math.round(corrected * 10) / 10) + ' sec'
} else if (corrected >= 60 && corrected < 60 * 60) {
//greater than a minute, less than an hour
str = '' + (Math.round(corrected / 6) / 10) + ' min';
} else if (corrected >= 60 * 60 && corrected < 60 * 60 * 24) {
//greater than an hour, less than a day
str = '' + (Math.round(corrected / (6 * 60)) / 10) + ' hr';
} else if (corrected >= 60 * 60 * 24) {
//greater than a day
str = '' + (Math.round(corrected / (6 * 60 * 24)) / 10) + ' day';
}
return { speed : speed, str : str }
};
var scaleAuto = function (ms) {
if (ms < 1000) {
return 'ms'
} else if (ms >= 1000 && ms < 1000 * 60) {
return 'sec'
} else if (ms >= 1000 * 60 && ms < 1000 * 60 * 60) {
return 'min'
} else if (ms >= 1000 * 60 * 60) {
return 'hour'
}
};
var scaleTime = function (raw, scale) {
if (scale === 'ms') {
return raw
} else if (scale === 'sec') {
return raw * 1000;
} else if (scale === 'min') {
return raw * (1000 * 60);
} else if (scale === 'hour') {
return raw * (1000 * 60 * 60);
}
};
var setExposureScale = function () {
const scale = document.getElementById('scale').value;
const elem = document.getElementById('exposure');
if (scale === 'ms') {
elem.value = STATE.exposure;
} else if (scale === 'sec') {
elem.value = STATE.exposure / 1000;
} else if (scale === 'min') {
elem.value = STATE.exposure / (1000 * 60);
} else if (scale === 'hour') {
elem.value = STATE.exposure / (1000 * 60 * 60);
}
STATE.scale = scale;
};
var setDelayScale = function () {
const scale = document.getElementById('delayScale').value;
const elem = document.getElementById('delay');
if (scale === 'ms') {
elem.value = STATE.delay;
} else if (scale === 'sec') {
elem.value = STATE.delay / 1000;
} else if (scale === 'min') {
elem.value = STATE.delay / (1000 * 60);
} else if (scale === 'hour') {
elem.value = STATE.delay / (1000 * 60 * 60);
}
STATE.delayScale = scale;
};
var setDirLabel = function (dir) {
const bwdLabel = document.getElementById('bwdLabel');
const fwdLabel = document.getElementById('fwdLabel');
const but = document.getElementById('frame');
if (dir) {
bwdLabel.classList.remove('selected');
fwdLabel.classList.add('selected');
but.innerHTML = '+1 FRAME';
} else {
fwdLabel.classList.remove('selected');
bwdLabel.classList.add('selected');
but.innerHTML = '-1 FRAME';
}
};
var incCounter = function (val) {
const elem = document.getElementById('counter');
const current = elem.value;
elem.value = (parseInt(current) + val);
STATE.counter += val;
};
var forceCounter = function (val) {
document.getElementById('counter').value = val;
}
var unsetPages = function () {
const pages = document.querySelectorAll('.page');
const icons = document.querySelectorAll('.icon');
for (let icon of icons) {
if (icon.classList.contains('selected')) icon.classList.remove('selected');
};
for (let page of pages){;
if (page.classList.contains('selected')) page.classList.remove('selected');
}
};
var setState = function (res) {
let exposure;
let exposureScale;
let delayScale;
if (res.frame.dir !== true) {
document.getElementById('dir').checked = true;
STATE.dir = res.frame.dir;
setDirLabel(false);
} else {
document.getElementById('dir').checked = false;
STATE.dir = res.frame.dir;
setDirLabel(true);
}
document.getElementById('counter').value = res.counter;
STATE.counter = res.counter;
//Exposure
if (res.frame.exposure === 0) {
res.frame.exposure = BOLEX.expected;
}
STATE.exposure = res.frame.exposure;
exposure = shutter(STATE.exposure);
exposureScale = scaleAuto(STATE.exposure);
document.getElementById('str').innerHTML = exposure.str;
document.getElementById('scale').value = exposureScale;
setExposureScale();
STATE.delay = res.frame.delay;
delayScale = scaleAuto(STATE.delay);
document.getElementById('delayScale').value = delayScale;
setDelayScale();
if (res.sequence == true) {
STATE.sequence = true;
if (mobile.ble) mobile.ble.active = true;
seqState(true);
} else {
seqState(false);
}
};
var seqState = function (state) {
const elem = document.getElementById('seq')
if (state) {
if (!elem.classList.contains('focus')) {
elem.classList.add('focus');
elem.innerHTML = 'STOP SEQUENCE';
}
} else {
if (elem.classList.contains('focus')) {
elem.classList.remove('focus');
elem.innerHTML = 'START SEQUENCE';
}
}
};
var appPage = function () {
unsetPages();
document.getElementById('app').classList.add('selected');
document.getElementById('appIcon').classList.add('selected');
};
var settingsPage = function () {
unsetPages();
document.getElementById('settings').classList.add('selected');
document.getElementById('settingsIcon').classList.add('selected');
};
var mscriptPage = function () {
unsetPages();
document.getElementById('mscript').classList.add('selected');
document.getElementById('mscriptIcon').classList.add('selected');
editor.cm.refresh();
};
var cameraPage = function () {
unsetPages();
document.getElementById('camera').classList.add('selected');
document.getElementById('cameraIcon').classList.add('selected');
};
var isNumeric = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
var UI = {};
UI.overlay = {
elem : document.getElementById('overlay')
}
UI.overlay.show = function () {
if (!UI.overlay.elem.classList.contains('active')) {
UI.overlay.elem.classList.add('active');
}
};
UI.overlay.hide = function () {
if (UI.overlay.elem.classList.contains('active')) {
UI.overlay.elem.classList.remove('active');
}
};
UI.spinner = {
elem : document.getElementById('spinner')
}
UI.spinner.opts = {
lines: 13 // The number of lines to draw
, length: 33 // The length of each line
, width: 11 // The line thickness
, radius: 30 // The radius of the inner circle
, scale: 0.5 // Scales overall size of the spinner
, corners: 1 // Corner roundness (0..1)
, color: '#fff' // #rgb or #rrggbb or array of colors
, opacity: 0.25 // Opacity of the lines
, rotate: 0 // The rotation offset
, direction: 1 // 1: clockwise, -1: counterclockwise
, speed: 1 // Rounds per second
, trail: 60 // Afterglow percentage
, fps: 20 // Frames per second when using setTimeout() as a fallback for CSS
, zIndex: 2e9 // The z-index (defaults to 2000000000)
, className: 'spinner' // The CSS class to assign to the spinner
, top: '50%' // Top position relative to parent
, left: '50%' // Left position relative to parent
, shadow: true // Whether to render a shadow
, hwaccel: true // Whether to use hardware acceleration
, position: 'relative' // Element positioning
};
UI.spinner.init = function () {
const spinner = new Spinner(UI.spinner.opts).spin(UI.spinner.elem);
};
UI.spinner.show = function (text) {
if (!UI.spinner.elem.classList.contains('active')) {
UI.spinner.elem.classList.add('active');
}
if (text) {
UI.message.show(text)
}
};
UI.spinner.hide = function () {
if (UI.spinner.elem.classList.contains('active')) {
UI.spinner.elem.classList.remove('active');
}
};
UI.message = {
elem : document.getElementById('msg')
};
UI.message.show = function (text) {
UI.message.elem.innerHTML = text
if (!UI.message.elem.classList.contains('active')) {
UI.message.elem.classList.add('active');
}
};
UI.message.hide = function () {
if (UI.message.elem.classList.contains('active')) {
UI.message.elem.classList.remove('active');
}
};
var init = function () {
document.querySelector('.angle').oninput = function () {
BOLEX.angle = parseInt(this.value);
};
document.querySelector('.iso').oninput = function () {
BOLEX.iso = parseInt(this.value);
};
document.querySelector('.fstop').oninput = function () {
BOLEX.fstop = parseFloat(this.value);
};
};

View File

@ -0,0 +1,781 @@
/* jshint esversion:6, strict:true, browser:true*/
/* global console, alert */
'use strict';
var mobile = {};
mobile.ble = {
BLENO_DEVICE_NAME : 'intval3',
DEVICE_ID : 'intval3',
SERVICE_ID : '149582bd-d49d-4b5c-acd1-1ae503d09e7a',
CHAR_ID : '47bf69fb-f62f-4ef8-9be8-eb727a54fae4', //general data
WIFI_ID : '3fe7d9cf-7bd2-4ff0-97c5-ebe87288c2cc', //wifi only
devices : [],
device : {},
connected : false,
active : false
};
mobile.wifi = {
current : 'null',
available : [],
ip : null
};
mobile.ble.scan = function () {
UI.spinner.show('Scanning for INTVAL3...');
UI.overlay.show();
ble.scan([], 5, mobile.ble.onDiscover, mobile.ble.onError);
mobile.ble.devices = [];
setTimeout(() => {
UI.spinner.hide();
UI.overlay.hide();
if (!mobile.ble.connected) {
mobile.alert('No devices found.')
settingsPage();
}
}, 5000);
};
mobile.ble.onDiscover = function (device) {
if (device && device.name && device.name.indexOf('intval3') !== -1) {
console.log('BLE - Discovered INTVAL3');
console.dir(device);
mobile.ble.devices.push(device);
if (!mobile.ble.connected) {
mobile.ble.connect(device);
}
} else {
//console.log(`BLE - Discovered Other ${device.id}`);
}
};
mobile.ble.connect = function (device) {
console.log(`BLE - Connecting to ${device.id}`);
ble.connect(device.id, (peripheral) => {
mobile.ble.onConnect(peripheral, device);
}, mobile.ble.onError);
};
mobile.ble.onConnect = function (peripheral, device) {
const elem = document.getElementById('bluetooth');
const option = document.createElement('option');
const disconnect = document.getElementById('disconnect');
const scan = document.getElementById('scan');
UI.spinner.hide();
UI.overlay.hide();
console.log(`BLE - Connected to ${device.id}`);
console.log(peripheral);
console.dir(device);
mobile.ble.device = device;
mobile.ble.connected = true;
elem.innerHTML = '';
option.text = device.name;
option.value = device.id;
elem.add(option);
disconnect.classList.add('active');
scan.classList.remove('active');
getState();
mobile.getWifi();
};
mobile.ble.disconnect = function () {
const elem = document.getElementById('bluetooth');
const option = document.createElement('option');
const disconnect = document.getElementById('disconnect');
const scan = document.getElementById('scan');
let device;
if (!mobile.ble.connected) {
console.warn('Not connected to any device');
return false;
}
device = mobile.ble.device;
console.log(`BLE - Disconnecting from ${device.id}`);
ble.disconnect(device.id, mobile.ble.onDisconnect, mobile.ble.onDisconnect);
elem.innerHTML = '';
option.text = 'N/A';
elem.add(option);
disconnect.classList.remove('active');
scan.classList.add('active');
UI.spinner.hide();
UI.overlay.hide();
};
mobile.ble.onDisconnect = function (res) {
console.log(`BLE - Disconnected from ${res}`);
mobile.ble.connected = false;
mobile.ble.device = {};
};
mobile.ble.onError = function (err) {
if (err.errorMessage && err.errorMessage === 'Peripheral Disconnected') {
console.log('Device disconnected');
mobile.ble.disconnect()
} else {
mobile.alert(JSON.stringify(err));
}
/*
Object
errorDescription: "The specified device has disconnected from us."
errorMessage: "Peripheral Disconnected"
id: "E8EF4B8B-0B5E-4E96-B337-E878DB1E3C4B"
name: "intval3_b827ebc7461d"
*/
};
mobile.init = function () {
const bleInputs = document.querySelectorAll('.ble');
const bolIso = document.querySelector('.iso');
const bolF = document.querySelector('.fstop');
document.querySelector('body').classList.add('mobile');
window.frame = mobile.frame;
window.getState = mobile.getState;
window.setDir = mobile.setDir;
window.setExposure = mobile.setExposure;
window.setDelay = mobile.setDelay;
window.setCounter = mobile.setCounter;
window.sequence = mobile.sequence;
window.reset = mobile.reset;
window.restart = mobile.restart;
window.update = mobile.update;
//show ble-specific fields in settings
for (let i of bleInputs) {
i.classList.add('active');
}
UI.spinner.init()
mobile.ble.scan();
mobile.cameraValues();
};
mobile.getState = function () {
if (!mobile.ble.connected) {
//returning here will prevent error alert
}
ble.read(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
mobile.stateSuccess,
mobile.ble.onError);
};
mobile.stateSuccess = function (data) {
let str = bytesToString(data);
let res = JSON.parse(str);
setState(res);
};
mobile.frame = function () {
const opts = {
type : 'frame'
};
if (!mobile.ble.connected) {
return mobile.alert('Not connected to an INTVAL3 device.');
}
if (mobile.ble.active) {
return false;
}
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.frameSuccess,
mobile.ble.onError);
document.getElementById('frame').classList.add('focus');
mobile.ble.active = true;
};
mobile.frameSuccess = function () {
if (STATE.exposure < 5000) {
console.log('Frame finished, getting state.');
mobile.ble.active = false;
document.getElementById('frame').classList.remove('focus');
mobile.getState();
} else {
setTimeout(() => {
console.log('Frame finished, getting state.');
mobile.ble.active = false;
document.getElementById('frame').classList.remove('focus');
mobile.getState();
}, STATE.exposure + 500)
}
}
mobile.setDir = function () {
const opts = {
type : 'dir',
dir : !document.getElementById('dir').checked
};
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.dirSuccess,
mobile.ble.onError);
};
mobile.dirSuccess = function () {
console.log('Set direction');
mobile.getState();
setTimeout(() => {
setDirLabel(STATE.dir);
}, 50);
};
mobile.setExposure = function () {
let exposure = document.getElementById('exposure').value;
let scaledExposure;
let opts = {
type : 'exposure'
};
if (exposure === '' || exposure === null) {
exposure = 0;
}
scaledExposure = scaleTime(exposure, STATE.scale);
opts.exposure = scaledExposure;
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.exposureSuccess,
mobile.ble.onError);
};
mobile.exposureSuccess = function () {
console.log('Set exposure');
mobile.getState();
};
mobile.setDelay = function () {
const delay = document.getElementById('delay').value;
const scaledDelay = scaleTime(delay, STATE.delayScale);
let opts = {
type : 'delay',
delay : scaledDelay
};
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.delaySuccess,
mobile.ble.onError);
}
mobile.delaySuccess = function () {
console.log('Set delay');
mobile.getState();
};
mobile.setCounter = function () {
let opts = {
type : 'counter',
counter : null
};
const counter = document.getElementById('counter').value;
function counterPrompt (results) {
let change = results.input1
if (results.buttonIndex === 1) {
if (change === null || !isNumeric(change)) return false;
opts.counter = change;
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.counterSuccess,
mobile.ble.onError);
}
}
navigator.notification.prompt(
`Change counter value?`,
counterPrompt,
'INTVAL3',
['Okay', 'Cancel'],
counter);
};
mobile.counterSuccess = function () {
console.log('Set counter');
mobile.getState();
};
mobile.sequence = function () {
const opts = {
type : 'sequence'
};
const elem = document.getElementById('seq');
if (!mobile.ble.connected) {
return mobile.alert('Not connected to an INTVAL3 device.');
}
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)), //check length?
mobile.sequenceSuccess,
mobile.ble.onError);
if (!elem.classList.contains('focus')) {
elem.classList.add('focus');
}
mobile.ble.active = true;
};
mobile.sequenceSuccess = function () {
console.log('Sequence state changed');
mobile.getState();
setTimeout(() => {
if (STATE.sequence) {
mobile.ble.active = true;
seqState(true);
} else {
mobile.ble.active = false;
seqState(false);
}
}, 20);
};
//retreive object with list of available Wifi APs,
//and state of current connection, if available
mobile.getWifi = function () {
UI.spinner.show('Refreshing WIFI...');
UI.overlay.show();
ble.read(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.WIFI_ID,
mobile.getWifiSuccess,
mobile.ble.onError);
};
mobile.getWifiSuccess = function (data) {
const elem = document.getElementById('available');
const wifi = document.getElementById('wifi');
const password = document.getElementById('password');
const ip = document.getElementById('ip');
let option = document.createElement('option');
let str = bytesToString(data);
let res = JSON.parse(str);
UI.spinner.hide();
UI.overlay.hide();
elem.innerHTML = ''
if (!res.available || res.available.length === 0) {
if (elem.classList.contains('active')) {
elem.classList.remove('active');
}
option.text = 'N/A'
elem.add(option);
elem.value = '';
} else {
for (let ap of res.available) {
option = document.createElement('option');
option.text = ap;
option.value = ap;
elem.add(option);
}
if (res.current && res.available.indexOf(res.current) !== -1) {
elem.value = res.current
if (!elem.classList.contains('active')) {
elem.classList.add('active');
}
if (wifi.classList.contains('active')) {
wifi.classList.remove('active');
}
if (password.classList.contains('active')) {
password.classList.remove('active');
}
} else {
if (!wifi.classList.contains('active')) {
wifi.classList.add('active');
}
if (!password.classList.contains('active')) {
password.classList.add('active');
}
}
}
if (typeof res.ip !== 'undefined' && res.ip != null ) {
ip.innerHTML = `Local IP: <span onclick="window.open('http://${res.ip}', '_system', 'location=yes');">${res.ip}</span>`
if (!ip.classList.contains('active')) {
ip.classList.add('active');
}
} else {
ip.innerHTML = 'Local IP: null'
if (ip.classList.contains('active')) {
ip.classList.remove('active');
}
}
mobile.wifi.current = res.current;
mobile.wifi.available = res.available;
mobile.wifi.ip = res.ip;
};
mobile.editWifi = function () {
const available = document.getElementById('available');
const wifi = document.getElementById('wifi');
const password = document.getElementById('password');
if (!wifi.classList.contains('active')) {
wifi.classList.add('active');
}
if (!password.classList.contains('active')) {
password.classList.add('active');
}
password.focus();
if (available.value !== mobile.wifi.current && available.classList.contains('active')) {
available.classList.remove('active');
}
};
mobile.setWifi = function () {
const ssid = document.getElementById('available').value;
const pwd = document.getElementById('password').value;
const opts = {
ssid : ssid,
pwd : pwd
};
UI.spinner.show('Setting WIFI...');
UI.overlay.show();
if (ssid === '' || ssid === null || ssid === undefined) {
return mobile.alert('Cannot set wireless credentials with a blank SSID');
}
if (pwd === '' || pwd === null || pwd === undefined) {
return mobile.alert('Cannot set wireless credentials with a blank passphrase');
}
if (pwd.length < 8 || pwd.length > 63) {
return mobile.alert('Passphrase must be 8..63 characters');
}
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.WIFI_ID,
stringToBytes(JSON.stringify(opts)),
mobile.setWifiSuccess,
mobile.ble.onError);
};
mobile.setWifiSuccess = function () {
UI.spinner.hide();
UI.overlay.hide();
console.log('Set new wifi credentials');
setTimeout(mobile.getWifi, 100);
};
mobile.exif = {}
mobile.getCamera = function () {
const opts = {
quality: 30,
sourceType: Camera.PictureSourceType.CAMERA,
destinationType: Camera.DestinationType.FILE_URI
};
navigator.camera.getPicture(mobile.cameraSuccess, mobile.cameraError, opts);
};
mobile.cameraSuccess = function (result) {
const thisResult = JSON.parse(result);
const metadata = JSON.parse(thisResult.json_metadata);
mobile.cameraExposure(metadata.Exif);
};
mobile.cameraError = function (err) {
console.error(err);
mobile.alert(JSON.stringify(err));
};
mobile.cameraExposure = function (exif) {
const cam_exp = document.getElementById('cam_exp');
const cam_f = document.getElementById('cam_f');
const cam_iso = document.getElementById('cam_iso');
const bol_exp = document.getElementById('bol_exp');
const bol_f = document.getElementById('bol_f');
const bol_iso = document.getElementById('bol_iso');
const bol_f_diff = document.getElementById('bol_f_diff');
const bol_iso_diff = document.getElementById('bol_iso_diff');
const bol_exp_diff = document.getElementById('bol_exp_diff');
const fstop = BOLEX.fstop || 5.6;
const iso = BOLEX.iso || 100;
const prism = BOLEX.prism || 0.8;
const cFstop = exif.ApertureValue || exif.FNumber;
const cExposure = exif.ExposureTime * 1000;
const cIso = exif.ISOSpeedRatings[0];
//convert fstop to "fnumber", an absolute scale where stops are scaled to 1.0
const f = fnumber(cFstop);
const target = fnumber(fstop); //bolex
let exposure = cExposure;
let isoStops = 0;
let fStops = 0;
let expDiff;
let scale_elem;
let exposure_elem;
let proceed;
let e1;
let e2;
mobile.exif = exif;
//Determine if fstop of phone camera "f"
if (target !== f) {
fStops = f - target;
exposure = exposure / Math.pow(2, fStops);
}
if (cIso != iso) {
isoStops = (Math.log(cIso) / Math.log(2)) - (Math.log(iso) / Math.log(2));
}
//Double or halve exposure based on the differences in ISO stops
exposure = exposure * Math.pow(2, isoStops);
//Compensate for Bolex prism
exposure = exposure * Math.pow(2, prism);
exposure = Math.round(exposure) //round to nearest millisecond
bol_f.value = fstop;
bol_iso.value = iso;
bol_exp.value = exposure;
//Total difference in exposure from phone camera to Bolex
expDiff = (Math.log(exposure) / Math.log(2)) - (Math.log(cExposure) / Math.log(2));
bol_exp_diff.innerHTML = floatDisplay(expDiff);
bol_iso_diff.innerHTML = floatDisplay(isoStops);
bol_f_diff.innerHTML = floatDisplay(-fStops);
cam_exp.value = cExposure;
cam_f.value = cFstop;
cam_iso.value = cIso;
function exposureConfirm (index) {
if (index === 1) {
e1 = new Event('change');
e2 = new Event('change');
scale_elem = document.getElementById('scale');
exposure_elem = document.getElementById('exposure');
scale_elem.value = 'ms';
scale_elem.dispatchEvent(e1);
exposure_elem.value = exposure;
exposure_elem.dispatchEvent(e2);
}
}
if (exposure > 500) {
navigator.notification.confirm(
`Set camera exposure to ${exposure}ms to match photo?`,
exposureConfirm,
'INTVAL3',
['Okay', 'Cancel']
);
}
/*
{
"Exif": {
"DateTimeOriginal": "2018:02:02 16:59:13",
"ExposureBiasValue": 0,
"SensingMethod": 2,
"BrightnessValue": -0.9969016228800144,
"LensMake": "Apple",
"FNumber": 1.8,
"FocalLength": 3.99,
"ShutterSpeedValue": 2.049355412374274,
"SceneType": 1,
"ApertureValue": 1.6959938131099002,
"SubjectArea": [
2015,
1511,
2217,
1330
],
"ColorSpace": 65535,
"LensSpecification": [
3.99,
3.99,
1.8,
1.8
],
"PixelYDimension": 3024,
"WhiteBalance": 0,
"DateTimeDigitized": "2018:02:02 16:59:13",
"ExposureMode": 0,
"ISOSpeedRatings": [
100
],
"PixelXDimension": 4032,
"LensModel": "iPhone 8 back camera 3.99mm f/1.8",
"ExposureTime": 0.25,
"Flash": 24,
"SubsecTimeDigitized": "209",
"SubsecTimeOriginal": "209",
"ExposureProgram": 2,
"FocalLenIn35mmFilm": 28,
"MeteringMode": 5
}
}
*/
};
mobile.refreshExposure = function () {
if (typeof mobile.exif.ExposureTime !== 'undefined') {
mobile.cameraExposure(mobile.exif);
}
};
mobile.EV = function (fstop, shutter) {
const sec = shutter / 1000; //shutter in ms => seconds
const square = Math.pow(fstop, 2);
return Math.log(square / sec);
};
mobile.reset = function () {
let opts = {
type : 'reset'
};
function resetConfirm (index) {
if (index === 1) {
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)),
mobile.resetSuccess,
mobile.ble.onError);
}
}
navigator.notification.confirm(
`Reset INTVAL3 to default settings and clear counter?`,
resetConfirm,
'INTVAL3',
['Okay', 'Cancel']
);
};
mobile.resetSuccess = function () {
console.log('Reset to default settings');
setTimeout(() => {
mobile.getState();
}, 100)
};
mobile.update = function () {
let opts = {
type : 'update'
};
function updateConfirm (index) {
if (index === 1) {
UI.spinner.show('Updating INTVAL3...');
UI.overlay.show();
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)),
mobile.updateSuccess,
mobile.ble.onError);
}
}
navigator.notification.confirm(
`Check for updates? You will be disconnected from the INTVAL3 during this process.`,
updateConfirm,
'INTVAL3',
['Okay', 'Cancel']
);
};
mobile.updateSuccess = function () {
console.log('Finished updating firmware, restarting...');
};
mobile.restart = function () {
let opts = {
type : 'restart'
};
function restartConfirm (index) {
if (index === 1) {
UI.spinner.show('Restarting INTVAL3...');
UI.overlay.show();
ble.write(mobile.ble.device.id,
mobile.ble.SERVICE_ID,
mobile.ble.CHAR_ID,
stringToBytes(JSON.stringify(opts)),
mobile.restartSuccess,
mobile.ble.onError);
}
}
navigator.notification.confirm(
`Restart the INTVAL3? You will be disconnected from it during this process.`,
restartConfirm,
'INTVAL3',
['Okay', 'Cancel']
);
};
mobile.restartSuccess = function () {
console.log('Restarting... ');
}
mobile.alert = function (msg) {
if (navigator && navigator.notification) {
navigator.notification.alert(
msg,
() => {},
'INTVAL3',
'Okay'
);
} else {
alert(msg);
}
};
/**
* Mobile helper functions
*/
function bytesToString (buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
function stringToBytes(string) {
var array = new Uint8Array(string.length);
for (var i = 0, l = string.length; i < l; i++) {
array[i] = string.charCodeAt(i);
}
return array.buffer;
}
function fnumber (fstop) {
return Math.log(fstop) / Math.log(Math.sqrt(2));
}
function floatDisplay (value) {
let str = value + '';
const period = str.indexOf('.');
if (period === -1) {
str = str + '.0';
} else {
str = roundTenth(value) + '';
}
if (value < 0) {
str = `<span class="neg">${(str + '')}</span>`;
} else if (value > 0) {
str = `<span class="pos">+${(str + '')}</span>`;
}
return str;
}
function roundTenth (value) {
return Math.round((value * 10) / 10)
}

View File

@ -0,0 +1,19 @@
'use strict'
const editor = {}
editor.init = function () {
const elem = document.getElementById('mscript_editor');
editor.cm = CodeMirror.fromTextArea(elem, {
lineNumbers: true,
theme : 'monokai',
gutters: ['CodeMirror-linenumbers']
});
setTimeout(() => {
editor.cm.setValue('CF');
editor.cm.refresh();
}, 10);
};
document.addEventListener('DOMContentLoaded', event => {
editor.init();
});

View File

@ -0,0 +1,232 @@
'use strict'
const web = {};
web._header = new Headers({ 'content-type' : 'application/json' })
web.frame = function () {
const opts = {
method : 'POST',
headers : web._header
};
fetch('/frame', opts)
.then(res => {
return res.json()
})
.then(web.frameSuccess)
.catch(err => {
console.error('Error triggering frame')
console.error(err)
});
}
web.frameSuccess = function (res) {
document.getElementById('frame').blur();
console.log(`Frame ${res.dir ? 'forward' : 'backward'} took ${res.len}ms`)
if (res.dir === true) {
incCounter(1);
} else {
incCounter(-1);
}
};
web.setDir = function () {
const dir = !document.getElementById('dir').checked;
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({ dir : dir })
};
fetch('/dir', opts)
.then(res => {
return res.json()
})
.then(web.setDirSuccess)
.catch(err => {
console.error('Error setting direction')
console.error(err);
});
};
web.setDirSuccess = function (res) {
STATE.dir = res.dir;
setDirLabel(res.dir);
console.log(`setDir to ${res.dir}`);
};
web.getState = function () {
const opts = {
method : 'GET'
}
fetch('/status', opts)
.then(res => {
return res.json();
})
.then(setState)
.catch(err => {
console.error('Error getting state');
console.error(err);
});
};
web.setExposure = function () {
let exposure = document.getElementById('exposure').value;
let scaledExposure;
let opts
if (exposure === '' || exposure === null) {
exposure = 0;
}
scaledExposure = scaleTime(exposure, STATE.scale);
opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({ exposure : scaledExposure })
}
fetch('/exposure', opts)
.then(web.useJson)
.then(web.setExposureSuccess)
.catch(err => {
console.error('Error setting exposure');
console.error(err);
});
};
web.setExposureSuccess = function (res) {
let exposure;
if (res.exposure < BOLEX.expected) {
res.exposure = BOLEX.expected;
}
STATE.exposure = res.exposure;
exposure = shutter(STATE.exposure);
document.getElementById('str').innerHTML = exposure.str;
console.log(`setExposure to ${res.exposure}`);
};
web.setDelay = function () {
const delay = document.getElementById('delay').value;
const scaledDelay = scaleTime(delay, STATE.delayScale)
let opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({ delay : scaledDelay })
}
fetch('/delay', opts)
.then(web.useJson)
.then(web.setDelaySuccess)
.catch(err => {
console.error('Error setting delay');
console.error(err);
})
};
web.setDelaySuccess = function (res) {
STATE.delay = res.delay;
console.log(`setDelay to ${res.delay}`);
};
web.setCounter = function () {
const counter = document.getElementById('counter').value;
const change = prompt(`Change counter value?`, counter);
if (change === null || !isNumeric(change)) return false;
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({ counter : change })
}
fetch('/counter', opts)
.then(web.useJson)
.then(web.setCounterSuccess)
.catch(err => {
console.error('Error setting counter');
console.error(err);
})
};
web.setCounterSuccess = function (res) {
STATE.counter = res.counter;
forceCounter(res.counter);
console.log(`setCounter to ${res.counter}`);
};
web.sequence = function () {
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({})
}
fetch('/sequence', opts)
.then(web.useJson)
.then(web.sequenceSuccess)
.catch(err => {
console.error('Error getting /sequence');
console.error(err);
})
};
web.sequenceSuccess = function (res) {
if (res.started && res.started != false) {
STATE.sequence = true;
document.getElementById('seq').focus();
seqState(true);
} else if (res.stopped) {
STATE.sequence = false;
document.getElementById('seq').blur();
seqState(false);
mobile.getState();
}
};
web.reset = function () {
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({})
}
const proceed = confirm(`Reset INTVAL3 to default settings and clear counter?`);
if (!proceed) return false
fetch('/reset', opts)
.then(web.useJson)
.then(setState)
.catch(err => {
console.error('Error posting to /reset');
console.error(err);
})
};
web.restart = function () {
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({})
}
const proceed = confirm(`Restart the INTVAL3? You will be disconnected from it during this process.`);
if (!proceed) return false;
fetch('/restart', opts)
.then(web.useJson)
.then(web.restartSuccess)
.catch(err => {
console.error('Error posting to /restart');
console.error(err);
})
};
web.restartSuccess = function (res) {
console.dir(res)
};
web.update = function () {
const opts = {
method : 'POST',
headers : web._header,
body : JSON.stringify({})
}
const proceed = confirm(`Check for updates? You will be disconnected from the INTVAL3 during this process.`);
if (!proceed) return false;
fetch('/update', opts)
.then(web.useJson)
.then(web.updateSuccess)
.catch(err => {
console.error('Error posting to /update');
console.error(err);
})
};
web.updateSuccess = function (res) {
console.dir(res)
};
web.useJson = function (res) {
return res.json();
};
web.init = function () {
window.frame = web.frame;
window.getState = web.getState;
window.setDir = web.setDir;
window.setDelay = web.setDelay;
window.setExposure = web.setExposure;
window.setCounter = web.setCounter;
window.sequence = web.sequence;
window.reset = web.reset;
window.restart = web.restart;
window.update = web.update;
console.log('started web')
};

File diff suppressed because it is too large Load Diff

2
app/www/static/js/spin.min.js vendored Normal file
View File

@ -0,0 +1,2 @@
// http://spin.js.org/#v2.3.2
!function(a,b){"object"==typeof module&&module.exports?module.exports=b():"function"==typeof define&&define.amd?define(b):a.Spinner=b()}(this,function(){"use strict";function a(a,b){var c,d=document.createElement(a||"div");for(c in b)d[c]=b[c];return d}function b(a){for(var b=1,c=arguments.length;c>b;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return m[e]||(k.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",k.cssRules.length),m[e]=1),e}function d(a,b){var c,d,e=a.style;if(b=b.charAt(0).toUpperCase()+b.slice(1),void 0!==e[b])return b;for(d=0;d<l.length;d++)if(c=l[d]+b,void 0!==e[c])return c}function e(a,b){for(var c in b)a.style[d(a,c)||c]=b[c];return a}function f(a){for(var b=1;b<arguments.length;b++){var c=arguments[b];for(var d in c)void 0===a[d]&&(a[d]=c[d])}return a}function g(a,b){return"string"==typeof a?a:a[b%a.length]}function h(a){this.opts=f(a||{},h.defaults,n)}function i(){function c(b,c){return a("<"+b+' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">',c)}k.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.scale*d.width,left:d.scale*d.radius,top:-d.scale*d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.scale*(d.length+d.width),k=2*d.scale*j,l=-(d.width+d.length)*d.scale*2+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d<e.childNodes.length&&(e=e.childNodes[b+d],e=e&&e.firstChild,e=e&&e.firstChild,e&&(e.opacity=c))}}var j,k,l=["webkit","Moz","ms","O"],m={},n={lines:12,length:7,width:5,radius:10,scale:1,corners:1,color:"#000",opacity:.25,rotate:0,direction:1,speed:1,trail:100,fps:20,zIndex:2e9,className:"spinner",top:"50%",left:"50%",shadow:!1,hwaccel:!1,position:"absolute"};if(h.defaults={},f(h.prototype,{spin:function(b){this.stop();var c=this,d=c.opts,f=c.el=a(null,{className:d.className});if(e(f,{position:d.position,width:0,zIndex:d.zIndex,left:d.left,top:d.top}),b&&b.insertBefore(f,b.firstChild||null),f.setAttribute("role","progressbar"),c.lines(f,c.opts),!j){var g,h=0,i=(d.lines-1)*(1-d.direction)/2,k=d.fps,l=k/d.speed,m=(1-d.opacity)/(l*d.trail/100),n=l/d.lines;!function o(){h++;for(var a=0;a<d.lines;a++)g=Math.max(1-(h+(d.lines-a)*n)%l*m,d.opacity),c.opacity(f,a*d.direction+i,g,d);c.timeout=c.el&&setTimeout(o,~~(1e3/k))}()}return c},stop:function(){var a=this.el;return a&&(clearTimeout(this.timeout),a.parentNode&&a.parentNode.removeChild(a),this.el=void 0),this},lines:function(d,f){function h(b,c){return e(a(),{position:"absolute",width:f.scale*(f.length+f.width)+"px",height:f.scale*f.width+"px",background:b,boxShadow:c,transformOrigin:"left",transform:"rotate("+~~(360/f.lines*k+f.rotate)+"deg) translate("+f.scale*f.radius+"px,0)",borderRadius:(f.corners*f.scale*f.width>>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k<f.lines;k++)i=e(a(),{position:"absolute",top:1+~(f.scale*f.width/2)+"px",transform:f.hwaccel?"translate3d(0,0,0)":"",opacity:f.opacity,animation:j&&c(f.opacity,f.trail,l+k*f.direction,f.lines)+" "+1/f.speed+"s linear infinite"}),f.shadow&&b(i,e(h("#000","0 0 4px #000"),{top:"2px"})),b(d,b(i,h(g(f.color,k),"0 0 1px rgba(0,0,0,.1)")));return d},opacity:function(a,b,c){b<a.childNodes.length&&(a.childNodes[b].style.opacity=c)}}),"undefined"!=typeof document){k=function(){var c=a("style",{type:"text/css"});return b(document.getElementsByTagName("head")[0],c),c.sheet||c.styleSheet}();var o=e(a("group"),{behavior:"url(#default#VML)"});!d(o,"transform")&&o.adj?i():j=d(o,"animation")}return h});

16
app/www/test/index.html Normal file
View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>INTVAL3 client tests</title>
<link href="../static/css/qunit-2.5.0.css" rel="stylesheet" />
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="../static/js/qunit-2.5.0.js"></script>
<script src="../static/js/intval.core.js"></script>
<script src="./tests.js"></script>
</body>
</html>

3
app/www/test/tests.js Normal file
View File

@ -0,0 +1,3 @@
QUnit.test('hello world', function (assert) {
assert.ok(true, 'this is ok')
})

View File

@ -0,0 +1,24 @@
# interfaces(5) file used by ifup(8) and ifdown(8)
# Please note that this file is written to be used with dhcpcd
# For static IP, consult /etc/dhcpcd.conf and 'man dhcpcd.conf'
# Include files from /etc/network/interfaces.d:
source-directory /etc/network/interfaces.d
auto lo
iface lo inet loopback
iface eth0 inet manual
auto wlan0
allow-hotplug wlan0
iface wlan0 inet manual
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
wireless-power off
auto wlan1
allow-hotplug wlan1
iface wlan1 inet manual
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf
wireless-power off

28
conf/nginx.conf Normal file
View File

@ -0,0 +1,28 @@
#blootstrap nginx conf
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:6699/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
#gzip on;
#gzip_comp_level 5;
#gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
}
#uncomment for static file server
location /static/ {
#uncomment to turn on caching
#expires modified 1y;
#access_log off;
#add_header Cache-Control "public";
#gzip on;
#gzip_comp_level 5;
#gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
#use project location
alias /home/pi/intval3/app/www/static/;
}
}

14
dev.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/bash
echo "Starting in dev mode"
rm run_dev.sh
jq -r ".apps[0].env | keys[]" ./process.json | while read key ; do
echo -n "$key=\"">> run_dev.sh
echo -n "$(jq ".apps[0].env.$key" ./process.json)" >> run_dev.sh
echo -n "\" ">> run_dev.sh
done
echo -n " node ." >> run_dev.sh
#cat run_dev.sh
sh run_dev.sh

8
docs.sh Normal file
View File

@ -0,0 +1,8 @@
#!/bin/bash
libs="./lib/*"
for l in $libs
do
echo "Generating documentation for $l"
./node_modules/.bin/jsdoc2md $l/index.js > $l/Readme.md
done

View File

@ -0,0 +1,3 @@
#!/bin/bash
curl -H "Content-Type: application/json" -X POST -d '{"dir" : false }' http://localhost:6699/dir

118
experiments/gpio.js Normal file
View File

@ -0,0 +1,118 @@
'use strict'
const Gpio = require('onoff').Gpio
let release
let micro
let fwd
let bwd
process.on('SIGINT', () => {
if (fwd && fwd.writeSync) {
console.log(`Setting fwd to 0`)
fwd.writeSync(0)
}
if (bwd && bwd.writeSync) {
console.log(`Setting bwd to 0`)
bwd.writeSync(0)
}
process.exit()
})
function releaseTest () {
const PIN = 6
release = Gpio(PIN, 'in', 'both')
console.log(`Watching input on GPIO 0${PIN}`)
let saveTime = 0
let active = false
release.watch((err, val) => {
const NOW = +new Date()
/* Button + 10K ohm resistor */
/* 1 = open */
/* 0 = closed */
if (err) {
return console.error(err)
}
//console.log(`Release switch val: ${val}`)
//console.log(`RELEASE: ${val} ${active} ${NOW} ${saveTime}`)
if (val === 0) {
//console.log('closed')
} else if (val === 1) {
//console.log('open')
}
if (val === 0) {
//closed
if ((!active && saveTime === 0) || (active && NOW - saveTime > 10 * 1000)) {
saveTime = NOW
active = true //maybe unncecessary
} else {
//saveTime = 0
//active = false
}
} else if (val === 1) {
//open
if (active) {
if (NOW - saveTime > 50 && NOW - saveTime < 1000) {
console.log('Started Frame')
} else if (NOW - saveTime >= 1000) {
console.log('Started Sequence')
}
//console.log(`Release closed for ${NOW - saveTime}`)
saveTime = 0
active = false
}
}
})
}
function microTest () {
const PIN = 5
micro = Gpio(PIN, 'in', 'both')
console.log(`Watching input on GPIO 0${PIN}`)
let saveTime = 0
let frameActive = true //this._state.frame.active
let primed = false //this._state.primed
micro.watch((err, val) => {
const NOW = +new Date()
if (err) {
return console.error(err)
}
console.log(`Micro switch val: ${val}`)
if (val === 0) {
//console.log('closed')
} else if (val === 1) {
//console.log('open')
}
if (val === 0 && frameActive) {
if (!primed) {
primed = true
saveTime = NOW
console.log('Primed')
}
} else if (val === 1 && frameActive) {
if (primed) {
primed = false
setTimeout( () => {
console.log(`Stop Frame after ${NOW - saveTime}`)
}, 10)
}
}
})
}
//test stepping up of 3.3V RPI logic via
//Sparkfun PRT-10968 (NPC1402)
function stepupTest () {
const FWD = 13 // RPIO PIN 13
const BWD = 19
fwd = Gpio(FWD, 'out')
bwd = Gpio(BWD, 'out')
console.log(`Setting pin ${BWD} high`)
fwd.writeSync(0)
bwd.writeSync(1)
}
releaseTest()
microTest()
//stepupTest()

169
experiments/mscript.js Normal file
View File

@ -0,0 +1,169 @@
'use strict'
const log = require('../lib/log')('mscript-tests')
const mscript = require('../lib/mscript')
//TODO: rewrite for mocha
const tests = function tests () {
log.info('Running mscript tests')
console.time('Tests took')
mscript.alts_unique(); //perform check only during tests
var fail = function (script, obj) {
log.error('...Failed :(')
log.error('script', script)
log.error('err', obj)
process.exit(1)
}
let script =
`CF
PF
CB
PB
BF
BB`
log.info('Basic function test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 0
&& obj.proj === 0
&& obj.arr.length === 6) {
log.info('...Passed!')
} else {
fail(script, obj)
}
})
script =
`CF
PF
CB
PB
BF
BB`
log.info('Functions with integers test...')
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 0
&& obj.proj === 0
&& obj.arr.length === 6) {
log.info('...Passed!')
} else {
fail(script, obj)
}
})
script =
`CF 1000
CB 1000
SET PROJ 200
PB 200`
log.info('Basic state test...')
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 0
&& obj.proj === 0) {
log.info('...Passed!')
} else {
fail(script, obj)
}
})
script =
`LOOP 10
CF 3
PF 1
END LOOP`
log.info('Basic loop test...')
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 30
&& obj.proj === 10
&& obj.arr.length === 40) {
log.info('...Passed!')
} else {
fail(script, obj)
}
});
script = `LOOP 4\nLOOP 4\nPF\nBF\nEND LOOP\nEND LOOP`
log.info('Recursive loop test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 16
&& obj.proj === 16
&& obj.arr.length === 32) {
log.info('...Passed!');
} else {
fail(script, obj);
}
});
//Lighting tests
script = `L 255,255,255\nCF\nPF`
log.info('Basic light test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 1
&& obj.proj === 1
&& obj.arr.length === 2
&& obj.light.length === 2
&& obj.light[0] === '255,255,255'
&& obj.light[1] === '') {
log.info('...Passed!');
} else {
fail(script, obj);
}
});
script = 'L 255,255,255\nCF\nPF\nBF';
log.info('Basic black test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 2
&& obj.proj === 1
&& obj.arr.length === 3
&& obj.light.length === 3
&& obj.light[0] === '255,255,255'
&& obj.light[1] === ''
&& obj.light[2] === mscript.black) {
log.info('...Passed!');
} else {
fail(script, obj);
}
});
script = 'LOOP 2\nL 1,1,1\nCF\nL 2,2,2\nCF\nEND';
log.info('Basic light loop test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 4
&& obj.proj === 0
&& obj.arr.length === 4
&& obj.light.length === 4
&& obj.light[0] === '1,1,1'
&& obj.light[3] === '2,2,2') {
log.info('...Passed!');
} else {
fail(script, obj);
}
});
//LOOP W/ CAM and PROJ
script = 'LOOP 2\nCAM 4\nPROJ 4\nEND';
log.info('Basic cam/proj loop test...');
mscript.interpret(script, function (obj) {
if (obj.success === true
&& obj.cam === 8
&& obj.proj === 8
&& obj.arr.length === 16
&& obj.light.length === 16
&& obj.light[0] === mscript.black) {
log.info('...Passed!');
} else {
fail(script, obj);
}
});
log.info('All tests completed');
console.timeEnd('Tests took');
}
tests()

4
experiments/wifi.js Normal file
View File

@ -0,0 +1,4 @@
'use strict'
const log = require('../lib/log')('wifi-tests')
const wifi = require('../lib/wifi')

466
hardware/case.scad Normal file
View File

@ -0,0 +1,466 @@
include <./modules.scad>
include <./variables.scad>
module l289N_holes (r = 3/2 - .2) {
$fn = 60;
DISTANCE = 36.5;
H = 50;
translate([0, 0, 0]) cylinder(r = r, h = H * 5, center = true);
translate([DISTANCE, 0, 0]) cylinder(r = r, h = H * 5, center = true);
translate([DISTANCE, DISTANCE, 0]) cylinder(r = r, h = H * 5, center = true);
translate([0, DISTANCE, 0]) cylinder(r = r, h = H * 5, center = true);
}
module l289N_hole_test () {
$fn = 40;
difference () {
cube([140, 40, 3], center = true);
cylinder(r = 3/2, h = 50, center = true);
translate([7, 0, 0]) cylinder(r = 3/2, h = 50, center = true);
translate([7 * 2, 0, 0]) cylinder(r = 3/2 - .1, h = 50, center = true);
translate([7 * 3, 0, 0]) cylinder(r = 3/2 - .2, h = 50, center = true);
translate([7 * 4, 0, 0]) cylinder(r = 3/2 - .3, h = 50, center = true);
}
}
module intval_panel_laser () {
$fn = 40;
difference () {
union () {
difference () {
translate ([0, 0, 8.5]) {
union () {
translate([12 - 10, , 0]) {
rotate([0, 0, -13]) {
rounded_cube([panel_2_x + 20 + 20, panel_2_y, 25.4/8], d = 20, center = true);
}
}
//reinforces
//translate([54, -12, -3]) rotate([0, 0, 89]) rounded_cube([110, 20, 4], 20, center = true);
//translate([-17, 2, -3]) rotate([0, 0, 72]) rounded_cube([94, 13, 4], 13, center = true);
}
}
for (i = [0 : len(xArray) - 1]) {
bolex_pin_inner_laser(xArray[i], yArray[i]);
}
}
//onetoone(26, 10, 4.5);
//extends for onetoone
}
//onetoone(9, 14, 8.5);
bearing_laser(54.5, 12, 6, width= 18, hole=false);
translate([-38, -1, 0]) rotate([0, 0, -13]) l289N_holes();
//translate ([6, -9, height + 3.5]) cylinder(r = bolt_inner, h = 50, center = true); //cover standoff hole
//frame_counter_access(); //use the space
m_p_access();
remove_front();
translate([6, 18, 0]) rotate([0, 0, -13]) cube([15, 25, 40], center=true); //motor wind key hole
for (i = [0 : len(mm_x) - 1]) {
translate([mm_x[i], mm_y[i], 0]) cylinder(r = bolt_inner, h = 100, center = true);
}
translate([0, 0, .25]) intval_laser_panel_cover();
translate([-35, -24, 15]) rotate([0, 0, -13]) {
translate([58 / 2, 23 / 2, 0]) cylinder(r = 3/2 - .2, h = 30, center = true, $fn = 20);
translate([-58 / 2, 23 / 2, 0]) cylinder(r = 3/2 - .2, h = 30, center = true, $fn = 20);
translate([58 / 2, -23 / 2, 0]) cylinder(r = 3/2 - .2, h = 30, center = true, $fn = 20);
translate([-58 / 2, -23 / 2, 0]) cylinder(r = 3/2 - .2, h = 30, center = true, $fn = 20);
}
}
}
module intval_panel_laser_debug () {
$fn = 40;
difference () {
union () {
difference () {
translate ([0, 0, 8.5]) {
union () {
translate([12 - 32.5, -5 + 9, 0]) {
rotate([0, 0, -13]) {
rounded_cube([panel_2_x + 20 + 65, panel_2_y, 25.4/8], d = 20, center = true);
}
}
//reinforces
//translate([54, -12, -3]) rotate([0, 0, 89]) rounded_cube([110, 20, 4], 20, center = true);
//translate([-17, 2, -3]) rotate([0, 0, 72]) rounded_cube([94, 13, 4], 13, center = true);
}
}
for (i = [0 : len(xArray) - 1]) {
bolex_pin_inner_laser(xArray[i], yArray[i]);
}
}
//onetoone(26, 10, 4.5);
//extends for onetoone
}
//onetoone(9, 14, 8.5);
bearing_laser(54.5, 12, 6, width= 18, hole=false);
translate([-38, -1, 0]) rotate([0, 0, -13]) l289N_holes();
//translate ([6, -9, height + 3.5]) cylinder(r = bolt_inner, h = 50, center = true); //cover standoff hole
//frame_counter_access(); //use the space
m_p_access();
remove_front();
translate([6, 18, 0]) rotate([0, 0, -13]) cube([15, 25, 40], center=true); //motor wind key hole
for (i = [0 : len(mm_x) - 1]) {
translate([mm_x[i], mm_y[i], 0]) cylinder(r = bolt_inner, h = 100, center = true);
}
intval_laser_panel_cover(DEBUG = true);
translate ([4, 12, 0]) {
translate([-51.5, -8.5, 0]) cylinder(r = 2.8/2, h = 100, center = true);
translate([-51.5 - 66, -8.5 + 15, 0]) cylinder(r = 2.8/2, h = 100, center = true);
translate([-51.5 + 11.5, -8 + 49, 0]) cylinder(r = 2.8/2, h = 100, center = true);
translate([-51.5 - 54.5, -8.5 + 49 + 16, 0]) cylinder(r = 2.8/2, h = 100, center = true);
}
}
}
module bolex_pin_laser (x, y) {
in = innerD;
$fn = 120;
translate ([x, y, 1]) {
difference () {
union () {
translate([0, 0, (height / 2) - 3]) cylinder(r = (outerD + 5) / 2, h = 2, center = true);
translate([0, 0, 1.175/2]) cylinder(r = outerD / 2, h = height + 1.175 , center = true);
}
cylinder(r = in / 2, h = height * 2, center = true);
translate([0, 0, (height / 2) - 1.9]) cylinder(r1 =4.5 / 2, r2 = 6.7 / 2, h = 2, center = true);
translate([0, 0, (height / 2) + 1]) cylinder(r = 6.7 / 2, h = 4, center = true);
}
}
}
module intval_laser_standoffs () {
$fn = 40;
for (i = [0 : len(xArray) - 1]) {
bolex_pin_laser(xArray[i], yArray[i]);
}
}
module intval_laser_standoffs_plate () {
$fn = 40;
rotate ([0, 180, 0]) {
bolex_pin_laser(0, 0);
bolex_pin_laser(15, 0);
bolex_pin_laser(0, 15);
bolex_pin_laser(15, 15);
}
//decoys
//translate([7, 7, 0]) decoys(23, 5.5, 6);
}
module bolex_pin_inner_laser (x, y) {
$fn = 40;
//innerD = 6.75;
innerD = 9;
translate ([x, y, 1]) {
cylinder(r = innerD / 2, h = height * 2, center = true);
//translate([0, 0, (height / 2) - 1]) cylinder(r1 =4.5 / 2, r2 = 6.5 / 2, h = 2, center = true);
}
}
module bearing_laser (x, y, z, width= 8, hole = true) {
innerD = 8.05;
outerD = 22.1 - .4;
fuzz = 0.1;
translate ([x, y, z]) {
difference () {
cylinder(r = outerD / 2 + fuzz, h = width, center = true);
if (hole) {
cylinder(r = innerD / 2 - fuzz, h = width, center = true);
}
}
}
}
module intval_laser_panel_cover (LASER = false, DEBUG = false, ALL_RED = false) {
$fn = 60;
cover_h = 16 + 3 + 4 + 10;
MATERIAL = 25.4 / 8;
module top () {
difference () {
rotate([0, 0, -13]) {
translate([-10, 0, 0]) rounded_cube([120, panel_2_y, MATERIAL], d = 20, center = true);
}
translate([53, 12, 0]) cylinder(r = 30, h = 60, center = true); //hole for motor mount
translate([22, 20, 0]) cylinder(r = 8, h = 60, center = true); // hole for moto mount bolt holder
translate([53, 42, 0]) cylinder(r = 15, h = 60, center = true); //removes pointy part
translate([-44 - 20, 8 + 5, -(cover_h / 2 ) - MATERIAL - 1]) rotate([0, 0, -13]) rotate([0, 90, 0]) back_side();
translate([2, 49, -(cover_h / 2 ) - MATERIAL - 1]) rotate([0, 0, -13]) rotate([90, 0, 0]) top_side();
translate([-22, -45, -(cover_h / 2 ) - MATERIAL - 1]) rotate([0, 0, -13]) rotate([90, 0, 0]) bottom_side();
translate([xArray[0], yArray[0], 0]) cylinder(r = 7 / 2, h = height * 20, center = true); //top standoff access
translate ([8, -9, height + 3.5]) cylinder(r = bolt_inner - .5, h = 50, center = true); //bottom mount attach
}
}
module back_side () {
difference () {
translate([0, 1.75, 0]) cube([cover_h + 2 + (MATERIAL * 2) + 1 + 3, panel_2_y - 10, MATERIAL], center = true);
//top negatives (strange)
translate([-(cover_h / 2) - (MATERIAL * 1.5), 20, 0]) cube([MATERIAL, 20, MATERIAL], center = true);
translate([-(cover_h / 2) - (MATERIAL * 1.5), -20, 0]) cube([MATERIAL, 20, MATERIAL], center = true);
translate([-(cover_h / 2) - (MATERIAL * 1.5), 47, 0]) cube([MATERIAL, 10, MATERIAL], center = true);
translate([-(cover_h / 2) - (MATERIAL * 1.5), -47, 0]) cube([MATERIAL, 10, MATERIAL], center = true);
//bottom negatives
translate([(cover_h / 2) + (MATERIAL * 1.5), 20, 0]) cube([MATERIAL, 20, MATERIAL], center = true);
translate([(cover_h / 2) + (MATERIAL * 1.5), -20 - 11, 0]) cube([MATERIAL, 35, MATERIAL], center = true);
translate([18 , -30 , 0]) cube([10, 15, 30], center = true); //access for microSD
translate([0, 50.5, 0]) cube([17.5, MATERIAL, MATERIAL], center = true);
translate([0, -50.5 + (1.75 / 2) + MATERIAL - 0.25, 0]) cube([17.5, MATERIAL, MATERIAL], center = true);
}
}
module top_side () {
difference () {
translate([-2.5 - 10 - 1, 0, 0]) cube([ panel_2_x - 41 + 20, cover_h + 2 + (MATERIAL * 2) + 1 + 3, MATERIAL], center = true);
//top and bottom negatives
translate([28 - 5, -(cover_h / 2) - (MATERIAL * 1.5), 0]) cube([35, MATERIAL, MATERIAL], center = true);
translate([28 - 5, (cover_h / 2) + (MATERIAL * 1.5), 0]) cube([35, MATERIAL, MATERIAL], center = true);
translate([-28 - 10 - 2, -(cover_h / 2) - (MATERIAL * 1.5), 0]) cube([30, MATERIAL, MATERIAL], center = true);
translate([-28 - 10 - 2, (cover_h / 2) + (MATERIAL * 1.5), 0]) cube([30, MATERIAL, MATERIAL], center = true);
//back side negatives
translate([-35.5 - 20 - 1, -13 - 8.1, 0]) cube([MATERIAL, 25, MATERIAL], center = true); //side tabs
translate([-35.5 - 20 - 1, 13 + 8.1, 0]) cube([MATERIAL, 25, MATERIAL], center = true); //side tabs
}
}
module bottom_side () {
difference () {
//main piece
translate([.25 - 10 - 1, 0, 0]) cube([ panel_2_x - 39.5 + 20, cover_h + 2 + (MATERIAL * 2) + 1 + 3, MATERIAL], center = true);
//top and bottom negatives
translate([25 - 27.5, -(cover_h / 2) - (MATERIAL * 1.5), 0]) cube([35, MATERIAL, MATERIAL], center = true);
translate([-25 - 29, -(cover_h / 2) - (MATERIAL * 1.5), 0]) cube([30, MATERIAL, MATERIAL], center = true);
translate([30, -(cover_h / 2) - (MATERIAL * 1.5), 0]) cube([15, MATERIAL, MATERIAL], center = true);
//
translate([30, (cover_h / 2) + (MATERIAL * 1.5), 0]) cube([35, MATERIAL, MATERIAL], center = true);
translate([-30 - 10 - 2, (cover_h / 2) + (MATERIAL * 1.5), 0]) cube([30, MATERIAL, MATERIAL], center = true);
//back side negatives
translate([-33.5 - 20 - 1, 17.3, 0]) cube([MATERIAL, 17.5, MATERIAL], center = true);
translate([-33.5 - 20 - 1, -17.3, 0]) cube([MATERIAL, 17.5, MATERIAL], center = true);
//hole for audio jack -> add countersink
translate([7, 10, 0]) cylinder(r = 6/2, h = 50, center = true);
//hole for female DC power jack, 12vdc
//translate([-15 - 20, 1 + 5, 0]) cylinder(r = 8/2, h = 20, center = true); //smaller DC jack
translate([23, 8, 0]) cylinder(r = 12/2, h = 20, center = true); //larger DC jack
//usb negative
translate([0, -15, 0]) cube([30, 10, 20], center = true);
}
}
if (LASER) {
projection() top();
if (!DEBUG) {
translate([-95, 20, 0]) rotate([0, 0, -13]) projection() back_side();
}
translate([20, 80, 0]) rotate([0, 0, -13]) projection() top_side();
translate([-20, -80, 0]) rotate([0, 0, -13]) projection() bottom_side();
} else {
//translate([0, 0, height + cover_h]) top();
if (!DEBUG) {
translate([-44 - 20, 8 + 5, height + (cover_h / 2 ) - 4.25]) rotate([0, 0, -13]) rotate([0, 90, 0]) back_side();
}
translate([2, 49, height + (cover_h / 2 ) - 4.25]) rotate([0, 0, -13]) rotate([90, 0, 0]) top_side();
translate([-22, -45, height + (cover_h / 2 ) - 4.25]) rotate([0, 0, -13]) rotate([90, 0, 0]) bottom_side();
}
}
module intval_laser_panel_cover_standoff (DECOYS = false) {
tight = 0.2;
cover_h = 21;
$fn = 40;
translate ([6, -9, height + 3.5]) {
difference() {
cylinder(r = bolt_inner + 1.4, h = cover_h - .5, center = true);
cylinder(r = bolt_inner - tight, h = cover_h, center = true);
}
if (DECOYS) {
decoys(12, -(cover_h / 2) + 2);
}
}
}
module remove_front () {
translate([87, 0, 4]) rotate([0, 0, 89]) cube([170, 40, 40], center = true);
}
module onetoone (size, height, z) {
translate ([one_to_one_x, one_to_one_y, z]) {
cylinder(r = size / 2, h = height, center = true);
}
}
module m_p_access () {
translate ([18, -44, 0]) {
rounded_cube([35, 17, 50], 17, true);
}
}
module bolex_pin (x, y) {
in = innerD;
translate ([x, y, 1]) {
difference () {
union () {
translate([0, 0, (height / 2) - 2]) cylinder(r = (outerD + 4) / 2, h = 4, center = true);
cylinder(r = outerD / 2, h = height, center = true);
}
cylinder(r = in / 2, h = height, center = true);
translate([0, 0, (height / 2) - 1]) cylinder(r1 =4.5 / 2, r2 = 6.5 / 2, h = 2, center = true);
}
}
}
module bolex_pin_inner (x, y) {
translate ([x, y, 1]) {
cylinder(r = innerD / 2, h = height * 2, center = true);
translate([0, 0, (height / 2) - 1]) cylinder(r1 =4.5 / 2, r2 = 6.5 / 2, h = 2, center = true);
}
}
module intval_pins () {
for (i = [0 : len(xArray) - 1]) {
bolex_pin(xArray[i], yArray[i]);
}
}
module key () {
tighten = 0.25;
difference () {
cylinder(r = 6.7 / 2, h = 5, center = true);
cylinder(r = (4.76 -+ tighten) / 2, h = 5, center = true);
}
translate ([0, 0, -7.5]) {
cylinder(r = 6.7 / 2, h = 10, center = true);
}
}
module keyHole () {
translate ([0, 0, 1.75]) {
cube([10, 2, 3.5], center = true);
}
}
module key_end (rotArr = [0, 0, 0], transArr = [0, 0, 0], ALT = false) {
translate(transArr) {
rotate (rotArr) {
difference () {
key();
keyHole();
if (ALT) {
translate([-2.5, 0, 1.75]) cube([5, 3, 3.5], center= true);
}
}
}
}
}
module frame_counter_access () {
x = 37.5;
y = 39;
translate([x, y, 8.5]) {
difference () {
union () {
rotate ([0, 0, 19]) {
translate([0, 9, 0]) {
cube([12, 16, 4], center = true);
}
}
rotate ([0, 0, -19]) {
translate([0, 9, 0]) {
cube([12, 16, 4], center = true);
}
}
}
translate([0, 15.5, 0]) {
cube([17, 6, 4], center = true);
}
}
cylinder(r = 6.2, h = 4, center = true);
}
}
module bearing (x, y, z, width= 8, hole = true, calval = 0) {
innerD = 8.05;
outerD = 22.1;
fuzz = 0.1;
translate ([x, y, z]) {
difference () {
cylinder(r = outerD / 2 + fuzz + calval, h = width, center = true);
if (hole) {
cylinder(r = innerD / 2 - fuzz, h = width, center = true);
}
}
}
}
module key_cap () {
$fn = 60;
thickness = .75;
innerD = 22.1;
outerD = innerD + (thickness * 2);
h = 18 - 2.5;
difference () {
cylinder(r = outerD / 2, h = h, center = true);
translate([0, 0, -1.01]) cylinder(r = innerD / 2, h = h - thickness, center = true);
//translate([100, 0, 0]) cube([200, 200, 200], center = true);
}
//decoys(23, 7);
}
module motor_cap_120 (HALF = false) {
$fn = 60;
base_d = 47;
base_inner = 29;
inner_h = 57;
difference () {
union () {
translate([-6, 0, 24]) cylinder(r = base_d/2, h = 15, center = true);
translate([0, 0, inner_h]) cylinder(r=(base_inner / 2) + 3, h=inner_h, center = true);
}
translate([-6, 0, -5.75]) cylinder(r = base_d/2 - 1, h = 50, center = true); //to grip edge of
translate([-6, 0, 3]) cylinder(r = base_d/2 - 3, h = 50, center = true);
translate([-25, 0, 19]) cube([10, 10, 15], center = true); //wire access
//120 motor
translate([0, 0, inner_h - 2]) cylinder(r=base_inner / 2, h=inner_h, center = true); //inner cylinder
if (HALF){
translate([100, 0, 0]) cube([200, 200, 200], center = true);
}
}
}
module bearing_calibrate (val = 0) {
mat = 25.4/8;
difference () {
cube([40, 40, mat], center = true);
bearing(0, 0, 0, hole = false, calval = val);
}
}
/*
//rpi zero w
translate([-35, -24, 15]) rotate([0, 0, -13]) {
difference () {
cube([67, 31, 3], center = true);
translate([58 / 2, 23 / 2, 0]) cylinder(r = 1.6, h = 3 + 1, center = true, $fn = 20);
translate([-58 / 2, 23 / 2, 0]) cylinder(r = 1.6, h = 3 + 1, center = true, $fn = 20);
translate([58 / 2, -23 / 2, 0]) cylinder(r = 1.6, h = 3 + 1, center = true, $fn = 20);
translate([-58 / 2, -23 / 2, 0]) cylinder(r = 1.6, h = 3 + 1, center = true, $fn = 20);
}
}
*/

58
hardware/intval3.scad Normal file
View File

@ -0,0 +1,58 @@
include <./case.scad>
include <./mount.scad>
include <./plunger.scad>
/*
INTVAL 3
*/
module stl_plate () {
//translate([0, 0, -0.5]) cube([150, 150, 1], center = true);
translate([-38, 41, 7.5]) rotate([0, 180, 0]) intval_laser_standoffs_plate();
translate([-27, 40, -9.5]) rotate([0, 0, 13]) translate([-40 + 2, -1, 14]) rotate([0, 0, -13]) l289N_mount();
translate([23, 1, -5.75]) rotate([0, 0, 90]) motor_mount_bottom();
translate([48, -13, 9]) rotate([0, 180, 0]) key_cap();
translate([-5, -11, 3]) rotate([0, 0, 190]) geared_motor_mount_120();
translate([65, 44, 22.5]) rotate([0, 180, 0]) motor_key();
translate([0, -42, 15]) plunger_plate();
translate([-52, -20, 66]) rotate([0, 180, 0]) motor_cap(false);
};
module dxf_plate () {
translate([125, 0, 0]) rotate([0, 0, 13]) projection() intval_panel_laser();
rotate([0, 0, 13]) intval_laser_panel_cover(LASER=true, ALL_RED=true);
};
module exploded_view () {
intval_panel_laser();
translate([0, 0, 5]) translate([-40 + 2, -1, 14]) rotate([0, 0, -13]) l289N_mount();
translate([0, 0, 5]) motor_mount_bottom();
translate([0, 0, 20]) motor_key_120();
translate([one_to_one_x, one_to_one_y, 50]) geared_motor_mount_120();
translate([one_to_one_x, one_to_one_y, 50]) motor_cap_120(false);
translate([0, 0, 60]) intval_laser_panel_cover(false, ALL_RED=true);
}
//bolex_pin_laser(0, 0);
//intval_laser_standoffs_plate();
//intval_electronics_mount("METRO");
//motor_mount_bottom();
//projection () intval_panel_laser();
//intval_laser_panel_cover(true, ALL_RED=true);
//rotate([0, 0, 13]) intval_panel_laser();
//rotate([0, 0, 13]) intval_laser_panel_cover();
key_cap();
//geared_motor_mount_120();
//motor_key();
//motor_key_120();
//plunger_plate();
//motor_cap(false);
//motor_cap_120(false);
//translate([0, 0, 39 / 2 + 5.75]) bolt_guide();
//exploded_view();
//stl_plate();
//dxf_plate();

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Binary file not shown.

213
hardware/modules.scad Normal file
View File

@ -0,0 +1,213 @@
module tube(o = 1, i = 0, h = 1, center = false, $fn = 12) {
$fn = $fn;
union () {
difference () {
cylinder(r = o, h = h, center = center);
cylinder(r = i, h = h, center = center);
}
}
}
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 c_battery () {
/* C Cell battery, 26.1 × 50 */
x = 26.1;
x_fuzz = .3;
y = 50;
y_fuzz = 2;
cylinder(r = (x + x_fuzz) / 2, h = y + y_fuzz, center = true);
}
module sub_c_battery () {
/* Sub C Cell battery, 22.2 × 42.9 */
x = 22.2;
x_fuzz = .3;
y = 42.9;
y_fuzz = 2;
cylinder(r = (x + x_fuzz) / 2, h = y + y_fuzz, center = true);
}
module hex (r = 1, h = 1, center = false) {
cylinder(r = r, h = h, center = center, $fn = 6);
}
module triangle (a = 1, b = 1, c = 1, h = 1, center = false) {
}
module cone_45 (d = 1, center = false) {
cylinder(r1 = d/2, r2 = 0, h = d, center = center);
}
module decoys (d = 10, z = 0, number = 4, cube_size = 4, debug = false) {
for (i = [0: number]) {
rotate([0, 0, (360/number) * i]) translate([d, 0, z]) cube([cube_size, cube_size, cube_size], center = true);
if (debug && i == 0) {
rotate([0, 0, (360/number) * i]) translate([d, 0, z]) cube([cube_size * 5, cube_size* 5, cube_size], center = true);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Paraboloid module for OpenScad
//
// Copyright (C) 2013 Lochner, Juergen
// http://www.thingiverse.com/Ablapo/designs
//
// This program is free software. It is
// licensed under the Attribution - Creative Commons license.
// http://creativecommons.org/licenses/by/3.0/
//////////////////////////////////////////////////////////////////////////////////////////////
module paraboloid (y=10, f=5, rfa=0, fc=1, detail=44){
// y = height of paraboloid
// f = focus distance
// fc : 1 = center paraboloid in focus point(x=0, y=f); 0 = center paraboloid on top (x=0, y=0)
// rfa = radius of the focus area : 0 = point focus
// detail = $fn of cone
hi = (y+2*f)/sqrt(2); // height and radius of the cone -> alpha = 45° -> sin(45°)=1/sqrt(2)
x =2*f*sqrt(y/f); // x = half size of parabola
translate([0,0,-f*fc]) // center on focus
rotate_extrude(convexity = 10,$fn=detail ) // extrude paraboild
translate([rfa,0,0]) // translate for fokus area
difference(){
union(){ // adding square for focal area
projection(cut = true) // reduce from 3D cone to 2D parabola
translate([0,0,f*2]) rotate([45,0,0]) // rotate cone 45° and translate for cutting
translate([0,0,-hi/2])cylinder(h= hi, r1=hi, r2=0, center=true, $fn=detail); // center cone on tip
translate([-(rfa+x ),0]) square ([rfa+x , y ]); // focal area square
}
translate([-(2*rfa+x ), -1/2]) square ([rfa+x ,y +1] ); // cut of half at rotation center
}
}
//Spiral Notes
//-------------------------------------------------------------------
//Height = center to center height of the end spheres which form the spirals. Ends will need to be flattened by the user as desired. Actual height of the rendering is Height+2*baseRadius
//Radius = the maximum distance from the axis of the spiral (the z axis) to the center of the sphere(s) forming the spiral
//baseRadius = cross sectional radius of the spiral
//frequency = the number of complete revolutions about the axis made by the spiral, whole numbers will result in spirals whose tops end directly above their bases
//resolution = integer number of spheres, not to be confused with $fn. The greater the number of spheres, the smoother the spiral will be (also longer render times!). Recommended that this number be 8*frequency or greater.
//numSpirals = integer number of spirals used in the spiralMulti modules spaced evenly around the axis (3 spirals are spaced 120 degrees apart, 4 spirals: 90 degrees apart, etc.)
//Instructions
//------------------------------------------------------------------
//1. Place spiral.scad in the "libraries" folder of your openscad installation. Find the libraries folder by File -> Show Library Folder...
//2. Then create a new or open one of your existing scad files and include spiral.scad with the following code:
//use<spiral.scad>;
//3. Then call the modules in your files with code similar to the following:
//spiral(20,20,3,1,25);
//spiralCone(20,20,3,1,25);
//spiralEllipse(20,20,3,1,25);
//spiralMulti(20,20,3,1,25,3);
//spiralMultiCone(20,20,3,1,25,3);
//spiralMultiEllipse(40,60,3,1,32,3);
//-------------------------------------------------------------
//simple spiral
module spiral (height = 20, Radius = 20, baseRadius = 3, frequency = 1, resolution = 25, $fn=50) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i]) translate ([Radius,0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)]) translate ([Radius,0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
//cone spiral
module spiralCone(height=20,Radius=20,baseRadius=3,frequency=1,resolution=25, $fn=50) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i]) translate ([Radius-(i-1)*Radius/resolution,0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)]) translate ([Radius-i*Radius/resolution,0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
//ellipse spiral
module spiralEllipse(height=20,Radius=20,baseRadius=3,frequency=1,resolution=25, $fn=50) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i]) translate ([Radius*sqrt(1-(i/(resolution-1)*(i/(resolution-1)))),0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)]) translate ([Radius*sqrt(1-((i+1)/(resolution-1)*((i+1)/(resolution-1)))),0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
// Multiple spirals arranged radially around the axis
module spiralMulti(height=20,Radius=20,baseRadius=3,frequency=1,resolution=25,numSpirals=3,$fn=50) {
shiftAngle=360/numSpirals;
for(total=[0:numSpirals-1]) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i+shiftAngle*total]) translate ([Radius,0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)+shiftAngle*total]) translate ([Radius,0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
}
// Multiple spirals arranged radially around the axis tapering in towards the axis
module spiralMultiCone(height=20,Radius=20,baseRadius=3,frequency=1,resolution=25,numSpirals=3,$fn=50) {
shiftAngle=360/numSpirals;
for(total=[0:numSpirals-1]) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i+shiftAngle*total]) translate ([Radius-(i-1)*Radius/resolution,0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)+shiftAngle*total]) translate ([Radius-i*Radius/resolution,0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
}
//multiple ellipse spiral
module spiralMultiEllipse(height=20,Radius=20,baseRadius=3,frequency=1,resolution=25,numSpirals=3,$fn=50) {
shiftAngle=360/numSpirals;
for(total=[0:numSpirals-1]) {
union(){
translate ([0,0,-(height/2)]) {
for(i=[0:resolution-2]){
hull(){
rotate ([0,0,frequency*360/(resolution-1)*i+shiftAngle*total]) translate ([Radius*sqrt(1-(i/(resolution-1)*(i/(resolution-1)))),0,i*height/(resolution-1)]) sphere(r=baseRadius, center=true);
rotate ([0,0,frequency*360/(resolution-1)*(i+1)+shiftAngle*total]) translate ([Radius*sqrt(1-((i+1)/(resolution-1)*((i+1)/(resolution-1)))),0,(i+1)*height/(resolution-1)]) sphere(r=baseRadius,center=true);
}
}
}
}
}
}

293
hardware/mount.scad Normal file
View File

@ -0,0 +1,293 @@
include <./modules.scad>
include <./variables.scad>
module motor_key_120 (half = false, DECOYS = false, sides = 1, ALT = false) {
innerD = 7.85;
outer_d = 27.5 + 2;
notch_d = 10;
height = 7 + 5 + 4;
diff = 14 + 2.5 + 2;
$fn = 60;
difference () {
union () {
translate([one_to_one_x, one_to_one_y, 12.1]) cylinder(r1 = 12 / 2, r2 = 12/2 + 4, h = 5, center = true);// padding against bearing
translate([one_to_one_x, one_to_one_y, diff + 1]) cylinder(r=outer_d/2, h= height -2, center= true, $fn=200); //large cylinder
translate([one_to_one_x, one_to_one_y, 6]) cylinder(r=innerD/2, h= 10, center= true);
//key_end([0, 180, 0], [one_to_one_x, one_to_one_y, -2.5]); //thicker-than-key_end cylinder for inner bearing
key_end([0, 180, -20], [one_to_one_x, one_to_one_y, -3.5], ALT = ALT); // longer for laser cut board
//key_end([0, 180, 0], [one_to_one_x, one_to_one_y, -4.5]); //experimental length
}
//1 notch
translate([one_to_one_x, one_to_one_y, diff]) {
translate ([-outer_d/2 - 2.5, 0, 0]) cylinder(r=notch_d/2, h= height, center= true); //notch
}
translate([one_to_one_x, one_to_one_y, diff]) {
translate ([-outer_d/2 -.5, -3.5 , 0]) rotate([0, 0, 100]) cube([15, 5, height], center = true); // smooth notch
translate ([-outer_d/2 -.5, 3.5, 0]) rotate([0, 0, -100]) cube([15, 5, height], center = true); // smooth notch
}
if (sides == 2) {
//2 notch
translate([one_to_one_x, one_to_one_y, diff]) {
translate ([outer_d/2 + 2.5, 0, 0]) cylinder(r=notch_d/2, h= height, center= true); //notch
}
translate([one_to_one_x, one_to_one_y, diff]) {
translate ([outer_d/2 +.5, -3.5, 0]) rotate([0, 0, -100]) cube([15, 5, height], center = true); // smooth notch
translate ([outer_d/2 +.5, 3.5, 0]) rotate([0, 0, 100]) cube([15, 5, height], center = true); // smooth notch
}
}
//slot for hobbled(?) end
translate([one_to_one_x, one_to_one_y, 17 + 2]) {
translate([0, 0, 6.5]) hobbled_rod_120(12);
//translate([6.42, 0, 6 - 1.7]) motor_set_screw_120();
translate([6.42 - .2, 0, 4.3 - 1]) rotate([0, 90, 0]) motor_set_screw_120_alt();
translate([14, 0, 4.3 - 1]) rotate([0, 90, 0]) cylinder(r2 = 6 / 2, r1 = 5.8 / 2, h = 6, center = true); //extension
}
//translate([one_to_one_x, one_to_one_y, 20.5]) cylinder(r = 11.5/2, h = 10, center = true);
translate([one_to_one_x, one_to_one_y, 17.5]) {
difference() {
//cylinder(r = 7.5/2, h = 2, center = true);
//translate([5, 0, 0]) cube([10, 10, 10], center = true);
}
}
if (half) {
translate([one_to_one_x - 50 , one_to_one_y, -50]) cube([100, 100, 200]);
}
}
// translate([one_to_one_x, one_to_one_y, 17]) translate([6.42 - .2, 0, 6 - 1.7]) rotate([0, 90, 0]) motor_set_screw_120_alt();
if (DECOYS) {
translate([one_to_one_x, one_to_one_y, 20.5]) decoys(24);
}
}
module motor_set_screw_120 () {
cube([10.19, 2.95, 2.95], center = true);
translate([(10.19 / 2) - (2.56 / 2), 0, 0]) cube([2.56, 5.8, 5.8], center = true);
}
module motor_set_screw_120_alt () {
$fn = 60;
cylinder(r = 2.95 / 2, h = 10.19, center= true);
translate([0, 0, (10.19 / 2) - (2.56 / 2)]) cylinder(r = 5.8 / 2, h = 2.56, center = true);
}
module hobbled_rod_120 (h = 10) {
d = 4.00;
diff = 3.33;
difference () {
cylinder(r = d/2, h = h, center = true, $fn = 60);
translate([d/2 + ((d/2) - (d - diff)), 0, 0]) cube([d, d, h + 1], center = true);
}
}
module motor_12v () {
motor_d = 37;
motor_h = 63;
end = 11.5;
len = 17;
cylinder(r = motor_d/2, h = motor_h, center=true);
translate([0, 0, (motor_h / 2) + (len / 2)]) cylinder(r = end/2, h = len, center=true);
}
module geared_motor_mount_120 (DECOYS = false) {
$fn = 160;
base_d = 45;
base_inner = 25.2;
base_thickness = 3;
hole_d = 7;
screw_d = 3.2;
bolt_end = 5.4;
height = 6;
screw_distance = 17;
difference () {
difference () {
translate([-6, 0, 2.5]) cylinder(r=base_d/2, h=height + 5, center = true); //outer cylinder
//translate([-6, 0, base_thickness + 2.5]) cylinder(r=base_inner/2, h=height + 5, center = true); //inner cylinder
translate([0, 0, base_thickness + 1.5]) cylinder(r=base_inner/2, h=height + 5, center = true); //inner cylinder
}
cylinder(r=hole_d/2, h=29, center = true); //center hole
//screw holes
translate([0, 0, 0]) {
translate([0, screw_distance/2, 0]) cylinder(r=screw_d/2, h=29, center = true);
translate([0, -screw_distance/2, 0]) cylinder(r=screw_d/2, h=29, center = true);
//bolt ends
translate([0, screw_distance/2, -3]) cylinder(r=bolt_end/2, h=2, center = true);
translate([0, -screw_distance/2, -3]) cylinder(r=bolt_end/2, h=2, center = true);
}
translate([2, 19, 0]) cylinder(r=5, h = 100, center = true); //hole for panel bolt access
}
//wings
translate ([-one_to_one_x, -one_to_one_y, 0]) bolt_holder([mm_x[0], mm_y[0], 0], mm_r[0], height, mm_l[0]);
translate ([-one_to_one_x, -one_to_one_y, 0]) bolt_holder([mm_x[1], mm_y[1], 0], mm_r[1], height, mm_l[1]);
//translate ([-one_to_one_x, -one_to_one_y, 0]) bolt_holder([mm_x[5] , mm_y[5], 0], mm_r[5], height, mm_l[5] - 1);
if (DECOYS) {
translate([-7, -6, 0]) decoys(40, -1, 4);
translate([-9, -2, 0]) rotate([0, 0, 49]) decoys(37, -1, 4);
}
}
module motor_mount_bottom () {
$fn = 60;
mount_d = 45;
base_d = 45;
outer_d = 28 + 2.3 + 4;
height = 19 + 3.5 + 4;
bolt_h = 22.3;
shelf_h = 6; //match to motor_mount
screw_d = 4;
module motor_mount_core () {
translate ([one_to_one_x, one_to_one_y, (height / 2 ) + 5.75]) {
difference() {
translate([-6, 0, 0]) cylinder(r = mount_d / 2, h = height, center = true); //main block
translate([0, 0, (height / 2) - (shelf_h / 2)]) cylinder(r = base_d / 2 + 7, h = shelf_h, center = true); //shelf for motor_mount
cylinder(r = outer_d / 2, h = 50, center = true); //space for spinning
translate ([-one_to_one_x, -one_to_one_y, 0]) remove_front(); //flatten side
translate([-32, -17, -19]) cube([40, 40, 40], center= true); //hole for notch
translate([-42, 0, -19]) rotate([0, 0, -39]) cube([40, 40, 40], center= true); //hole for notch
translate([2.5, 19.5, 0]) cylinder(r=10/2, h = 60, center=true); // hole for panel bolt
translate([22.5, 19.5, 0]) cube([40, 40, 60], center = true); //remove front entirely
translate([-6.5, 0, 7.5]) {
translate([0, screw_distance/2, 0]) sphere(r=screw_d, center = true);
translate([0, -screw_distance/2, 0]) sphere(r=screw_d, center = true);
}
}
translate ([-one_to_one_x, -one_to_one_y, 0]) bolt_holder([mm_x[0], mm_y[0], -shelf_h / 2], mm_r[0], height - shelf_h, mm_l[0], tight = 0.2); //Bottom bolt holder
translate ([-one_to_one_x, -one_to_one_y, 0]) bolt_holder([mm_x[1] , mm_y[1], -shelf_h / 2], mm_r[1], height - shelf_h, mm_l[1], tight = 0.2); //Left bolt holder
translate ([-one_to_one_x, -one_to_one_y, -2]) bolt_holder([mm_x[5] , mm_y[5], -shelf_h / 2], mm_r[5], height - shelf_h - 4, mm_l[5]); //Top bolt holder
}
}
module microswitch_holder () {
difference () {
translate([29, -1, 14]) cube([36, 65, height - shelf_h - 4], center = true);//Base shape
translate ([25.5, -14, 15]) {
cube([17, 28, 39.5], center = true); //rectangle hole for center
translate([4.5, 5.6, 0]) rotate([0, 0, -23]) cube([17, 25, 39.5], center = true); //bottom right inner
translate([-2, -18, -3.5]) cube([7, 11, 12], center = true); // hole for bottom pins
translate([-9.5, -1, -3.5]) cube([30, 4, 12], center = true); //hole for side pin
}
translate ([14, 37.5, 15]) rotate([0, 0, 44]) cube([55, 30, 30], center= true); //top left outer
translate ([one_to_one_x, one_to_one_y, 18]) {
cylinder(r = outer_d / 2, h = 50, center = true); //space for spinning
}
translate ([32, 6, 15]) {
difference () {
translate([3, 0, 0]) rotate([0, 0, 0]) cube([20, 25, 39.5], center = true); //removes area for microswitch arm
translate([-2, 16, 0]) rotate([0, 0, -55]) cube([20, 50, 39.5], center = true);
}
}
translate ([58, -25, 15]) {
rotate([0, 0, 75]) cube([45, 30, 30], center= true); //bottom right outer
}
translate([mm_x[4], mm_y[4], 0]) cylinder(r = bolt_inner, h = 100, center = true); // extra bolt hole
translate([mm_x[1], mm_y[1], 0]) cylinder(r = 4, h = 100, center = true); //clear out top left bolt hole
}
}
module panel_attachment () {
difference () {
union() {
translate([0, 0, 7.75 + 3]) cylinder(r = 10/2, h = 44 - shelf_h, center = true);
translate([3.5, 0, 0]) cube([7, 7, height - shelf_h - 4], center = true);
}
translate([0, 0, 25]) cylinder(r = 3.2/2, h = 50, center = true);
}
}
translate([8, -9, (height - shelf_h) / 2 + 3.75]) panel_attachment();
motor_mount_core();
microswitch_holder();
bolt_holder([mm_x[2], mm_y[2], ((height - shelf_h)/ 2) + 3.75], 0, height - shelf_h - 4, 6); //bottom left mount
bolt_holder([mm_x[3], mm_y[3], ((height - shelf_h)/ 2) + 3.75], 180, height - shelf_h - 4, 6); //bottom right mount
if (DECOYS) {
difference () {
translate([35, 0 , 0]) decoys(44, 8, 6);
}
translate([0, 0, 8]) cube([4, 4, 4], center = true);
translate([40, 55, 8]) cube([4, 4, 4], center = true);
}
}
module bolt_holder (position = [0, 0, 0], rotate_z = 0, h = 17, length = 4.5, hole = true, tight = 0) {
bolt_r = 6;
translate (position) {
difference () {
union() {
cylinder(r = bolt_r + 0, h = h, center = true);
rotate([0, 0, rotate_z]) translate([length/2, 0, 0]) cube([length, bolt_r * 2, h], center=true);
}
if (hole) {
cylinder(r = bolt_inner - tight, h = h + 2, center = true);
}
}
}
}
module microswitch (position = [0, 0, 0], rotation = [0, 0, 0]) {
translate(position) {
rotate(rotation) {
cube([16, 28, 9.5], center = true);
translate([10, 8, 0]) rotate([0, 0, -7]) cube([1, 28, 4], center = true);
translate([8 + 7, 14 + 8, 0]) cylinder(r = 2.5, h = 4, center = true);
translate([0, -19, 0]) cube([6, 11, 9.5], center = true);
}
}
}
module l289N_mount () {
$fn = 60;
DISTANCE = 36.5;
H = 4;
THICKNESS = 3;
module stand () {
difference () {
cylinder(r1 = 4, r2 = 3, h = H, center = true);
cylinder(r = 1.5, h = H, center = true);
}
}
translate([0, 0, 0]) stand();
translate([DISTANCE, 0, 0]) stand();
translate([DISTANCE, DISTANCE, 0]) stand();
translate([0, DISTANCE, 0]) stand();
difference () {
translate([DISTANCE/2, DISTANCE/2, -3]) rounded_cube([DISTANCE + 8, DISTANCE + 8, THICKNESS], 8, center = true); //base
translate([DISTANCE/2, DISTANCE/2, -3]) rounded_cube([DISTANCE - 5, DISTANCE - 5, THICKNESS], 10, center = true); //base
translate([0, 0, 0]) cylinder(r = 1.5, h = H * 5, center = true);
translate([DISTANCE, 0, 0]) cylinder(r = 1.5, h = H * 5, center = true);
translate([DISTANCE, DISTANCE, 0]) cylinder(r = 1.5, h = H * 5, center = true);
translate([0, DISTANCE, 0]) cylinder(r = 1.5, h = H * 5, center = true);
}
}
module pcb_mount () {
DISTANCE_X = 41;
DISTANCE_Y = 66;
OUTER = 10;
H = 8;
module stand () {
difference () {
cylinder(r1 = 5, r2 = 4, h = H, center = true);
cylinder(r = 1.75, h = H, center = true);
}
}
translate([0, 0, 0]) stand();
translate([DISTANCE_X, 0, 0]) stand();
translate([DISTANCE_X, DISTANCE_Y, 0]) stand();
translate([0, DISTANCE_Y, 0]) stand();
translate([DISTANCE_X/2, DISTANCE_Y/2, -4]) rounded_cube([DISTANCE_X + OUTER, DISTANCE_Y + OUTER, 4], OUTER, center = true);
}
module bolt_guide () {
$fn = 60;
H = 39;
difference () {
union() {
cylinder(r = 10 / 2, h = H, center = true);
translate([0, 0, -(H / 2) + 1]) cylinder(r = 14 / 2, h = 2, center = true);
}
cylinder(r = 7 / 2, h = H + 1, center = true);
translate([12, 0, -(H / 2) + 1]) cube([14, 14, 3], center = true);
}
}

51
hardware/plunger.scad Normal file
View File

@ -0,0 +1,51 @@
include <./modules.scad>
include <./variables.scad>
module plunger () {
$fn = 60;
FINGER = 39;
CYL_D = 9;
WALL = 3;
difference () {
union () {
cylinder(r1 = CYL_D, r2 = CYL_D - 1, h = 30, center = true); //outer cylinder
difference () {
translate([0, 0, -9]) rotate([90, 0, 0]) rounded_cube([50, 12, 10], d = 5, center = true);
translate([23, 0, 9]) rotate([90, 0, 0]) cylinder(r = FINGER/2, h = 20, center = true);
translate([-23, 0, 9]) rotate([90, 0, 0]) cylinder(r = FINGER/2, h = 20, center = true);
}
}
translate([0, 0, 2]) cylinder( r = CYL_D - WALL, h = 30, center = true); //inner cylinder
cylinder(r = 7/2, h = 50, center = true); // button hole
}
//cylinder(r= 5, h = 50, center = true); button
}
module plunger_top () {
$fn = 60;
CYL_D = 9;
WALL = 3;
difference () {
union () {
cylinder(r = CYL_D - WALL - 0.015, h =6, center = true);
translate([0, 0, 2]) cylinder (r = CYL_D - 1, h = 2, center = true);
}
translate([0, 0, -2]) cylinder(r = CYL_D - WALL - 0.015 - 1, h =6, center = true);
//cylinder(r = 3/2, h = 50, center = true); // wire
cylinder(r = 3.9/2, h = 50, center = true); //3.5mm wire
}
}
module plunger_plate () {
translate([40, 0, -12]) rotate([180, 0, 0]) plunger_top();
plunger();
//decoys
/*translate([44,20,-13]) cube([4, 4, 4], center = true);
translate([44,-20,-13]) cube([4, 4, 4], center = true);
translate([-23,20,-13]) cube([4, 4, 4], center = true);
translate([-23,-20,-13]) cube([4, 4, 4], center = true);*/
}

21
hardware/variables.scad Normal file
View File

@ -0,0 +1,21 @@
mm_x = [61.5, 21.5, 6, 45.5, 18, 39];
mm_y = [-18, 21, -27.5, -27.5, 7, 39];
mm_r = [110, -15, 0, 0, 0, -70];
mm_l = [13, 9, 0, 0, 0, 8];
xArray = [-3, 57, 55, -26]; //NO MIDDLE PIN
yArray = [38, 31, -56, -33]; //NO MIDDLE PIN
outerD = 9;
innerD = 4.5;
height = 17;
panel_2_x = 110;
panel_2_y = 110;
one_to_one_x = 54.5;
one_to_one_y = 12;
bolt_inner = 2.55;
screw_distance = 31;

605
index.js
View File

@ -1,57 +1,616 @@
'use strict'
const ble = require('./lib/blootstrap')
const intval = require('./lib/intval')
const restify = require('restify')
const logger = require('winston')
const log = require('./lib/log')('main')
const fs = require('fs')
const pin = {}
const { exec } = require('child_process')
const BLE = require('./lib/ble')
const intval = require('./lib/intval')
const sequence = require('./lib/sequence')
const PACKAGE = require('./package.json')
const PORT = process.env.PORT || 6699
const APPNAME = PACKAGE.name
const INDEX = fs.readFileSync('./app/www/index.html', 'utf8')
const INDEXPATH = './app/www/index.html'
let app = restify.createServer({
name: APPNAME,
version: '0.0.1'
version: PACKAGE.version
})
let ble
function createServer () {
app.get('/', index)
app.get('/frame', rFrame)
app.use(restify.plugins.queryParser())
app.use(restify.plugins.bodyParser({ mapParams: false }))
app.get( '/', index)
app.get( '/dir', rDir)
app.post('/dir', rDir)
app.get( '/exposure', rExposure)
app.post('/exposure', rExposure)
app.get( '/delay', rDelay)
app.post('/delay', rDelay)
app.get( '/counter', rCounter)
app.post('/counter', rCounter)
app.get( '/frame', rFrame)
app.post('/frame', rFrame)
app.get('/sequence', () => {})
app.post('/sequence', () => {})
app.get('/status', rStatus)
app.get( '/sequence', rSequence)
app.post('/sequence', rSequence)
app.get( '/status', rStatus)
app.post('/reset', rReset)
app.post('/update', rUpdate)
app.post('/restart', rRestart)
app.listen(PORT, () => {
console.log(`${APPNAME} listening on port ${PORT}!`)
log.info('server', { name : APPNAME, port : PORT })
})
}
function rFrame (req, res, next) {
res.send({})
function createBLE () {
ble = new BLE(() => {
return intval.status()
})
ble.on('frame', bFrame)
ble.on('dir', bDir)
ble.on('exposure', bExposure)
ble.on('delay', bDelay)
ble.on('counter', bCounter)
ble.on('sequence', bSequence)
ble.on('reset', bReset)
ble.on('update', bUpdate)
ble.on('restart', bRestart)
}
//Restify functions
function rDir (req, res, next) {
let dir = true
let set = false
if (req.query && typeof req.query.dir !== 'undefined') {
if (typeof req.query.dir === 'string') {
dir = (req.query.dir === 'true')
} else {
dir = req.query.dir
}
set = true
} else if (req.body && typeof req.body.dir !== 'undefined') {
if (typeof req.body.dir === 'string') {
dir = (req.body.dir === 'true')
} else {
dir = req.body.dir
}
set = true
}
if (set) {
intval.setDir(dir)
} else {
dir = intval._state.frame.dir
}
log.info('/dir', { method: req.method, set : set, dir : dir})
res.send({ dir : dir })
return next()
}
function rExposure (req, res, next) {
let exposure = 0
let set = false
if (req.query && typeof req.query.exposure !== 'undefined') {
if (typeof req.query.exposure === 'string') {
exposure = parseInt(req.query.exposure)
} else {
exposure = req.query.exposure
}
set = true
} else if (req.body && typeof req.body.exposure !== 'undefined') {
if (typeof req.body.exposure === 'string') {
exposure = parseInt(req.body.exposure)
} else {
exposure = req.body.exposure
}
set = true
}
if (set) {
if (exposure <= intval._frame.expected) {
exposure = 0;
}
intval.setExposure(exposure)
} else {
exposure = intval._state.frame.exposure
}
log.info('/exposure', { method: req.method, set : set, exposure : exposure })
res.send({ exposure : exposure })
return next()
}
function rDelay (req, res, next) {
let delay = 0
let set = false
if (req.query && typeof req.query.delay !== 'undefined') {
if (typeof req.query.delay === 'string') {
delay = parseInt(req.query.delay)
} else {
delay = req.query.delay
}
set = true
}
if (req.body && typeof req.body.delay !== 'undefined') {
if (typeof req.body.delay === 'string') {
delay = parseInt(req.body.delay)
} else {
delay = req.body.delay
}
set = true
}
if (set) {
intval.setDelay(delay)
} else {
delay = intval._state.frame.delay
}
log.info('/delay', { method: req.method, set : set, delay : delay })
res.send({ delay : delay })
return next()
}
function rCounter (req, res, next) {
let counter = 0
let set = false
if (req.query && typeof req.query.counter !== 'undefined') {
if (typeof req.query.counter === 'string') {
counter = parseInt(req.query.counter)
} else {
counter = req.query.counter
}
set = true
}
if (req.body && typeof req.body.counter !== 'undefined') {
if (typeof req.body.counter !== 'string') {
counter = parseInt(req.body.counter)
} else {
counter = req.body.counter
}
set = true
}
if (set) {
intval.setCounter(counter)
} else {
counter = intval._state.counter
}
log.info('/counter', { method : req.method, set : set, counter : counter })
res.send({ counter : counter })
return next()
}
function rFrame (req, res, next) {
let dir = true
let exposure = 0
if (intval._state.frame.dir !== true) {
dir = false
}
if (intval._state.frame.exposure !== 0) {
exposure = intval._state.frame.exposure
}
if (req.query && typeof req.query.dir !== 'undefined') {
if (typeof req.query.dir === 'string') {
dir = (req.query.dir === 'true')
} else {
dir = req.query.dir
}
}
if (req.body && typeof req.body.dir !== 'undefined') {
if (typeof req.body.dir === 'string') {
dir = (req.body.dir === 'true')
} else {
dir = req.body.dir
}
}
if (req.query && typeof req.query.exposure !== 'undefined') {
if (typeof req.query.exposure === 'string') {
exposure = parseInt(req.query.exposure)
} else {
exposure = req.query.exposure
}
}
if (req.body && typeof req.body.exposure !== 'undefined') {
if (typeof req.body.exposure === 'string') {
exposure = parseInt(req.body.exposure)
} else {
exposure = req.body.exposure
}
}
if (req.query && typeof req.query.delay !== 'undefined') {
if (typeof req.query.delay === 'string') {
delay = parseInt(req.query.delay)
} else {
delay = req.query.delay
}
}
if (req.body && typeof req.body.delay !== 'undefined') {
if (typeof req.body.delay === 'string') {
delay = parseInt(req.body.delay)
} else {
delay = req.body.delay
}
}
log.info('/frame', { method : req.method, dir : dir, exposure : exposure })
intval.frame(dir, exposure, (len) => {
res.send({ dir : dir, len : len})
return next()
})
}
function rStatus (req, res, next) {
const obj = intval.status()
res.send({})
res.send(obj)
return next()
}
function index (req, res, next) {
res.end(INDEX)
return next()
function rSequence (req, res, next) {
let dir = true
let exposure = 0
let delay = 0
if (intval._state.frame.dir !== true) {
dir = false
}
if (intval._state.frame.exposure !== 0) {
exposure = intval._state.frame.exposure
}
if (intval._state.frame.delay !== 0) {
delay = intval._state.frame.delay
}
if (req.query && typeof req.query.dir !== 'undefined') {
if (typeof req.query.dir === 'string') {
dir = (req.query.dir === 'true')
} else {
dir = req.query.dir
}
}
if (req.body && typeof req.body.dir !== 'undefined') {
if (typeof req.body.dir === 'string') {
dir = (req.body.dir === 'true')
} else {
dir = req.body.dir
}
}
if (req.query && typeof req.query.exposure !== 'undefined') {
if (typeof req.query.exposure === 'string') {
exposure = parseInt(req.query.exposure)
} else {
exposure = req.query.exposure
}
}
if (req.body && typeof req.body.exposure !== 'undefined') {
if (typeof req.body.exposure === 'string') {
exposure = parseInt(req.body.exposure)
} else {
exposure = req.body.exposure
}
}
if (req.query && typeof req.query.delay !== 'undefined') {
if (typeof req.query.delay === 'string') {
delay = parseInt(req.query.delay)
} else {
delay = req.query.delay
}
}
if (req.body && typeof req.body.delay!== 'undefined') {
if (typeof req.body.delay === 'string') {
delay = parseInt(req.body.delay)
} else {
delay = req.body.delay
}
}
if (intval._state.sequence && sequence._state.active) {
sequence.setStop()
intval._state.sequence = false
res.send({ stopped : true })
return next()
} else {
console.time('sequence time')
intval._state.sequence = true
let seq_id = sequence.start({
loop : [ (next) => {
intval.frame(dir, exposure, (len) => {
next()
})
}, (next) => {
setTimeout(() => {
next()
}, delay)
}]
}, (seq) => {
console.timeEnd('sequence time')
})
if (seq_id === false) {
res.send({ started : false })
} else {
res.send({ started : true , id : seq_id })
}
return next()
}
}
function rReset (req, res, next) {
log.info(`/reset`, {time : +new Date()})
intval.reset()
setTimeout(() => {
const obj = intval.status()
res.send(obj)
return next()
}, 10)
}
function init () {
createServer()
ble.on('data', (str) => {
console.log(str)
function rUpdate (req, res, next) {
log.info(`/update`, { time : +new Date() })
exec('sh ./scripts/update.sh', (err, stdio, stderr) => {
if (err) {
log.error(err)
}
log.info(`/update`, { git : stdio })
res.send({ success : true, action : 'update', output : stdio })
res.end()
next()
setTimeout(() => {
process.exit(0)
}, 100)
})
}
function rRestart (req, res, next) {
log.info(`/restart`, { time : +new Date() })
res.send({ success : true, action : 'restart' })
res.end()
next()
setTimeout(() => {
process.exit(0)
}, 100)
}
//Ble functions
function bFrame (obj, cb) {
let dir = true
let exposure = 0
if (intval._state.frame.dir !== true) {
dir = false
}
if (intval._state.frame.exposure !== 0) {
exposure = intval._state.frame.exposure
}
if (typeof obj.dir !== 'undefined') {
if (typeof obj.dir === 'string') {
dir = (obj.dir === 'true')
} else {
dir = obj.dir
}
}
if (typeof obj.exposure !== 'undefined') {
if (typeof obj.exposure === 'string') {
exposure = parseInt(obj.exposure)
} else {
exposure = obj.exposure
}
}
log.info('frame', { method : 'ble', dir : dir, exposure : exposure })
if (exposure < 5000) {
intval.frame(dir, exposure, (len) => {
return cb()
})
} else {
intval.frame(dir, exposure, (len) => {})
return cb()
}
//setTimeout(cb, exposure === 0 ? 630 : exposure)
}
function bDir (obj, cb) {
let dir = true
let set = false
if (obj.dir !== 'undefined') {
if (typeof obj.dir === 'string') {
dir = (obj.dir === 'true')
} else {
dir = obj.dir
}
}
intval.setDir(dir)
log.info('dir', { method: 'ble', dir : dir })
cb()
}
function bExposure (obj, cb) {
let exposure = 0
if (typeof obj.exposure !== 'undefined') {
if (typeof obj.exposure === 'string') {
exposure = parseInt(obj.exposure)
} else {
exposure = obj.exposure
}
}
intval.setExposure(exposure)
log.info('exposure', { method: 'ble', exposure : exposure })
return cb()
}
function bDelay (obj, cb) {
let delay = 0
let set = false
if (typeof obj.delay !== 'undefined') {
if (typeof obj.delay === 'string') {
delay = parseInt(obj.delay)
} else {
delay = obj.delay
}
set = true
}
intval.setDelay(delay)
log.info('delay', { method: 'ble', delay : delay })
return cb()
}
function bCounter (obj, cb) {
let counter = 0
if (typeof obj.counter !== 'undefined') {
if (typeof obj.counter !== 'string') {
counter = parseInt(obj.counter)
} else {
counter = obj.counter
}
}
intval.setCounter(counter)
log.info('counter', { method : 'ble', counter : counter })
return cb()
}
function bSequence (obj, cb) {
let dir = true
let exposure = 0
let delay = 0
if (intval._state.frame.dir !== true) {
dir = false
}
if (intval._state.frame.exposure !== 0) {
exposure = intval._state.frame.exposure
}
if (intval._state.frame.delay !== 0) {
delay = intval._state.frame.delay
}
if (typeof obj.dir !== 'undefined') {
if (typeof obj.dir === 'string') {
dir = (obj.dir === 'true')
} else {
dir = obj.dir
}
}
if (typeof obj.exposure !== 'undefined') {
if (typeof obj.exposure === 'string') {
exposure = parseInt(obj.exposure)
} else {
exposure = obj.exposure
}
}
if (intval._state.sequence && sequence._state.active) {
//should not occur with single client
sequence.setStop()
intval._state.sequence = false
log.info('sequence stop', { method : 'ble' })
return cb()
} else {
console.time('sequence time')
intval._state.sequence = true
let seq_id = sequence.start({
loop : [ (next) => {
intval.frame(dir, exposure, (len) => {
next()
})
}, (next) => {
setTimeout(() => {
next()
}, delay)
}]
}, (seq) => {
console.timeEnd('sequence time')
})
if (seq_id !== false) {
log.info('sequence start', { method : 'ble', id : seq_id })
}
return cb()
}
}
function bSequenceStop (obj, cb) {
//
if (intval._state.sequence && sequence._state.active) {
sequence.setStop()
intval._state.sequence = false
log.info('sequence stop', { method : 'ble' })
return cb()
}
}
function bReset (obj, cb) {
log.info(`reset`, { method: 'ble' })
intval.reset()
setTimeout(cb, 10)
}
function bUpdate (obj, cb) {
log.info('update', { method : 'ble' })
exec('sh ./scripts/update.sh', (err, stdio, stderr) => {
if (err) {
log.error('update', err)
}
log.info('update', { stdio : stdio })
cb()
setTimeout(() => {
process.exit(0)
}, 20)
})
}
function bRestart (obj, cb) {
log.info('restart', { method : 'ble' })
cb()
setTimeout(() => {
process.exit(0)
}, 20)
}
function seq () {
let dir = intval._state.frame.dir
let exposure = intval._state.frame.exposure
let delay = intval._state.frame.delay
if (intval._state.sequence && sequence._state.active) {
log.info('sequence', { method : 'release' , stop: true })
sequence.setStop()
intval._state.sequence = false
return cb()
} else {
console.time('sequence time')
log.info('sequence', { method : 'release', start : true })
intval._state.sequence = true
sequence.start({
loop : [ (next) => {
intval.frame(dir, exposure, (len) => {
next()
})
}, (next) => {
setTimeout(() => {
next()
}, delay)
}]
}, (seq) => {
console.timeEnd('sequence time')
})
}
}
function index (req, res, next) {
fs.readFile(INDEXPATH, 'utf8', (err, data) => {
if (err) {
return next(err)
}
res.end(data)
next()
})
}
function init () {
intval.init()
intval.sequence = seq
createServer()
createBLE()
}
init()

44
lib/ble/Readme.md Normal file
View File

@ -0,0 +1,44 @@
<a name="module_ble"></a>
## ble
* [ble](#module_ble)
* [~BLE](#module_ble..BLE)
* [new BLE()](#new_module_ble..BLE_new)
* [.on(eventName, callback)](#module_ble..BLE+on)
* [~os](#module_ble..os)
<a name="module_ble..BLE"></a>
### ble~BLE
Class representing the bluetooth interface
**Kind**: inner class of [<code>ble</code>](#module_ble)
* [~BLE](#module_ble..BLE)
* [new BLE()](#new_module_ble..BLE_new)
* [.on(eventName, callback)](#module_ble..BLE+on)
<a name="new_module_ble..BLE_new"></a>
#### new BLE()
Establishes Bluetooth Low Energy services, accessible to process through this class
<a name="module_ble..BLE+on"></a>
#### blE.on(eventName, callback)
Binds functions to events that are triggered by BLE messages
**Kind**: instance method of [<code>BLE</code>](#module_ble..BLE)
| Param | Type | Description |
| --- | --- | --- |
| eventName | <code>string</code> | Name of the event to to bind |
| callback | <code>function</code> | Invoked when the event is triggered |
<a name="module_ble..os"></a>
### ble~os
Bluetooth Low Energy module
**Kind**: inner constant of [<code>ble</code>](#module_ble)

254
lib/ble/index.js Normal file
View File

@ -0,0 +1,254 @@
'use strict'
/** @module ble */
/** Bluetooth Low Energy module */
const util = require('util')
const os = require('os')
const log = require('../log')('ble')
const wifi = require('../wifi')
const DEVICE_NAME = process.env.DEVICE_NAME || 'intval3'
const SERVICE_ID = process.env.SERVICE_ID || 'intval3_ble'
const CHAR_ID = process.env.CHAR_ID || 'intval3char'
const WIFI_ID = process.env.WIFI_ID || 'wifichar'
const NETWORK = os.networkInterfaces()
const MAC = getMac() || spoofMac()
//Give the device a unique device name, needs to be in env
process.env.BLENO_DEVICE_NAME += '_' + MAC
const bleno = require('bleno')
let currentWifi = 'disconnected'
let currentAddr = null
let getState
const chars = []
function createChar(name, uuid, prop, write, read) {
function characteristic () {
bleno.Characteristic.call(this, {
uuid : uuid,
properties: prop
})
}
util.inherits(characteristic, bleno.Characteristic)
if (prop.indexOf('read') !== -1) {
//data, offset, withoutResponse, callback
characteristic.prototype.onReadRequest = read
}
if (prop.indexOf('write') !== -1) {
characteristic.prototype.onWriteRequest = write
}
chars.push(new characteristic())
}
function createChars (onWrite, onRead) {
createChar('intval3', CHAR_ID, ['read', 'write'], onWrite, onRead)
createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead)
}
function onWifiWrite (data, offset, withoutResponse, callback) {
let result
let utf8
let obj
let ssid
let pwd
if (offset) {
log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result)
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
ssid = obj.ssid
pwd = obj.pwd
log.info(`connecting to AP`, { ssid : ssid })
return wifi.createPSK(ssid, pwd, (err, hash, plaintext) => {
if (err) {
log.error('Error hashing wifi password', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result)
}
return wifi.setNetwork(ssid, plaintext, hash, (err, data) => {
if (err) {
log.error('Error configuring wifi', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result)
}
currentWifi = ssid
currentAddr = getIp()
log.info(`Connected to AP`, { ssid : ssid, ip : currentAddr })
result = bleno.Characteristic.RESULT_SUCCESS
return callback(result)
})
})
}
function onWifiRead (offset, callback) {
const result = bleno.Characteristic.RESULT_SUCCESS
const wifiRes = {}
let data
wifi.list((err, list) => {
if (err) {
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result)
}
wifiRes.available = list
wifiRes.current = currentWifi
wifiRes.ip = currentAddr
log.info('Discovered available APs', { found : list.length })
data = new Buffer(JSON.stringify(wifiRes))
callback(result, data.slice(offset, data.length))
})
}
function getMac () {
const colonRe = new RegExp(':', 'g')
if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) {
return NETWORK.wlan0[0].mac.replace(colonRe, '')
}
return undefined
}
function spoofMac () {
const fs = require('fs')
const FSPATH = require.resolve('uuid')
const IDFILE = os.homedir() + '/.intval3id'
let uuid
let UUIDPATH
let TMP
let MACTMP
let dashRe
delete require.cache[FSPATH]
if (fs.existsSync(IDFILE)) {
return fs.readFileSync(IDFILE, 'utf8')
}
uuid = require('uuid').v4
UUIDPATH = require.resolve('uuid')
delete require.cache[UUIDPATH]
TMP = uuid()
MACTMP = TMP.replace(dashRe, '').substring(0, 12)
dashRe = new RegExp('-', 'g')
fs.writeFileSync(IDFILE, MACTMP, 'utf8')
return MACTMP
}
function getIp () {
let addr = null
let ipv4
const ifaces = os.networkInterfaces()
if (ifaces && ifaces.wlan0) {
ipv4 = ifaces.wlan0.filter(iface => {
if (iface.family === 'IPv4') {
return iface
}
})
if (ipv4.length === 1) {
addr = ipv4[0].address
}
}
return addr
}
function capitalize (s) {
return s[0].toUpperCase() + s.slice(1)
}
/** Class representing the bluetooth interface */
class BLE {
/**
* Establishes Bluetooth Low Energy services, accessible to process through this class
*
* @constructor
*/
constructor (bleGetState) {
log.info('Starting bluetooth service')
getState = bleGetState
bleno.on('stateChange', state => {
log.info('stateChange', { state : state })
if (state === 'poweredOn') {
log.info('Starting advertising', { DEVICE_NAME: DEVICE_NAME, DEVICE_ID : process.env.BLENO_DEVICE_NAME })
bleno.startAdvertising(DEVICE_NAME, [CHAR_ID])
} else {
bleno.stopAdvertising()
}
})
bleno.on('advertisingStart', err => {
log.info('advertisingStart', { res : (err ? 'error ' + err : 'success') })
createChars(this._onWrite.bind(this), this._onRead.bind(this))
if (!err) {
bleno.setServices([
new bleno.PrimaryService({
uuid : SERVICE_ID, //hardcoded across panels
characteristics : chars
})
])
}
})
bleno.on('accept', clientAddress => {
log.info('accept', { clientAddress : clientAddress })
})
bleno.on('disconnect', clientAddress => {
log.info('disconnect', { clientAddress : clientAddress })
})
wifi.getNetwork((err, ssid) => {
if (err) {
return log.error('wifi.getNetwork', err)
}
currentWifi = ssid
currentAddr = getIp()
log.info('wifi.getNetwork', {ssid : ssid, ip : currentAddr })
})
}
_onWrite (data, offset, withoutResponse, callback) {
let result = {}
let utf8
let obj
let fn
if (offset) {
log.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result)
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
result = bleno.Characteristic.RESULT_SUCCESS
fn = `_on${capitalize(obj.type)}`
if (obj.type && this[fn]) {
return this[fn](obj, () => {
callback(result)
})
} else {
return callback(result)
}
}
_onRead (offset, callback) {
const result = bleno.Characteristic.RESULT_SUCCESS
const state = getState()
const data = new Buffer(JSON.stringify( state ))
callback(result, data.slice(offset, data.length))
}
/**
* Binds functions to events that are triggered by BLE messages
*
* @param {string} eventName Name of the event to to bind
* @param {function} callback Invoked when the event is triggered
*/
on (eventName, callback) {
this[`_on${capitalize(eventName)}`] = callback
}
}
module.exports = BLE

View File

@ -1,32 +0,0 @@
'use strict'
const ipc = require('node-ipc')
function capitalize (s) {
return s[0].toUpperCase() + s.slice(1)
}
class Blootstrap {
constructor () {
this._onData = () => {}
ipc.connectTo('blootstrap_ble', () => {
ipc.of.blootstrap_ble.on('connect', () => {
ipc.log(`Connected to the blootstrap_ble service`)
})
ipc.of.blootstrap_ble.on('data', data => {
const str = data.toString()
ipc.log(str)
this._onData(str)
})
ipc.of.blootstrap_ble.on('disconnect', () => {
ipc.log(`Disconnected from the blootstrap_ble service`)
})
})
}
on (eventName, callback) {
this[`_on${capitalize(eventName)}`] = callback
}
}
module.exports = new Blootstrap()

52
lib/db/index.js Normal file
View File

@ -0,0 +1,52 @@
'use strict'
const fs = require('fs')
const os = require('os')
const path = require('path')
const sqlite3 = require('sqlite3').verbose()
const squel = require('squel')
const DB_FILE = path.join(os.homedir(), '.intval3.db')
const db = new sqlite3.Database(DB_FILE)
class DB {
constructor () {
this._table = 'frames'
this.createTable()
}
createTable () {
const query = `CREATE TABLE
IF NOT EXISTS ${this._table} (
dir INTEGER,
exposure INTEGER,
start INTEGER,
stop INTEGER,
len INTEGER,
counter INTEGER,
sequence INTEGER
);`
db.run(query)
}
insert (obj) {
const query = squel.insert()
.into(this._table)
.setFields(obj) //dir, exposure, start, stop, len, counter
.toString()
db.run(query)
}
find (where, cb) {
const query = squel.select()
.from(this._table)
.where(where)
.toString()
db.all(query, cb)
}
list (cb) {
const query = squel.select()
.from(this._table)
.toString()
db.all(query, cb)
}
}
module.exports = new DB()

123
lib/intval/Readme.md Normal file
View File

@ -0,0 +1,123 @@
<a name="intval"></a>
## intval
Object representing the intval3 features
**Kind**: global constant
* [intval](#intval)
* [._declarePins()](#intval._declarePins)
* [._undeclarePins()](#intval._undeclarePins)
* [._startFwd()](#intval._startFwd)
* [._startBwd()](#intval._startBwd)
* [._stop()](#intval._stop)
* [._watchMicro(err, val)](#intval._watchMicro)
* [._watchRelease(err, val)](#intval._watchRelease)
* [.setDir([dir])](#intval.setDir)
* [.frame([dir], [time])](#intval.frame)
* [.sequence()](#intval.sequence)
<a name="intval._declarePins"></a>
### intval._declarePins()
(internal function) Declares all Gpio pins that will be used
**Kind**: static method of [<code>intval</code>](#intval)
<a name="intval._undeclarePins"></a>
### intval._undeclarePins()
(internal function) Undeclares all Gpio in event of uncaught error
that interupts the node process
**Kind**: static method of [<code>intval</code>](#intval)
<a name="intval._startFwd"></a>
### intval._startFwd()
Start motor in forward direction by setting correct pins in h-bridge
**Kind**: static method of [<code>intval</code>](#intval)
<a name="intval._startBwd"></a>
### intval._startBwd()
Start motor in backward direction by setting correct pins in h-bridge
**Kind**: static method of [<code>intval</code>](#intval)
<a name="intval._stop"></a>
### intval._stop()
Stop motor by setting both motor pins to 0 (LOW)
**Kind**: static method of [<code>intval</code>](#intval)
<a name="intval._watchMicro"></a>
### intval._watchMicro(err, val)
Callback for watching relese switch state changes.
Using GPIO 06 on Raspberry Pi Zero W.
1) If closed AND frame active, start timer, set state primed to `true`.
1) If opened AND frame active, stop frame
Microswitch + 10K ohm resistor
* 1 === open
* 0 === closed
**Kind**: static method of [<code>intval</code>](#intval)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | Error object present if problem reading pin |
| val | <code>integer</code> | Current value of the pin |
<a name="intval._watchRelease"></a>
### intval._watchRelease(err, val)
Callback for watching relese switch state changes.
Using GPIO 05 on Raspberry Pi Zero W.
1) If closed, start timer.
2) If opened, check timer AND
3) If `press` (`now - intval._state.release.time`) greater than minimum and less than `intval._release.seq`, start frame
4) If `press` greater than `intval._release.seq`, start sequence
Button + 10K ohm resistor
* 1 === open
* 0 === closed
**Kind**: static method of [<code>intval</code>](#intval)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | Error object present if problem reading pin |
| val | <code>integer</code> | Current value of the pin |
<a name="intval.setDir"></a>
### intval.setDir([dir])
Set the default direction of the camera.
* forward = true
* backward = false
**Kind**: static method of [<code>intval</code>](#intval)
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [dir] | <code>boolean</code> | <code>true</code> | Direction of the camera |
<a name="intval.frame"></a>
### intval.frame([dir], [time])
Begin a single frame with set variables or defaults
**Kind**: static method of [<code>intval</code>](#intval)
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [dir] | <code>boolean</code> | <code>&quot;null&quot;</code> | (optional) Direction of the frame |
| [time] | <code>integer</code> | <code>&quot;null&quot;</code> | (optional) Exposure time, 0 = minimum |
<a name="intval.sequence"></a>
### intval.sequence()
Start a sequence of frames, using defaults or explicit instructions
**Kind**: static method of [<code>intval</code>](#intval)

View File

@ -1,24 +1,423 @@
'use strict'
const gpio = require('gpio')
const db = require('../db')
const log = require('../log')('intval')
const storage = require('node-persist')
const fs = require('fs')
class Intval {
constructor () {
this._pin = {}
this._declarePins()
}
_declarePins () {
this._pin.four = gpio.export(4, {
direction: 'out',
interval: 100,
ready : () => {
console.info(`Set pin 4 to OUTPUT`)
}
})
}
status () {
return {}
let Gpio
try {
Gpio = require('onoff').Gpio
} catch (e) {
log.warn('Failed including Gpio, using sim')
Gpio = require('../../lib/onoffsim').Gpio
}
const PINS = {
fwd : {
pin : 13,
dir : 'out'
},
bwd : {
pin : 19,
dir : 'out'
},
micro : {
pin : 5,
dir : 'in',
edge : 'both'
},
release : {
pin : 6,
dir : 'in',
edge : 'both'
}
}
module.exports = new Intval()
/** Object representing the intval3 features */
const intval = {}
intval._frame = {
open : 250, //delay before pausing frame in open state
openBwd : 400,
closed : 100, //time that frame actually remains closed for
expected : 630 //expected length of frame, in ms
}
intval._release = {
min : 20,
seq : 1000
}
intval._microDelay = 10 // delay after stop signal before stopping motors
intval._pin = {}
/**
*
*/
intval.init = function () {
if (!fs.existsSync('./state')) fs.mkdirSync('./state')
storage.init({
dir: './state',
stringify: JSON.stringify,
parse: JSON.parse,
encoding: 'utf8',
logging: false, // can also be custom logging function
continuous: true, // continously persist to disk
interval: false, // milliseconds, persist to disk on an interval
ttl: false, // ttl* [NEW], can be true for 24h default or a number in MILLISECONDS
expiredInterval: 2 * 60 * 1000, // [NEW] every 2 minutes the process will clean-up the expired cache
forgiveParseErrors: false // [NEW]
}).then(intval._restoreState).catch((err) => {
log.warn('init', err)
intval.reset()
intval._declarePins()
})
process.on('SIGINT', intval._undeclarePins)
process.on('uncaughtException', intval._undeclarePins)
}
intval._restoreState = function (res) {
storage.getItem('_state', 'test').then(intval._setState).catch((err) => {
intval._setState(undefined)
log.error('_restoreState', err)
})
intval._declarePins()
}
intval._setState = function (data) {
if (typeof data !== 'undefined') {
intval._state = data
intval._state.frame.cb = () => {}
log.info('_setState', 'Restored intval state from disk')
return true
}
log.info('_setState', 'Setting state from defaults')
intval._state = {
frame : {
dir : true, //forward
start : 0, //time frame started, timestamp
active : false, //should frame be running
paused : false,
exposure : 0, //length of frame exposure, in ms
delay : 0, //delay before start of frame, in ms
current : {}, //current settings
cb : () => {}
},
release : {
time: 0,
active : false //is pressed
},
micro : {
time : 0,
primed : false //is ready to stop frame
},
counter : 0,
sequence : false
}
intval._storeState()
}
intval._storeState = function () {
storage.setItem('_state', intval._state)
.then(() => {})
.catch((err) => {
log.error('_storeState', err)
})
}
/**
* (internal function) Declares all Gpio pins that will be used
*
*/
intval._declarePins = function () {
let pin
for (let p in PINS) {
pin = PINS[p]
if (pin.edge) intval._pin[p] = Gpio(pin.pin, pin.dir, pin.edge)
if (!pin.edge) intval._pin[p] = Gpio(pin.pin, pin.dir)
log.info('_declarePins', { pin : pin.pin, dir : pin.dir, edge : pin.edge })
}
intval._pin.release.watch(intval._watchRelease)
}
/**
* (internal function) Undeclares all Gpio in event of uncaught error
* that interupts the node process
*
*/
intval._undeclarePins = function (e) {
log.error(e)
if (!intval._pin) {
log.warn('_undeclarePins', { reason : 'No pins'})
return process.exit()
}
log.warn('_undeclarePins', { pin : PINS.fwd.pin, val : 0, reason : 'exiting'})
intval._pin.fwd.writeSync(0)
log.warn('_undeclarePins', { pin : PINS.bwd.pin, val : 0, reason : 'exiting'})
intval._pin.bwd.writeSync(0)
intval._pin.fwd.unexport()
intval._pin.bwd.unexport()
intval._pin.micro.unexport()
intval._pin.release.unexport()
process.exit()
}
/**
* Start motor in forward direction by setting correct pins in h-bridge
*
*/
intval._startFwd = function () {
intval._pin.fwd.writeSync(1)
intval._pin.bwd.writeSync(0)
}
/**
* Start motor in backward direction by setting correct pins in h-bridge
*
*/
intval._startBwd = function () {
intval._pin.fwd.writeSync(0)
intval._pin.bwd.writeSync(1)
}
intval._pause = function () {
intval._pin.fwd.writeSync(0)
intval._pin.bwd.writeSync(0)
//log.info('_pause', 'frame paused')
}
/**
* Stop motor by setting both motor pins to 0 (LOW)
*
*/
intval._stop = function () {
const entry = {}
const now = +new Date()
const len = now - intval._state.frame.start
intval._pin.fwd.writeSync(0)
intval._pin.bwd.writeSync(0)
log.info(`_stop`, { frame : len })
intval._pin.micro.unwatch()
intval._state.frame.active = false
if (intval._state.frame.cb) intval._state.frame.cb(len)
entry.start = intval._state.frame.start
entry.stop = now
entry.len = len
entry.dir = intval._state.frame.current.dir ? 1 : 0
entry.exposure = intval._state.frame.current.exposure
entry.counter = intval._state.counter
entry.sequence = intval._state.sequence ? 1 : 0
db.insert(entry)
intval._state.frame.current = {}
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 06 on Raspberry Pi Zero W.
*
* 1) If closed AND frame active, start timer, set state primed to `true`.
* 1) If opened AND frame active, stop frame
*
* Microswitch + 10K ohm resistor
* * 1 === open
* * 0 === closed
*
*
* @param {object} err Error object present if problem reading pin
* @param {integer} val Current value of the pin
*
*/
intval._watchMicro = function (err, val) {
const now = +new Date()
if (err) {
log.error('_watchMicro', err)
}
//log.info(`Microswitch val: ${val}`)
//determine when to stop
if (val === 0 && intval._state.frame.active) {
if (!intval._state.micro.primed) {
intval._state.micro.primed = true
intval._state.micro.time = now
log.info('Microswitch primed to stop motor')
}
} else if (val === 1 && intval._state.frame.active) {
if (intval._state.micro.primed && !intval._state.micro.paused && (now - intval._state.frame.start) > intval._frame.open) {
intval._state.micro.primed = false
intval._state.micro.time = 0
setTimeout( () => {
intval._stop()
}, intval._microDelay)
}
}
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 05 on Raspberry Pi Zero W.
*
* 1) If closed, start timer.
* 2) If opened, check timer AND
* 3) If `press` (`now - intval._state.release.time`) greater than minimum and less than `intval._release.seq`, start frame
* 4) If `press` greater than `intval._release.seq`, start sequence
*
* Button + 10K ohm resistor
* * 1 === open
* * 0 === closed
*
* @param {object} err Error object present if problem reading pin
* @param {integer} val Current value of the pin
*
*/
intval._watchRelease = function (err, val) {
const now = +new Date()
let press = 0
if (err) {
return log.error(err)
}
//log.info(`Release switch val: ${val}`)
if (val === 0) {
//closed
if (intval._releaseClosedState(now)) {
intval._state.release.time = now
intval._state.release.active = true //maybe unncecessary
}
} else if (val === 1) {
//opened
if (intval._state.release.active) {
press = now - intval._state.release.time
if (press > intval._release.min && press < intval._release.seq) {
intval.frame()
} else if (press >= intval._release.seq) {
intval.sequence()
}
//log.info(`Release closed for ${press}ms`)
intval._state.release.time = 0
intval._state.release.active = false
}
}
}
intval._releaseClosedState = function (now) {
if (!intval._state.release.active && intval._state.release.time === 0) {
return true
}
if (intval._state.release.active && (now - intval._state.release.time) > (intval._release.seq * 10)) {
return true
}
return false
}
intval.reset = function () {
intval._setState()
intval._storeState()
}
/**
* Set the default direction of the camera.
* * forward = true
* * backward = false
*
* @param {boolean} [dir=true] Direction of the camera
*
*/
intval.setDir = function (val = true) {
if (typeof val !== 'boolean') {
return log.warn('Direction must be represented as either true or false')
}
intval._state.frame.dir = val
intval._storeState()
log.info('setDir', { direction : val ? 'forward' : 'backward' })
}
intval.setExposure = function (val = 0) {
intval._state.frame.exposure = val
intval._storeState()
log.info('setExposure', { exposure : val })
}
intval.setDelay = function (val = 0) {
intval._state.frame.delay = val
intval._storeState()
log.info('setDelay', { delay : val })
}
intval.setCounter = function (val = 0) {
intval._state.counter = val
intval._storeState()
log.info('setCounter', { counter : val })
}
/**
* Begin a single frame with set variables or defaults
*
* @param {?boolean} [dir="null"] (optional) Direction of the frame
* @param {?integer} [exposure="null"] (optional) Exposure time, 0 = minimum
*
*/
intval.frame = function (dir = null, exposure = null, cb = () => {}) {
if (dir === true || (dir === null && intval._state.frame.dir === true) ) {
dir = true
} else {
dir = false
}
if (exposure === null && intval._state.frame.exposure !== 0) {
exposure = intval._state.frame.exposure
} else if (exposure === null) {
exposure = 0 //default speed
}
intval._state.frame.start = +new Date()
intval._state.frame.active = true
intval._pin.micro.watch(intval._watchMicro)
log.info('frame', {dir : dir ? 'forward' : 'backward', exposure : exposure})
if (dir) {
intval._startFwd()
} else {
intval._startBwd()
}
if (exposure !== 0) {
intval._state.frame.paused = true
if (dir) {
setTimeout(intval._pause, intval._frame.open)
//log.info('frame', { pausing : time + intval._frame.open })
setTimeout( () => {
intval._state.frame.paused = false
intval._startFwd()
}, exposure + intval._frame.closed)
} else {
setTimeout(intval._pause, intval._frame.openBwd)
setTimeout( () => {
//log.info('frame', 'restarting')
intval._state.frame.paused = false
intval._startBwd()
}, exposure + intval._frame.closed)
}
}
if (dir) {
intval._state.frame.cb = (len) => {
intval._state.counter++
intval._storeState()
cb(len)
}
} else {
intval._state.frame.cb = (len) => {
intval._state.counter--
intval._storeState()
cb(len)
}
}
intval._state.frame.current = {
dir: dir,
exposure: exposure
}
}
intval.status = function () {
return intval._state
}
module.exports = intval

13
lib/log/Readme.md Normal file
View File

@ -0,0 +1,13 @@
<a name="createLog"></a>
## createLog(label, filename) ⇒ <code>object</code>
createLog() - Returns a winston logger configured to service
**Kind**: global function
**Returns**: <code>object</code> - Winston logger
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| label | <code>string</code> | | Label appearing on logger |
| filename | <code>string</code> | <code>null</code> | Optional file to write log to |

22
lib/log/index.js Normal file
View File

@ -0,0 +1,22 @@
'use strict'
const winston = require('winston')
/**
* createLog() - 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 transports = [ new (winston.transports.Console)({ label : label }) ]
if (filename !== null) {
transports.push( new (winston.transports.File)({ label : label, filename : filename }) )
}
return new (winston.Logger)({
transports: transports
})
}
module.exports = createLog

35
lib/mscript/Readme.md Normal file
View File

@ -0,0 +1,35 @@
<a name="mscript"></a>
## mscript
Object representing mscript parser
**Kind**: global constant
* [mscript](#mscript)
* [.arg(shrt, lng)](#mscript.arg)
* [.arg_pos(shrt, lng)](#mscript.arg_pos)
<a name="mscript.arg"></a>
### mscript.arg(shrt, lng)
Determine whether or not argument flag has been set
**Kind**: static method of [<code>mscript</code>](#mscript)
| Param | Type | Description |
| --- | --- | --- |
| shrt | <code>string</code> | Short flag name (ie `-a`) |
| lng | <code>string</code> | Long flag name (ie `--apple`) |
<a name="mscript.arg_pos"></a>
### mscript.arg_pos(shrt, lng)
Determine position of flag, in argument array
**Kind**: static method of [<code>mscript</code>](#mscript)
| Param | Type | Description |
| --- | --- | --- |
| shrt | <code>string</code> | Short flag name (ie `-a`) |
| lng | <code>string</code> | Long flag name (ie `--apple`) |

441
lib/mscript/index.js Normal file
View File

@ -0,0 +1,441 @@
'use strict'
let fs
let input;
/** Object representing mscript parser */
const mscript = {}
/**
* Determine whether or not argument flag has been set
*
*
* @param {string} shrt Short flag name (ie `-a`)
* @param {string} lng Long flag name (ie `--apple`)
*
*/
mscript.arg = function arg (shrt, lng) {
if (process.argv.indexOf(shrt) !== -1 ||
process.argv.indexOf(lng) !== -1) {
return true
}
return false
}
/**
* Determine position of flag, in argument array
*
*
* @param {string} shrt Short flag name (ie `-a`)
* @param {string} lng Long flag name (ie `--apple`)
*
*/
mscript.arg_pos = function arg_pos (shrt, lng) {
let pos = process.argv.indexOf(shrt)
if (pos === -1) {
pos = process.argv.indexOf(lng)
}
return pos
}
mscript.black = '0,0,0'
mscript.cmd = [
'CF',
'PF',
'BF',
'CB',
'PB',
'BB',
'D'
]
mscript.alts = {
'CF' : ['CAMERA FORWARD', 'CAM FORWARD', 'C'],
'PF' : ['PROJECTOR FORWARD', 'PROJ FORWARD', 'P'],
'BF' : ['BLACK FORWARD'],
'CB' : ['CAMERA BACKWARD', 'CAM BACKWARD', 'CAMERA BACK', 'CAM BACK'],
'PB' : ['PROJECTOR FORWARD', 'PROJ FORWARD', 'PROJECTOR BACK', 'PROJ BACK'],
'BB' : ['BLACK BACKWARD', 'BLACK BACK'],
'L ' : ['LIGHT', 'COLOR', 'LAMP']
}
mscript.state = {}
//TODO: This will memory leak
mscript.state_clear = function state_clear () {
mscript.state = {
cam : 0,
proj : 0,
color : '',
loops : [],
rec : -1
}
}
mscript.alts_unique = function alts_unique () {
const ids = Object.keys(mscript.alts)
const all = []
for (let i = 0; i < ids.length; i++) {
if (all.indexOf(ids[i]) === -1) {
all.push(ids[i])
} else {
mscript.fail(1, "Can't parse")
}
}
}
mscript.interpret = function interpret (text, callback) {
mscript.state_clear()
if (typeof text === 'undefined') {
mscript.fail(2, 'No input')
}
const lines = text.split('\n')
const arr = []
const light = []
const output = {}
let two = ''
let target = 0
let dist = 0 //?
let x
//loop through all lines
for (let line of lines) {
//preprocess line
line = mscript.preprocess(line)
two = line.substring(0, 2)
if (mscript.cmd.indexOf(two) !== -1) {
if (mscript.state.loops.length > 0) {
//hold generated arr in state loop array
mscript.state.loops[mscript.state.rec].arr.push.apply(mscript.state.loops[mscript.state.rec].arr, mscript.str_to_arr(line, two))
mscript.state.loops[mscript.state.rec].light.push.apply(mscript.state.loops[mscript.state.rec].light, mscript.light_to_arr(line, two))
} else {
arr.push.apply(arr, mscript.str_to_arr(line, two))
light.push.apply(light, mscript.light_to_arr(line, two))
}
} else if (line.substring(0, 4) === 'LOOP') {
mscript.state.rec++
mscript.state.loops[mscript.state.rec] = {
arr : [],
light : [],
cam : 0,
proj : 0,
cmd : line + ''
}
} else if (line.substring(0, 2) === 'L ') {
mscript.light_state(line)
} else if (line.substring(0, 3) === 'END') {
for (x = 0; x < mscript.loop_count(mscript.state.loops[mscript.state.rec].cmd); x++) {
if (mscript.state.rec === 0) {
arr.push.apply(arr, mscript.state.loops[mscript.state.rec].arr);
light.push.apply(light, mscript.state.loops[mscript.state.rec].light);
} else if (mscript.state.rec >= 1) {
mscript.state.loops[mscript.state.rec - 1].arr.push.apply(mscript.state.loops[mscript.state.rec - 1].arr, mscript.state.loops[mscript.state.rec].arr)
mscript.state.loops[mscript.state.rec - 1].light.push.apply(mscript.state.loops[mscript.state.rec - 1].light, mscript.state.loops[mscript.state.rec].light)
}
}
mscript.state_update('END', mscript.loop_count(mscript.state.loops[mscript.state.rec].cmd));
delete mscript.state.loops[mscript.state.rec]
mscript.state.rec--
} else if (line.substring(0, 3) === 'CAM') { //directly go to that frame (black?)
target = parseInt(line.split('CAM ')[1])
if (mscript.state.loops.length > 0) {
if (target > mscript.state.cam) {
dist = target - mscript.state.cam
for (x = 0; x < dist; x++) {
mscript.state.loops[mscript.state.rec].arr.push('BF')
mscript.state.loops[mscript.state.rec].light.push(mscript.black)
mscript.state_update('BF')
}
} else {
dist = mscript.state.cam - target
for (x = 0; x < dist; x++) {
mscript.state.loops[mscript.state.rec].arr.push('BB')
mscript.state.loops[mscript.state.rec].light.push(mscript.black)
mscript.state_update('BB')
}
}
} else {
if (target > mscript.state.cam) {
dist = target - mscript.state.cam
for (x = 0; x < dist; x++) {
arr.push('BF')
light.push(mscript.black)
mscript.state_update('BF')
}
} else {
dist = mscript.state.cam - target
for (x = 0; x < dist; x++) {
arr.push('BB')
light.push(mscript.black)
mscript.state_update('BB')
}
}
}
} else if (line.substring(0, 4) === 'PROJ') { //directly go to that frame
target = parseInt(line.split('PROJ ')[1])
if (mscript.state.loops.length > 0) {
if (target > mscript.state.proj) {
dist = target - mscript.state.proj
for (x = 0; x < dist; x++) {
mscript.state.loops[mscript.state.rec].arr.push('PF')
mscript.state.loops[mscript.state.rec].light.push('')
mscript.state_update('PF')
}
} else {
dist = mscript.state.proj - target
for (x = 0; x < dist; x++) {
mscript.state.loops[mscript.state.rec].arr.push('PB')
mscript.state.loops[mscript.state.rec].light.push('')
mscript.state_update('PB')
}
}
} else {
if (target > mscript.state.proj) {
dist = target - mscript.state.proj
for (x = 0; x < dist; x++) {
arr.push('PF')
light.push('')
mscript.state_update('PF')
}
} else {
dist = mscript.state.proj - target
for (x = 0; x < dist; x++) {
arr.push('PB')
light.push('')
mscript.state_update('PB');
}
}
}
} else if (line.substring(0, 3) === 'SET') { //set that state
if (line.substring(0, 7) === 'SET CAM') {
mscript.state.cam = parseInt(line.split('SET CAM')[1]);
} else if (line.substring(0, 8) === 'SET PROJ') {
mscript.state.proj = parseInt(line.split('SET PROJ')[1]);
}
} else if (line.substring(0, 1) === '#' || line.substring(0, 2) === '//') {
//comments
//ignore while parsing
}
}
output.success = true
output.arr = arr
output.light = light
output.cam = mscript.state.cam
output.proj = mscript.state.proj
if (typeof callback !== 'undefined') {
//should only be invoked by running mscript.tests()
callback(output)
} else {
return mscript.output(output)
}
}
mscript.preprocess = function preprocess (line) {
line = line.replace(/\t+/g, '') //strip tabs
line = line.trim() //remove excess whitespace before and after command
line = line.toUpperCase()
return line
}
mscript.last_loop = function last_loop () {
return mscript.state.loops[mscript.state.loops.length - 1]
}
mscript.parent_loop = function parent_loop () {
return mscript.state.loops[mscript.state.loops.length - 2]
}
mscript.state_update = function state_update (cmd, val) {
if (cmd === 'END') {
for (var i = 0; i < val; i++) {
if (mscript.state.rec === 0) {
mscript.state.cam += mscript.state.loops[mscript.state.rec].cam
mscript.state.proj += mscript.state.loops[mscript.state.rec].proj
} else if (mscript.state.rec >= 1) {
mscript.state.loops[mscript.state.rec - 1].cam += mscript.state.loops[mscript.state.rec].cam
mscript.state.loops[mscript.state.rec - 1].proj += mscript.state.loops[mscript.state.rec].proj
}
}
} else if (cmd === 'CF') {
if (mscript.state.loops.length < 1) {
mscript.state.cam++
} else {
mscript.state.loops[mscript.state.rec].cam++
}
} else if (cmd === 'CB') {
if (mscript.state.loops.length < 1) {
mscript.state.cam--
} else {
mscript.state.loops[mscript.state.rec].cam--
}
} else if (cmd === 'PF') {
if (mscript.state.loops.length < 1) {
mscript.state.proj++
} else {
mscript.state.loops[mscript.state.rec].proj++
}
} else if (cmd === 'PB') {
if (mscript.state.loops.length < 1) {
mscript.state.proj--
} else {
mscript.state.loops[mscript.state.rec].proj--
}
} else if (cmd === 'BF') {
if (mscript.state.loops.length < 1) {
mscript.state.cam++
} else {
mscript.state.loops[mscript.state.rec].cam++
}
} else if (cmd === 'BB') {
if (mscript.state.loops.length < 1) {
mscript.state.cam--
} else {
mscript.state.loops[mscript.state.rec].cam++
}
} else if (cmd === 'L ') {
//TODO : ????
}
}
mscript.str_to_arr = function str_to_arr (str, cmd) {
const cnt = str.split(cmd)
let arr = []
let c = parseInt(cnt[1])
if (cnt[1] === '') {
c = 1
} else {
c = parseInt(cnt[1])
}
for (var i = 0; i < c; i++) {
arr.push(cmd)
mscript.state_update(cmd)
}
return arr
}
mscript.light_state = function light_state (str) {
//add parsers for other color spaces
const color = str.replace('L ', '').trim()
mscript.state.color = color
}
mscript.light_to_arr = function light_to_arr (str, cmd) {
const cnt = str.split(cmd)
const arr = []
let c = parseInt(cnt[1])
if (cnt[1] === '') {
c = 1
} else {
c = parseInt(cnt[1])
}
for (var i = 0; i < c; i++) {
if (cmd === 'CF' || cmd === 'CB') {
arr.push(mscript.state.color)
} else if (cmd === 'BF' || cmd === 'BB') {
arr.push(mscript.black)
} else {
arr.push('')
}
}
return arr
}
mscript.loop_count = function loop_count (str) {
return parseInt(str.split('LOOP ')[1])
}
mscript.fail = function fail (code, reason) {
const obj = { success: false, error: true, msg : reason }
console.error(JSON.stringify(obj))
if (process) process.exit()
}
mscript.output = function output (data) {
let json = true; //default
if (mscript.arg('-j', '--json')) {
json = true
}
if (mscript.arg('-t', '--text')) {
json = false
}
if (json) {
console.log(JSON.stringify(data))
} else {
var ids = Object.keys(data)
for (var i = 0; i < ids.length; i++) {
console.log(ids[i] + ': ' + data[ids[i]])
}
}
}
mscript.init = function init () {
if (mscript.arg('-t', '--tests')) {
return mscript.tests()
}
if (mscript.arg('-v', '--verbose')) {
console.time('mscript')
}
if (mscript.arg('-c', '--cam')) {
mscript.state.cam = parseInt(process.argv[mscript.arg_pos('-c', '--cam') + 1])
}
if (mscript.arg('-p', '--proj')) {
mscript.state.proj = parseInt(process.argv[mscript.arg_pos('-p', '--proj') + 1])
}
if (mscript.arg('-f', '--file')) {
input = process.argv[mscript.arg_pos('-f', '--file') + 1]
mscript.interpret(fs.readFileSync(input, 'utf8'))
} else {
mscript.interpret(input)
}
if (mscript.arg('-v', '--verbose')) {
console.timeEnd('mscript')
}
};
if (typeof document === 'undefined' && typeof module !== 'undefined' && !module.parent) {
//node script
fs = require('fs')
input = process.argv[2]
mscript.init()
} else if (typeof module !== 'undefined' && module.parent) {
//module
fs = require('fs')
module.exports = mscript
} else {
//web
}
/*
CAM # - go to camera frame #
PROJ # - go to projector frame #
SET CAM # - sets camera count to #
SET PROJ # - sets projector count to #
LOOP # - begin loop, can nest recursively, # times
END LOOP - (or END) closes loop
L #RGB - sets light to rgb value
FADE
CF - Camera forwards
PF - Projector forwards
BF - Black forwards
CB - Camera backwards
PB - Projector backwards
BB - Black backwards
*/

20
lib/onoffsim/Readme.md Normal file
View File

@ -0,0 +1,20 @@
<a name="onoffsim"></a>
## onoffsim
Object representing a fake onoff Gpio class
**Kind**: global constant
<a name="onoffsim.Gpio"></a>
### onoffsim.Gpio(no, dir, additional) ⇒ <code>object</code>
Returns a Gpio class in the case of running on a dev machine
**Kind**: static method of [<code>onoffsim</code>](#onoffsim)
**Returns**: <code>object</code> - Fake Gpio object
| Param | Type | Description |
| --- | --- | --- |
| no | <code>integer</code> | Number of the GPIO pin |
| dir | <code>string</code> | Dirction of the pin, 'input' or 'output' |
| additional | <code>string</code> | Additional instructions for the GPIO pin, for 'input' type |

35
lib/onoffsim/index.js Normal file
View File

@ -0,0 +1,35 @@
'use strict'
/** Object representing a fake onoff Gpio class */
const onoffsim = {
/**
* Returns a Gpio class in the case of running on a dev machine
*
* @param {integer} no Number of the GPIO pin
* @param {string} dir Dirction of the pin, 'input' or 'output'
* @param {string} additional Additional instructions for the GPIO pin, for 'input' type
* @returns {object} Fake Gpio object
*/
Gpio : function (no, dir = 'in', additional = 'none') {
return {
no : no,
dir : dir,
additional : additional,
val : null,
watchFunc : null,
set : function (val) {
console.log(`onoffsim set ${this.no} to ${val}`)
},
get : function () {
return this.val
},
watch : function (cb) {
this.watchFunc = cb
},
unexport : function () {
}
}
}
}
module.exports = onoffsim

114
lib/sequence/index.js Normal file
View File

@ -0,0 +1,114 @@
'use strict'
const uuid = require('uuid').v4
const log = require('../log')('seq')
/** Object sequence features */
const sequence = {};
sequence._state = {
arr : [],
active : false,
paused : false,
frame: false,
delay : false,
count : 0,
stop : null
}
sequence._loop = {
arr : [],
count : 0,
max : 0
}
sequence.start = function (options, cb) {
if (sequence._state.active) {
return false
}
sequence._state.active = true
sequence._state.count = 0
if (options.arr) {
sequence._state.arr = options.arr
}
if (options.loop) {
sequence._loop.arr = options.loop
sequence._loop.count = 0
}
if (options.maxLoop) {
sequence._loop.max = options.maxLoop
} else {
sequence._loop.max = 0
}
sequence._state.stop = cb
sequence.step()
sequence._state.id = uuid()
return sequence._state.id
}
sequence.setStop = function () {
sequence._state.active = false
}
sequence.stop = function () {
sequence._state.active = false
sequence._state.count = 0
sequence._state.arr = []
sequence._loop.count = 0
sequence._loop.max = 0
sequence._loop.arr = []
if (sequence._state.stop) sequence._state.stop()
sequence._state.stop = null
}
sequence.pause = function () {
sequence._state.paused = true
}
sequence.resume = function () {
sequence._state.paused = false
sequence.step()
}
sequence.step = function () {
if (sequence._state.active && !sequence._state.paused) {
if (sequence._state.arr.length > 0) {
if (sequence._state.count > sequence._state.arr.length - 1) {
return sequence.stop()
}
log.info('step', { count : sequence._state.count, id : sequence._state.id })
return sequence._state.arr[sequence._state.count](() => {
sequence._state.count++
sequence.step()
})
} else if (sequence._loop.arr.length > 0) {
if (sequence._state.count > sequence._loop.arr.length - 1) {
sequence._state.count = 0
sequence._loop.count++
}
if (sequence._loop.max > 0 && sequence._loop.count > sequence._loop.max) {
return sequence.stop()
}
log.info('step', { count : sequence._state.count, id : sequence._state.id })
return sequence._loop.arr[sequence._state.count](() => {
sequence._state.count++
sequence.step()
})
} else{
return sequence.stop()
}
} else if (sequence._state.paused) {
log.info('step', 'Sequence paused', { loop : sequence._loop.count, count : sequence._state.count })
} else if (!sequence._state.active) {
log.info('step', 'Sequence stopped', { loop : sequence._loop.count, count : sequence._state.count })
}
}
module.exports = sequence

102
lib/wifi/Readme.md Normal file
View File

@ -0,0 +1,102 @@
<a name="Wifi"></a>
## Wifi
Class representing the wifi features
**Kind**: global class
* [Wifi](#Wifi)
* [.list(callback)](#Wifi+list)
* [._readConfigCb(err, data)](#Wifi+_readConfigCb)
* [._writeConfigCb(err)](#Wifi+_writeConfigCb)
* [._reconfigureCb(err, stdout, stderr)](#Wifi+_reconfigureCb)
* [._refreshCb(err, stdout, stderr)](#Wifi+_refreshCb)
* [.setNetwork(ssid, pwd, callback)](#Wifi+setNetwork)
* [.getNetwork(callback)](#Wifi+getNetwork)
<a name="Wifi+list"></a>
### wifi.list(callback)
List available wifi access points
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| callback | <code>function</code> | Function which gets invoked after list is returned |
<a name="Wifi+_readConfigCb"></a>
### wifi._readConfigCb(err, data)
(internal function) Invoked after config file is read,
then invokes file write on the config file
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | (optional) Error object only present if problem reading config file |
| data | <code>string</code> | Contents of the config file |
<a name="Wifi+_writeConfigCb"></a>
### wifi._writeConfigCb(err)
(internal function) Invoked after config file is written,
then executes reconfiguration command
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | (optional) Error object only present if problem writing config file |
<a name="Wifi+_reconfigureCb"></a>
### wifi._reconfigureCb(err, stdout, stderr)
(internal function) Invoked after reconfiguration command is complete
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | (optional) Error object only present if configuration command fails |
| stdout | <code>string</code> | Standard output from reconfiguration command |
| stderr | <code>string</code> | Error output from command if fails |
<a name="Wifi+_refreshCb"></a>
### wifi._refreshCb(err, stdout, stderr)
(internal function) Invoked after wifi refresh command is complete
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| err | <code>object</code> | (optional) Error object only present if refresh command fails |
| stdout | <code>string</code> | Standard output from refresh command |
| stderr | <code>string</code> | Error output from command if fails |
<a name="Wifi+setNetwork"></a>
### wifi.setNetwork(ssid, pwd, callback)
Function which initializes the processes for adding a wifi access point authentication
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| ssid | <code>string</code> | SSID of network to configure |
| pwd | <code>string</code> | Password of access point, plaintext |
| callback | <code>function</code> | Function invoked after process is complete, or fails |
<a name="Wifi+getNetwork"></a>
### wifi.getNetwork(callback)
Executes command which gets the currently connected network
**Kind**: instance method of [<code>Wifi</code>](#Wifi)
| Param | Type | Description |
| --- | --- | --- |
| callback | <code>function</code> | Function which is invoked after command is completed |

View File

@ -9,15 +9,24 @@ const refresh = '/sbin/ifdown wlan0 && /sbin/ifup --force wlan0'
const iwlist = '/sbin/iwlist wlan0 scanning | grep "ESSID:"'
const iwgetid = '/sbin/iwgetid'
const log = require('../log')('wifi')
const exec = require('child_process').exec
const fs = require('fs')
class wifi {
let _entry = null
let _ssid = null
let _cb = null
/** Class representing the wifi features */
class Wifi {
constructor () {
this._callback = () => {}
this._entry = null
this._ssid = null
}
/**
* List available wifi access points
*
* @param {function} callback Function which gets invoked after list is returned
*/
list (callback) {
exec(iwlist, (err, stdout, stderr) => {
if (err) {
@ -25,69 +34,175 @@ class wifi {
return callback(err)
}
const lines = stdout.split('\n')
const output = []
let output = []
let line
let i = 0
for (let l of lines) {
line = l.replace('ESSID:', '').trim()
if (line !== '""') {
if (line !== '""' && i < 5) {
line = line.replace(quoteRe, '')
output.push(line)
}
i++
}
output = output.filter(ap => {
if (ap !== '') return ap
})
return callback(null, output)
})
}
/**
* (internal function) Invoked after config file is read,
* then invokes file write on the config file
*
* @param {object} err (optional) Error object only present if problem reading config file
* @param {string} data Contents of the config file
*/
_readConfigCb (err, data) {
let parsed
let current
if (err) {
console.error(err)
return this._callback(err)
return _cb(err)
}
if (data.search(networkPattern) === -1) {
data += `\n${this._entry}`
parsed = this._parseConfig(data)
current = parsed.find(network => {
return network.ssid === _ssid
})
if (typeof current !== 'undefined') {
data = data.replace(current.raw, _entry)
} else {
data = data.replace(networkPattern, this._entry)
data += '\n\n' + _entry
}
this._entry = null
fs.writeFile(filePath, data, 'utf8', this._writeConfigCb)
_entry = null
fs.writeFile(filePath, data, 'utf8', this._writeConfigCb.bind(this))
}
/**
* (internal function) Invoked after config file is written,
* then executes reconfiguration command
*
* @param {object} err (optional) Error object only present if problem writing config file
*/
_writeConfigCb (err) {
if (err) {
console.error(err)
return this._callback(err)
return _cb(err)
}
exec(reconfigure, this._reconfigureCb)
exec(reconfigure, this._reconfigureCb.bind(this))
}
/**
* (internal function) Invoked after reconfiguration command is complete
*
* @param {object} err (optional) Error object only present if configuration command fails
* @param {string} stdout Standard output from reconfiguration command
* @param {string} stderr Error output from command if fails
*/
_reconfigureCb (err, stdout, stderr) {
if (err) {
console.error(err)
return this._callback(err)
return _cb(err)
}
console.log('Wifi reconfigured')
exec(refresh, this._refreshCb)
exec(refresh, this._refreshCb.bind(this))
}
/**
* (internal function) Invoked after wifi refresh command is complete
*
* @param {object} err (optional) Error object only present if refresh command fails
* @param {string} stdout Standard output from refresh command
* @param {string} stderr Error output from command if fails
*/
_refreshCb (err, stdout, stderr) {
if (err) {
console.error(err)
return this._callback(err)
return _cb(err)
}
console.log('Wifi refreshed')
//this._callback(null, { ssid : ssid, pwd : pwd.length })
this._callback = () => {}
_cb(null, { ssid : _ssid })
_cb = () => {}
}
setNetwork (ssid, pwd, callback) {
this._entry = `network={\n\tssid="${ssid}"\n\tpsk="${pwd}"\n}\n`
this._callback = callback
this._ssid = ssid
fs.readFile(filePath, 'utf8', this._readConfigCb)
_parseConfig (str) {
const networks = []
const lines = str.split('\n')
let network = {}
for (let line of lines) {
if (line.substring(0, 9) === 'network={') {
network = {}
network.raw = line
} else if (network.raw && line.indexOf('ssid=') !== -1) {
network.ssid = line.replace('ssid=', '').trim().replace(quoteRe, '')
if (network.raw) {
network.raw += '\n' + line
}
} else if (network.raw && line.substring(0, 1) === '}') {
network.raw += '\n' + line
networks.push(network)
network = {}
} else if (network.raw) {
network.raw += '\n' + line
}
}
return networks
}
/**
* Create sanitized wpa_supplicant.conf stanza for
* configuring wifi without storing plaintext passwords
* @example
* network={
* ssid="YOUR_SSID"
* #psk="YOUR_PASSWORD"
* psk=6a24edf1592aec4465271b7dcd204601b6e78df3186ce1a62a31f40ae9630702
* }
*
* @param {string} ssid SSID of wifi network
* @param {string} pwd Plaintext passphrase of wifi network
* @param {function} callback Function called after psk hash is generated
*/
createPSK (ssid, pwd, callback) {
const cmd = `wpa_passphrase "${ssid}" "${pwd}" | grep "psk="`
let lines
let hash
let plaintext
exec(cmd, (err, stdout, stderr) => {
if (err) {
return callback(err)
}
lines = stdout.replace('#psk=', '').split('psk=')
hash = lines[1]
plaintext = lines[0]
callback(null, hash.trim(), plaintext.trim())
})
}
/**
* Function which initializes the processes for adding a wifi access point authentication
*
* @param {string} ssid SSID of network to configure
* @param {string} pwd Password of access point, plaintext to be masked
* @param {string} hash Password/SSID of access point, securely hashed
* @param {function} callback Function invoked after process is complete, or fails
*/
setNetwork (ssid, pwd, hash, callback) {
let masked = pwd.split('').map(char => { return char !== '"' ? '*' : '"' }).join('')
_entry = `network={\n\tssid="${ssid}"\n\t#psk=${masked}\n\tpsk=${hash}\n}\n`
_cb = callback
_ssid = ssid
fs.readFile(filePath, 'utf8', this._readConfigCb.bind(this))
}
/**
* Executes command which gets the currently connected network
*
* @param {function} callback Function which is invoked after command is completed
*/
getNetwork (callback) {
let output
exec(iwgetid, (err, stdout, stderr) => {
if (err) {
return callback(err)
}
callback(null, stdout)
output = stdout.split('ESSID:')[1].replace(quoteRe, '').trim()
callback(null, output)
})
}
}
module.exports = new wifi()
module.exports = new Wifi()

View File

@ -1,48 +0,0 @@
#blootstrap nginx conf
#uncomment and modify following files for ssl
#server {
#listen 80;
#server_name my_project;
#return 301 https://$server_name$request_uri;
#}
server {
listen 80;
#listen 443 ssl;
#ssl on;
#ssl_certificate {{SSL_CERT_PATH}};
#ssl_certificate_key {{SSL_KEY_PATH}};
#ssl_session_timeout 5m;
#ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
#ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
#ssl_prefer_server_ciphers on;
#server_name my_project;
location / {
proxy_pass http://127.0.0.1:6699/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
gzip on;
gzip_comp_level 5;
gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
}
#uncomment for static file server
#location /static/ {
#uncomment to turn on caching
#expires modified 1y;
#access_log off;
#add_header Cache-Control "public";
#gzip on;
#gzip_comp_level 5;
#gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript application/json;
#use project location
#alias /var/node/intval3/static/;
#}
}

5602
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{
"name": "intval3",
"version": "0.0.1",
"version": "3.0.1",
"description": "Intervalometer for the Bolex",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "./node_modules/.bin/qunit",
"docs": "sh docs.sh"
},
"repository": {
"type": "git",
@ -27,8 +28,17 @@
"cron": "^1.2.1",
"gpio": "^0.2.7",
"node-ipc": "^9.1.0",
"node-persist": "^2.1.0",
"onoff": "^1.1.5",
"restify": "^5.2.0",
"rpio": "^0.9.20",
"sqlite3": "^3.1.13",
"squel": "^5.12.0",
"uuid": "^3.1.0",
"winston": "^2.3.1"
},
"devDependencies": {
"jsdoc-to-markdown": "^3.0.0",
"qunit": "^2.5.0"
}
}

View File

@ -1,12 +1,12 @@
{
"apps" : [
{
"name" : "ble",
"script" : "./services/bluetooth/index.js",
"name" : "intval3",
"script" : "./index.js",
"watch" : false,
"env" : {
"BLENO_DEVICE_NAME" : "intval3",
"DEVICE_ID" : "intval3",
"DEVICE_NAME" : "intval3",
"SERVICE_ID" : "149582bd-d49d-4b5c-acd1-1ae503d09e7a",
"CHAR_ID" : "47bf69fb-f62f-4ef8-9be8-eb727a54fae4",
"WIFI_ID" : "3fe7d9cf-7bd2-4ff0-97c5-ebe87288c2cc",

View File

@ -1,17 +1,17 @@
#!/bin/bash
echo "Running blootstrap install script"
echo "Running intval3 dependency install script"
apt-get update
apt-get install git ufw nginx -y
apt-get install git ufw nginx jq -y
echo "Installing node.js dependencies.."
apt-get install nodejs npm -y
npm install -g n
n latest
npm install -g npm@latest
npm install -g pm2
npm install -g pm2 node-gyp
echo "Installing bluetooth dependencies..."
apt-get install bluetooth bluez libbluetooth-dev libudev-dev -y
echo "Finished installing blootstrap dependencies"
echo "Finished installing intval3 dependencies"

View File

@ -1,18 +1,20 @@
#!/bin/bash
echo "Running blootstrap install script"
echo "Running intval3 install script"
apt-get update
apt-get install git ufw nginx -y
apt-get install git ufw nginx jq -y
echo "Installing node.js dependencies.."
apt-get install nodejs npm -y
npm install -g n
n latest
n 9.1.0
npm install -g npm@latest
npm install -g pm2
npm install -g pm2 node-gyp
echo "Installing bluetooth dependencies..."
apt-get install bluetooth bluez libbluetooth-dev libudev-dev -y
systemctl disable bluetooth
hciconfig hci0 up
echo "Configuring ufw (firewall)..."
ufw default deny incoming
@ -22,13 +24,13 @@ ufw allow http
ufw allow https
ufw enable
echo "Installing blootstrap project..."
wget https://github.com/mattmcw/blootstrap/archive/master.zip
unzip master.zip -d blootstrap/
echo "Installing intval3 project..."
wget https://github.com/sixteenmillimeter/intval3/archive/master.zip
unzip master.zip -d intval3/
rm master.zip
cd blootstrap
cd intval3
npm install
pm2 start process.json
echo "Finished installing blootstrap"
echo "Finished installing intval3"

10
scripts/sequence.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
URL=$1
COUNTER=0
FRAMES=25
while [ $COUNTER -lt $FRAMES ]; do
echo The counter is $COUNTER
curl "$URL/frame"
sleep 60
((COUNTER++))
done

5
scripts/update.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
sudo -u pi -i<< EOF
cd /home/pi/intval3 && git pull
EOF

View File

@ -1,160 +0,0 @@
'use strict'
const ipc = require('node-ipc')
const os = require('os')
const bleno = require('bleno')
const util = require('util')
const wifi = require('../../lib/wifi')
const BLENO_DEVICE_NAME = process.env.BLENO_DEVICE_NAME || 'my_project'
const DEVICE_ID = process.env.DEVICE_ID || 'my_project_id'
const SERVICE_ID = process.env.SERVICE_ID || 'blootstrap'
const CHAR_ID = process.env.CHAR_ID || 'blootstrapchar'
const WIFI_ID = process.env.WIFI_ID || 'blootstrapwifi'
const NETWORK = os.networkInterfaces()
const MAC = getMac() || spoofMac()
let currentWifi = 'disconnected'
const chars = []
ipc.config.id = 'blootstrap_ble'
ipc.config.retry = 1500
ipc.config.rawBuffer = true
ipc.config.encoding = 'hex'
function createChar(name, uuid, prop, write, read) {
function characteristic () {
bleno.Characteristic.call(this, {
uuid : uuid,
properties: prop
})
}
util.inherits(characteristic, bleno.Characteristic)
if (prop.indexOf('read') !== -1) {
//data, offset, withoutResponse, callback
characteristic.prototype.onReadRequest = read
}
if (prop.indexOf('write') !== -1) {
characteristic.prototype.onWriteRequest = write
}
chars.push(new characteristic())
}
function createChars () {
createChar('wifi', WIFI_ID, ['read', 'write'], onWifiWrite, onWifiRead)
}
function onWifiWrite (data, offset, withoutResponse, callback) {
let result
let utf8
let obj
let ssid
let pwd
if (offset) {
console.warn(`Offset scenario`)
result = bleno.Characteristic.RESULT_ATTR_NOT_LONG
return callback(result)
}
utf8 = data.toString('utf8')
obj = JSON.parse(utf8)
ssid = obj.ssid
pwd = obj.pwd
console.log(`Connecting to AP: ${ssid}...`)
return wifi.setNetwork(ssid, pwd, (err, data) => {
if (err) {
console.error('Error configuring wifi', err)
result = bleno.Characteristic.RESULT_UNLIKELY_ERROR
return callback(result)
}
currentWifi = ssid
console.log(`Connected to ${ssid}`)
result = bleno.Characteristic.RESULT_SUCCESS
return callback(result)
})
}
function onWifiRead (offset, callback) {
const result = bleno.Characteristic.RESULT_SUCCESS
const data = new Buffer(JSON.stringify(currentWifi))
callback(result, data.slice(offset, data.length))
}
function getMac () {
const colonRe = new RegExp(':', 'g')
if (NETWORK && NETWORK.wlan0 && NETWORK.wlan0[0] && NETWORK.wlan0[0].mac) {
return NETWORK.wlan0[0].mac.replace(colonRe, '')
}
return undefined
}
function spoofMac () {
const fs = require('fs')
const FSPATH = require.resolve('uuid')
const IDFILE = os.homedir() + '/.intval3id'
let uuid
let UUIDPATH
let TMP
let MACTMP
let dashRe
delete require.cache[FSPATH]
if (fs.existsSync(IDFILE)) {
return fs.readFileSync(IDFILE, 'utf8')
}
uuid = require('uuid').v4
UUIDPATH = require.resolve('uuid')
delete require.cache[UUIDPATH]
TMP = uuid()
MACTMP = TMP.replace(dashRe, '').substring(0, 12)
dashRe = new RegExp('-', 'g')
fs.writeFileSync(IDFILE, MACTMP, 'utf8')
return MACTMP
}
console.log('Starting bluetooth service')
bleno.on('stateChange', state => {
const BLE_ID = `${DEVICE_ID}_${MAC}`
console.log(`on -> stateChange: ${state}`)
if (state === 'poweredOn') {
console.log(`Started advertising BLE serveses as ${BLE_ID}`)
bleno.startAdvertising(BLENO_DEVICE_NAME, [BLE_ID])
} else {
bleno.stopAdvertising()
}
})
bleno.on('advertisingStart', err => {
console.log('on -> advertisingStart: ' + (err ? 'error ' + err : 'success'))
createChars()
if (!err) {
bleno.setServices([
new bleno.PrimaryService({
uuid : SERVICE_ID, //hardcoded across panels
characteristics : chars
})
])
}
})
bleno.on('accept', clientAddress => {
console.log(`${clientAddress} accepted`)
})
bleno.on('disconnect', clientAddress => {
console.log(`${clientAddress} disconnected`)
})
ipc.serve(() => {
ipc.server.on('connect', socket => {
ipc.log('Client connected to socket')
})
ipc.server.on('disconnect', () => {
ipc.log('Client disconnected from socket')
})
ipc.server.on('data', (data, socket) => {
ipc.server.emit(socket, JSON.stringify({}))
})
})
ipc.server.start()

16
test/index.js Normal file
View File

@ -0,0 +1,16 @@
'use strict'
QUnit.test('hello world', function (assert) {
assert.ok(true, 'this is true')
})
//sequence tests
//db tests
//onoffsim
//intval tests
//ble tests
//mscript tests
//wifi tests
//log tests (tricky)