initial import
parent
dcbcae5c0c
commit
3af643d418
|
@ -0,0 +1,11 @@
|
|||
.tscache
|
||||
.project
|
||||
arduino/main/main.ino.nodemcu.bin
|
||||
htdocs
|
||||
node_modules
|
||||
package-lock.json
|
||||
tmp
|
||||
electronics/electronics-cache.lib
|
||||
electronics/electronics.kicad_prl
|
||||
electronics/fp-info-cache
|
||||
electronics/sym-lib-table
|
|
@ -0,0 +1,117 @@
|
|||
module.exports = function(grunt) {
|
||||
|
||||
// Project configuration.
|
||||
grunt.initConfig({
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
copy: {
|
||||
release: {
|
||||
files: [
|
||||
{ src: "node_modules/jquery/dist/jquery.min.js", dest: "htdocs/jquery.min.js" },
|
||||
{ src: "node_modules/jquery-toast-plugin/dist/jquery.toast.min.js", dest: "htdocs/jquery.toast.min.js" },
|
||||
{ src: "node_modules/jquery-toast-plugin/dist/jquery.toast.min.css", dest: "htdocs/jquery.toast.min.css" },
|
||||
{ src: "node_modules/mustache/mustache.min.js", dest: "htdocs/mustache.min.js" },
|
||||
{ cwd: "js/templates/", src:"*.mst", dest: "htdocs/", expand: true},
|
||||
{ src: "node_modules/mustache/mustache.min.js", dest: "htdocs/mustache.min.js" },
|
||||
]
|
||||
},
|
||||
static: {
|
||||
files: [
|
||||
{ cwd: "static/", src: "**/*", dest: "htdocs/", expand:true},
|
||||
]
|
||||
}
|
||||
},
|
||||
ts: {
|
||||
default: {
|
||||
src: ['ts/**/*.ts'],
|
||||
outDir: 'tmp/',
|
||||
options: {
|
||||
module: 'none',
|
||||
moduleResolution: 'node',
|
||||
sourceMap: true,
|
||||
target: 'es5',
|
||||
rootDir: ['ts']
|
||||
}
|
||||
}
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
beautify: true,
|
||||
mangle:false,
|
||||
sourceMap: false
|
||||
},
|
||||
bootstrap: {
|
||||
files: {
|
||||
'htdocs/bootstrap.min.js': ['tmp/view.js','tmp/*.js'],
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
jsbootstrapts: {
|
||||
files: ['ts/*.ts'],
|
||||
tasks: ['ts', 'newer:uglify:bootstrap']
|
||||
},
|
||||
|
||||
lessdefault: {
|
||||
files: ['less/*.less'],
|
||||
tasks: ['less']
|
||||
},
|
||||
concat: {
|
||||
files: ['templates/base/*'],
|
||||
tasks: ['concat']
|
||||
},
|
||||
jstemplate: {
|
||||
files: ['templates/*.mst'],
|
||||
tasks: ['copy']
|
||||
},
|
||||
json_merge: {
|
||||
files: ["lang/*/*.json"],
|
||||
tasks: ['json_merge']
|
||||
},
|
||||
static: {
|
||||
files: ['static/**/*'],
|
||||
tasks: ['copy:static']
|
||||
},
|
||||
},
|
||||
less: {
|
||||
default: {
|
||||
options: {
|
||||
"strictImports": true,
|
||||
"compress": true
|
||||
},
|
||||
files: {
|
||||
"htdocs/default.css": "less/default.less"
|
||||
}
|
||||
},
|
||||
},
|
||||
concat: {
|
||||
options: {
|
||||
sourceMap: false
|
||||
},
|
||||
base: { src: ['templates/base/*'], dest: 'htdocs/base.mst'},
|
||||
},
|
||||
json_merge: {
|
||||
options: {
|
||||
replacer: null,
|
||||
space: " "
|
||||
},
|
||||
en: {
|
||||
files: [
|
||||
{ 'htdocs/en_main.json': ['lang/en/generic.json','lang/en/menu.json', 'lang/en/wifi.json', 'lang/en/error.json', 'lang/en/mapping.json', 'lang/en/main.json', 'lang/en/update.json'] },
|
||||
]
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify');
|
||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||
grunt.loadNpmTasks('grunt-contrib-less');
|
||||
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||
grunt.loadNpmTasks('grunt-newer');
|
||||
grunt.loadNpmTasks('grunt-json-merge');
|
||||
grunt.loadNpmTasks("grunt-ts");
|
||||
|
||||
// Default task(s).
|
||||
grunt.registerTask('default', ['less', 'concat', 'ts', 'newer:uglify', 'json_merge', 'copy', 'watch']);
|
||||
grunt.registerTask('release', ['less', 'concat', 'ts', 'uglify', 'json_merge', 'copy']);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
filter=-build/include_subdir,-whitespace/line_length
|
||||
root=./
|
|
@ -0,0 +1 @@
|
|||
abc123;42758142061871992313;client
|
|
@ -0,0 +1,599 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
|
||||
#include <ESP8266mDNS.h>
|
||||
#include <ESP8266WebServer.h>
|
||||
#include <LittleFS.h>
|
||||
#include "wlanmanager.h"
|
||||
|
||||
#define MORSEPIN 2
|
||||
#define MORSEON LOW
|
||||
#define MORSEOFF HIGH
|
||||
char lang[3]="en";
|
||||
|
||||
/// \brief handle config call
|
||||
void handleConfig();
|
||||
/// \brief handle SSID call
|
||||
void handleSSID();
|
||||
/// \brief handle wifi call and set new credentials
|
||||
void handleWifi();
|
||||
|
||||
/// should program be run
|
||||
volatile bool run;
|
||||
|
||||
/// translation of D0...D7 to physical pins
|
||||
char pinTranslation[8] = { 16, 5, 4, 0, 2, 14, 12, 13 };
|
||||
|
||||
// default config expects this:
|
||||
// camera @d0 + side of optocoppler
|
||||
// valve1 @d1 + side of optocoppler
|
||||
// valve2 @d2 + side of optocoppler
|
||||
// Trigger @d3 connects to ground (uses internal pull-up)
|
||||
// LED @d4 + side of LED
|
||||
// Flash @d5 + side of optocoppler
|
||||
// IR @d6 + side of LED
|
||||
|
||||
/// our local ip
|
||||
char localIP[17]={0};
|
||||
/// morse ip info
|
||||
volatile int8_t morseIP;
|
||||
/// web server
|
||||
ESP8266WebServer server(80);
|
||||
/// wlan manager
|
||||
WLANManager wlanManager;
|
||||
/// start timestamp (in local time) of the main loop
|
||||
uint32_t start;
|
||||
|
||||
/// handle OTA update response
|
||||
void handleUpdate () {
|
||||
server.sendHeader("Connection", "close");
|
||||
if (Update.hasError()) {
|
||||
server.send(500, "application/json", "{\"error\":\"failed\"}");
|
||||
} else {
|
||||
server.send(200, "application/json", "{\"success\":true}");
|
||||
}
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
/// handle OTA update
|
||||
void handleUpdateProgress() {
|
||||
HTTPUpload& upload = server.upload();
|
||||
if (upload.status == UPLOAD_FILE_START) {
|
||||
Serial.setDebugOutput(true);
|
||||
WiFiUDP::stopAll();
|
||||
Serial.printf("Update: %s\n", upload.filename.c_str());
|
||||
uint32_t maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
|
||||
if (!Update.begin(maxSketchSpace)) { //start with max available size
|
||||
Update.printError(Serial);
|
||||
}
|
||||
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
||||
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
} else if (upload.status == UPLOAD_FILE_END) {
|
||||
if (Update.end(true)) { //true to set the size to the current progress
|
||||
Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
|
||||
} else {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
Serial.setDebugOutput(false);
|
||||
}
|
||||
yield();
|
||||
}
|
||||
|
||||
/// setup routine
|
||||
void setup() {
|
||||
LittleFS.begin();
|
||||
Serial.begin(115200);
|
||||
wlanManager.setSSID("cameratrigger","cameratrigger"); // set default
|
||||
wlanManager.loadConfigFromFile("/config"); // load from config file
|
||||
wlanManager.start(); // start access point or connect to access point
|
||||
uint8_t ip[4];
|
||||
*((uint32_t*)&ip) = wlanManager.getIP();
|
||||
sprintf(localIP,"%i.%i.%i.%i",ip[0],ip[1],ip[2],ip[3]);
|
||||
Serial.println(localIP);
|
||||
morseIP = 0;
|
||||
server.onNotFound([]() { // If the client requests any URI
|
||||
if (!handleFileRead(server.uri())) // send it if it exists
|
||||
server.send(404, "text/plain", "404: Not Found"); // otherwise, respond with a 404 (Not Found) error
|
||||
});
|
||||
server.on("/api/config.json", HTTP_GET, handleConfig);
|
||||
server.on("/api/ssid.json", HTTP_GET, handleSSID);
|
||||
server.on("/api/wifi", HTTP_POST, handleWifi);
|
||||
server.on("/api/pinmapping", HTTP_POST, handlePinMapping);
|
||||
server.on("/api/run", HTTP_POST, handleRun);
|
||||
server.on("/api/upload", HTTP_POST, handleUpdate, handleUpdateProgress);
|
||||
server.on("/update", HTTP_POST, handleUpdate, handleUpdateProgress);
|
||||
server.begin();
|
||||
run = false;
|
||||
start = millis();
|
||||
pinMode(LED_BUILTIN, OUTPUT);
|
||||
pinMode(D8, OUTPUT);
|
||||
digitalWrite(D8, false);
|
||||
}
|
||||
|
||||
/// handle config call
|
||||
void handleConfig() {
|
||||
morseIP = -1;
|
||||
String result = String("{\"wifimode\":\"") + (wlanManager.isAccessPoint()? "ap" : "client") + "\",\"ssid\": \"" + wlanManager.getSSID() +"\"}";
|
||||
server.send(200, "application/json", result); // Send HTTP status 200 (Ok) and send some text to the browser/client
|
||||
}
|
||||
|
||||
/// handle SSID scan call and return list of WLAN
|
||||
void handleSSID() {
|
||||
int n = WiFi.scanNetworks();
|
||||
String out="[";
|
||||
char line[1024] = {0};
|
||||
for (int i = 0; i < n; ++i) {
|
||||
snprintf(line,1023,"[\"%s\",%i,%i,\"%s\"]%c",WiFi.SSID(i).c_str(), WiFi.RSSI(i), WiFi.channel(i), WiFi.encryptionType(i) == ENC_TYPE_NONE ? "open": "", ((i+1<n) ? ',' : 0));
|
||||
out += line;
|
||||
}
|
||||
out +="]";
|
||||
server.send(200, "application/json", out); // Send HTTP status 200 (Ok) and send some text to the browser/client
|
||||
}
|
||||
|
||||
/// handle wifi call and set new credentials
|
||||
void handleWifi() {
|
||||
char newssid[64] = {0};
|
||||
char newpassword[64] = {0};
|
||||
strncpy(newssid, server.arg("ssid").c_str(), 63);
|
||||
strncpy(newpassword, server.arg("password").c_str(), 63);
|
||||
if (server.arg("mode") == "ap") {
|
||||
// set own AP credentials
|
||||
wlanManager.setSSID(newssid, newpassword);
|
||||
server.send(200, "application/json", "{}"); // Send HTTP status 200 (Ok) and send some text to the browser/client
|
||||
wlanManager.openAP();
|
||||
wlanManager.save("/config");
|
||||
} else {
|
||||
// try client credentials
|
||||
bool success = wlanManager.connectToAp(newssid, newpassword, [](){
|
||||
String out = String("{\"redirect\":\"") + toStringIp(WiFi.localIP()) + "\"}";
|
||||
server.send(200, "application/json", out.c_str()); // Send HTTP status 200 (Ok) and send some text to the browser/client
|
||||
wlanManager.save("/config");
|
||||
delay(1000);
|
||||
uint8_t ip[4];
|
||||
*((uint32_t*)&ip) = wlanManager.getIP();
|
||||
sprintf(localIP,"%i.%i.%i.%i",ip[0],ip[1],ip[2],ip[3]);
|
||||
morseIP = 0;
|
||||
});
|
||||
//Serial.printf("conn to %s - %i\n",newssid, success);
|
||||
if (!success) {
|
||||
server.send(403, "application/json", "{\"error\":\"invalidpassword\"}"); // Send HTTP status 200 (Ok) and send some text to the browser/client
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// handle of gettting pin mapping configuration
|
||||
void handlePinMapping() {
|
||||
morseIP = -1;
|
||||
File file = LittleFS.open("/htdocs/api/pinmapping.json", "w");
|
||||
const String& data = server.arg("plain");
|
||||
file.write(data.c_str(), data.length());
|
||||
file.close();
|
||||
handleFileRead("/api/pinmapping.json");
|
||||
}
|
||||
|
||||
enum Command {
|
||||
/// enable: value is pin
|
||||
E = 'E',
|
||||
/// disable: value is pin
|
||||
D = 'D',
|
||||
/// trigger a Canon IR command
|
||||
C = 'C',
|
||||
/// trigger a Nikon IR command
|
||||
N = 'N',
|
||||
/// trigger a Sony IR command
|
||||
S = 'S',
|
||||
/// delay
|
||||
d = 'd',
|
||||
};
|
||||
/// actual program step
|
||||
struct cmd {
|
||||
/// command
|
||||
Command c;
|
||||
/// value for command
|
||||
unsigned int value;
|
||||
};
|
||||
|
||||
/// storage for up to 256 command
|
||||
cmd cmds[256];
|
||||
|
||||
unsigned char cmdIndex = 0;
|
||||
IRAM_ATTR void triggerSequence() {
|
||||
run = true;
|
||||
}
|
||||
void handleRun() {
|
||||
morseIP = -2;
|
||||
const String& data = server.arg("cmd");
|
||||
cmdIndex = 0;
|
||||
const char* input = data.c_str();
|
||||
bool error = false;
|
||||
while (*input && !error) {
|
||||
bool parseValue = true;
|
||||
bool advance = true;
|
||||
bool enableOutput = false;
|
||||
bool enableInput = false;
|
||||
switch (*input) {
|
||||
case 'E': // enable: value is pin
|
||||
case 'D': // disable: value is pin
|
||||
case 'C':
|
||||
case 'N':
|
||||
case 'S':
|
||||
enableOutput = true;
|
||||
// fall through
|
||||
case 'd': // delay: value is time
|
||||
cmds[cmdIndex].c = static_cast<Command>(*input);
|
||||
break;
|
||||
case 'T': // trigger on rising input
|
||||
enableInput = true;
|
||||
break;
|
||||
default:
|
||||
error = true;
|
||||
parseValue = false;
|
||||
advance = false;
|
||||
server.send(400, "application/json", "{\"error\":\"invalidcommand\"}");
|
||||
}
|
||||
if (parseValue) {
|
||||
unsigned int value = 0;
|
||||
++input;
|
||||
while ((*input) && (*input) != ';') {
|
||||
if ((*input) >= '0' && (*input) <= '9') {
|
||||
value = value * 10 + ((*input)-'0');
|
||||
}
|
||||
if (*input)
|
||||
++input;
|
||||
}
|
||||
cmds[cmdIndex].value = value;
|
||||
}
|
||||
|
||||
if (enableOutput || enableInput) {
|
||||
if (cmds[cmdIndex].value > 7) {
|
||||
server.send(400, "application/json", "{\"error\":\"invalidpin\"}");
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
cmds[cmdIndex].value = pinTranslation[cmds[cmdIndex].value];
|
||||
if (enableOutput) {
|
||||
pinMode(cmds[cmdIndex].value, OUTPUT);
|
||||
} else if (enableInput) {
|
||||
pinMode(cmds[cmdIndex].value, INPUT_PULLUP);
|
||||
attachInterrupt(digitalPinToInterrupt(cmds[cmdIndex].value), triggerSequence, FALLING);
|
||||
}
|
||||
}
|
||||
if (advance) {
|
||||
if (cmdIndex < 255)
|
||||
++cmdIndex;
|
||||
else {
|
||||
error = true;
|
||||
server.send(400, "application/json", "{\"error\":\"toomanycommands\"}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (*input)
|
||||
++input;
|
||||
}
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
server.send(200, "application/json", "{\"message\":\"ok\"}");
|
||||
// wait for trigger ?
|
||||
const String& trigger = server.arg("trigger");
|
||||
if (trigger == "true") {
|
||||
// run now
|
||||
run = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// send a trigger impulse for Nikon cameras (confirmed, works)
|
||||
/// \param pin which pin
|
||||
/// \param reps number of repeats
|
||||
void sendNikonPulse(uint8_t pin, uint8_t reps) {
|
||||
// 13us on, 13us off
|
||||
while (reps) {
|
||||
digitalWrite(pin,HIGH);
|
||||
delayMicroseconds(10);
|
||||
delayTicks(40);
|
||||
digitalWrite(pin,LOW);
|
||||
delayMicroseconds(10);
|
||||
delayTicks(39);
|
||||
--reps;
|
||||
}
|
||||
}
|
||||
|
||||
/// send a trigger for Nikon cameras (confirmed, works)
|
||||
/// \param pin which pin
|
||||
void triggerNikon(uint16_t pin) { // takes 135.6ms
|
||||
noInterrupts();
|
||||
// SendSequence generates a full command signal by repeatedly calling SendPulse().
|
||||
// The duration of each pulse and the time gaps between pulses is exactly the same as the
|
||||
// values obtained from the analysis^ by oscilloscope
|
||||
for(uint8_t i=0;i<2;++i) {
|
||||
//pulse for 2.0 millis
|
||||
sendNikonPulse(pin, 77);
|
||||
//delay for 27.8 millis
|
||||
//using a combination of delay() and delayMicroseconds()
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(7786);
|
||||
//on pulse for 0.4 millis
|
||||
sendNikonPulse(pin, 15);
|
||||
//delay for 1.5 millis
|
||||
delayMicroseconds(1499);
|
||||
//on pulse for 0.4 millis
|
||||
sendNikonPulse(pin, 15);
|
||||
//delay for 3.5 millis
|
||||
delayMicroseconds(3496);
|
||||
//send pulse for 0.4 millis
|
||||
sendNikonPulse(pin, 15);
|
||||
if(!i) {
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(10000);
|
||||
delayMicroseconds(2977);
|
||||
}
|
||||
}
|
||||
interrupts();
|
||||
}
|
||||
|
||||
/// send a 32kHz pulse
|
||||
/// \param pin pin number to use
|
||||
/// \param reps number of pulses
|
||||
void send32kHzPulse(uint8_t pin, uint16_t reps) {
|
||||
while (reps) {
|
||||
digitalWrite(pin,HIGH);
|
||||
delayMicroseconds(12);
|
||||
delayTicks(64);
|
||||
digitalWrite(pin,LOW);
|
||||
delayMicroseconds(12);
|
||||
delayTicks(64);
|
||||
--reps;
|
||||
}
|
||||
}
|
||||
|
||||
/// send a trigger for Canon cameras (not confirmed)
|
||||
void triggerCanon(uint16_t pin) { // takes 8.3 ms
|
||||
noInterrupts();
|
||||
//16 pulses for at 32kHz
|
||||
send32kHzPulse(pin, 16);
|
||||
delayMicroseconds(5310);
|
||||
//on pulse for 0.5 millis
|
||||
send32kHzPulse(pin, 16);
|
||||
interrupts();
|
||||
}
|
||||
|
||||
/// get current tick count
|
||||
/// \param tick count
|
||||
uint32_t inline asm_ccount() {
|
||||
uint32_t r;
|
||||
asm volatile ("rsr %0, ccount": "=r"(r));
|
||||
return r;
|
||||
}
|
||||
/// delay for given number of ticks
|
||||
/// \param ticks number of ticks
|
||||
void delayTicks(int32_t ticks) {
|
||||
uint32_t expire_ticks = asm_ccount() + ticks;
|
||||
do {
|
||||
ticks = expire_ticks - asm_ccount();
|
||||
} while(ticks>0);
|
||||
}
|
||||
|
||||
/// send a pulse for Sony cameras
|
||||
/// \param pin pin number to use
|
||||
/// \param count number of pulses
|
||||
void sendSonyPulse(uint16_t pin, uint16_t count) {
|
||||
// warm up cache
|
||||
digitalWrite(pin,LOW);
|
||||
delayTicks(40);
|
||||
while (count) { // 1 pulse = 7uS high, 18us Low = 25uS at 40kHz
|
||||
digitalWrite(pin,HIGH);
|
||||
delayMicroseconds(4);
|
||||
delayTicks(39);
|
||||
digitalWrite(pin,LOW);
|
||||
delayMicroseconds(15);
|
||||
delayTicks(50);
|
||||
--count;
|
||||
}
|
||||
}
|
||||
|
||||
/// send a trigger for Sony cameras (not confirmed)
|
||||
/// \param pin pin number to use
|
||||
void triggerSony(uint16_t pin) { // takes ? 6ms
|
||||
noInterrupts();
|
||||
bool cmd[] = {1,0,1,1,0,1,0,0,1,0,1,1,1,0,0,0,1,1,1,1};
|
||||
//2,4ms start pulse, 600uS gap, 20 bit data
|
||||
sendSonyPulse(pin, 96); // 96 cycles = 2,4ms
|
||||
delayMicroseconds(600);
|
||||
for (uint8_t i = 0; i < 20; ++i) {
|
||||
sendSonyPulse(pin, cmd[i] ? 48 : 24);
|
||||
delayMicroseconds(600);
|
||||
}
|
||||
interrupts();
|
||||
}
|
||||
|
||||
/// rund defined program
|
||||
void runProgram() {
|
||||
for (unsigned int i = 0 ; i < cmdIndex; ++i) {
|
||||
switch(cmds[i].c) {
|
||||
|
||||
case E:
|
||||
digitalWrite(cmds[i].value, true);
|
||||
break;
|
||||
case D:
|
||||
digitalWrite(cmds[i].value, false);
|
||||
break;
|
||||
case C:
|
||||
triggerCanon(cmds[i].value);
|
||||
break;
|
||||
case N:
|
||||
triggerNikon(cmds[i].value);
|
||||
break;
|
||||
case S:
|
||||
triggerSony(cmds[i].value);
|
||||
break;
|
||||
case d:
|
||||
uint32_t d = cmds[i].value;
|
||||
while (d>16000) {
|
||||
d-= 16000;
|
||||
delay(16000);
|
||||
}
|
||||
delay(d);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// guess content type based on file name
|
||||
/// \param filename filename to check
|
||||
/// \return content type
|
||||
String getContentType(String filename) { // convert the file extension to the MIME type
|
||||
if (filename.endsWith(".html")) return "text/html";
|
||||
else if (filename.endsWith(".css")) return "text/css";
|
||||
else if (filename.endsWith(".js")) return "application/javascript";
|
||||
else if (filename.endsWith(".ico")) return "image/x-icon";
|
||||
else if (filename.endsWith(".png")) return "image/png";
|
||||
else if (filename.endsWith(".svg")) return "image/svg+xml";
|
||||
else if (filename.endsWith(".json")) return "application/json";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
/// send a file to the client
|
||||
/// \param path file/path to send
|
||||
/// \return true on success
|
||||
bool handleFileRead(String path) { // send the right file to the client (if it exists)
|
||||
morseIP = -2;
|
||||
if (path.endsWith("/")) path += "index.html"; // If a folder is requested, send the index file
|
||||
String contentType = getContentType(path); // Get the MIME type
|
||||
path = "/htdocs" + path;
|
||||
if (LittleFS.exists(path)) { // If the file exists
|
||||
File file = LittleFS.open(path, "r"); // Open it
|
||||
server.sendHeader("X-Version", "1.2");
|
||||
server.streamFile(file, contentType); // And send it to the client
|
||||
file.close(); // Then close the file again
|
||||
return true;
|
||||
}
|
||||
if (captivePortal()) {
|
||||
return true;
|
||||
}
|
||||
return false; // If the file doesn't exist, return false
|
||||
}
|
||||
|
||||
/// crude check if string is an IP
|
||||
/// \return true if an IP
|
||||
bool isIp(String str) {
|
||||
for (size_t i = 0; i < str.length(); i++) {
|
||||
int c = str.charAt(i);
|
||||
if (c != '.' && (c < '0' || c > '9')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// IP to String
|
||||
/// \param ip IP to convert
|
||||
/// return formatted string
|
||||
String toStringIp(IPAddress ip) {
|
||||
String res = "";
|
||||
for (int i = 0; i < 3; i++) {
|
||||
res += String((ip >> (8 * i)) & 0xFF) + ".";
|
||||
}
|
||||
res += String(((ip >> 8 * 3)) & 0xFF);
|
||||
return res;
|
||||
}
|
||||
|
||||
/// Redirect to captive portal if we got a request for another domain.
|
||||
/// \return true in that case so the page handler do not try to handle the request again.
|
||||
bool captivePortal() {
|
||||
morseIP = -2;
|
||||
if (!isIp(server.hostHeader()) ) {
|
||||
//Serial.println("Request redirected to captive portal");
|
||||
server.sendHeader("Location", String("http://") + toStringIp(server.client().localIP()) + "/", true);
|
||||
server.send ( 302, "text/plain", ""); // Empty content inhibits Content-length header so we have to close the socket ourselves.
|
||||
server.client().stop(); // Stop is needed because we sent no content length
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///
|
||||
// char msg[4];
|
||||
// int msgLen = 0;
|
||||
// uint32_t nextmsg;
|
||||
// bool out = false;
|
||||
|
||||
/// "morse" a digit (blink number of times)
|
||||
/// \param count number of times to blink
|
||||
void morseDigit(uint8_t count) {
|
||||
while (count) {
|
||||
server.handleClient();
|
||||
wlanManager.loop();
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
delay(500);
|
||||
digitalWrite(MORSEPIN, MORSEOFF);
|
||||
delay(250);
|
||||
--count;
|
||||
}
|
||||
delay(2000);
|
||||
}
|
||||
/// is ready LED on
|
||||
bool readyLEDActive = false;
|
||||
|
||||
/// main loop
|
||||
void loop() {
|
||||
if (morseIP > -1) {
|
||||
if (morseIP == 0) {
|
||||
readyLEDActive = false;
|
||||
// send long start indicator
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
delay(1000);
|
||||
digitalWrite(MORSEPIN, MORSEOFF);
|
||||
delay(1000);
|
||||
}
|
||||
// morse localIP[morseIP]
|
||||
char digit = localIP[morseIP];
|
||||
if (digit == '.') {
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
delay(10);
|
||||
digitalWrite(MORSEPIN, MORSEOFF);
|
||||
// wait 1 sec
|
||||
delay(1000);
|
||||
} else if (digit == 0) {
|
||||
morseIP = -1; // restart
|
||||
// send long end indicator
|
||||
for (uint8_t i = 0; i < 50; ++i) {
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
delay(30);
|
||||
digitalWrite(MORSEPIN, MORSEOFF);
|
||||
delay(10);
|
||||
}
|
||||
// wait 2 sec
|
||||
delay(2000);
|
||||
} else {
|
||||
morseDigit(digit - '0');
|
||||
}
|
||||
++morseIP;
|
||||
} else if (!readyLEDActive) {
|
||||
readyLEDActive = true;
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
}
|
||||
|
||||
server.handleClient();
|
||||
wlanManager.loop();
|
||||
|
||||
if (run) {
|
||||
digitalWrite(MORSEPIN, MORSEOFF);
|
||||
runProgram();
|
||||
digitalWrite(MORSEPIN, MORSEON);
|
||||
run = false;
|
||||
delay(1000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include "wlanmanager.h"
|
||||
|
||||
#define _connectTimeout 5000
|
||||
|
||||
WLANManager::WLANManager() {
|
||||
bzero(m_ssid, sizeof(m_ssid));
|
||||
bzero(m_password, sizeof(m_password));
|
||||
m_dnsServer = NULL;
|
||||
m_isAccessPoint = true;
|
||||
}
|
||||
|
||||
void WLANManager::setSSID(const char* ssid, const char* password) {
|
||||
strncpy(m_ssid, ssid, sizeof(m_ssid) - 1);
|
||||
strncpy(m_password, password, sizeof(m_password) - 1);
|
||||
}
|
||||
|
||||
void WLANManager::loadConfigFromFile(const char* filename) {
|
||||
File file = LittleFS.open(filename, "r"); // Open it
|
||||
unsigned int size = file.size();
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
char mode[20]={0};
|
||||
char* config = reinterpret_cast<char*>(malloc(size + 1));
|
||||
config[size] = 0;
|
||||
file.read((unsigned char*)config, size);
|
||||
file.close();
|
||||
while (config[size - 1] == '\n') {
|
||||
config[size - 1] = 0;
|
||||
--size;
|
||||
}
|
||||
// format: "ssid;password;ap/client"
|
||||
char* next = nextSemi(config);
|
||||
strncpy(m_ssid, config, sizeof(m_ssid)-1);
|
||||
if (!next) return;
|
||||
char* start = next;
|
||||
next = nextSemi(start);
|
||||
strncpy(m_password, start, sizeof(m_password)-1);
|
||||
start = next;
|
||||
next = nextSemi(start);
|
||||
strncpy(mode, start, sizeof(mode)-1);
|
||||
m_isAccessPoint = strcmp(mode, "ap") == 0;
|
||||
free(config);
|
||||
}
|
||||
|
||||
void WLANManager::start() {
|
||||
// try to connect to stored AP
|
||||
if (m_isAccessPoint || !connectToAp(NULL, NULL, NULL)) {
|
||||
// fire up own AP
|
||||
openAP();
|
||||
m_dnsServer = new DNSServer();
|
||||
m_dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
m_dnsServer->start(53, "*", WiFi.softAPIP());
|
||||
}
|
||||
}
|
||||
|
||||
bool WLANManager::connectToAp(const char* ssid, const char* pass, std::function<void()> callback) {
|
||||
m_isAccessPoint = false;
|
||||
if (ssid == NULL) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
return true;
|
||||
}
|
||||
if (ssid) {
|
||||
WiFi.begin(ssid, pass);
|
||||
} else {
|
||||
if (WiFi.SSID().length()) {
|
||||
ETS_UART_INTR_DISABLE();
|
||||
wifi_station_disconnect();
|
||||
ETS_UART_INTR_ENABLE();
|
||||
WiFi.begin();
|
||||
} else {
|
||||
WiFi.begin(m_ssid, m_password);
|
||||
}
|
||||
}
|
||||
int connRes = waitForConnectResult();
|
||||
if (connRes != WL_CONNECTED) {
|
||||
WiFi.beginWPSConfig();
|
||||
// should be connected at the end of WPS
|
||||
connRes = waitForConnectResult();
|
||||
}
|
||||
if (connRes == WL_CONNECTED) {
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
WiFi.mode(WIFI_STA);
|
||||
if (m_dnsServer) {
|
||||
delete m_dnsServer;
|
||||
m_dnsServer = NULL;
|
||||
}
|
||||
return connRes == WL_CONNECTED;
|
||||
}
|
||||
|
||||
uint8_t WLANManager::waitForConnectResult() {
|
||||
uint32_t start = millis();
|
||||
boolean keepConnecting = true;
|
||||
uint8_t status;
|
||||
while (keepConnecting) {
|
||||
status = WiFi.status();
|
||||
if (millis() > start + _connectTimeout) {
|
||||
keepConnecting = false;
|
||||
}
|
||||
if (status == WL_CONNECTED || status == WL_CONNECT_FAILED) {
|
||||
keepConnecting = false;
|
||||
}
|
||||
delay(100);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void WLANManager::openAP() {
|
||||
m_isAccessPoint = true;
|
||||
// do we have stored settings?
|
||||
if (!WiFi.isConnected()) {
|
||||
WiFi.persistent(false);
|
||||
// disconnect sta, start ap
|
||||
WiFi.disconnect(); // this alone is not enough to stop the autoconnecter
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.persistent(true);
|
||||
} else {
|
||||
// setup AP
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
}
|
||||
Serial.println(m_ssid);
|
||||
Serial.println(m_password);
|
||||
WiFi.softAP(m_ssid, m_password);
|
||||
}
|
||||
|
||||
char* WLANManager::nextSemi(char* start) {
|
||||
char* semi = strchr(start, ';');
|
||||
if (!semi) return NULL;
|
||||
*semi = 0;
|
||||
return semi + 1;
|
||||
}
|
||||
|
||||
bool WLANManager::isAccessPoint() const {
|
||||
return m_isAccessPoint;
|
||||
}
|
||||
|
||||
void WLANManager::save(const char* filename) {
|
||||
String line = String(m_ssid) + ";" + m_password + ";" + (m_isAccessPoint ? "ap" : "client");
|
||||
File file = LittleFS.open(filename, "w");
|
||||
file.write(line.c_str(), line.length());
|
||||
file.close();
|
||||
}
|
||||
|
||||
const char* WLANManager::getSSID() const {
|
||||
return m_ssid;
|
||||
}
|
||||
|
||||
void WLANManager::loop() {
|
||||
if (m_dnsServer) {
|
||||
m_dnsServer->processNextRequest();
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t WLANManager::getIP() const {
|
||||
if (m_isAccessPoint)
|
||||
return WiFi.softAPIP();
|
||||
return WiFi.localIP();
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef WLANMANAGER_H_
|
||||
#define WLANMANAGER_H_
|
||||
#include <DNSServer.h>
|
||||
|
||||
/**
|
||||
* \class WLANManager
|
||||
* \brief manager for and connecting to WLAN
|
||||
*/
|
||||
class WLANManager {
|
||||
public:
|
||||
/// \brief constructor
|
||||
WLANManager();
|
||||
|
||||
/// \brief set SSID and password
|
||||
/// \param ssid SSID to set
|
||||
/// \param password password to set
|
||||
void setSSID(const char* ssid, const char* password);
|
||||
|
||||
/// \brief load config from given file
|
||||
/// \param filename file name of config file
|
||||
void loadConfigFromFile(const char* filename);
|
||||
|
||||
/// \brief start WLAN
|
||||
void start();
|
||||
|
||||
/// \brief set mode to access point (true) or client (false)
|
||||
/// \param isAccessPoint true if we are an access point
|
||||
void setAccessPoint(bool isAccessPoint);
|
||||
|
||||
/// \brief are we an access point?
|
||||
/// \return true if we are
|
||||
bool isAccessPoint() const;
|
||||
|
||||
/// \brief save configuration
|
||||
/// \param filename filename of config file
|
||||
void save(const char* filename);
|
||||
|
||||
/// \brief connect to an access point
|
||||
/// \param ssid ssid of AP
|
||||
/// \param pass password of AP
|
||||
/// \param callbal function to be called if connected
|
||||
/// \retval true on success
|
||||
bool connectToAp(const char* ssid, const char* pass, std::function<void()> callback);
|
||||
|
||||
/// \brief get SSID
|
||||
/// \return current SSID
|
||||
const char* getSSID() const;
|
||||
|
||||
/// \brief open access point
|
||||
void openAP();
|
||||
|
||||
/// \brief internal loop, need to be called from main loop
|
||||
void loop();
|
||||
|
||||
/// \brief get current IP
|
||||
/// \return current IP
|
||||
uint32_t getIP() const;
|
||||
|
||||
private:
|
||||
/// \brief parse input and search for next semicolon
|
||||
/// \param start start offset. Input string will be modified
|
||||
/// \return pointer to char after semicolor or NULL if not found
|
||||
char* nextSemi(char* start);
|
||||
|
||||
/// \brief wait for connection to access point
|
||||
/// \return WLAN status
|
||||
uint8_t waitForConnectResult();
|
||||
|
||||
/// pointer to our dns server
|
||||
DNSServer* m_dnsServer;
|
||||
|
||||
/// are we an access point
|
||||
bool m_isAccessPoint;
|
||||
|
||||
/// our ssid
|
||||
char m_ssid[64];
|
||||
|
||||
// our password
|
||||
char m_password[64];
|
||||
};
|
||||
|
||||
#endif // WLANMANAGER_H_
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,435 @@
|
|||
{
|
||||
"board": {
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"board_outline_line_width": 0.049999999999999996,
|
||||
"copper_line_width": 0.19999999999999998,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.049999999999999996,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": false,
|
||||
"text_position": 0,
|
||||
"units_format": 1
|
||||
},
|
||||
"fab_line_width": 0.09999999999999999,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.09999999999999999,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.762,
|
||||
"height": 1.524,
|
||||
"width": 1.524
|
||||
},
|
||||
"silk_line_width": 0.12,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.15,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"45_degree_only": false,
|
||||
"min_clearance": 0.508
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [
|
||||
{
|
||||
"gap": 0.0,
|
||||
"via_gap": 0.0,
|
||||
"width": 0.0
|
||||
}
|
||||
],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"filename": "board_design_settings.json",
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"copper_edge_clearance": "error",
|
||||
"courtyards_overlap": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint_type_mismatch": "error",
|
||||
"hole_clearance": "error",
|
||||
"hole_near_hole": "error",
|
||||
"invalid_outline": "error",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"net_conflict": "warning",
|
||||
"npth_inside_courtyard": "ignore",
|
||||
"padstack": "error",
|
||||
"pth_inside_courtyard": "ignore",
|
||||
"shorting_items": "error",
|
||||
"silk_over_copper": "warning",
|
||||
"silk_overlap": "warning",
|
||||
"skew_out_of_range": "error",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zone_has_empty_net": "error",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rule_severitieslegacy_courtyards_overlap": true,
|
||||
"rule_severitieslegacy_no_courtyard_defined": false,
|
||||
"rules": {
|
||||
"allow_blind_buried_vias": false,
|
||||
"allow_microvias": false,
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.0,
|
||||
"min_copper_edge_clearance": 0.0,
|
||||
"min_hole_clearance": 0.25,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.19999999999999998,
|
||||
"min_microvia_drill": 0.09999999999999999,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.19999999999999998,
|
||||
"min_via_annular_width": 0.049999999999999996,
|
||||
"min_via_diameter": 0.39999999999999997,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"track_widths": [
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"via_dimensions": [
|
||||
{
|
||||
"diameter": 0.0,
|
||||
"drill": 0.0
|
||||
}
|
||||
],
|
||||
"zones_allow_external_fillets": false,
|
||||
"zones_use_no_outline": true
|
||||
},
|
||||
"layer_presets": []
|
||||
},
|
||||
"boards": [],
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_label_syntax": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"extra_units": "error",
|
||||
"global_label_dangling": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"label_dangling": "error",
|
||||
"lib_symbol_issues": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"similar_labels": "warning",
|
||||
"unannotated": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "electronics.kicad_pro",
|
||||
"version": 1
|
||||
},
|
||||
"net_settings": {
|
||||
"classes": [
|
||||
{
|
||||
"bus_width": 12.0,
|
||||
"clearance": 0.2,
|
||||
"diff_pair_gap": 0.25,
|
||||
"diff_pair_via_gap": 0.25,
|
||||
"diff_pair_width": 0.2,
|
||||
"line_style": 0,
|
||||
"microvia_diameter": 0.3,
|
||||
"microvia_drill": 0.1,
|
||||
"name": "Default",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 1.0,
|
||||
"via_diameter": 0.8,
|
||||
"via_drill": 0.4,
|
||||
"wire_width": 6.0
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"net_colors": null
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"gencad": "",
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"drawing": {
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.25,
|
||||
"pin_symbol_size": 0.0,
|
||||
"text_offset_ratio": 0.08
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"net_format_name": "",
|
||||
"ngspice": {
|
||||
"fix_include_paths": true,
|
||||
"fix_passive_vals": false,
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"model_mode": 0,
|
||||
"workbook_filename": ""
|
||||
},
|
||||
"page_layout_descr_file": "${KICAD6_TEMPLATE_DIR}/pagelayout_default.kicad_wks",
|
||||
"plot_directory": "",
|
||||
"spice_adjust_passive_values": false,
|
||||
"spice_external_command": "spice \"%I\"",
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0
|
||||
},
|
||||
"sheets": [
|
||||
[
|
||||
"102cb741-e9ae-4781-bb1b-b26fba217145",
|
||||
""
|
||||
]
|
||||
],
|
||||
"text_variables": {}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,249 @@
|
|||
update=Mo 13 Jul 2020 19:09:55 CEST
|
||||
version=1
|
||||
last_client=kicad
|
||||
[general]
|
||||
version=1
|
||||
RootSch=
|
||||
BoardNm=
|
||||
[cvpcb]
|
||||
version=1
|
||||
NetIExt=net
|
||||
[eeschema]
|
||||
version=1
|
||||
LibDir=
|
||||
[eeschema/libraries]
|
||||
[schematic_editor]
|
||||
version=1
|
||||
PageLayoutDescrFile=empty.kicad_wks
|
||||
PlotDirectoryName=
|
||||
SubpartIdSeparator=0
|
||||
SubpartFirstId=65
|
||||
NetFmtName=
|
||||
SpiceAjustPassiveValues=0
|
||||
LabSize=50
|
||||
ERC_TestSimilarLabels=1
|
||||
[pcbnew]
|
||||
version=1
|
||||
PageLayoutDescrFile=
|
||||
LastNetListRead=
|
||||
CopperLayerCount=2
|
||||
BoardThickness=1.6
|
||||
AllowMicroVias=0
|
||||
AllowBlindVias=0
|
||||
RequireCourtyardDefinitions=0
|
||||
ProhibitOverlappingCourtyards=1
|
||||
MinTrackWidth=0.2
|
||||
MinViaDiameter=0.4
|
||||
MinViaDrill=0.3
|
||||
MinMicroViaDiameter=0.2
|
||||
MinMicroViaDrill=0.09999999999999999
|
||||
MinHoleToHole=0.25
|
||||
TrackWidth1=0.25
|
||||
TrackWidth2=1
|
||||
ViaDiameter1=0.8
|
||||
ViaDrill1=0.4
|
||||
dPairWidth1=0.2
|
||||
dPairGap1=0.25
|
||||
dPairViaGap1=0.25
|
||||
SilkLineWidth=0.12
|
||||
SilkTextSizeV=1
|
||||
SilkTextSizeH=1
|
||||
SilkTextSizeThickness=0.15
|
||||
SilkTextItalic=0
|
||||
SilkTextUpright=1
|
||||
CopperLineWidth=0.2
|
||||
CopperTextSizeV=1.5
|
||||
CopperTextSizeH=1.5
|
||||
CopperTextThickness=0.3
|
||||
CopperTextItalic=0
|
||||
CopperTextUpright=1
|
||||
EdgeCutLineWidth=0.05
|
||||
CourtyardLineWidth=0.05
|
||||
OthersLineWidth=0.15
|
||||
OthersTextSizeV=1
|
||||
OthersTextSizeH=1
|
||||
OthersTextSizeThickness=0.15
|
||||
OthersTextItalic=0
|
||||
OthersTextUpright=1
|
||||
SolderMaskClearance=0.051
|
||||
SolderMaskMinWidth=0.25
|
||||
SolderPasteClearance=0
|
||||
SolderPasteRatio=-0
|
||||
[pcbnew/Layer.F.Cu]
|
||||
Name=F.Cu
|
||||
Type=0
|
||||
Enabled=1
|
||||
[pcbnew/Layer.In1.Cu]
|
||||
Name=In1.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In2.Cu]
|
||||
Name=In2.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In3.Cu]
|
||||
Name=In3.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In4.Cu]
|
||||
Name=In4.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In5.Cu]
|
||||
Name=In5.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In6.Cu]
|
||||
Name=In6.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In7.Cu]
|
||||
Name=In7.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In8.Cu]
|
||||
Name=In8.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In9.Cu]
|
||||
Name=In9.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In10.Cu]
|
||||
Name=In10.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In11.Cu]
|
||||
Name=In11.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In12.Cu]
|
||||
Name=In12.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In13.Cu]
|
||||
Name=In13.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In14.Cu]
|
||||
Name=In14.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In15.Cu]
|
||||
Name=In15.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In16.Cu]
|
||||
Name=In16.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In17.Cu]
|
||||
Name=In17.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In18.Cu]
|
||||
Name=In18.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In19.Cu]
|
||||
Name=In19.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In20.Cu]
|
||||
Name=In20.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In21.Cu]
|
||||
Name=In21.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In22.Cu]
|
||||
Name=In22.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In23.Cu]
|
||||
Name=In23.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In24.Cu]
|
||||
Name=In24.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In25.Cu]
|
||||
Name=In25.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In26.Cu]
|
||||
Name=In26.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In27.Cu]
|
||||
Name=In27.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In28.Cu]
|
||||
Name=In28.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In29.Cu]
|
||||
Name=In29.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.In30.Cu]
|
||||
Name=In30.Cu
|
||||
Type=0
|
||||
Enabled=0
|
||||
[pcbnew/Layer.B.Cu]
|
||||
Name=B.Cu
|
||||
Type=0
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.Adhes]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.Adhes]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.Paste]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.Paste]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.SilkS]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.SilkS]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.Mask]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.Mask]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Dwgs.User]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Cmts.User]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Eco1.User]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Eco2.User]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Edge.Cuts]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Margin]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.CrtYd]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.CrtYd]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.B.Fab]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.F.Fab]
|
||||
Enabled=1
|
||||
[pcbnew/Layer.Rescue]
|
||||
Enabled=0
|
||||
[pcbnew/Netclasses]
|
||||
[pcbnew/Netclasses/Default]
|
||||
Name=Default
|
||||
Clearance=0.2
|
||||
TrackWidth=0.25
|
||||
ViaDiameter=0.8
|
||||
ViaDrill=0.4
|
||||
uViaDiameter=0.3
|
||||
uViaDrill=0.1
|
||||
dPairWidth=0.2
|
||||
dPairGap=0.25
|
||||
dPairViaGap=0.25
|
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"error": {
|
||||
"invalidpassword": "SSID or password wrong"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"generic": {
|
||||
"save": "Save",
|
||||
"savesuccess": "Saved successfully"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"main": {
|
||||
"edittiming": "Edit Timing",
|
||||
"save": "Just save",
|
||||
"start": "Start time",
|
||||
"trigger": "Save and Go!",
|
||||
"needpause": "IR Triggering needs $1 ms pause until next action"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"mapping": {
|
||||
"edittitle": "Edit mapping",
|
||||
"pin": "Pin",
|
||||
"type": "Type",
|
||||
"inverted": "Inverted",
|
||||
"digitalout": "digital output",
|
||||
"ircanon": "Canon IR",
|
||||
"irnikon": "Nikon IR",
|
||||
"irsony": "Sony IR",
|
||||
"notconn": "Not connected",
|
||||
"trigger": "trigger signal"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"menu": {
|
||||
"setupwifi": "Setup WiFi",
|
||||
"setuppinmapping": "Setup Pin Mapping"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"update": {
|
||||
"header": "Update Camera Trigger",
|
||||
"fileselect": "select the new file:",
|
||||
"submit": "do update"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"wifi": {
|
||||
"accessmode": "Access mode",
|
||||
"accesspoint": "Access point",
|
||||
"client": "Client device",
|
||||
"accessdetails": "Access details",
|
||||
"rescan": "Rescan networks",
|
||||
"channel": "Channel",
|
||||
"ssid": "SSID",
|
||||
"password": "Password",
|
||||
"encryptionType": "Encryption",
|
||||
"encryption": "yes",
|
||||
"encryptionopen": "no"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
@labelColor: #222222;
|
||||
@labelBorder: transparent;
|
||||
@fontColor: #dedede;
|
||||
@backgroundColor: #303030;
|
||||
@headerBackground: #606060;
|
||||
@headerBottom: rgb(19,19,19);
|
||||
@boxBorderColor: rgb(80,80,80);
|
||||
@boxShadowTLColor: rgb(100,100,100);
|
||||
@boxShadowBRColor: rgb(0,0,0);
|
||||
@boxBackgroundColor: #404040;
|
||||
@loadingColor: rgba(255,255,255,0.25);
|
||||
|
||||
@buttonUnCheckedColorInner: #808080;
|
||||
@buttonUnCheckedColorOuter: #606060;
|
||||
@buttonCheckedColorInner: #839eff;
|
||||
@buttonCheckedColorOuter: #334eb0;
|
||||
@buttonShadowTLColor: rgb(150,150,150);
|
||||
@buttonShadowBRColor: rgb(50,50,50);
|
||||
|
||||
@buttonUnCheckedColorInnerGreen: mix(@buttonUnCheckedColorInner, rgb(0,255,0), 95%);
|
||||
@buttonUnCheckedColorOuterGreen: mix(@buttonUnCheckedColorOuter, rgb(0,255,0), 95%);
|
||||
@buttonUnCheckedColorInnerRed: mix(@buttonUnCheckedColorInner, rgb(255,0,0), 95%);
|
||||
@buttonUnCheckedColorOuterRed: mix(@buttonUnCheckedColorOuter, rgb(255,0,0), 95%);
|
||||
|
||||
|
||||
@labelBackground: @buttonUnCheckedColorOuter;
|
||||
@inputBorderColor:rgb(150,150,150);
|
||||
@inputBorderColorActive:mix(@inputBorderColor, rgb(255,255,255),50%);
|
||||
|
||||
@errorColor: rgb(255,0,0);
|
|
@ -0,0 +1,114 @@
|
|||
@import "colors.less";
|
||||
@import "header.less";
|
||||
@import "fform.less";
|
||||
@import "mapping.less";
|
||||
@import "main.less";
|
||||
@import "wifi.less";
|
||||
@import "update.less";
|
||||
|
||||
html,body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
body {
|
||||
background:@backgroundColor;
|
||||
color: @fontColor;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.hsplit {
|
||||
display: flex;
|
||||
> div + div {
|
||||
margin-left:40px;
|
||||
}
|
||||
}
|
||||
.half {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display:none;
|
||||
}
|
||||
|
||||
ul.none {
|
||||
list-style-type: none;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
ul.inline li {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.errormsg {
|
||||
color:#FF0000;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading:before {
|
||||
display:inline-block;
|
||||
content: ' ';
|
||||
border-radius: 50%;
|
||||
border: .5rem solid @loadingColor;
|
||||
border-top-color: #000;
|
||||
animation: spin 1s infinite linear;
|
||||
width:16px;
|
||||
height:16px;
|
||||
}
|
||||
|
||||
.loading.smallani:before {
|
||||
margin-left:-20px;
|
||||
border-width: .3rem;
|
||||
position:absolute;
|
||||
width:8px;
|
||||
height:8px;
|
||||
}
|
||||
|
||||
a, input, button {
|
||||
outline:0;
|
||||
}
|
||||
::-moz-focus-inner {
|
||||
border:0;
|
||||
}
|
||||
#main {
|
||||
position:relative;
|
||||
padding:10px;
|
||||
}
|
||||
#main {
|
||||
h1, h2, h3 {
|
||||
font-weight:500;
|
||||
margin-left:10px;
|
||||
margin-right:10px;
|
||||
}
|
||||
}
|
||||
|
||||
#mapping {
|
||||
td {
|
||||
vertical-align:middle;
|
||||
}
|
||||
input[type=text] {
|
||||
margin-top:0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color:inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
#main {
|
||||
padding:0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
.fform {
|
||||
min-width:320px;
|
||||
background-color: @boxBackgroundColor;
|
||||
box-shadow: -5px -5px 30px @boxShadowTLColor, 5px 5px 30px @boxShadowBRColor;
|
||||
border-radius:20px;
|
||||
border:1px solid @boxBorderColor;
|
||||
margin:10px;
|
||||
padding:10px;
|
||||
position:relative;
|
||||
|
||||
.loading {
|
||||
&:before {
|
||||
width:25px;
|
||||
height:25px;
|
||||
}
|
||||
}
|
||||
input[type=text] + label,
|
||||
input[type=password] + label {
|
||||
box-sizing: border-box;
|
||||
display:block;
|
||||
position:absolute;
|
||||
pointer-events: none;
|
||||
margin: ~"calc(-1.85em + 1px) 1em 0";
|
||||
&:before {
|
||||
content: attr(placeholder);
|
||||
display: inline-block;
|
||||
color: @labelColor;
|
||||
white-space: nowrap;
|
||||
transition-property: transform, color, font-size;
|
||||
transition-duration: 0.2s;
|
||||
transition-timing-function: ease-out;
|
||||
transform-origin: left center;
|
||||
border:1px solid @labelBorder;
|
||||
border-bottom:0;
|
||||
padding:3px;
|
||||
border-radius:5px 5px 0 0;
|
||||
}
|
||||
}
|
||||
input,button,.button,select {
|
||||
box-shadow: -3px -3px 10px @buttonShadowTLColor, 3px 3px 10px @buttonShadowBRColor;
|
||||
background: radial-gradient(@buttonUnCheckedColorInner 20%, @buttonUnCheckedColorOuter 100%);
|
||||
border:1px solid @inputBorderColor;
|
||||
border-radius:1em;
|
||||
color: @labelColor;
|
||||
transition: box-shadow .5s, border .5s;
|
||||
&:hover, &:focus, &:focus-within {
|
||||
box-shadow: none;
|
||||
border:1px solid @inputBorderColorActive;
|
||||
transition: box-shadow 0.5s, border .5s;
|
||||
}
|
||||
&:checked, &:active {
|
||||
box-shadow: -5px -5px 13px @buttonShadowBRColor, 3px 3px 10px @buttonShadowTLColor;
|
||||
transition: box-shadow 0.5s;
|
||||
}
|
||||
&.green {
|
||||
background: radial-gradient(@buttonUnCheckedColorInnerGreen 20%, @buttonUnCheckedColorOuterGreen 100%);
|
||||
}
|
||||
&.red {
|
||||
background: radial-gradient(@buttonUnCheckedColorInnerRed 20%, @buttonUnCheckedColorOuterRed 100%);
|
||||
}
|
||||
}
|
||||
input[type=radio], input[type=checkbox], select {
|
||||
height: 2em;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
select {
|
||||
margin-top:-4px;
|
||||
vertical-align:top;
|
||||
padding:0 3px;
|
||||
}
|
||||
|
||||
input[type=radio], input[type=checkbox] {
|
||||
width:2em;
|
||||
&:checked {
|
||||
background: radial-gradient(@buttonCheckedColorInner 20%, @buttonCheckedColorOuter 100%);
|
||||
}
|
||||
|
||||
& + label {
|
||||
margin:0.5em 0 0 5px;
|
||||
display:inline-block;
|
||||
height:2em;
|
||||
vertical-align:top;
|
||||
}
|
||||
}
|
||||
input[type=text], input[type=password], input[type=submit], button {
|
||||
font-size: 100%;
|
||||
margin: 1.3em 0 0;
|
||||
padding: 0.2em 0.5em 0;
|
||||
height: 2em;
|
||||
box-sizing: border-box;
|
||||
width:100%;
|
||||
}
|
||||
input[type=text], input[type=password] {
|
||||
&:active {
|
||||
box-shadow:none;
|
||||
}
|
||||
}
|
||||
button {
|
||||
cursor:pointer;
|
||||
outline:none;
|
||||
}
|
||||
|
||||
label + a, label + p {
|
||||
margin-top:1.3em;
|
||||
display:inline-block;
|
||||
}
|
||||
input[type=text], input[type=password], input[type=file], select, button {
|
||||
&:focus,
|
||||
&.active,
|
||||
&:valid,
|
||||
&.invalid,
|
||||
&:disabled {
|
||||
& + label:before {
|
||||
transform: translateY(-2.0em) translateX(0.5em);
|
||||
border-color: @inputBorderColor;
|
||||
font-size:70%;
|
||||
background: @labelBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errormsg {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.successmsg {
|
||||
margin-top:1em;
|
||||
color:#009900;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
&.small, .small {
|
||||
font-size: 75%;
|
||||
input[type=checkbox] + label {
|
||||
margin:0.6em 0 0;
|
||||
}
|
||||
input[type=text], input[type=password], input[type=file], select {
|
||||
&:focus,
|
||||
&.active,
|
||||
&:valid,
|
||||
&.invalid,
|
||||
&:disabled {
|
||||
& + label:before {
|
||||
transform: translateY(-2.3em) translateX(0.5em);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
header {
|
||||
padding:0px 1em 1.5em;
|
||||
background: linear-gradient(0deg, @backgroundColor 0%, @headerBottom 1.0em, @headerBackground 1.0em 100%);
|
||||
h1 {
|
||||
display:inline-block;
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
#menu {
|
||||
display:inline-block;
|
||||
margin:0;
|
||||
li+li{
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color:inherit;
|
||||
text-decoration: none;
|
||||
&:focus, &:active {
|
||||
text-decoration: underline;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
#trigger {
|
||||
button.trigger {
|
||||
height:1.5em;
|
||||
margin-top:0;
|
||||
text-align:center;
|
||||
font-size:2em;
|
||||
img {
|
||||
margin-right:20px;
|
||||
vertical-align:top;
|
||||
height:1em;
|
||||
}
|
||||
align-items:center;
|
||||
}
|
||||
}
|
||||
#edittiming {
|
||||
> div {
|
||||
float:left;
|
||||
margin-bottom:20px;
|
||||
.outer {
|
||||
transition: font-size .1s, opacity 0.1s 0.1s;
|
||||
}
|
||||
overflow:hidden;
|
||||
.minmax {
|
||||
display:block;
|
||||
position:absolute;
|
||||
right:20px;
|
||||
width:18px;
|
||||
height:18px;
|
||||
border-radius:50%;
|
||||
background: @buttonUnCheckedColorOuter;
|
||||
|
||||
&:before {
|
||||
top:5px;
|
||||
right:1px;
|
||||
content: '';
|
||||
display: block;
|
||||
border-top: 8px solid @buttonCheckedColorInner;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position:absolute;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
}
|
||||
h3 {
|
||||
text-align:center;
|
||||
margin:0;
|
||||
font-weight:500;
|
||||
}
|
||||
input[type=text] {
|
||||
width:80px;
|
||||
}
|
||||
|
||||
button.del {
|
||||
width:40px;
|
||||
}
|
||||
.time {
|
||||
white-space:nowrap;
|
||||
}
|
||||
hr {
|
||||
margin:10px -10px -10px;
|
||||
border:0;
|
||||
border-top:1px solid rgba(0,0,0,.3);
|
||||
border-bottom:1px solid rgba(255,255,255,.3);
|
||||
}
|
||||
&.min {
|
||||
.outer {
|
||||
font-size:0;
|
||||
opacity:0;
|
||||
transition: opacity 0.1s, font-size 0.1s .1s;
|
||||
}
|
||||
.minmax:before{
|
||||
transform: rotate(90deg);
|
||||
transition:transform .2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#mainview .error {
|
||||
color:@errorColor;
|
||||
margin:25px 10px;
|
||||
}
|
||||
@media screen and (min-width: 800px) {
|
||||
#trigger {
|
||||
width:400px;
|
||||
}
|
||||
#edittiming > div {
|
||||
width:400px;
|
||||
.time br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
#edittiming {
|
||||
> div {
|
||||
width:auto;
|
||||
float:none;
|
||||
.time {
|
||||
white-space: normal;
|
||||
}
|
||||
button.del {
|
||||
display:block;
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
#mapping {
|
||||
max-width:500px;
|
||||
.pin {
|
||||
display:inline-block;
|
||||
height:2em;
|
||||
margin:0.5em 0 0;
|
||||
vertical-align:top;
|
||||
}
|
||||
.mapping {
|
||||
margin-bottom:0.5em;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
.overlay {
|
||||
position:fixed;
|
||||
z-index:128000;
|
||||
top:0;
|
||||
left:0;
|
||||
width:100%;
|
||||
height:100vh;
|
||||
background:rgba(0,0,0,0.5);
|
||||
|
||||
> div {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
margin: 12vh auto 0;
|
||||
max-width:960px;
|
||||
min-width:350px;
|
||||
min-height:300px;
|
||||
width:75%;
|
||||
height:75vh;
|
||||
background:#FFF;
|
||||
border:1px solid #cacaca;
|
||||
border-radius:5px;
|
||||
box-shadow:0 0 10px #000;
|
||||
.header {
|
||||
padding:5px;
|
||||
border-bottom:1px solid #cacaca;
|
||||
h2 {
|
||||
margin:0;
|
||||
padding:0;
|
||||
font-size:120%;
|
||||
text-align:center;
|
||||
}
|
||||
span.close {
|
||||
float:right;
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
form {
|
||||
overflow:auto;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
#update {
|
||||
> div {
|
||||
width:400px;
|
||||
}
|
||||
label {
|
||||
display:block;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
#wifi {
|
||||
margin-top:20px;
|
||||
max-width:600px;
|
||||
.hsplit {
|
||||
>div + div {
|
||||
width:400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
#wifi {
|
||||
.hsplit {
|
||||
display:block;
|
||||
>div + div {
|
||||
margin:0;
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "CameraTrigger",
|
||||
"version": "1.0.0",
|
||||
"homepage": "https://contentnation.net/grumpydevelop/",
|
||||
"author": {
|
||||
"name" : "Sascha Nitsch",
|
||||
"url" : "https://contentnation.net/grumpydevelop/"
|
||||
},
|
||||
"funding": {
|
||||
"type": "ContentNation",
|
||||
"url": "https://contentnation.net/grumpydevelop/"
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/jquery": "^3.5.16",
|
||||
"grunt": "^1.5.3",
|
||||
"grunt-cli": "^1.4.3",
|
||||
"grunt-contrib-concat": "^2.1.0",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-less": "^3.0.0",
|
||||
"grunt-contrib-uglify": "^5.2.2",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-json-merge": "^0.2.2",
|
||||
"grunt-newer": "^1.3.0",
|
||||
"grunt-ts": "^6.0.0-beta.22",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.2.1",
|
||||
"@types/mustache": "^4.2.2",
|
||||
"jquery": "^3.6.3",
|
||||
"jquery-toast-plugin": "^1.3.2",
|
||||
"mustache": "^4.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"wifimode": "ap",
|
||||
"ssid": "CameraTrigger"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"d0": ["Flash 1", "digitalout", false],
|
||||
"d1": ["Valve 1", "digitalout", false],
|
||||
"d2": ["Valve 2", "digitalout", false],
|
||||
"d3": ["trigger", "trigger", false],
|
||||
"d4": ["", "none", false],
|
||||
"d5": ["Flash 2", "digitalout", false],
|
||||
"d6": ["Camera", "irsony", false],
|
||||
"d7": ["", "none", false]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"message": "ok"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
["abc", 70, 2, ""],
|
||||
["def", 65, 3, "open"]
|
||||
]
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"error": "invalidpassword"
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Camera Trigger</title>
|
||||
<script src="jquery.min.js"></script>
|
||||
<script src="jquery.toast.min.js"></script>
|
||||
<script src="mustache.min.js"></script>
|
||||
<link rel="stylesheet" href="default.css" type="text/css" />
|
||||
<link rel="stylesheet" href="jquery.toast.min.css" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="#"><img src="cameratrigger_logo.png" alt="logo" height="24"/> Camera Trigger</a></h1>
|
||||
<ul id="menu" class="none inline"></ul>
|
||||
</header>
|
||||
<div id="main">loading ...</div>
|
||||
</body>
|
||||
<script src="bootstrap.min.js"></script>
|
||||
</html>
|
|
@ -0,0 +1,26 @@
|
|||
<script id="tplMain" type="x-tmpl-mustache">
|
||||
<form><div id="mainview">
|
||||
<div class="fform" id="trigger">
|
||||
<button class="green trigger"><img src="cameratrigger_logo.png" alt="logo"/> {{l.main.trigger}}</button>
|
||||
<button class="green save"> {{l.main.save}}</button>
|
||||
</div>
|
||||
<div class="error"></div>
|
||||
<h2>{{l.main.edittiming}}</h2>
|
||||
<div id="edittiming">
|
||||
{{#pinmapping}}
|
||||
<div class="fform" data-pin="{{pin}}">
|
||||
<a href="#" class="minmax button" data-collapse="{{pin}}"> </a>
|
||||
<h3>{{name}}</h3>
|
||||
<div class="outer"><div class="inner">
|
||||
<button class="add green">+</button>
|
||||
</div></div>
|
||||
</div>
|
||||
{{/pinmapping}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script id="tplMainTime" type="x-tmpl-mustache">
|
||||
<div class="time">{{l.main.start}}: <input type="text" name="start_{{name}}_{{index}}" value="0"/> ms {{#delta}}Δ <input type="text" name="delta_{{name}}_{{index}}" value="1"/> ms {{/delta}}<button class="del red">-</button></div>
|
||||
<hr />
|
||||
</script>
|
|
@ -0,0 +1,4 @@
|
|||
<script id="tplMenu" type="x-tmpl-mustache">
|
||||
<li><a href="#wifi">{{l.menu.setupwifi}}</a></li>
|
||||
<li><a href="#pinmapping">{{l.menu.setuppinmapping}}</a></li>
|
||||
</script>
|
|
@ -0,0 +1,23 @@
|
|||
<script id="tplPinMapping" type="x-tmpl-mustache">
|
||||
<form class="fform" id="mapping"><div>
|
||||
<h2>{{l.mapping.edittitle}}</h2>
|
||||
{{#pinmapping}}
|
||||
<div class="mapping">
|
||||
<span class="pin">{{l.mapping.pin}} {{pin}}</span>
|
||||
<span class="pin"><label for="type_{{pin}}">{{l.mapping.type}}</label> <select name="type_{{pin}}" id="type_{{pin}}">
|
||||
<option value="none">{{l.mapping.notconn}}</option>
|
||||
<option value="digitalout">{{l.mapping.digitalout}}</option>
|
||||
<option value="ircanon">{{l.mapping.ircanon}}</option>
|
||||
<option value="irnikon">{{l.mapping.irnikon}}</option>
|
||||
<option value="irsony">{{l.mapping.irsony}}</option>
|
||||
<option value="trigger">{{l.mapping.trigger}}</option>
|
||||
<option value="none">{{l.mapping.notconn}}</option>
|
||||
</select></span
|
||||
<span><input type="checkbox" name="inverted_{{pin}}" id="inverted_{{pin}}"{{#inverted}} checked="checked"{{/inverted}}/><label for="inverted_{{pin}}">{{l.mapping.inverted}}</label></span
|
||||
<span><input type="text" name="name_{{pin}}" value="{{name}}" /></span>
|
||||
</div>
|
||||
{{/pinmapping}}
|
||||
<input type="submit" value="{{l.generic.save}}" />
|
||||
</table>
|
||||
</div></form>
|
||||
</script>
|
|
@ -0,0 +1,8 @@
|
|||
<script id="tplUpdate" type="x-tmpl-mustache">
|
||||
<form action="api/upload" method="post" enctype="multipart/form-data" id="update"><div class="fform">
|
||||
<h2>{{l.update.header}}</h2>
|
||||
<label for="update">{{l.update.fileselect}}</label>
|
||||
<input type="file" name="update" /><br />
|
||||
<input type="submit" value="{{l.update.submit}}" class="green" />
|
||||
</div></form>
|
||||
</script>
|
|
@ -0,0 +1,28 @@
|
|||
<script id="tplWifi" type="x-tmpl-mustache">
|
||||
<form action="api/wifi" method="post" class="fform" id="wifi">
|
||||
<div class="hsplit">
|
||||
<div>
|
||||
<h2>{{l.wifi.accessmode}}</h2>
|
||||
<div><input type="radio" name="mode" id="ap" value="ap"><label for="ap">{{l.wifi.accesspoint}}</label></div>
|
||||
<div><input type="radio" name="mode" id="client" value="client"><label for="client">{{l.wifi.client}}</label></div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{l.wifi.accessdetails}}</h2>
|
||||
<div class="hidden" id="ssidscan">
|
||||
<button class="smallani">{{l.wifi.rescan}}</button>
|
||||
<ul class="small none"></ul>
|
||||
</div>
|
||||
<div><input type="text" name="ssid" id="ssid" value="{{ssid}}" /><label for="ssid" placeholder="{{l.wifi.ssid}}"></label></div>
|
||||
<div><input type="password" name="password" id="password" value="" /><label for="password" placeholder="{{l.wifi.password}}"></label></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<button id="save" class="smallani">{{l.generic.save}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script id="tplWifiSSID" type="x-tmpl-mustache">
|
||||
{{#ssids}}
|
||||
<li><input type="radio" name="ssidlist" id="radio{{index}}" value="{{name}}"{{#active}} checked="checked"{{/active}}><label for="radio{{index}}">{{name}} {{quality}}% {{l.wifi.channel}} {{channel}} {{l.wifi.encryptionType}} {{encryption}}</label></li>
|
||||
{{/ssids}}
|
||||
</script>
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
|
||||
class Api {
|
||||
/**
|
||||
* ajax get call
|
||||
* @param {string} path url path to call
|
||||
* @param {object} parameter parameters to pass to api call
|
||||
* @param {object} context optional parameter for setting the context the success and error function will be called
|
||||
* @param {function} success optional success callback function
|
||||
* @param {function} error optional error callback function
|
||||
*/
|
||||
static get(path: string, parameter, context, success, error) {
|
||||
jQuery.ajax({
|
||||
url : "api" + (path[0] != '/' ? '/' : '') + path,
|
||||
dataType : 'json',
|
||||
type : "GET",
|
||||
data : parameter,
|
||||
success : success,
|
||||
error : error,
|
||||
context : context
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* brief ajax post call api
|
||||
* @param {string} path url path to call
|
||||
* @param {object} parameter parameters to pass to api call
|
||||
* @param {object} context optional parameter for setting the context the success and error function will be called
|
||||
* @param {function} success optional success callback function
|
||||
* @param {function} error optional error callback function
|
||||
*/
|
||||
static post(path: string, parameter, context, success, error) {
|
||||
jQuery.ajax({
|
||||
url : "api" + (path[0] != '/' ? '/' : '') + path,
|
||||
dataType : 'json',
|
||||
type : "POST",
|
||||
data : parameter,
|
||||
processData: !(parameter instanceof FormData),
|
||||
success : success,
|
||||
error : error,
|
||||
context : context,
|
||||
contentType: parameter instanceof FormData ? false : "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* brief ajax del call api
|
||||
* @param {string} path url path to call
|
||||
* @param {object} parameter parameters to pass to api call
|
||||
* @param {object} context optional parameter for setting the context the success and error function will be called
|
||||
* @param {function} success optional success callback function
|
||||
* @param {function} error optional error callback function
|
||||
*/
|
||||
static del(path: string, context, success, error) {
|
||||
jQuery.ajax({
|
||||
url : "api" + (path[0] != '/' ? '/' : '') + path,
|
||||
dataType : 'json',
|
||||
type : "DELETE",
|
||||
success : success,
|
||||
error : error,
|
||||
context : context
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="forminfo.ts" />
|
||||
/// <reference path="loader.ts" />
|
||||
/// <reference path="view.ts" />
|
||||
/// <reference path="toast.d.ts" />
|
||||
/// <reference path="api.ts" />
|
||||
|
||||
interface Wifi {
|
||||
mode:string;
|
||||
ssid:string;
|
||||
}
|
||||
|
||||
type PinMappings = Record<string,[string, string, boolean]>;
|
||||
|
||||
class CameraTrigger {
|
||||
public form:FormInfo;
|
||||
public lang: any;
|
||||
public loader: Loader;
|
||||
public routing: any;
|
||||
public oldhash: string;
|
||||
public view: View;
|
||||
public wifi: Wifi;
|
||||
public pinMapping: PinMappings;
|
||||
|
||||
constructor() {
|
||||
this.routing = {
|
||||
"": "MainView",
|
||||
"#wifi": "WifiView",
|
||||
"#pinmapping": "PinMappingView",
|
||||
"#update": "UpdateView"
|
||||
};
|
||||
|
||||
this.loader = new Loader(this);
|
||||
this.loadConfig();
|
||||
this.loader.template("base", this, this.start, null);
|
||||
}
|
||||
|
||||
public getVersion() : string {
|
||||
return "beta1";
|
||||
}
|
||||
|
||||
/**
|
||||
* default error function
|
||||
* - disables loading indicator
|
||||
* - fills in fields with class errormsg
|
||||
* - show error toaster
|
||||
*/
|
||||
public error(jqXHR, textStatus, errorThrown) {
|
||||
var error="error";
|
||||
if (jqXHR.responseJSON) {
|
||||
error = jqXHR.responseJSON.error;
|
||||
} else {
|
||||
if (errorThrown in app.lang.error) {
|
||||
error = app.lang.error[errorThrown];
|
||||
} else {
|
||||
error = errorThrown;
|
||||
}
|
||||
}
|
||||
if (this.form) {
|
||||
$(this.form.target).removeClass("loading");
|
||||
$(this.form.target).find('.errormsg').text(error);
|
||||
}
|
||||
$.toast({
|
||||
text: error,
|
||||
position: 'top-right',
|
||||
icon: 'error'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* default success function
|
||||
* - disables loading indicator
|
||||
* - fills in fields with class successmsg
|
||||
* - show toaster
|
||||
*/
|
||||
public success(jqXHR, textStatus, errorThrown) {
|
||||
var message="ok";
|
||||
if (jqXHR.responseJSON) {
|
||||
message = jqXHR.responseJSON.message;
|
||||
if (message in app.lang.message) {
|
||||
message = app.lang.message[message];
|
||||
}
|
||||
}
|
||||
if (this.form) {
|
||||
$(this.form.target).removeClass("loading");
|
||||
$(this.form.target).find('.successmsg').text(message);
|
||||
}
|
||||
$.toast({
|
||||
text: message,
|
||||
position: 'top-right',
|
||||
icon: 'success'
|
||||
});
|
||||
};
|
||||
|
||||
loadConfig() {
|
||||
Api.get("config.json", {}, this, this.applyConfig, this.error);
|
||||
}
|
||||
|
||||
applyConfig(config) {
|
||||
// load language file
|
||||
this.loader.addLang("en", "main", this, this.start, this.error);
|
||||
this.wifi = {mode: config.wifimode, ssid: config.ssid};
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.lang || $("#tplMenu").length == 0) {
|
||||
// try again the next time
|
||||
return;
|
||||
}
|
||||
// render menu
|
||||
var template = $("#tplMenu").html();
|
||||
var data:any = {};
|
||||
data.l = this.lang;
|
||||
var html = Mustache.render(template, data);
|
||||
$("#menu").prepend(html);
|
||||
$(window).on('hashchange', this, this.doRouting);
|
||||
this.doRouting();
|
||||
}
|
||||
|
||||
doRouting(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.data.doRouting();
|
||||
return false;
|
||||
}
|
||||
var hash = document.location.hash;
|
||||
if (hash != this.oldhash && this.view) {
|
||||
this.view.finish();
|
||||
}
|
||||
this. oldhash = hash;
|
||||
var className = this.routing[hash];
|
||||
if (className) {
|
||||
this.newClass(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a new instance of given classname with optional construction parameter
|
||||
* - loads class if not yet known, filename: js/lowerCaseClassName.min.js
|
||||
* - creates instance of class
|
||||
* @param {any} classname class name
|
||||
* @param {object} parameter optional parameter for new instance
|
||||
* @private
|
||||
*/
|
||||
newClass(classname: any, parameter?: object) : any {
|
||||
if (typeof classname === "object") {
|
||||
let newclass: any = window[classname.name];
|
||||
new newclass(this, classname.parameter);
|
||||
return;
|
||||
}
|
||||
if (!window[classname]) {
|
||||
this.loader.js(classname.toLowerCase(), this, this.newClass, {
|
||||
name : classname,
|
||||
parameter : parameter
|
||||
});
|
||||
return;
|
||||
}
|
||||
let newclass: any = window[classname];
|
||||
new newclass(this, parameter);
|
||||
};
|
||||
|
||||
getPinMapping(context: any, callback: any) {
|
||||
Api.get("pinmapping.json",{}, {t:this, context:context, callback:callback}, this.gotPinMapping, this.error);
|
||||
}
|
||||
|
||||
gotPinMapping(this:any, data: any) {
|
||||
var self = this.t;
|
||||
var cbcontext = this.context;
|
||||
var callback = this.callback;
|
||||
self.pinMapping = data;
|
||||
if (callback) {
|
||||
callback.call(cbcontext);
|
||||
}
|
||||
}
|
||||
|
||||
setPinMapping(data: PinMappings) {
|
||||
this.pinMapping = data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var app: CameraTrigger;
|
||||
$(document).ready(function() {
|
||||
app = new CameraTrigger();
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
class FormInfo {
|
||||
public enctype: string;
|
||||
public target: JQuery.Node;
|
||||
public path: string;
|
||||
public parentElement: JQuery<HTMLElement>;
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
|
||||
// IE stuff
|
||||
interface HTMLElement {
|
||||
readyState: any;
|
||||
onreadystatechange: any;
|
||||
}
|
||||
|
||||
class Loader {
|
||||
private loading: object;
|
||||
private app: CameraTrigger;
|
||||
|
||||
constructor(app: CameraTrigger) {
|
||||
this.loading = {};
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* load css
|
||||
* @param {string} file file to load
|
||||
* - checks for duplicates
|
||||
* - file name: /css/ + file + .css?v=version
|
||||
*/
|
||||
css(file: string) {
|
||||
if (!document.getElementById("css_" + file)) {
|
||||
var link = document.createElement('link');
|
||||
link.setAttribute("rel", "stylesheet");
|
||||
link.setAttribute("type", "text/css");
|
||||
link.setAttribute("id", "css_" + file);
|
||||
if (file[0] === "/") {
|
||||
link.setAttribute("href", file);
|
||||
} else {
|
||||
link.setAttribute("href", "css/" + file + ".css?v=" + this.app.getVersion());
|
||||
}
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* gerneic callback wrapper supporting App scope or optional other scope
|
||||
* @param {object} scope to use (undefined or false uses App instance)
|
||||
* @param {Function} funct function to call
|
||||
* @param {object} parameter paramter for funct
|
||||
* @private
|
||||
*/
|
||||
private callback(scope: object, funct: Function, parameter: object) {
|
||||
if (!scope) {
|
||||
funct.call(this, parameter);
|
||||
} else {
|
||||
funct.call(scope, parameter);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* load javascript
|
||||
* @param {string} file file to load
|
||||
* @param {object} scope scope for callback
|
||||
* @param {Function} callback callback function
|
||||
* @param {object} parameter parameter for callback
|
||||
* - checks for duplicates
|
||||
* - file name: /js/ + file + .min.js?v=version
|
||||
*/
|
||||
js(file: string, scope?: object, callback?: Function, parameter?: object) : boolean {
|
||||
if (this.loading[file]) {
|
||||
this.loading[file].push([scope,callback, parameter]);
|
||||
return true;
|
||||
}
|
||||
if (!document.getElementById("script_" + file.replace("/","_"))) {
|
||||
var script:HTMLElement = document.createElement('script');
|
||||
if (file[0] === "/") {
|
||||
script.setAttribute("src", file);
|
||||
} else {
|
||||
script.setAttribute("src", file + ".min.js?v=" + this.app.getVersion());
|
||||
}
|
||||
if (!callback) {
|
||||
callback = function() {
|
||||
script.setAttribute("id", "script_" + file);
|
||||
// console.log("trigger", "scriptload." + file);
|
||||
$(document).trigger("scriptload." + file);
|
||||
};
|
||||
}
|
||||
this.loading[file] = [[scope,callback, parameter]];
|
||||
if (callback) {
|
||||
if (script.readyState) { // IE
|
||||
script.onreadystatechange = function() {
|
||||
if (script.readyState === "loaded" || script.readyState === "complete") {
|
||||
script.onreadystatechange = null;
|
||||
script.setAttribute("id", "script_" + file.replace("/","_"));
|
||||
this.callbackList(file);
|
||||
delete this.loading[file];
|
||||
}
|
||||
}.bind(this);
|
||||
} else { // Others
|
||||
script.onload = function() {
|
||||
script.setAttribute("id", "script_" + file.replace("/","_"));
|
||||
this.callbackList(file);
|
||||
delete this.loading[file];
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
document.getElementsByTagName("head")[0].appendChild(script);
|
||||
return true;
|
||||
}
|
||||
if (callback) {
|
||||
this.callback(scope, callback, parameter);
|
||||
} else {
|
||||
$(document).trigger("scriptload." + file);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* calls multiple callsbacks if a file has been loaded
|
||||
* @param {string} file file that has been loaded
|
||||
* @private
|
||||
*/
|
||||
callbackList(file: string) {
|
||||
for (var i = 0; i < this.loading[file].length; ++i) {
|
||||
var c = this.loading[file][i];
|
||||
if (c[0]) {
|
||||
this.callback(c[0], c[1], c[2]);
|
||||
}
|
||||
}
|
||||
$(document).trigger("scriptload." + file);
|
||||
};
|
||||
|
||||
/**
|
||||
* add translation file to global translation object
|
||||
* @param {string} language
|
||||
* @param {string} group language group
|
||||
* @param {object} scope scope for callback function
|
||||
* @param {Function} callback callback function
|
||||
* @param {object} parameter parameter for callback
|
||||
*/
|
||||
addLang(lang: string, group: string, scope?: object, callback?:Function, parameter?: object) {
|
||||
if (this.app.lang && this.app.lang[group]) {
|
||||
if (callback) {
|
||||
this.callback(scope, callback, parameter);
|
||||
}
|
||||
} else {
|
||||
jQuery.ajax({
|
||||
url : lang + "_" + group + ".json?v=" + this.app.getVersion(),
|
||||
dataType : 'json',
|
||||
type : "GET",
|
||||
data : parameter,
|
||||
success : function(a) {
|
||||
this.app.lang = jQuery.extend(this.app.lang, a);
|
||||
if (callback) {
|
||||
this.callback(scope, callback, parameter);
|
||||
}
|
||||
},
|
||||
error : this.app.error,
|
||||
context : this
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
template(file: string, scope: object, callback: Function, param?: object) {
|
||||
jQuery.ajax({
|
||||
url: file+'.mst?v=' + this.app.getVersion(),
|
||||
dataType: 'text',
|
||||
context: this,
|
||||
success: function(template) {
|
||||
$("body").append(template);
|
||||
if (callback) {
|
||||
this.callback(scope, callback, param);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="view.ts" />
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
|
||||
class MainView extends View {
|
||||
|
||||
constructor(app: CameraTrigger) {
|
||||
super(app);
|
||||
if (!this.app.pinMapping) {
|
||||
this.app.getPinMapping(this, this.render);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
}
|
||||
|
||||
render() {
|
||||
var template = $("#tplMain").html();
|
||||
var data:any = {};
|
||||
data.l = this.app.lang;
|
||||
var keys = Object.keys(this.app.pinMapping);
|
||||
data.pinmapping = [];
|
||||
keys.forEach(function(key){
|
||||
var pin = this.app.pinMapping[key];
|
||||
if (pin[1] !== "none" && pin[1] !== "trigger") {
|
||||
data.pinmapping.push({pin: key, name: pin[0]});
|
||||
}
|
||||
});
|
||||
var html = Mustache.render(template, data);
|
||||
this.root.html(html);
|
||||
this.root.find("button.add").on("click", this, this.addTime);
|
||||
this.root.on("click","button.del", this, this.delTime);
|
||||
this.root.find(".minmax").on("click", this, this.toggleExpansion);
|
||||
this.root.find("#trigger button.trigger").on("click", this, this.trigger);
|
||||
this.root.find("#trigger button.save").on("click", this, this.save);
|
||||
}
|
||||
|
||||
addTime(event: JQuery.TriggeredEvent) {
|
||||
event.preventDefault();
|
||||
var target = $(event.currentTarget);
|
||||
var template = $("#tplMainTime").html();
|
||||
var data:any = {};
|
||||
var fform = target.parents(".fform");
|
||||
data.name = fform.data("pin");
|
||||
data.index = fform.find(".time").length;
|
||||
data.delta = event.data.app.pinMapping[data.name][1]==="digitalout";
|
||||
data.l = event.data.app.lang;
|
||||
var html = Mustache.render(template, data);
|
||||
target.before(html);
|
||||
}
|
||||
|
||||
delTime(event: JQuery.TriggeredEvent) {
|
||||
event.preventDefault();
|
||||
var target = $(event.currentTarget).parent();
|
||||
target.remove();
|
||||
}
|
||||
|
||||
toggleExpansion(event: JQuery.TriggeredEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
var form = $(event.currentTarget).parents("div.fform")
|
||||
form.toggleClass("min");
|
||||
return false;
|
||||
}
|
||||
|
||||
trigger(event: JQuery.TriggeredEvent) {
|
||||
event.preventDefault();
|
||||
event.data.realTrigger(true)
|
||||
}
|
||||
|
||||
save(event: JQuery.TriggeredEvent) {
|
||||
event.preventDefault();
|
||||
event.data.realTrigger(false)
|
||||
}
|
||||
|
||||
realTrigger(alsoTrigger: boolean) {
|
||||
// get list of active pins
|
||||
var times={};
|
||||
var inputs = this.root.find("form").serializeArray();
|
||||
for (var i = 0 ; i < inputs.length; ++i) {
|
||||
var tokens = inputs[i].name.split("_");
|
||||
var type = tokens[0]
|
||||
var pin = parseInt(tokens[1].substr(1));
|
||||
var time = parseInt(inputs[i].value);
|
||||
if (type === "delta") {
|
||||
time += parseInt(inputs[i-1].value);
|
||||
}
|
||||
if (!times[time]) {
|
||||
times[time] = [];
|
||||
}
|
||||
var start = type === "start";
|
||||
if (this.app.pinMapping[tokens[1]][2]) // inverted
|
||||
start = !start;
|
||||
switch (this.app.pinMapping[tokens[1]][1]) { // type
|
||||
case "digitalout":
|
||||
times[time].push((start ? "E" : "D") + pin);
|
||||
break;
|
||||
case "ircanon":
|
||||
times[time].push("C" + pin);
|
||||
break;
|
||||
case "irnikon":
|
||||
times[time].push("N" + pin);
|
||||
break;
|
||||
case "irsony":
|
||||
times[time].push("S" + pin);
|
||||
break;
|
||||
}
|
||||
}
|
||||
var keys = Object.keys(times).sort(function(a,b){var A=parseInt(a);var B=parseInt(b);return A<B ? -1 : ((A==B) ? 0 : 1);});
|
||||
var cmdString = "";
|
||||
var curTime = 0;
|
||||
var needDelay = 0;
|
||||
this.root.find(".error").html("");
|
||||
for (var j = 0; j < keys.length ; ++j) {
|
||||
var diff = parseInt(keys[j]) - curTime;
|
||||
if (needDelay > diff) {
|
||||
var err = this.app.lang.main.needpause.replace("$1", needDelay);
|
||||
$.toast({
|
||||
text: err,
|
||||
position: 'top-right',
|
||||
icon: 'error'
|
||||
});
|
||||
this.root.find(".error").html(err);
|
||||
return;
|
||||
}
|
||||
if (diff>0) {
|
||||
cmdString += "d" + diff + ";";
|
||||
}
|
||||
needDelay = 0;
|
||||
times[keys[j]].forEach(function(a) {
|
||||
if (a[0] === 'C')
|
||||
needDelay = 10;
|
||||
else if (a[0] === 'N')
|
||||
needDelay = 136;
|
||||
else if (a[0] === 'S')
|
||||
needDelay = 39;
|
||||
});
|
||||
cmdString += times[keys[j]].join(";") + ";";
|
||||
curTime = parseInt(keys[j]);
|
||||
}
|
||||
//check for trigger
|
||||
keys = Object.keys(this.app.pinMapping)
|
||||
for (var k = 0; k < keys.length ; ++k) {
|
||||
if (this.app.pinMapping[keys[k]][1] === "trigger") {
|
||||
cmdString += "T" + keys[k].substr(1) + ";";
|
||||
}
|
||||
console.log(this.app.pinMapping[keys[k]]);
|
||||
}
|
||||
Api.post("/run", {"cmd": cmdString, "trigger": alsoTrigger}, this, this.app.success, this.app.error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="view.ts" />
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
class PinMappingView extends View {
|
||||
public form: any;
|
||||
constructor(app: CameraTrigger) {
|
||||
super(app);
|
||||
if (!this.app.pinMapping) {
|
||||
this.app.getPinMapping(this, this.render);
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.root.find("input[type=submit]").off("click", this.saveEdit);
|
||||
}
|
||||
|
||||
render() {
|
||||
var template = $("#tplPinMapping").html();
|
||||
var data:any = {};
|
||||
data.l = this.app.lang;
|
||||
data.pinmapping = [];
|
||||
var keys = Object.keys(this.app.pinMapping);
|
||||
keys.forEach(function(key){
|
||||
var pin = this.app.pinMapping[key];
|
||||
data.pinmapping.push({pin: key, name: pin[0], type: pin[1], inverted: pin[2]});
|
||||
});
|
||||
var html = Mustache.render(template, data);
|
||||
this.root.html(html);
|
||||
this.root.find("input[type=submit]").on("click", this, this.saveEdit);
|
||||
// set select boxes
|
||||
for (var i = 0; i < 8; ++i) {
|
||||
this.root.find("select[name=type_d" + i + "]").val(this.app.pinMapping["d" + i][1]);
|
||||
}
|
||||
}
|
||||
|
||||
saveEdit(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.preventDefault();
|
||||
event.data.saveEdit();
|
||||
return;
|
||||
}
|
||||
var data = {};
|
||||
var keys = Object.keys(this.app.pinMapping);
|
||||
var root = this.root;
|
||||
keys.forEach(function(key){
|
||||
var pin = this.app.pinMapping[key];
|
||||
data[key] = [root.find("input[name=name_" + key + "]").val(), root.find("#type_" + key).val(), root.find("input[name=inverted_" + key + "]").prop("checked")===true];
|
||||
});
|
||||
Api.post("pinmapping", JSON.stringify(data), this, this.success, this.app.error);
|
||||
}
|
||||
|
||||
success(data: PinMappings) {
|
||||
this.app.setPinMapping(data);
|
||||
$.toast({
|
||||
text: this.app.lang.generic.savesuccess,
|
||||
position: 'top-right',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
|
||||
interface JQueryStatic {
|
||||
toast(options?: any, callback?: Function) : any;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="view.ts" />
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
class UpdateView extends View {
|
||||
public form: any;
|
||||
constructor(app: CameraTrigger) {
|
||||
super(app);
|
||||
this.render();
|
||||
}
|
||||
|
||||
finish() {
|
||||
}
|
||||
|
||||
render() {
|
||||
var template = $("#tplUpdate").html();
|
||||
var data:any = {};
|
||||
data.l = this.app.lang;
|
||||
var html = Mustache.render(template, data);
|
||||
this.root.html(html);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
class View {
|
||||
protected app: CameraTrigger;
|
||||
protected root:JQuery<HTMLElement>;
|
||||
constructor(app: CameraTrigger) {
|
||||
this.app = app;
|
||||
app.view = this;
|
||||
this.root = $("#main");
|
||||
this.root.empty();
|
||||
}
|
||||
|
||||
finish() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Sascha Nitsch (@grumpydevelop@contentnation.net) https://contentnation.net/en/grumpydevelop
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
/// <reference path="view.ts" />
|
||||
/// <reference path="cameratrigger.ts" />
|
||||
interface SSID {
|
||||
name: string;
|
||||
quality: number;
|
||||
active: boolean;
|
||||
channel: number;
|
||||
encryption: string;
|
||||
index: number;
|
||||
}
|
||||
class WifiView extends View {
|
||||
private ssidlist: Array<SSID>;
|
||||
public form: any;
|
||||
constructor(app: CameraTrigger) {
|
||||
super(app);
|
||||
this.render();
|
||||
this.ssidlist = [];
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.root.off("change", "input[name=mode]");
|
||||
this.root.find("#ssidscan button").off("click");
|
||||
this.root.find("#ssidscan ul").off("change", "input[name=ssidlist]");
|
||||
this.root.find("#save").off("click");
|
||||
}
|
||||
|
||||
render() {
|
||||
var template = $("#tplWifi").html();
|
||||
var data:any = {};
|
||||
data.l = this.app.lang;
|
||||
data.ssid = this.app.wifi.ssid;
|
||||
var html = Mustache.render(template, data);
|
||||
this.root.html(html);
|
||||
var active = $("#" + this.app.wifi.mode);
|
||||
active.prop("checked", true);
|
||||
this.root.on("change", "input[name=mode]", this, this.modechange);
|
||||
this.root.find("#ssidscan button").on("click", this, this.loadSSIDList);
|
||||
this.root.find("#save").on("click", this, this.save);
|
||||
this.modechange();
|
||||
}
|
||||
|
||||
modechange(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.data.modechange();
|
||||
return false;
|
||||
}
|
||||
|
||||
var mode = this.root.find("input[name=mode]:checked").val();
|
||||
var sync = this.root.find("#ssidscan");
|
||||
var ssid = this.root.find("#ssid");
|
||||
if (mode == "client") {
|
||||
// show sync button and optionally load first list
|
||||
sync.removeClass("hidden");
|
||||
ssid.attr("disabled","disabled");
|
||||
if (!this.ssidlist || this.ssidlist.length == 0) {
|
||||
this.loadSSIDList();
|
||||
}
|
||||
} else {
|
||||
// hide sync button
|
||||
sync.addClass("hidden");
|
||||
ssid.removeAttr("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
loadSSIDList(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.preventDefault();
|
||||
event.data.loadSSIDList()
|
||||
return false;
|
||||
}
|
||||
var button = this.root.find("#ssidscan button");
|
||||
button.addClass("loading");
|
||||
Api.get("ssid.json", {}, this, this.ssidListReceived, this.app.error);
|
||||
}
|
||||
|
||||
ssidListReceived(listdata) {
|
||||
var i;
|
||||
this.ssidlist = [];
|
||||
var button = this.root.find("#ssidscan button");
|
||||
button.removeClass("loading");
|
||||
var existing = this.root.find("#ssid").val();
|
||||
for (i=0; i < listdata.length; ++i) {
|
||||
this.ssidlist.push({name:listdata[i][0], quality: -listdata[i][1], active: listdata[i][0] === existing, channel:listdata[i][2], encryption: this.app.lang.wifi["encryption" + listdata[i][3]], index:i});
|
||||
}
|
||||
var template = $("#tplWifiSSID").html();
|
||||
var data:any = {};
|
||||
data.l = this.app.lang;
|
||||
data.ssids = this.ssidlist;
|
||||
var html = Mustache.render(template, data);
|
||||
var list = this.root.find("#ssidscan ul");
|
||||
list.html(html);
|
||||
list.on("change", "input[name=ssidlist]", this, this.selectSSID);
|
||||
}
|
||||
|
||||
selectSSID(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.data.selectSSID();
|
||||
return false;
|
||||
}
|
||||
var ssid = this.root.find("#ssidscan input[name=ssidlist]:checked").val();
|
||||
this.root.find("#ssid").val(ssid);
|
||||
}
|
||||
|
||||
save(event?: JQuery.TriggeredEvent) {
|
||||
if (event && event.data) {
|
||||
event.preventDefault();
|
||||
event.data.save();
|
||||
return false;
|
||||
}
|
||||
var save = this.root.find("#save");
|
||||
save.addClass("loading");
|
||||
this.form = {target: save[0]};
|
||||
var data = {
|
||||
ssid: this.root.find("input[name=ssid]").val(),
|
||||
mode: this.root.find("input[name=mode]:checked").val(),
|
||||
password: this.root.find("input[name=password]").val()
|
||||
};
|
||||
Api.post("wifi",data, this, this.saved, this.app.error);
|
||||
}
|
||||
|
||||
saved(response: any) {
|
||||
this.root.find("#save").removeClass("loading");
|
||||
if (response.error) {
|
||||
$.toast({
|
||||
text: this.app.lang.error[response.error],
|
||||
position: 'top-right',
|
||||
icon: 'error'
|
||||
});
|
||||
} else {
|
||||
$.toast({
|
||||
text: this.app.lang.generic.savesuccess,
|
||||
position: 'top-right',
|
||||
icon: 'success'
|
||||
});
|
||||
window.setTimeout(function(){document.location.href="http://" + response.redirect;}, 3000);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue