Add Arduino rewrite of firmware
This commit is contained in:
parent
52bb9fbf9c
commit
5fdc005613
69
.github/workflows/arduino.yaml
vendored
Executable file
69
.github/workflows/arduino.yaml
vendored
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
name: arduino
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
permissions: write-all
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arduino-platform: ["esp8266:esp8266"]
|
||||||
|
include:
|
||||||
|
- arduino-platform: "esp8266:esp8266"
|
||||||
|
fqbn: "esp8266:esp8266:generic"
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Arduino CLI
|
||||||
|
uses: arduino/setup-arduino-cli@v1
|
||||||
|
|
||||||
|
- name: Install platform
|
||||||
|
run: >
|
||||||
|
arduino-cli core install
|
||||||
|
--additional-urls=http://arduino.esp8266.com/stable/package_esp8266com_index.json
|
||||||
|
${{ matrix.arduino-platform }}
|
||||||
|
|
||||||
|
- name: Install time lib
|
||||||
|
run: arduino-cli lib install time wifimanager ezTime
|
||||||
|
|
||||||
|
- name: Make timezones
|
||||||
|
run: python firmware/cities.py > firmware/cities.h
|
||||||
|
|
||||||
|
- name: Compile Sketch
|
||||||
|
run: >
|
||||||
|
arduino-cli compile --fqbn ${{ matrix.fqbn }} -e
|
||||||
|
firmware
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: Release ${{ github.ref }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
- name: Upload Release Asset
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./firmware/build/esp8266.esp8266.generic/firmware.ino.bin
|
||||||
|
asset_name: nixiesp12.bin
|
||||||
|
asset_content_type: application/bin
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,9 +3,8 @@ _autosave*
|
|||||||
export
|
export
|
||||||
*.kicad_pcb-bak
|
*.kicad_pcb-bak
|
||||||
*rescue.lib
|
*rescue.lib
|
||||||
firmware/*.bin
|
|
||||||
*.drl
|
*.drl
|
||||||
*.g*
|
*.gcode
|
||||||
*.ps
|
*.ps
|
||||||
*.zip
|
*.zip
|
||||||
fp-info-cache
|
fp-info-cache
|
||||||
|
2
firmware/.gitignore
vendored
Normal file
2
firmware/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
build
|
||||||
|
cities.h
|
@ -1,23 +1,21 @@
|
|||||||
NAME=esp8266-1m-20210618-v1.16.bin
|
SKETCH_FOLDER := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||||
|
UPLOAD_PORT ?= /dev/ttyUSB0
|
||||||
|
|
||||||
flash:
|
all: $(SKETCH_FOLDER)/build/firmware.ino.bin
|
||||||
wget -c http://micropython.org/resources/firmware/${NAME}
|
|
||||||
esptool.py -p /dev/ttyUSB0 write_flash --flash_size=1MB 0 ${NAME}
|
|
||||||
|
|
||||||
erase:
|
$(SKETCH_FOLDER)/cities.h: cities.py
|
||||||
esptool.py -p /dev/ttyUSB0 erase_flash
|
python3 cities.py > $(SKETCH_FOLDER)/cities.h
|
||||||
|
|
||||||
upload:
|
$(SKETCH_FOLDER)/build/firmware.ino.bin: $(SKETCH_FOLDER)/firmware.ino $(SKETCH_FOLDER)/cities.h
|
||||||
ampy -p /dev/ttyUSB0 put picoweb.py
|
arduino-cli compile -e -b esp8266:esp8266:generic $(SKETCH_FOLDER)
|
||||||
ampy -p /dev/ttyUSB0 put timezone.py
|
|
||||||
ampy -p /dev/ttyUSB0 put main.py
|
deps:
|
||||||
|
arduino-cli core install \
|
||||||
|
--additional-urls=http://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266
|
||||||
|
arduino-cli lib install wifimanager ESP8266TimerInterrupt ezTime
|
||||||
|
|
||||||
|
flash: $(SKETCH_FOLDER)/firmware.ino.bin
|
||||||
|
arduino-cli upload -b esp8266:esp8266:generic -p $(UPLOAD_PORT) $(SKETCH_FOLDER)
|
||||||
|
|
||||||
console:
|
console:
|
||||||
echo "Ctrl-A + Ctrl-Q to close Picocom"
|
picocom -b 9600 $(UPLOAD_PORT)
|
||||||
picocom -b115200 /dev/ttyUSB0
|
|
||||||
|
|
||||||
clone_read:
|
|
||||||
esptool.py -p /dev/ttyUSB0 read_flash 0 0x100000 clone.bin
|
|
||||||
|
|
||||||
clone_write:
|
|
||||||
esptool.py -p /dev/ttyUSB0 write_flash --flash_size=1MB 0 clone.bin
|
|
||||||
|
42
firmware/cities.py
Normal file
42
firmware/cities.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import requests
|
||||||
|
import csv
|
||||||
|
|
||||||
|
# Looks like ESP-IDF pulls in whole POSIX stack
|
||||||
|
# Some forums mention ESP-IDF uses this CSV
|
||||||
|
|
||||||
|
coords = {}
|
||||||
|
|
||||||
|
r = requests.get("https://gist.githubusercontent.com/erdem/8c7d26765831d0f9a8c62f02782ae00d/raw/248037cd701af0a4957cce340dabb0fd04e38f4c/countries.json")
|
||||||
|
data = r.json()
|
||||||
|
for j in data:
|
||||||
|
for tz in j["timezones"]:
|
||||||
|
coords[tz] = j["latlng"]
|
||||||
|
|
||||||
|
|
||||||
|
r = requests.get("https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv")
|
||||||
|
cr = csv.reader(r.text.splitlines(), delimiter=',', quotechar='"')
|
||||||
|
|
||||||
|
print("""const char cities[] = R"(
|
||||||
|
<br/>
|
||||||
|
<label for="city">Nearest city</label>
|
||||||
|
<select name="city" id="city" onchange="
|
||||||
|
document.getElementById('timezone').value = this.value;
|
||||||
|
document.getElementById('long').value = this.options[this.selectedIndex].dataset.long;
|
||||||
|
document.getElementById('lat').value = this.options[this.selectedIndex].dataset.lat;
|
||||||
|
">""")
|
||||||
|
for name, code in cr:
|
||||||
|
if name.startswith("Etc/"):
|
||||||
|
continue
|
||||||
|
if not name.startswith("Europe/"):
|
||||||
|
continue
|
||||||
|
longlat = coords.get(name)
|
||||||
|
selected = name == "Europe/Tallinn"
|
||||||
|
print("""<option value="%s" data-long="%s" data-lat="%s"%s>%s</option>""" % (
|
||||||
|
code,
|
||||||
|
int(longlat[0]) if longlat else "",
|
||||||
|
int(longlat[1]) if longlat else "",
|
||||||
|
" selected" if selected else "",
|
||||||
|
name))
|
||||||
|
print("""</select>
|
||||||
|
)";
|
||||||
|
""")
|
511
firmware/firmware.ino
Normal file
511
firmware/firmware.ino
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
/*
|
||||||
|
https://github.com/laurivosandi/nixiesp12/blob/master/firmware/main.py
|
||||||
|
https://randomnerdtutorials.com/wifimanager-with-esp8266-autoconnect-custom-parameter-and-manage-your-ssid-and-password/
|
||||||
|
*/
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <WiFiManager.h>
|
||||||
|
#include <ezTime.h>
|
||||||
|
#include "cities.h"
|
||||||
|
|
||||||
|
// #define DEBUG 1
|
||||||
|
// #define DIMMING_ENABLED 1
|
||||||
|
// #define TEST_SEQUENCE 1
|
||||||
|
|
||||||
|
#define PIN_CLOCK 3
|
||||||
|
#define PIN_DATA 2
|
||||||
|
#define PIN_LATCH 0
|
||||||
|
|
||||||
|
#define SUNRISE 6
|
||||||
|
#define SUNSET 22
|
||||||
|
|
||||||
|
// ezTime structs
|
||||||
|
tmElements_t tm;
|
||||||
|
Timezone local;
|
||||||
|
|
||||||
|
int configDimmingDutyCycle = 0;
|
||||||
|
|
||||||
|
int current_dimming_duty_cycle;
|
||||||
|
enum typeOperationMode {
|
||||||
|
OPERATION_MODE_NORMAL,
|
||||||
|
OPERATION_MODE_DIMMED,
|
||||||
|
} operationModeCurrent = OPERATION_MODE_NORMAL;
|
||||||
|
|
||||||
|
#define DISPLAY_MODE_TIME 1
|
||||||
|
#define DISPLAY_MODE_DATE 2
|
||||||
|
#define DISPLAY_MODE_DATETIME 3
|
||||||
|
|
||||||
|
int configDisplayModesEnabled = 1;
|
||||||
|
int displayModeCurrent = 1;
|
||||||
|
|
||||||
|
WiFiManager wm;
|
||||||
|
|
||||||
|
const char displayModesCombobox[] = R"(
|
||||||
|
<br/>
|
||||||
|
<label for="displayModeCombobox">Clock display format</label>
|
||||||
|
<select name="timeDisplay" id="displayModeCombobox" onchange="document.getElementById('displayMode').value = this.value">
|
||||||
|
<option value="1">Time</option>
|
||||||
|
<option value="2">Date</option>
|
||||||
|
<option value="3">Time and Date</option>
|
||||||
|
</select>
|
||||||
|
<script>
|
||||||
|
document.getElementById("displayModeCombobox").value = document.getElementById("displayMode").value;
|
||||||
|
document.querySelector("[for='displayMode']").hidden = true;
|
||||||
|
document.getElementById("displayMode").hidden = true;
|
||||||
|
</script>)";
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
const char dimmerSliderSnippet[] = R"(
|
||||||
|
<br/><label for='dimming_duty_cycle_slider'>Night time dimming</label>
|
||||||
|
<input type="range" min="1" max="3840" value="1000" class="slider" id="dimming_duty_cycle_slider" onchange="document.getElementById('dimming_duty_cycle').value = this.value">
|
||||||
|
<script>
|
||||||
|
document.getElementById('dimming_duty_cycle').hidden = true;
|
||||||
|
</script>
|
||||||
|
)";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFiManagerParameter paramNetworkTimeServer("networkTimeServer", "Network time server", "ee.pool.ntp.org", 63);
|
||||||
|
WiFiManagerParameter paramDisplayMode("displayMode", "Will be hidden", "1", 2);
|
||||||
|
WiFiManagerParameter paramDisplayModeCombobox(displayModesCombobox);
|
||||||
|
WiFiManagerParameter paramCity(cities);
|
||||||
|
WiFiManagerParameter paramTimezone("timezone", "Timezone encoding", "EET-2EEST,M3.5.0/3,M10.5.0/4", 30);
|
||||||
|
//WiFiManagerParameter paramLong("long", "Longitude", "26", 10);
|
||||||
|
//WiFiManagerParameter paramLat("lat", "Latitude", "59", 10);
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
WiFiManagerParameter paramDimmingDutyCycle("dimming_duty_cycle", "", "1000", 4);
|
||||||
|
WiFiManagerParameter paramDimmingDutyCycleSlider(dimmerSliderSnippet);
|
||||||
|
|
||||||
|
// Dimmer settings
|
||||||
|
volatile long displayInterruptCount = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
volatile bool blink = true;
|
||||||
|
int lookup[] = {11, 9, 12, 8, 0, 4, 1, 3, 2, 10};
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR bitbang_bit(int value){
|
||||||
|
if(value & 1){
|
||||||
|
digitalWrite(PIN_DATA, HIGH);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
digitalWrite(PIN_DATA, LOW);
|
||||||
|
}
|
||||||
|
digitalWrite(PIN_CLOCK, HIGH);
|
||||||
|
digitalWrite(PIN_CLOCK, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR bitbang_digit(int digit){
|
||||||
|
int i = 0;
|
||||||
|
if (!blink && timeStatus() != timeSet) {
|
||||||
|
for(i=0;i<4;i++){
|
||||||
|
bitbang_bit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for(i=0;i<4;i++){
|
||||||
|
bitbang_bit(lookup[digit] << i >> 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR renderTest(int j) {
|
||||||
|
for(int i=0; i<6; i++){
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(j);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
}
|
||||||
|
digitalWrite(PIN_LATCH, HIGH);
|
||||||
|
digitalWrite(PIN_LATCH, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR renderTime(){
|
||||||
|
int hour = tm.Hour;
|
||||||
|
int minute = tm.Minute;
|
||||||
|
int second = tm.Second;
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(hour / 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(hour % 10);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_digit(minute / 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(minute % 10);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_bit(blink);
|
||||||
|
|
||||||
|
bitbang_bit(blink);
|
||||||
|
bitbang_digit(second / 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(second % 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR renderDate(){
|
||||||
|
int day = tm.Day;
|
||||||
|
int month = tm.Month;
|
||||||
|
int year = tm.Year-30;
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit((year) / 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit((year) % 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(1);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(month/ 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(month % 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(1);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(day / 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_digit(day % 10);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR renderDisplay() {
|
||||||
|
switch (configDisplayModesEnabled) {
|
||||||
|
case 1:
|
||||||
|
renderTime();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
renderDate();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if (millis() % 30000 < 15000) {
|
||||||
|
renderTime();
|
||||||
|
} else {
|
||||||
|
renderDate();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
digitalWrite(PIN_LATCH, HIGH);
|
||||||
|
digitalWrite(PIN_LATCH, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR clearDisplay() {
|
||||||
|
for(int i=0; i<6; i++){
|
||||||
|
bitbang_bit(1);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(0);
|
||||||
|
bitbang_bit(1);
|
||||||
|
bitbang_bit(1);
|
||||||
|
bitbang_bit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
digitalWrite(PIN_LATCH, HIGH);
|
||||||
|
digitalWrite(PIN_LATCH, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
struct timeval tv;
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
void ICACHE_RAM_ATTR dimmerTimerCallback() {
|
||||||
|
noInterrupts();
|
||||||
|
displayInterruptCount++;
|
||||||
|
gettimeofday(&tv, NULL);
|
||||||
|
blink = tv.tv_usec < 500000;
|
||||||
|
int j = current_dimming_duty_cycle;
|
||||||
|
if (j < configDimmingDutyCycle) {
|
||||||
|
j = configDimmingDutyCycle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (counter == 0) {
|
||||||
|
renderDisplay();
|
||||||
|
timer1_write(j+1);
|
||||||
|
// counter = 1;
|
||||||
|
// } else if (counter == 1) {
|
||||||
|
// clearDisplay();
|
||||||
|
// timer1_write(3840-j+1);
|
||||||
|
// counter = 0;
|
||||||
|
// }
|
||||||
|
interrupts();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int loadClockConfig() {
|
||||||
|
|
||||||
|
unsigned char timezone[30] = {'\0'};
|
||||||
|
File file = LittleFS.open("/timezone", "r");
|
||||||
|
if (!file) { return 1; }
|
||||||
|
if (!file.read(timezone, sizeof(timezone))) { return 2; }
|
||||||
|
Serial.print("Using timezone: ");
|
||||||
|
Serial.println((const char*)timezone);
|
||||||
|
|
||||||
|
unsigned char timeserver[63] = {'\0'};
|
||||||
|
file = LittleFS.open("/timeserver", "r");
|
||||||
|
if (!file) { return 3; }
|
||||||
|
if (!file.read(timeserver, sizeof(timeserver))) { return 4; }
|
||||||
|
Serial.print("Using time server: ");
|
||||||
|
Serial.println((const char*)timeserver);
|
||||||
|
|
||||||
|
setServer((const char*)timeserver);
|
||||||
|
local.setPosix((const char*)timezone);
|
||||||
|
|
||||||
|
unsigned char modes[1] = {'\0'};
|
||||||
|
file = LittleFS.open("/modes", "r");
|
||||||
|
if (!file) { return 5; }
|
||||||
|
if (!file.read(modes, sizeof(modes))) { return 6; }
|
||||||
|
configDisplayModesEnabled = atoi((const char*)modes);
|
||||||
|
Serial.print("Enabled display modes:");
|
||||||
|
if (configDisplayModesEnabled & DISPLAY_MODE_DATE) {
|
||||||
|
Serial.print(" DATE");
|
||||||
|
}
|
||||||
|
if (configDisplayModesEnabled & DISPLAY_MODE_TIME) {
|
||||||
|
Serial.print(" TIME");
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
unsigned char bufDimming[10] = {'\0'};
|
||||||
|
file = LittleFS.open("/dimming", "r");
|
||||||
|
if (!file) { return 7; }
|
||||||
|
if (!file.read(bufDimming, sizeof(bufDimming))) { return 8; }
|
||||||
|
configDimmingDutyCycle = atoi((const char*)bufDimming);
|
||||||
|
Serial.print("Night time dimming duty cycle: ");
|
||||||
|
Serial.println((const char*)bufDimming);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveParamsCallback() {
|
||||||
|
File file = LittleFS.open("/timeserver", "w");
|
||||||
|
file.print(paramNetworkTimeServer.getValue());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
file = LittleFS.open("/timezone", "w");
|
||||||
|
file.print(paramTimezone.getValue());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
file = LittleFS.open("/dimming", "w");
|
||||||
|
file.print(paramDimmingDutyCycle.getValue());
|
||||||
|
file.close();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
file = LittleFS.open("/modes", "w");
|
||||||
|
file.print(paramDisplayMode.getValue());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
loadClockConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleMetrics(){
|
||||||
|
char tbuf[30];
|
||||||
|
|
||||||
|
String buf = "";
|
||||||
|
|
||||||
|
buf += "nixie_sketch_size_bytes ";
|
||||||
|
buf += ESP.getSketchSize();
|
||||||
|
buf += "\n";
|
||||||
|
|
||||||
|
buf += "nixie_flash_space_bytes ";
|
||||||
|
buf += ESP.getFlashChipRealSize();
|
||||||
|
buf += "\n";
|
||||||
|
|
||||||
|
buf += "nixie_free_heap_bytes ";
|
||||||
|
buf += ESP.getFreeHeap();
|
||||||
|
buf += "\n";
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
buf += "nixie_display_interrupt_count ";
|
||||||
|
buf += displayInterruptCount;
|
||||||
|
buf += "\n";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
buf += "nixie_ntp_last_sync_timestamp_seconds ";
|
||||||
|
buf += lastNtpUpdateTime();
|
||||||
|
buf += "\n";
|
||||||
|
|
||||||
|
wm.server->send(200, "text/plain", buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializePins() {
|
||||||
|
pinMode(PIN_CLOCK, OUTPUT);
|
||||||
|
pinMode(PIN_LATCH, OUTPUT);
|
||||||
|
pinMode(PIN_DATA, OUTPUT);
|
||||||
|
digitalWrite(PIN_CLOCK, LOW);
|
||||||
|
digitalWrite(PIN_LATCH, LOW);
|
||||||
|
digitalWrite(PIN_DATA, LOW);
|
||||||
|
clearDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(9600);
|
||||||
|
Serial.println("Nixie clock booting up");
|
||||||
|
initializePins();
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
setDebug(INFO);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef TEST_SEQUENCE
|
||||||
|
for(int i = 0; i < 10; i++) {
|
||||||
|
renderTest(i);
|
||||||
|
delay(3000);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
wm.setDebugOutput(true);
|
||||||
|
wm.setMinimumSignalQuality(50);
|
||||||
|
|
||||||
|
if (!LittleFS.begin()) {
|
||||||
|
Serial.println("LittleFS mount failed");
|
||||||
|
} else {
|
||||||
|
if(loadClockConfig() != 0) {
|
||||||
|
Serial.println("Failed to load clock configuration from LittleFS");
|
||||||
|
} else {
|
||||||
|
Serial.println("Configuration loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wm.addParameter(¶mNetworkTimeServer);
|
||||||
|
wm.addParameter(¶mCity);
|
||||||
|
wm.addParameter(¶mTimezone);
|
||||||
|
//wm.addParameter(¶mLong);
|
||||||
|
//wm.addParameter(¶mLat);
|
||||||
|
wm.addParameter(¶mDisplayMode);
|
||||||
|
wm.addParameter(¶mDisplayModeCombobox);
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
wm.addParameter(¶mDimmingDutyCycle);
|
||||||
|
wm.addParameter(¶mDimmingDutyCycleSlider);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
wm.setSaveParamsCallback(saveParamsCallback);
|
||||||
|
wm.setShowInfoUpdate(false); // https://github.com/tzapu/WiFiManager/issues/1262
|
||||||
|
wm.setShowInfoErase(false);
|
||||||
|
wm.setConfigPortalBlocking(false);
|
||||||
|
|
||||||
|
Serial.println("Autostarting wireless");
|
||||||
|
|
||||||
|
wm.autoConnect();
|
||||||
|
|
||||||
|
Serial.println("Starting config portal");
|
||||||
|
|
||||||
|
|
||||||
|
wm.startConfigPortal();
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
wm.server->on("/metrics", handleMetrics);
|
||||||
|
#else
|
||||||
|
wm.setDebugOutput(false);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
renderNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void renderNormal() {
|
||||||
|
gettimeofday(&tv, NULL);
|
||||||
|
blink = tv.tv_usec < 500000;
|
||||||
|
breakTime(local.now(), tm);
|
||||||
|
|
||||||
|
renderDisplay();
|
||||||
|
delay((500 - (local.ms(LAST_READ) % 500)) + 1);
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.print(1970+tm.Year);
|
||||||
|
Serial.print("-");
|
||||||
|
Serial.print(tm.Month);
|
||||||
|
Serial.print("-");
|
||||||
|
Serial.print(tm.Day);
|
||||||
|
Serial.print(" ");
|
||||||
|
Serial.print(tm.Hour);
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(tm.Minute);
|
||||||
|
Serial.print(":");
|
||||||
|
Serial.print(tm.Second);
|
||||||
|
Serial.print(".");
|
||||||
|
Serial.println(local.ms(LAST_READ));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
wm.process();
|
||||||
|
events(); // this invokes yield()
|
||||||
|
|
||||||
|
#ifdef DIMMING_ENABLED
|
||||||
|
switch (operationModeCurrent) {
|
||||||
|
case OPERATION_MODE_NORMAL:
|
||||||
|
|
||||||
|
if ((tm.Hour < SUNRISE || tm.Hour > SUNSET) && timeStatus() == timeSet) {
|
||||||
|
operationModeCurrent = OPERATION_MODE_DIMMED;
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.println("Clock synchronized, disabling wireless, enabling dimming");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
|
||||||
|
timer1_attachInterrupt(dimmerTimerCallback);
|
||||||
|
timer1_isr_init();
|
||||||
|
timer1_enable(TIM_DIV256, TIM_EDGE, TIM_SINGLE);
|
||||||
|
timer1_write(100);
|
||||||
|
} else {
|
||||||
|
renderNormal();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OPERATION_MODE_DIMMED:
|
||||||
|
if (tm.Hour > SUNRISE && tm.Hour < SUNSET) {
|
||||||
|
operationModeCurrent = OPERATION_MODE_NORMAL;
|
||||||
|
timer1_detachInterrupt();
|
||||||
|
timer1_disable();
|
||||||
|
|
||||||
|
#ifdef DEBUG
|
||||||
|
Serial.println("Disabling dimming");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
} else if (tm.Hour == SUNRISE) {
|
||||||
|
current_dimming_duty_cycle = tm.Minute << 6;
|
||||||
|
} else if (tm.Hour == SUNSET) {
|
||||||
|
current_dimming_duty_cycle = (61 - tm.Minute) << 6;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
renderNormal();
|
||||||
|
#endif
|
||||||
|
}
|
187
firmware/main.py
187
firmware/main.py
@ -1,187 +0,0 @@
|
|||||||
import gc
|
|
||||||
import network
|
|
||||||
import picoweb
|
|
||||||
import json
|
|
||||||
from time import sleep_ms
|
|
||||||
from timezone import TIMEZONES
|
|
||||||
|
|
||||||
app = picoweb.WebApp(__name__)
|
|
||||||
ap_if = network.WLAN(network.AP_IF)
|
|
||||||
sta_if = network.WLAN(network.STA_IF)
|
|
||||||
sta_if.active(True)
|
|
||||||
nets = sta_if.scan()
|
|
||||||
config = dict()
|
|
||||||
try:
|
|
||||||
with open("config.json") as fh:
|
|
||||||
config = json.loads(fh.read())
|
|
||||||
sta_if.connect(config.get("ssid"), config.get("password"))
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("Scanning for wireless networks...")
|
|
||||||
|
|
||||||
@app.route("/connect")
|
|
||||||
def index(req, resp):
|
|
||||||
if req.method == "POST":
|
|
||||||
yield from req.read_form_data()
|
|
||||||
else:
|
|
||||||
req.parse_qs()
|
|
||||||
yield from picoweb.start_response(resp)
|
|
||||||
with open("config.json", "w") as fh:
|
|
||||||
fh.write(json.dumps(req.form))
|
|
||||||
yield from resp.awrite("Setting saved please power cycle device")
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def index(req, resp):
|
|
||||||
print("Serving index")
|
|
||||||
yield from picoweb.start_response(resp)
|
|
||||||
yield from resp.awrite("<html>")
|
|
||||||
yield from resp.awrite("<head>")
|
|
||||||
yield from resp.awrite("<meta name='viewport' content='width=device-width, initial-scale=1'>")
|
|
||||||
yield from resp.awrite("</head>")
|
|
||||||
yield from resp.awrite("<body>")
|
|
||||||
yield from resp.awrite("<h>Welcome to NixiESP12 configuration wizard</h>")
|
|
||||||
yield from resp.awrite("<p>Detected wireless networks:</p>")
|
|
||||||
yield from resp.awrite("<form action='/connect' method='post'>")
|
|
||||||
yield from resp.awrite("<p>Select wireless network:</p>")
|
|
||||||
yield from resp.awrite("<select name='ssid'>")
|
|
||||||
for ssid, _, _, snr, crypto, _ in nets:
|
|
||||||
ssid = ssid.decode("utf-8")
|
|
||||||
yield from resp.awrite("<option>" + ssid + "</option>")
|
|
||||||
yield from resp.awrite("</select>")
|
|
||||||
yield from resp.awrite("<p>Wireless password is applicable:</p>")
|
|
||||||
yield from resp.awrite("<input type='password' name='password'/>")
|
|
||||||
yield from resp.awrite("<p>Timezone:</p>")
|
|
||||||
yield from resp.awrite("<select name='timezone'>")
|
|
||||||
for index, (dst, offset, title) in enumerate(TIMEZONES):
|
|
||||||
yield from resp.awrite("<option value='%d'>%s</option>" % (index, title))
|
|
||||||
yield from resp.awrite("</select>")
|
|
||||||
yield from resp.awrite("<p>NTP resynchronization interval:</p>")
|
|
||||||
yield from resp.awrite("<select name='interval'>")
|
|
||||||
for j in range(0, 73):
|
|
||||||
yield from resp.awrite("<option value='%d'>%d hours</option>" % (j*3600, j))
|
|
||||||
yield from resp.awrite("</select>")
|
|
||||||
yield from resp.awrite("<p> </p>")
|
|
||||||
yield from resp.awrite("<input type='submit'/>")
|
|
||||||
yield from resp.awrite("</form>")
|
|
||||||
yield from resp.awrite("</body>")
|
|
||||||
yield from resp.awrite("<html>")
|
|
||||||
|
|
||||||
timed_out = True
|
|
||||||
if config:
|
|
||||||
print("Connecting to", config.get("ssid"))
|
|
||||||
for j in range(0,30):
|
|
||||||
if sta_if.isconnected():
|
|
||||||
ap_if.active(False)
|
|
||||||
timed_out = False
|
|
||||||
break
|
|
||||||
sleep_ms(200)
|
|
||||||
|
|
||||||
if timed_out:
|
|
||||||
ap_if.active(True)
|
|
||||||
print("Starting setup wizard on", ap_if.ifconfig())
|
|
||||||
app.run()
|
|
||||||
|
|
||||||
|
|
||||||
TIMEZONE = TIMEZONES[int(config.get("timezone", 30))][1]
|
|
||||||
print("Using timezone", TIMEZONES[int(config.get("timezone", 30))])
|
|
||||||
RESYNC = int(config.get("interval")) # Resync once in 8 hours
|
|
||||||
print("NTP resynchronization interval", RESYNC, "seconds")
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
print("Press Ctrl-C now to abort main.py execution and retain keyboard input")
|
|
||||||
sleep_ms(2000)
|
|
||||||
|
|
||||||
import time
|
|
||||||
import ntptime
|
|
||||||
from machine import Pin, Timer
|
|
||||||
|
|
||||||
# Note that keyboard input is lost beyond this point!
|
|
||||||
clock = Pin(3, mode=Pin.OUT)
|
|
||||||
latch = Pin(0, mode=Pin.OUT)
|
|
||||||
data = Pin(2, mode=Pin.OUT)
|
|
||||||
blink = False
|
|
||||||
lookup = 11, 9, 12, 8, 0, 4, 1, 3, 2, 10
|
|
||||||
countdown = 0
|
|
||||||
|
|
||||||
def bitbang_bit(value):
|
|
||||||
if value & 1:
|
|
||||||
data.on()
|
|
||||||
else:
|
|
||||||
data.off()
|
|
||||||
clock.on()
|
|
||||||
clock.off()
|
|
||||||
|
|
||||||
def bitbang_digit(digit):
|
|
||||||
bitbang_bit(blink)
|
|
||||||
for i in range(0,4):
|
|
||||||
bitbang_bit(lookup[digit] << i >> 3)
|
|
||||||
bitbang_bit(blink)
|
|
||||||
bitbang_bit(blink)
|
|
||||||
bitbang_bit(blink)
|
|
||||||
|
|
||||||
def dst_offset(month, day, dow):
|
|
||||||
if month < 3 or month > 10:
|
|
||||||
return 0
|
|
||||||
if month > 3 and month < 10:
|
|
||||||
return 1
|
|
||||||
previous_sunday = day - dow
|
|
||||||
if month == 3:
|
|
||||||
return int(previous_sunday >= 25)
|
|
||||||
return int(previous_sunday < 25)
|
|
||||||
|
|
||||||
|
|
||||||
def dump_time(year, month, day, hour, minute, second, dow):
|
|
||||||
offset = dst_offset(month, day, dow)
|
|
||||||
if DEBUG:
|
|
||||||
print("Time is %02d:%02d:%02d, dst offset %d" % (hour, minute, second, offset))
|
|
||||||
hour = (hour + TIMEZONE + offset) % 24
|
|
||||||
bitbang_digit(hour // 10)
|
|
||||||
bitbang_digit(hour % 10)
|
|
||||||
bitbang_digit(minute // 10)
|
|
||||||
bitbang_digit(minute % 10)
|
|
||||||
bitbang_digit(second // 10)
|
|
||||||
bitbang_digit(second % 10)
|
|
||||||
|
|
||||||
# RTC accuracy is still garbage, time.ticks_ms() which is bound to CPU ticks seems to be more accurate
|
|
||||||
# https://forum.micropython.org/viewtopic.php?t=3251#p19092
|
|
||||||
|
|
||||||
|
|
||||||
# Boot up test sequence
|
|
||||||
for j in range(0, 10):
|
|
||||||
for i in range(0, 6):
|
|
||||||
bitbang_digit(j)
|
|
||||||
latch.on()
|
|
||||||
latch.off()
|
|
||||||
sleep_ms(500)
|
|
||||||
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if countdown <= 0:
|
|
||||||
try:
|
|
||||||
ticks_then, time_then = time.ticks_ms(), ntptime.time()
|
|
||||||
except OSError:
|
|
||||||
sleep_ms(500)
|
|
||||||
print("Resync failed")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
countdown = RESYNC
|
|
||||||
print("Resync done")
|
|
||||||
else:
|
|
||||||
year, month, day, hour, minute, second, dow, _ = time.localtime(time_then + (time.ticks_ms() - ticks_then) // 1000)
|
|
||||||
sleep_ms(500-(time.ticks_ms() - ticks_then) % 1000)
|
|
||||||
blink = True
|
|
||||||
dump_time(year, month, day, hour, minute, second, dow)
|
|
||||||
latch.on()
|
|
||||||
latch.off()
|
|
||||||
countdown -= 1
|
|
||||||
|
|
||||||
year, month, day, hour, minute, second, dow, _ = time.localtime(time_then + (time.ticks_ms() - ticks_then) // 1000)
|
|
||||||
sleep_ms(1001-(time.ticks_ms() - ticks_then) % 1000)
|
|
||||||
blink = False
|
|
||||||
dump_time(year, month, day, hour, minute, second, dow)
|
|
||||||
latch.on()
|
|
||||||
latch.off()
|
|
||||||
|
|
||||||
main()
|
|
@ -1,279 +0,0 @@
|
|||||||
# Picoweb web pico-framework for MicroPython
|
|
||||||
# Copyright (c) 2014-2018 Paul Sokolovsky
|
|
||||||
# SPDX-License-Identifier: MIT
|
|
||||||
import sys
|
|
||||||
import gc
|
|
||||||
import micropython
|
|
||||||
import utime
|
|
||||||
import uio
|
|
||||||
import ure as re
|
|
||||||
import uerrno
|
|
||||||
import uasyncio as asyncio
|
|
||||||
|
|
||||||
def unquote_plus(s):
|
|
||||||
# TODO: optimize
|
|
||||||
s = s.replace("+", " ")
|
|
||||||
arr = s.split("%")
|
|
||||||
arr2 = [chr(int(x[:2], 16)) + x[2:] for x in arr[1:]]
|
|
||||||
return arr[0] + "".join(arr2)
|
|
||||||
|
|
||||||
def parse_qs(s):
|
|
||||||
res = {}
|
|
||||||
if s:
|
|
||||||
pairs = s.split("&")
|
|
||||||
for p in pairs:
|
|
||||||
vals = [unquote_plus(x) for x in p.split("=", 1)]
|
|
||||||
if len(vals) == 1:
|
|
||||||
vals.append(True)
|
|
||||||
old = res.get(vals[0])
|
|
||||||
if old is not None:
|
|
||||||
if not isinstance(old, list):
|
|
||||||
old = [old]
|
|
||||||
res[vals[0]] = old
|
|
||||||
old.append(vals[1])
|
|
||||||
else:
|
|
||||||
res[vals[0]] = vals[1]
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_mime_type(fname):
|
|
||||||
# Provide minimal detection of important file
|
|
||||||
# types to keep browsers happy
|
|
||||||
if fname.endswith(".html"):
|
|
||||||
return "text/html"
|
|
||||||
if fname.endswith(".css"):
|
|
||||||
return "text/css"
|
|
||||||
if fname.endswith(".png") or fname.endswith(".jpg"):
|
|
||||||
return "image"
|
|
||||||
return "text/plain"
|
|
||||||
|
|
||||||
def sendstream(writer, f):
|
|
||||||
buf = bytearray(64)
|
|
||||||
while True:
|
|
||||||
l = f.readinto(buf)
|
|
||||||
if not l:
|
|
||||||
break
|
|
||||||
yield from writer.awrite(buf, 0, l)
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify(writer, dict):
|
|
||||||
import ujson
|
|
||||||
yield from start_response(writer, "application/json")
|
|
||||||
yield from writer.awrite(ujson.dumps(dict))
|
|
||||||
|
|
||||||
def start_response(writer, content_type="text/html", status="200", headers=None):
|
|
||||||
yield from writer.awrite("HTTP/1.0 %s NA\r\n" % status)
|
|
||||||
yield from writer.awrite("Content-Type: ")
|
|
||||||
yield from writer.awrite(content_type)
|
|
||||||
if not headers:
|
|
||||||
yield from writer.awrite("\r\n\r\n")
|
|
||||||
return
|
|
||||||
yield from writer.awrite("\r\n")
|
|
||||||
if isinstance(headers, bytes) or isinstance(headers, str):
|
|
||||||
yield from writer.awrite(headers)
|
|
||||||
else:
|
|
||||||
for k, v in headers.items():
|
|
||||||
yield from writer.awrite(k)
|
|
||||||
yield from writer.awrite(": ")
|
|
||||||
yield from writer.awrite(v)
|
|
||||||
yield from writer.awrite("\r\n")
|
|
||||||
yield from writer.awrite("\r\n")
|
|
||||||
|
|
||||||
def http_error(writer, status):
|
|
||||||
yield from start_response(writer, status=status)
|
|
||||||
yield from writer.awrite(status)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPRequest:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read_form_data(self):
|
|
||||||
size = int(self.headers[b"Content-Length"])
|
|
||||||
data = yield from self.reader.read(size)
|
|
||||||
form = parse_qs(data.decode())
|
|
||||||
self.form = form
|
|
||||||
|
|
||||||
def parse_qs(self):
|
|
||||||
form = parse_qs(self.qs)
|
|
||||||
self.form = form
|
|
||||||
|
|
||||||
|
|
||||||
class WebApp:
|
|
||||||
|
|
||||||
def __init__(self, pkg, routes=None):
|
|
||||||
if routes:
|
|
||||||
self.url_map = routes
|
|
||||||
else:
|
|
||||||
self.url_map = []
|
|
||||||
if pkg and pkg != "__main__":
|
|
||||||
self.pkg = pkg.split(".", 1)[0]
|
|
||||||
else:
|
|
||||||
self.pkg = None
|
|
||||||
self.mounts = []
|
|
||||||
self.inited = False
|
|
||||||
# Instantiated lazily
|
|
||||||
self.template_loader = None
|
|
||||||
self.headers_mode = "parse"
|
|
||||||
|
|
||||||
def parse_headers(self, reader):
|
|
||||||
headers = {}
|
|
||||||
while True:
|
|
||||||
l = yield from reader.readline()
|
|
||||||
if l == b"\r\n":
|
|
||||||
break
|
|
||||||
k, v = l.split(b":", 1)
|
|
||||||
headers[k] = v.strip()
|
|
||||||
return headers
|
|
||||||
|
|
||||||
def _handle(self, reader, writer):
|
|
||||||
close = True
|
|
||||||
try:
|
|
||||||
request_line = yield from reader.readline()
|
|
||||||
if request_line == b"":
|
|
||||||
yield from writer.aclose()
|
|
||||||
return
|
|
||||||
req = HTTPRequest()
|
|
||||||
# TODO: bytes vs str
|
|
||||||
request_line = request_line.decode()
|
|
||||||
method, path, proto = request_line.split()
|
|
||||||
path = path.split("?", 1)
|
|
||||||
qs = ""
|
|
||||||
if len(path) > 1:
|
|
||||||
qs = path[1]
|
|
||||||
path = path[0]
|
|
||||||
|
|
||||||
#print("================")
|
|
||||||
#print(req, writer)
|
|
||||||
#print(req, (method, path, qs, proto), req.headers)
|
|
||||||
|
|
||||||
# Find which mounted subapp (if any) should handle this request
|
|
||||||
app = self
|
|
||||||
while True:
|
|
||||||
found = False
|
|
||||||
for subapp in app.mounts:
|
|
||||||
root = subapp.url
|
|
||||||
#print(path, "vs", root)
|
|
||||||
if path[:len(root)] == root:
|
|
||||||
app = subapp
|
|
||||||
found = True
|
|
||||||
path = path[len(root):]
|
|
||||||
if not path.startswith("/"):
|
|
||||||
path = "/" + path
|
|
||||||
break
|
|
||||||
if not found:
|
|
||||||
break
|
|
||||||
|
|
||||||
# We initialize apps on demand, when they really get requests
|
|
||||||
if not app.inited:
|
|
||||||
app.init()
|
|
||||||
|
|
||||||
# Find handler to serve this request in app's url_map
|
|
||||||
found = False
|
|
||||||
for e in app.url_map:
|
|
||||||
pattern = e[0]
|
|
||||||
handler = e[1]
|
|
||||||
extra = {}
|
|
||||||
if len(e) > 2:
|
|
||||||
extra = e[2]
|
|
||||||
|
|
||||||
if path == pattern:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
elif not isinstance(pattern, str):
|
|
||||||
# Anything which is non-string assumed to be a ducktype
|
|
||||||
# pattern matcher, whose .match() method is called. (Note:
|
|
||||||
# Django uses .search() instead, but .match() is more
|
|
||||||
# efficient and we're not exactly compatible with Django
|
|
||||||
# URL matching anyway.)
|
|
||||||
m = pattern.match(path)
|
|
||||||
if m:
|
|
||||||
req.url_match = m
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not found:
|
|
||||||
headers_mode = "skip"
|
|
||||||
else:
|
|
||||||
headers_mode = extra.get("headers", self.headers_mode)
|
|
||||||
|
|
||||||
if headers_mode == "skip":
|
|
||||||
while True:
|
|
||||||
l = yield from reader.readline()
|
|
||||||
if l == b"\r\n":
|
|
||||||
break
|
|
||||||
elif headers_mode == "parse":
|
|
||||||
req.headers = yield from self.parse_headers(reader)
|
|
||||||
else:
|
|
||||||
assert headers_mode == "leave"
|
|
||||||
|
|
||||||
if found:
|
|
||||||
req.method = method
|
|
||||||
req.path = path
|
|
||||||
req.qs = qs
|
|
||||||
req.reader = reader
|
|
||||||
close = yield from handler(req, writer)
|
|
||||||
else:
|
|
||||||
yield from start_response(writer, status="404")
|
|
||||||
yield from writer.awrite("404\r\n")
|
|
||||||
#print(req, "After response write")
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if close is not False:
|
|
||||||
yield from writer.aclose()
|
|
||||||
|
|
||||||
def mount(self, url, app):
|
|
||||||
"Mount a sub-app at the url of current app."
|
|
||||||
# Inspired by Bottle. It might seem that dispatching to
|
|
||||||
# subapps would rather be handled by normal routes, but
|
|
||||||
# arguably, that's less efficient. Taking into account
|
|
||||||
# that paradigmatically there's difference between handing
|
|
||||||
# an action and delegating responisibilities to another
|
|
||||||
# app, Bottle's way was followed.
|
|
||||||
app.url = url
|
|
||||||
self.mounts.append(app)
|
|
||||||
|
|
||||||
def route(self, url, **kwargs):
|
|
||||||
def _route(f):
|
|
||||||
self.url_map.append((url, f, kwargs))
|
|
||||||
return f
|
|
||||||
return _route
|
|
||||||
|
|
||||||
def add_url_rule(self, url, func, **kwargs):
|
|
||||||
# Note: this method skips Flask's "endpoint" argument,
|
|
||||||
# because it's alleged bloat.
|
|
||||||
self.url_map.append((url, func, kwargs))
|
|
||||||
|
|
||||||
def _load_template(self, tmpl_name):
|
|
||||||
if self.template_loader is None:
|
|
||||||
import utemplate.source
|
|
||||||
self.template_loader = utemplate.source.Loader(self.pkg, "templates")
|
|
||||||
return self.template_loader.load(tmpl_name)
|
|
||||||
|
|
||||||
def render_template(self, writer, tmpl_name, args=()):
|
|
||||||
tmpl = self._load_template(tmpl_name)
|
|
||||||
for s in tmpl(*args):
|
|
||||||
yield from writer.awrite(s)
|
|
||||||
|
|
||||||
def render_str(self, tmpl_name, args=()):
|
|
||||||
#TODO: bloat
|
|
||||||
tmpl = self._load_template(tmpl_name)
|
|
||||||
return ''.join(tmpl(*args))
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
"""Initialize a web application. This is for overriding by subclasses.
|
|
||||||
This is good place to connect to/initialize a database, for example."""
|
|
||||||
self.inited = True
|
|
||||||
|
|
||||||
def run(self, host="0.0.0.0", port=80, lazy_init=False):
|
|
||||||
gc.collect()
|
|
||||||
self.init()
|
|
||||||
if not lazy_init:
|
|
||||||
for app in self.mounts:
|
|
||||||
app.init()
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.create_task(asyncio.start_server(self._handle, host, port))
|
|
||||||
loop.run_forever()
|
|
||||||
loop.close()
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
|||||||
TIMEZONES = (
|
|
||||||
(0, -12, "(GMT-12:00) International Date Line West"),
|
|
||||||
(0, -11, "(GMT-11:00) Midway Island, Samoa"),
|
|
||||||
(0, -10, "(GMT-10:00) Hawaii"),
|
|
||||||
(1, -9, "(GMT-09:00) Alaska"),
|
|
||||||
(1, -8, "(GMT-08:00) Pacific Time (US & Canada)"),
|
|
||||||
(1, -8, "(GMT-08:00) Tijuana, Baja California"),
|
|
||||||
(0, -7, "(GMT-07:00) Arizona"),
|
|
||||||
(1, -7, "(GMT-07:00) Chihuahua, La Paz, Mazatlan"),
|
|
||||||
(1, -7, "(GMT-07:00) Mountain Time (US & Canada)"),
|
|
||||||
(0, -6, "(GMT-06:00) Central America"),
|
|
||||||
(1, -6, "(GMT-06:00) Central Time (US & Canada)"),
|
|
||||||
(1, -6, "(GMT-06:00) Guadalajara, Mexico City, Monterrey"),
|
|
||||||
(0, -6, "(GMT-06:00) Saskatchewan"),
|
|
||||||
(0, -5, "(GMT-05:00) Bogota, Lima, Quito, Rio Branco"),
|
|
||||||
(1, -5, "(GMT-05:00) Eastern Time (US & Canada)"),
|
|
||||||
(1, -5, "(GMT-05:00) Indiana (East)"),
|
|
||||||
(1, -4, "(GMT-04:00) Atlantic Time (Canada)"),
|
|
||||||
(0, -4, "(GMT-04:00) Caracas, La Paz"),
|
|
||||||
(0, -4, "(GMT-04:00) Manaus"),
|
|
||||||
(1, -4, "(GMT-04:00) Santiago"),
|
|
||||||
(1, -3, "(GMT-03:00) Brasilia"),
|
|
||||||
(0, -3, "(GMT-03:00) Buenos Aires, Georgetown"),
|
|
||||||
(1, -3, "(GMT-03:00) Greenland"),
|
|
||||||
(1, -3, "(GMT-03:00) Montevideo"),
|
|
||||||
(1, -2, "(GMT-02:00) Mid-Atlantic"),
|
|
||||||
(0, -1, "(GMT-01:00) Cape Verde Is."),
|
|
||||||
(1, -1, "(GMT-01:00) Azores"),
|
|
||||||
(0, 0, "(GMT+00:00) Casablanca, Monrovia, Reykjavik"),
|
|
||||||
(1, 0, "(GMT+00:00) Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London"),
|
|
||||||
(1, 1, "(GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"),
|
|
||||||
(1, 1, "(GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague"),
|
|
||||||
(1, 1, "(GMT+01:00) Brussels, Copenhagen, Madrid, Paris"),
|
|
||||||
(1, 1, "(GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb"),
|
|
||||||
(1, 1, "(GMT+01:00) West Central Africa"),
|
|
||||||
(1, 2, "(GMT+02:00) Amman"),
|
|
||||||
(1, 2, "(GMT+02:00) Athens, Bucharest, Istanbul"),
|
|
||||||
(1, 2, "(GMT+02:00) Beirut"),
|
|
||||||
(1, 2, "(GMT+02:00) Cairo"),
|
|
||||||
(0, 2, "(GMT+02:00) Harare, Pretoria"),
|
|
||||||
(1, 2, "(GMT+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"),
|
|
||||||
(1, 2, "(GMT+02:00) Jerusalem"),
|
|
||||||
(1, 2, "(GMT+02:00) Minsk"),
|
|
||||||
(1, 2, "(GMT+02:00) Windhoek"),
|
|
||||||
(0, 3, "(GMT+03:00) Kuwait, Riyadh, Baghdad"),
|
|
||||||
(1, 3, "(GMT+03:00) Moscow, St. Petersburg, Volgograd"),
|
|
||||||
(0, 3, "(GMT+03:00) Nairobi"),
|
|
||||||
(0, 3, "(GMT+03:00) Tbilisi"),
|
|
||||||
(0, 4, "(GMT+04:00) Abu Dhabi, Muscat"),
|
|
||||||
(1, 4, "(GMT+04:00) Baku"),
|
|
||||||
(1, 4, "(GMT+04:00) Yerevan"),
|
|
||||||
(1, 5, "(GMT+05:00) Yekaterinburg"),
|
|
||||||
(0, 5, "(GMT+05:00) Islamabad, Karachi, Tashkent"),
|
|
||||||
(1, 6, "(GMT+06:00) Almaty, Novosibirsk"),
|
|
||||||
(0, 6, "(GMT+06:00) Astana, Dhaka"),
|
|
||||||
(0, 7, "(GMT+07:00) Bangkok, Hanoi, Jakarta"),
|
|
||||||
(1, 7, "(GMT+07:00) Krasnoyarsk"),
|
|
||||||
(0, 8, "(GMT+08:00) Beijing, Chongqing, Hong Kong, Urumqi"),
|
|
||||||
(0, 8, "(GMT+08:00) Kuala Lumpur, Singapore"),
|
|
||||||
(0, 8, "(GMT+08:00) Irkutsk, Ulaan Bataar"),
|
|
||||||
(0, 8, "(GMT+08:00) Perth"),
|
|
||||||
(0, 8, "(GMT+08:00) Taipei"),
|
|
||||||
(0, 9, "(GMT+09:00) Osaka, Sapporo, Tokyo"),
|
|
||||||
(0, 9, "(GMT+09:00) Seoul"),
|
|
||||||
(1, 9, "(GMT+09:00) Yakutsk"),
|
|
||||||
(0, 10, "(GMT+10:00) Brisbane"),
|
|
||||||
(1, 10, "(GMT+10:00) Canberra, Melbourne, Sydney"),
|
|
||||||
(1, 10, "(GMT+10:00) Hobart"),
|
|
||||||
(0, 10, "(GMT+10:00) Guam, Port Moresby"),
|
|
||||||
(1, 10, "(GMT+10:00) Vladivostok"),
|
|
||||||
(1, 11, "(GMT+11:00) Magadan, Solomon Is., New Caledonia"),
|
|
||||||
(1, 12, "(GMT+12:00) Auckland, Wellington"),
|
|
||||||
(0, 12, "(GMT+12:00) Fiji, Kamchatka, Marshall Is."),
|
|
||||||
(0, 13, "(GMT+13:00) Nuku'alofa")
|
|
||||||
)
|
|
Loading…
Reference in New Issue
Block a user