├── images ├── logo.png ├── logo_red.png ├── logo_small.png ├── skjermskudd.png └── micropython_logo_black.svg ├── css ├── src │ ├── nanogram │ │ ├── _Image.sass │ │ ├── _Divider.sass │ │ ├── _Link.sass │ │ ├── _Blockquote.sass │ │ ├── _Color.sass │ │ ├── _Table.sass │ │ ├── _List.sass │ │ ├── _Navbar.sass │ │ ├── _Spacing.sass │ │ ├── _Utility.sass │ │ ├── nanogram.sass │ │ ├── _Code.sass │ │ ├── _Base.sass │ │ ├── _Typography.sass │ │ ├── _Button.sass │ │ ├── _Form.sass │ │ └── _Grid.sass │ └── milligram │ │ ├── _Image.sass │ │ ├── _Divider.sass │ │ ├── _Link.sass │ │ ├── _Blockquote.sass │ │ ├── _Color.sass │ │ ├── _List.sass │ │ ├── _Table.sass │ │ ├── _Spacing.sass │ │ ├── _Utility.sass │ │ ├── milligram.sass │ │ ├── _Code.sass │ │ ├── _Base.sass │ │ ├── _Typography.sass │ │ ├── _Button.sass │ │ ├── _Form.sass │ │ └── _Grid.sass └── build │ ├── compile_nanogram.sh │ ├── README.md │ └── nanogram.css ├── src ├── core │ ├── boot.py │ ├── crypt.py │ ├── microajax.js │ ├── logfile.py │ ├── nanogram.min.css │ ├── index.html │ ├── scheduler_light.py │ ├── wifi.py │ ├── webserver.py │ └── umqttsimple.py ├── drivers │ ├── leds.py │ ├── buttons.py │ └── tm1637.py ├── utilities │ └── timers.py ├── config.py └── main.py ├── test ├── logfile_test.py └── scheduler_test.py ├── Makefile ├── examples └── relayapp │ └── main.py └── README.md /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aslake/mipy_esp/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/logo_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aslake/mipy_esp/HEAD/images/logo_red.png -------------------------------------------------------------------------------- /images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aslake/mipy_esp/HEAD/images/logo_small.png -------------------------------------------------------------------------------- /images/skjermskudd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aslake/mipy_esp/HEAD/images/skjermskudd.png -------------------------------------------------------------------------------- /css/src/nanogram/_Image.sass: -------------------------------------------------------------------------------- 1 | 2 | // Image 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | img 6 | max-width: 100% 7 | -------------------------------------------------------------------------------- /css/src/milligram/_Image.sass: -------------------------------------------------------------------------------- 1 | 2 | // Image 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | img 6 | max-width: 100% 7 | -------------------------------------------------------------------------------- /css/src/milligram/_Divider.sass: -------------------------------------------------------------------------------- 1 | 2 | // Divider 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | hr 6 | border: 0 7 | border-top: .1rem solid $color-tertiary 8 | margin: 3.0rem 0 9 | -------------------------------------------------------------------------------- /css/src/nanogram/_Divider.sass: -------------------------------------------------------------------------------- 1 | 2 | // Divider 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | hr 6 | border: 0 7 | border-top: .1rem solid $color-tertiary 8 | margin: 3.0rem 0 9 | -------------------------------------------------------------------------------- /css/src/milligram/_Link.sass: -------------------------------------------------------------------------------- 1 | 2 | // Link 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | a 6 | color: $color-primary 7 | text-decoration: none 8 | 9 | &:focus, 10 | &:hover 11 | color: $color-secondary 12 | -------------------------------------------------------------------------------- /css/src/nanogram/_Link.sass: -------------------------------------------------------------------------------- 1 | 2 | // Link 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | a 6 | color: $color-primary 7 | text-decoration: none 8 | 9 | &:focus, 10 | &:hover 11 | color: $color-secondary 12 | -------------------------------------------------------------------------------- /src/core/boot.py: -------------------------------------------------------------------------------- 1 | # boot.py 2 | # 3 | # MiPy-ESP - A basic microPython framework for microcontroller IoT projects. 4 | # Authors: Torbjørn Pettersen and Aslak Einbu 5 | # 6 | # This file needs to overwrite existing boot.py from newly flashed Micropython ESP 7 | # 8 | -------------------------------------------------------------------------------- /css/src/nanogram/_Blockquote.sass: -------------------------------------------------------------------------------- 1 | 2 | // Blockquote 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | blockquote 6 | border-left: .3rem solid $color-quaternary 7 | margin-left: 0 8 | margin-right: 0 9 | padding: 1rem 1.5rem 10 | 11 | *:last-child 12 | margin-bottom: 0 13 | -------------------------------------------------------------------------------- /css/src/milligram/_Blockquote.sass: -------------------------------------------------------------------------------- 1 | 2 | // Blockquote 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | blockquote 6 | border-left: .3rem solid $color-quaternary 7 | margin-left: 0 8 | margin-right: 0 9 | padding: 1rem 1.5rem 10 | 11 | *:last-child 12 | margin-bottom: 0 13 | -------------------------------------------------------------------------------- /css/src/milligram/_Color.sass: -------------------------------------------------------------------------------- 1 | 2 | // Color 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | $color-initial: #fff !default 6 | $color-primary: #9b4dca !default 7 | $color-secondary: #606c76 !default 8 | $color-tertiary: #f4f5f6 !default 9 | $color-quaternary: #d1d1d1 !default 10 | $color-quinary: #e1e1e1 !default 11 | -------------------------------------------------------------------------------- /css/src/nanogram/_Color.sass: -------------------------------------------------------------------------------- 1 | 2 | // Color 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | $color-initial: 'lime' !default 6 | $color-primary: 'yellow' !default 7 | $color-secondary: 'lime' !default 8 | $color-tertiary: 'lime' !default 9 | $color-quaternary: 'lime' !default 10 | $color-quinary: 'lime' !default 11 | $color-navbar: 'lime' !default 12 | -------------------------------------------------------------------------------- /css/src/nanogram/_Table.sass: -------------------------------------------------------------------------------- 1 | 2 | // Table 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | table 6 | border-spacing: 0 7 | width: 100% 8 | 9 | td, 10 | th 11 | border-bottom: .1rem solid $color-quinary 12 | padding: 1.2rem 1.5rem 13 | text-align: left 14 | 15 | &:first-child 16 | padding-left: 0 17 | 18 | &:last-child 19 | padding-right: 0 20 | -------------------------------------------------------------------------------- /css/src/milligram/_List.sass: -------------------------------------------------------------------------------- 1 | 2 | // List 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | dl, 6 | ol, 7 | ul 8 | list-style: none 9 | margin-top: 0 10 | padding-left: 0 11 | 12 | dl, 13 | ol, 14 | ul 15 | font-size: 90% 16 | margin: 1.5rem 0 1.5rem 3.0rem 17 | 18 | ol 19 | list-style: decimal inside 20 | 21 | ul 22 | list-style: circle inside 23 | -------------------------------------------------------------------------------- /css/src/milligram/_Table.sass: -------------------------------------------------------------------------------- 1 | 2 | // Table 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | table 6 | border-spacing: 0 7 | width: 100% 8 | 9 | td, 10 | th 11 | border-bottom: .1rem solid $color-quinary 12 | padding: 1.2rem 1.5rem 13 | text-align: left 14 | 15 | &:first-child 16 | padding-left: 0 17 | 18 | &:last-child 19 | padding-right: 0 20 | -------------------------------------------------------------------------------- /css/src/nanogram/_List.sass: -------------------------------------------------------------------------------- 1 | 2 | // List 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | dl, 6 | ol, 7 | ul 8 | list-style: none 9 | margin-top: 0 10 | padding-left: 0 11 | 12 | dl, 13 | ol, 14 | ul 15 | font-size: 90% 16 | margin: 1.5rem 0 1.5rem 3.0rem 17 | 18 | ol 19 | list-style: decimal inside 20 | 21 | ul 22 | list-style: circle inside 23 | -------------------------------------------------------------------------------- /css/src/nanogram/_Navbar.sass: -------------------------------------------------------------------------------- 1 | .navbar 2 | margin-bottom: 10px 3 | ul 4 | list-style-type: none 5 | margin: 0 6 | padding: 0 7 | overflow: hidden 8 | background-color: $color-navbar 9 | li 10 | float: left 11 | margin-bottom: 0 12 | a 13 | display: block 14 | color: white 15 | text-align: center 16 | padding: 14px 16px 17 | text-decoration: none 18 | 19 | -------------------------------------------------------------------------------- /css/src/nanogram/_Spacing.sass: -------------------------------------------------------------------------------- 1 | 2 | // Spacing 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | .button, 6 | button, 7 | dd, 8 | dt, 9 | li 10 | margin-bottom: 1.0rem 11 | 12 | fieldset, 13 | input, 14 | select, 15 | textarea 16 | margin-bottom: 1.5rem 17 | 18 | blockquote, 19 | dl, 20 | figure, 21 | form, 22 | ol, 23 | p, 24 | pre, 25 | table, 26 | ul 27 | margin-bottom: 2.5rem 28 | -------------------------------------------------------------------------------- /css/src/milligram/_Spacing.sass: -------------------------------------------------------------------------------- 1 | 2 | // Spacing 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | .button, 6 | button, 7 | dd, 8 | dt, 9 | li 10 | margin-bottom: 1.0rem 11 | 12 | fieldset, 13 | input, 14 | select, 15 | textarea 16 | margin-bottom: 1.5rem 17 | 18 | blockquote, 19 | dl, 20 | figure, 21 | form, 22 | ol, 23 | p, 24 | pre, 25 | table, 26 | ul 27 | margin-bottom: 2.5rem 28 | -------------------------------------------------------------------------------- /css/src/milligram/_Utility.sass: -------------------------------------------------------------------------------- 1 | 2 | // Utility 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // Clear a float with .clearfix 6 | .clearfix 7 | 8 | &:after 9 | clear: both 10 | content: ' ' // The space content is one way to avoid an Opera bug. 11 | display: table 12 | 13 | // Float either direction 14 | .float-left 15 | float: left 16 | 17 | .float-right 18 | float: right 19 | -------------------------------------------------------------------------------- /css/src/milligram/milligram.sass: -------------------------------------------------------------------------------- 1 | 2 | // Sass Modules 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | @import Color 6 | @import Base 7 | @import Blockquote 8 | @import Button 9 | @import Code 10 | @import Divider 11 | @import Form 12 | @import Grid 13 | @import Link 14 | @import List 15 | @import Spacing 16 | @import Table 17 | @import Typography 18 | @import Image 19 | @import Utility 20 | -------------------------------------------------------------------------------- /css/src/nanogram/_Utility.sass: -------------------------------------------------------------------------------- 1 | 2 | // Utility 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // Clear a float with .clearfix 6 | .clearfix 7 | 8 | &:after 9 | clear: both 10 | content: ' ' // The space content is one way to avoid an Opera bug. 11 | display: table 12 | 13 | // Float either direction 14 | .float-left 15 | float: left 16 | 17 | .float-right 18 | float: right 19 | -------------------------------------------------------------------------------- /css/src/nanogram/nanogram.sass: -------------------------------------------------------------------------------- 1 | 2 | // Sass Modules 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | @import Color 6 | //@import Navbar 7 | @import Base 8 | //@import Blockquote 9 | //@import Button 10 | //@import Code 11 | //@import Divider 12 | @import Form 13 | @import Grid 14 | //@import Link 15 | //@import List 16 | //@import Spacing 17 | @import Table 18 | //@import Typography 19 | //@import Image 20 | //@import Utility 21 | -------------------------------------------------------------------------------- /css/src/nanogram/_Code.sass: -------------------------------------------------------------------------------- 1 | 2 | // Code 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | code 6 | background: $color-tertiary 7 | border-radius: .4rem 8 | font-size: 86% 9 | margin: 0 .2rem 10 | padding: .2rem .5rem 11 | white-space: nowrap 12 | 13 | pre 14 | background: $color-tertiary 15 | border-left: .3rem solid $color-primary 16 | overflow-y: hidden 17 | 18 | & > code 19 | border-radius: 0 20 | display: block 21 | padding: 1rem 1.5rem 22 | white-space: pre 23 | -------------------------------------------------------------------------------- /css/src/milligram/_Code.sass: -------------------------------------------------------------------------------- 1 | 2 | // Code 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | code 6 | background: $color-tertiary 7 | border-radius: .4rem 8 | font-size: 86% 9 | margin: 0 .2rem 10 | padding: .2rem .5rem 11 | white-space: nowrap 12 | 13 | pre 14 | background: $color-tertiary 15 | border-left: .3rem solid $color-primary 16 | overflow-y: hidden 17 | 18 | & > code 19 | border-radius: 0 20 | display: block 21 | padding: 1rem 1.5rem 22 | white-space: pre 23 | -------------------------------------------------------------------------------- /css/build/compile_nanogram.sh: -------------------------------------------------------------------------------- 1 | # Script for compiling og minified nanogram CSS in chip code source. 2 | # Tailoring of Nanogram CSS framework from .sass files in /css/src/nanogram 3 | # 4 | # Author: Aslak Einbu, Jan 2020 5 | 6 | sass ../src/nanogram/nanogram.sass nanogram.css 7 | echo "New nanogram.css compiled from sass files!" 8 | 9 | cat nanogram.css| tr -d " \t\n\r" > nanogram.min.css 10 | echo "Nanogram successfully minified!" 11 | 12 | cp nanogram.min.css ../../src/nanogram.min.css 13 | echo "New nanogram.min.css written to /src !" 14 | -------------------------------------------------------------------------------- /src/drivers/leds.py: -------------------------------------------------------------------------------- 1 | """ 2 | Led functions for toggling and blinking of leds. 3 | 4 | Aslak Einbu Jan 2020 5 | 6 | """ 7 | 8 | import utime 9 | 10 | 11 | def toggle(led): 12 | """ Toggle led on/off. """ 13 | if led.value() == 1: 14 | led.value(0) 15 | else: 16 | led.value(1) 17 | 18 | 19 | def blink(led, times, on_time, off_time): 20 | """ Led blink sequence. """ 21 | for i in range(times): 22 | led.on() 23 | utime.sleep(on_time) 24 | led.off() 25 | utime.sleep(off_time) 26 | 27 | -------------------------------------------------------------------------------- /css/build/README.md: -------------------------------------------------------------------------------- 1 | # Nanogram CSS compiler 2 | -------------------------------------- 3 | ## Cookbook: 4 | 5 | - Edit sass files in /css/src/nanogram 6 | - Edit /css/src/nanogram/nanogram.sass for selection inclusions 7 | - Use sass to compile to css 8 | - Minify css 9 | - Copy css to /src for chip upload 10 | 11 | Install Sass with: 12 | ``` 13 | sudo npm install -g sass 14 | ``` 15 | 16 | Convert to css: 17 | ``` 18 | sass ../src/nanogram/nanogram.sass nanogram.css 19 | ``` 20 | 21 | Minify: 22 | ``` 23 | cat nanogram.css| tr -d " \t\n\r" > nanogram.min.css 24 | ``` 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/logfile_test.py: -------------------------------------------------------------------------------- 1 | """Tester for logfile.py.""" 2 | 3 | import logfile 4 | 5 | levels = "DEBUG INFO WARNING ERROR".split() 6 | 7 | log = logfile.LogFile("TestLog", "testlog.txt", 200, "DEBUG", False) 8 | for i in range(10): 9 | ind = i % 4 10 | msg = "Melding {}".format(i) 11 | if levels[ind] == "DEBUG": 12 | log.debug(msg) 13 | elif levels[ind] == "INFO": 14 | log.info(msg) 15 | elif levels[ind] == "WARNING": 16 | log.warning(msg) 17 | elif levels[ind] == "ERROR": 18 | log.error(msg) 19 | 20 | log.level = "DEBUG" 21 | 22 | log.debug("Sjekk om vi får ut debug melding til slutt") 23 | -------------------------------------------------------------------------------- /src/utilities/timers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various utility functions. 3 | 4 | Aslak Einbu Jan 2020 5 | """ 6 | 7 | import utime 8 | 9 | 10 | class Countdown: 11 | """ Timer object for elapsed time check. """ 12 | 13 | def __init__(self, countdowntime): 14 | self.countdowntime = countdowntime 15 | self.starttime = 0 16 | 17 | def start(self): 18 | """ Starts countdown timer. """ 19 | self.starttime = utime.time() 20 | 21 | def is_overdue(self): 22 | """ Check if countdowntime has elapsed since start. """ 23 | if (utime.time() - self.starttime) > self.countdowntime : 24 | return True 25 | else: 26 | return False 27 | -------------------------------------------------------------------------------- /css/src/milligram/_Base.sass: -------------------------------------------------------------------------------- 1 | 2 | // Base 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // Set box-sizing globally to handle padding and border widths 6 | *, 7 | *:after, 8 | *:before 9 | box-sizing: inherit 10 | 11 | // The base font-size is set at 62.5% for having the convenience 12 | // of sizing rems in a way that is similar to using px: 1.6rem = 16px 13 | html 14 | box-sizing: border-box 15 | font-size: 62.5% 16 | 17 | // Default body styles 18 | body 19 | color: $color-secondary 20 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif 21 | font-size: 1.6em // Currently ems cause chrome bug misinterpreting rems on body element 22 | font-weight: 300 23 | letter-spacing: .01em 24 | line-height: 1.6 25 | -------------------------------------------------------------------------------- /css/src/nanogram/_Base.sass: -------------------------------------------------------------------------------- 1 | 2 | // Base 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // Set box-sizing globally to handle padding and border widths 6 | *, 7 | *:after, 8 | *:before 9 | box-sizing: inherit 10 | 11 | // The base font-size is set at 62.5% for having the convenience 12 | // of sizing rems in a way that is similar to using px: 1.6rem = 16px 13 | html 14 | box-sizing: border-box 15 | font-size: 62.5% 16 | 17 | // Default body styles 18 | body 19 | color: $color-secondary 20 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif 21 | font-size: 1.6em // Currently ems cause chrome bug misinterpreting rems on body element 22 | font-weight: 300 23 | letter-spacing: .01em 24 | line-height: 1.6 25 | -------------------------------------------------------------------------------- /css/src/milligram/_Typography.sass: -------------------------------------------------------------------------------- 1 | 2 | // Typography 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | b, 6 | strong 7 | font-weight: bold 8 | 9 | p 10 | margin-top: 0 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 18 | font-weight: 300 19 | letter-spacing: -.1rem 20 | margin-bottom: 2.0rem 21 | margin-top: 0 22 | 23 | h1 24 | font-size: 4.6rem 25 | line-height: 1.2 26 | 27 | h2 28 | font-size: 3.6rem 29 | line-height: 1.25 30 | 31 | h3 32 | font-size: 2.8rem 33 | line-height: 1.3 34 | 35 | h4 36 | font-size: 2.2rem 37 | letter-spacing: -.08rem 38 | line-height: 1.35 39 | 40 | h5 41 | font-size: 1.8rem 42 | letter-spacing: -.05rem 43 | line-height: 1.5 44 | 45 | h6 46 | font-size: 1.6rem 47 | letter-spacing: 0 48 | line-height: 1.4 49 | -------------------------------------------------------------------------------- /css/src/nanogram/_Typography.sass: -------------------------------------------------------------------------------- 1 | 2 | // Typography 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | b, 6 | strong 7 | font-weight: bold 8 | 9 | p 10 | margin-top: 0 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 18 | font-weight: 300 19 | letter-spacing: -.1rem 20 | margin-bottom: 2.0rem 21 | margin-top: 0 22 | 23 | h1 24 | font-size: 4.6rem 25 | line-height: 1.2 26 | 27 | h2 28 | font-size: 3.6rem 29 | line-height: 1.25 30 | 31 | h3 32 | font-size: 2.8rem 33 | line-height: 1.3 34 | 35 | h4 36 | font-size: 2.2rem 37 | letter-spacing: -.08rem 38 | line-height: 1.35 39 | 40 | h5 41 | font-size: 1.8rem 42 | letter-spacing: -.05rem 43 | line-height: 1.5 44 | 45 | h6 46 | font-size: 1.6rem 47 | letter-spacing: 0 48 | line-height: 1.4 49 | -------------------------------------------------------------------------------- /test/scheduler_test.py: -------------------------------------------------------------------------------- 1 | from scheduler_light import Scheduler, Job 2 | import time 3 | import machine 4 | 5 | 6 | def job1(): 7 | print("jobb 1") 8 | 9 | 10 | def job2(): 11 | print("jobb 2") 12 | 13 | 14 | schedule = Scheduler("Test program", 15 | Job("Job 1", job1, 2, 3), 16 | Job("Job 2", job2, 4, 3)) 17 | 18 | 19 | while len(schedule.jobs) > 0: 20 | ind = schedule.follow_up_jobs() 21 | time.sleep(0.25) 22 | 23 | 24 | # Test av next_time metoden 25 | j3 = Job("Job 3", job1, 5, 5) 26 | now = machine.RTC().datetime() 27 | print("now = ", now) 28 | now_sec = time.time() 29 | yyyy, mm, dd, wday, hh, mm, sec, ms = now 30 | mm += 1 31 | j3.set_next_run(mm=mm) 32 | 33 | schedule.add_jobs(j3) 34 | while len(schedule.jobs) > 0: 35 | ind = schedule.follow_up_jobs() 36 | time.sleep(0.25) 37 | -------------------------------------------------------------------------------- /css/src/nanogram/_Button.sass: -------------------------------------------------------------------------------- 1 | 2 | // Button 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | .button, 6 | button, 7 | input[type='button'], 8 | input[type='reset'], 9 | input[type='submit'] 10 | background-color: $color-primary 11 | border: .1rem solid $color-primary 12 | border-radius: .4rem 13 | color: $color-initial 14 | cursor: pointer 15 | display: inline-block 16 | font-size: 1.1rem 17 | font-weight: 700 18 | height: 3.8rem 19 | letter-spacing: .1rem 20 | line-height: 3.8rem 21 | padding: 0 3.0rem 22 | text-align: center 23 | text-decoration: none 24 | text-transform: uppercase 25 | white-space: nowrap 26 | 27 | &:focus, 28 | &:hover 29 | background-color: $color-secondary 30 | border-color: $color-secondary 31 | color: $color-initial 32 | outline: 0 33 | 34 | &[disabled] 35 | cursor: default 36 | opacity: .5 37 | 38 | &:focus, 39 | &:hover 40 | background-color: $color-primary 41 | border-color: $color-primary 42 | 43 | -------------------------------------------------------------------------------- /src/drivers/buttons.py: -------------------------------------------------------------------------------- 1 | """ 2 | Button interactions - with callback. 3 | 4 | Aslak Einbu, Jan 2020 5 | 6 | """ 7 | 8 | from machine import Pin 9 | import logfile 10 | import config 11 | 12 | log = logfile.LogFile("Button-test", config.log_filename, config.log_sizelimit, 13 | config.log_level, config.log_print) 14 | 15 | 16 | class Button: 17 | """ Attach pin and callback to button.""" 18 | def __init__(self, pin, callback): 19 | self.pin = pin 20 | self.state = Pin(pin, Pin.IN, Pin.PULL_UP) 21 | self.laststate = self.state.value() 22 | self.callback = callback 23 | log.debug("Button attached to pin %s with callback %s" % (str(pin), str(callback))) 24 | 25 | def check_pressed(self): 26 | """ Run callback function if button was pressed since last check. """ 27 | state = self.state.value() 28 | if state != self.laststate: 29 | if state == 1: 30 | self.callback() 31 | self.laststate = state 32 | 33 | def is_on(self): 34 | """ Check if button is pressed. """ 35 | if self.state.value() == 0: 36 | return True 37 | else: 38 | return False 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/core/crypt.py: -------------------------------------------------------------------------------- 1 | """ 2 | crypt: simple password encryption / decryption 3 | 4 | Created: Sun Jan 5 2020 5 | """ 6 | __version__ = '0.1.0' 7 | __author__ = 'Torbjørn Pettersen ' 8 | 9 | import ucryptolib 10 | import ubinascii 11 | 12 | 13 | def _to16(seed): 14 | """Convert a seed to a nx16 byte string.""" 15 | return seed + " " * (16 - len(seed) % 16) 16 | 17 | 18 | class Crypt: 19 | """Simple encryption/decryption of tekst. 20 | 21 | Usage: 22 | ------ 23 | cipher = Crypt("secret seed") 24 | coded = cipher.encode("Message") 25 | decoded = cipher.decrypt(coded) 26 | """ 27 | def __init__(self, seed): 28 | """Initsialise based on seed.""" 29 | self.seed = _to16(seed.encode()) 30 | self.en = ucryptolib.aes(self.seed, 1) 31 | self.de = ucryptolib.aes(self.seed, 1) 32 | 33 | def encrypt(self, password): 34 | """Encrypt password.""" 35 | en_pwd = self.en.encrypt(_to16(password)) 36 | ascii_pwd = ubinascii.hexlify(en_pwd) 37 | return ascii_pwd.decode() 38 | 39 | def decrypt(self, ascii_pwd): 40 | """Decrypt password.""" 41 | en_pwd = ubinascii.unhexlify(ascii_pwd.encode()) 42 | pwd = self.de.decrypt(en_pwd).decode() 43 | return pwd.strip() 44 | -------------------------------------------------------------------------------- /src/core/microajax.js: -------------------------------------------------------------------------------- 1 | 2 | // MicroAjax function stolen from Stefan Lange (2008) https://github.com/forkineye/microajax/blob/master/microajax.js 3 | 4 | function microAjax(url, callbackFunction) 5 | { 6 | this.bindFunction = function (caller, object) { 7 | return function() { 8 | return caller.apply(object, [object]); 9 | }; 10 | }; 11 | 12 | this.stateChange = function (object) { 13 | if (this.request.readyState==4) 14 | this.callbackFunction(this.request.responseText); 15 | }; 16 | 17 | this.getRequest = function() { 18 | if (window.ActiveXObject) 19 | return new ActiveXObject('Microsoft.XMLHTTP'); 20 | else if (window.XMLHttpRequest) 21 | return new XMLHttpRequest(); 22 | return false; 23 | }; 24 | 25 | this.postBody = (arguments[2] || ""); 26 | 27 | this.callbackFunction=callbackFunction; 28 | this.url=url; 29 | this.request = this.getRequest(); 30 | 31 | if(this.request) { 32 | var req = this.request; 33 | req.onreadystatechange = this.bindFunction(this.stateChange, this); 34 | 35 | if (this.postBody!=="") { 36 | req.open("POST", url, true); 37 | req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 38 | req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 39 | req.setRequestHeader('Connection', 'close'); 40 | } else { 41 | req.open("GET", url, true); 42 | } 43 | 44 | req.send(this.postBody); 45 | } 46 | }; 47 | 48 | -------------------------------------------------------------------------------- /css/src/nanogram/_Form.sass: -------------------------------------------------------------------------------- 1 | 2 | // Form 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | input[type='password'], 5 | input[type='text'], 6 | textarea, 7 | select 8 | appearance: none // Removes awkward default styles on some inputs for iOS 9 | background-color: transparent 10 | border: .1rem solid $color-quaternary 11 | border-radius: .4rem 12 | box-shadow: none 13 | box-sizing: inherit // Forced to replace inherit values of the normalize.css 14 | height: 3.8rem 15 | padding: .6rem 1.0rem // The .6rem vertically centers text on FF, ignored by Webkit 16 | width: 100% 17 | 18 | &:focus 19 | border-color: $color-primary 20 | outline: 0 21 | 22 | select 23 | background: url('data:image/svg+xml;utf8,') center right no-repeat 24 | padding-right: 3.0rem 25 | 26 | &:focus 27 | background-image: url('data:image/svg+xml;utf8,') 28 | 29 | textarea 30 | min-height: 6.5rem 31 | 32 | label, 33 | legend 34 | display: block 35 | font-size: 1.6rem 36 | font-weight: 700 37 | margin-bottom: .5rem 38 | 39 | fieldset 40 | border-width: 0 41 | padding: 0 42 | 43 | input[type='checkbox'], 44 | input[type='radio'] 45 | display: inline 46 | 47 | .label-inline 48 | display: inline-block 49 | font-weight: normal 50 | margin-left: .5rem 51 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | # Micropython ESP framework system config parameters 2 | # ---------------------------------------------------- 3 | 4 | # Chip info: 5 | device_name = "uniquename" 6 | framework_commit = "a2ce907 " # Framework code applied for application (Repository commit) 7 | 8 | # Logging and debug: 9 | esp_osdebug = 0 # 0:debug to uart, None: no debug 10 | debug = True # Logging & debug [True/False] 11 | 12 | # Access Point: 13 | ap_ssid = "pythonESP" # ESP AP SSID 14 | ap_password = "secret!!!" # ESP AP SSID password 15 | utc_delta = 1 # adjusting time to Norwegian time 16 | 17 | # Encryption: 18 | crypt_seed = "Passord4passorD" # encryption key 19 | 20 | # MQTT: 21 | mqtt_port = 1883 # MQTT broker port 22 | mqtt_user = 'username' # MQTT broker user name 23 | mqtt_password = 'password' # MQTT broker user name password 24 | client_id = 'uniqueid' # MQTT client unique ID 25 | mqtt_broker = '85.119.83.194' # IP address to test.mosquitto.org MQTT broker 26 | heartbeat_interval = 5. # Interval [sec] for chip sign-of-life MQTT beacon 27 | heartbeat_message = 'alive' # Chip sign-of-life message 28 | 29 | # Logging 30 | log_sizelimit = 1000 # log file < x bytes 31 | log_level = "INFO" # DEBUG < INFO < WARNING < ERROR (dont write to logfile if log level lower than) 32 | log_print = True # UART print log entries [True/False] 33 | log_filename = "client.log" # Log file name 34 | -------------------------------------------------------------------------------- /css/src/milligram/_Button.sass: -------------------------------------------------------------------------------- 1 | 2 | // Button 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | .button, 6 | button, 7 | input[type='button'], 8 | input[type='reset'], 9 | input[type='submit'] 10 | background-color: $color-primary 11 | border: .1rem solid $color-primary 12 | border-radius: .4rem 13 | color: $color-initial 14 | cursor: pointer 15 | display: inline-block 16 | font-size: 1.1rem 17 | font-weight: 700 18 | height: 3.8rem 19 | letter-spacing: .1rem 20 | line-height: 3.8rem 21 | padding: 0 3.0rem 22 | text-align: center 23 | text-decoration: none 24 | text-transform: uppercase 25 | white-space: nowrap 26 | 27 | &:focus, 28 | &:hover 29 | background-color: $color-secondary 30 | border-color: $color-secondary 31 | color: $color-initial 32 | outline: 0 33 | 34 | &[disabled] 35 | cursor: default 36 | opacity: .5 37 | 38 | &:focus, 39 | &:hover 40 | background-color: $color-primary 41 | border-color: $color-primary 42 | 43 | &.button-outline 44 | background-color: transparent 45 | color: $color-primary 46 | 47 | &:focus, 48 | &:hover 49 | background-color: transparent 50 | border-color: $color-secondary 51 | color: $color-secondary 52 | 53 | &[disabled] 54 | 55 | &:focus, 56 | &:hover 57 | border-color: inherit 58 | color: $color-primary 59 | 60 | &.button-clear 61 | background-color: transparent 62 | border-color: transparent 63 | color: $color-primary 64 | 65 | &:focus, 66 | &:hover 67 | background-color: transparent 68 | border-color: transparent 69 | color: $color-secondary 70 | 71 | &[disabled] 72 | 73 | &:focus, 74 | &:hover 75 | color: $color-primary 76 | -------------------------------------------------------------------------------- /css/src/milligram/_Form.sass: -------------------------------------------------------------------------------- 1 | 2 | // Form 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | input[type='email'], 6 | input[type='number'], 7 | input[type='password'], 8 | input[type='search'], 9 | input[type='tel'], 10 | input[type='text'], 11 | input[type='url'], 12 | textarea, 13 | select 14 | appearance: none // Removes awkward default styles on some inputs for iOS 15 | background-color: transparent 16 | border: .1rem solid $color-quaternary 17 | border-radius: .4rem 18 | box-shadow: none 19 | box-sizing: inherit // Forced to replace inherit values of the normalize.css 20 | height: 3.8rem 21 | padding: .6rem 1.0rem // The .6rem vertically centers text on FF, ignored by Webkit 22 | width: 100% 23 | 24 | &:focus 25 | border-color: $color-primary 26 | outline: 0 27 | 28 | select 29 | background: url('data:image/svg+xml;utf8,') center right no-repeat 30 | padding-right: 3.0rem 31 | 32 | &:focus 33 | background-image: url('data:image/svg+xml;utf8,') 34 | 35 | textarea 36 | min-height: 6.5rem 37 | 38 | label, 39 | legend 40 | display: block 41 | font-size: 1.6rem 42 | font-weight: 700 43 | margin-bottom: .5rem 44 | 45 | fieldset 46 | border-width: 0 47 | padding: 0 48 | 49 | input[type='checkbox'], 50 | input[type='radio'] 51 | display: inline 52 | 53 | .label-inline 54 | display: inline-block 55 | font-weight: normal 56 | margin-left: .5rem 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #SHELL := /bin/bash 2 | 3 | # MiPy-ESP MicroPython Microcontroller Basic Framework Makefile. 4 | # 5 | # .py conversion to .mpy needs installation of the mpy-cross compiler: 6 | # https://github.com/micropython/micropython/tree/master/mpy-cross 7 | # 8 | # ESP commands depends on the ESPTOOL software: 9 | # https://github.com/espressif/esptool) 10 | 11 | 12 | build: ## Compile src .py files and move to build 13 | @echo ---------------------------------------------------- 14 | @echo - Prepare for chip upload - 15 | @echo ---------------------------------------------------- 16 | @mkdir -p build 17 | @cp src/core/*.* build 18 | @cp src/*.* build 19 | @rm build/main.py 20 | @rm build/config.py 21 | @echo Compiles .py files to .mpy: 22 | @for f in build/*.py; do mpy-cross $$f; echo " mpy-cross $$f"; done; 23 | @rm build/*.py 24 | @cp src/config.py build 25 | @cp src/main.py build 26 | @echo ---------------------------------------------------- 27 | @echo Project files in build compiled and ready for upload to chip: 28 | @tree -sh build 29 | 30 | 31 | erase_chip: ## Erase ESP flash memory 32 | @echo ------------------------------------ 33 | @echo - Erases ESP flash memory - 34 | @echo ------------------------------------ 35 | @esptool.py --port /dev/ttyUSB0 erase_flash 36 | @echo Microcontroller flash erased. 37 | 38 | 39 | flash_chip: ## Uploads Micropython to ESP 40 | @echo ------------------------------------ 41 | @echo - Upload MicroPython to ESP chip - 42 | @echo ------------------------------------ 43 | @esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect 0 ~/Downloads/esp8266-20191220-v1.12.bin 44 | @echo MicroPython installed on ESP microcontroller! 45 | 46 | 47 | clean: ## Remove build folder 48 | @echo ---------------------------------------------- 49 | @echo - Removes build/ folder and its content - 50 | @echo ---------------------------------------------- 51 | @rm -r build 52 | @echo Build folder erased. 53 | 54 | 55 | .PHONY: build erase_chip flash_chip clean 56 | -------------------------------------------------------------------------------- /src/core/logfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | logfile: storage of logfiles 3 | 4 | Created: Sat Jan 11 2020 5 | Authors: Torbjørn Pettersen, Aslak Einbu 6 | """ 7 | __version__ = '0.1.0' 8 | import os 9 | import machine 10 | import config 11 | 12 | 13 | _level_dict = {"ERROR": 40, 14 | "WARNING": 30, 15 | "INFO": 20, 16 | "DEBUG": 10, 17 | "NOTSET": 0} 18 | 19 | 20 | def _timestamp(): 21 | """Get timestamp from the chip.""" 22 | yr, mnt, day, wday, hh, mm, sec, ms = machine.RTC().datetime() 23 | return "{}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(yr, mnt, day, hh, mm, sec) 24 | 25 | 26 | class LogFile: 27 | """Administration of log files on microcontroller devices.""" 28 | def __init__(self, name, filename, sizelimit=1000, level="INFO", logprint=False): 29 | self.name = name 30 | self.filename = filename 31 | self.sizelimit = sizelimit 32 | self.level = level 33 | self.logprint = logprint 34 | if not filename in os.listdir(): 35 | with open(filename, "w") as f: 36 | stamp = _timestamp() 37 | f.write("Log {} created {}\n".format(name, stamp)) 38 | 39 | def debug(self, msg): 40 | self.log("DEBUG", msg) 41 | 42 | def info(self, msg): 43 | self.log("INFO", msg) 44 | 45 | def warning(self, msg): 46 | self.log("WARNING", msg) 47 | 48 | def error(self, msg): 49 | self.log("ERROR", msg) 50 | 51 | def log(self, flag, msg): 52 | """Add item to logfile. Drop old items if logfile>sizelimit.""" 53 | if config.debug: 54 | stamp = _timestamp() 55 | if self.logprint: 56 | print("{} - {}: {}".format(stamp, flag, msg)) 57 | if _level_dict[self.level] <= _level_dict[flag]: 58 | if (os.stat(self.filename)[6] > self.sizelimit): 59 | with open(self.filename) as f: 60 | log = f.readlines() 61 | log = log[1:] 62 | 63 | with open(self.filename, "w") as f: 64 | for _line in log: 65 | f.write(_line) 66 | else: 67 | with open(self.filename, "a+") as f: 68 | f.write("{} - {}: {}\n".format(stamp, flag, msg)) 69 | -------------------------------------------------------------------------------- /src/core/nanogram.min.css: -------------------------------------------------------------------------------- 1 | *,*:after,*:before{box-sizing:inherit;}html{box-sizing:border-box;font-size:62.5%;}body{color:"lime";font-family:"Roboto","HelveticaNeue","Helvetica","Arial",sans-serif;font-size:1.6em;font-weight:300;letter-spacing:0.01em;line-height:1.6;}input[type=password],input[type=text],textarea,select{appearance:none;background-color:transparent;border:0.1remsolid"lime";border-radius:0.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:0.6rem1rem;width:100%;}input[type=password]:focus,input[type=text]:focus,textarea:focus,select:focus{border-color:"yellow";outline:0;}select{background:url('data:image/svg+xml;utf8,')centerrightno-repeat;padding-right:3rem;}select:focus{background-image:url('data:image/svg+xml;utf8,');}textarea{min-height:6.5rem;}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:0.5rem;}fieldset{border-width:0;padding:0;}input[type=checkbox],input[type=radio]{display:inline;}.label-inline{display:inline-block;font-weight:normal;margin-left:0.5rem;}.container{margin:0auto;max-width:112rem;padding:02rem;position:relative;width:100%;}.row{display:flex;flex-direction:column;padding:0;width:100%;}.row.row-no-padding{padding:0;}.row.row-no-padding>.column{padding:0;}.row.row-wrap{flex-wrap:wrap;}.row.row-top{align-items:flex-start;}.row.row-bottom{align-items:flex-end;}.row.row-center{align-items:center;}.row.row-stretch{align-items:stretch;}.row.row-baseline{align-items:baseline;}.row.column{display:block;flex:11auto;margin-left:0;max-width:100%;width:100%;}.row.column.column-10{flex:0010%;max-width:10%;}.row.column.column-20{flex:0020%;max-width:20%;}.row.column.column-25{flex:0025%;max-width:25%;}.row.column.column-33,.row.column.column-34{flex:0033.3333%;max-width:33.3333%;}.row.column.column-40{flex:0040%;max-width:40%;}.row.column.column-50{flex:0050%;max-width:50%;}.row.column.column-60{flex:0060%;max-width:60%;}.row.column.column-66,.row.column.column-67{flex:0066.6666%;max-width:66.6666%;}.row.column.column-75{flex:0075%;max-width:75%;}.row.column.column-80{flex:0080%;max-width:80%;}.row.column.column-90{flex:0090%;max-width:90%;}.row.column.column-top{align-self:flex-start;}.row.column.column-bottom{align-self:flex-end;}.row.column.column-center{align-self:center;}table{border-spacing:0;width:100%;}td,th{border-bottom:0.1remsolid"lime";padding:1.2rem1.5rem;text-align:left;}td:first-child,th:first-child{padding-left:0;}td:last-child,th:last-child{padding-right:0;}/*#sourceMappingURL=nanogram.css.map*/ -------------------------------------------------------------------------------- /css/src/nanogram/_Grid.sass: -------------------------------------------------------------------------------- 1 | 2 | // Grid 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // .container is main centered wrapper with a max width of 112.0rem (1120px) 6 | .container 7 | margin: 0 auto 8 | max-width: 112.0rem 9 | padding: 0 2.0rem 10 | position: relative 11 | width: 100% 12 | 13 | // Using flexbox for the grid, inspired by Philip Walton: 14 | // http://philipwalton.github.io/solved-by-flexbox/demos/grids/ 15 | // By default each .column within a .row will evenly take up 16 | // available width, and the height of each .column with take 17 | // up the height of the tallest .column in the same .row 18 | .row 19 | display: flex 20 | flex-direction: column 21 | padding: 0 22 | width: 100% 23 | 24 | &.row-no-padding 25 | padding: 0 26 | 27 | &> .column 28 | padding: 0 29 | 30 | &.row-wrap 31 | flex-wrap: wrap 32 | 33 | // Vertically Align Columns 34 | // .row-* vertically aligns every .col in the .row 35 | &.row-top 36 | align-items: flex-start 37 | 38 | &.row-bottom 39 | align-items: flex-end 40 | 41 | &.row-center 42 | align-items: center 43 | 44 | &.row-stretch 45 | align-items: stretch 46 | 47 | &.row-baseline 48 | align-items: baseline 49 | 50 | .column 51 | display: block 52 | // IE 11 required specifying the flex-basis otherwise it breaks mobile 53 | flex: 1 1 auto 54 | margin-left: 0 55 | max-width: 100% 56 | width: 100% 57 | 58 | // Explicit Column Percent Sizes 59 | // By default each grid column will evenly distribute 60 | // across the grid. However, you can specify individual 61 | // columns to take up a certain size of the available area 62 | &.column-10 63 | flex: 0 0 10% 64 | max-width: 10% 65 | 66 | &.column-20 67 | flex: 0 0 20% 68 | max-width: 20% 69 | 70 | &.column-25 71 | flex: 0 0 25% 72 | max-width: 25% 73 | 74 | &.column-33, 75 | &.column-34 76 | flex: 0 0 33.3333% 77 | max-width: 33.3333% 78 | 79 | &.column-40 80 | flex: 0 0 40% 81 | max-width: 40% 82 | 83 | &.column-50 84 | flex: 0 0 50% 85 | max-width: 50% 86 | 87 | &.column-60 88 | flex: 0 0 60% 89 | max-width: 60% 90 | 91 | &.column-66, 92 | &.column-67 93 | flex: 0 0 66.6666% 94 | max-width: 66.6666% 95 | 96 | &.column-75 97 | flex: 0 0 75% 98 | max-width: 75% 99 | 100 | &.column-80 101 | flex: 0 0 80% 102 | max-width: 80% 103 | 104 | &.column-90 105 | flex: 0 0 90% 106 | max-width: 90% 107 | 108 | // .column-* vertically aligns an individual .column 109 | .column-top 110 | align-self: flex-start 111 | 112 | .column-bottom 113 | align-self: flex-end 114 | 115 | .column-center 116 | align-self: center 117 | 118 | -------------------------------------------------------------------------------- /src/core/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Hovedside 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
21 |

OPPKOBLING AV CHIP TIL WIFI

22 |
23 |
24 |

Velg blant synlige nettverk:

25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 |

Eller angi nettverk her:

47 |
48 | 49 |
50 | 51 |
52 |

Passord for valgt nettverk:

53 |
54 | 55 |
56 |
57 | 58 |
59 |
60 |

61 |
62 |
63 | 64 |
65 | 66 |
67 | 68 | 69 | 70 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /css/src/milligram/_Grid.sass: -------------------------------------------------------------------------------- 1 | 2 | // Grid 3 | // –––––––––––––––––––––––––––––––––––––––––––––––––– 4 | 5 | // .container is main centered wrapper with a max width of 112.0rem (1120px) 6 | .container 7 | margin: 0 auto 8 | max-width: 112.0rem 9 | padding: 0 2.0rem 10 | position: relative 11 | width: 100% 12 | 13 | // Using flexbox for the grid, inspired by Philip Walton: 14 | // http://philipwalton.github.io/solved-by-flexbox/demos/grids/ 15 | // By default each .column within a .row will evenly take up 16 | // available width, and the height of each .column with take 17 | // up the height of the tallest .column in the same .row 18 | .row 19 | display: flex 20 | flex-direction: column 21 | padding: 0 22 | width: 100% 23 | 24 | &.row-no-padding 25 | padding: 0 26 | 27 | &> .column 28 | padding: 0 29 | 30 | &.row-wrap 31 | flex-wrap: wrap 32 | 33 | // Vertically Align Columns 34 | // .row-* vertically aligns every .col in the .row 35 | &.row-top 36 | align-items: flex-start 37 | 38 | &.row-bottom 39 | align-items: flex-end 40 | 41 | &.row-center 42 | align-items: center 43 | 44 | &.row-stretch 45 | align-items: stretch 46 | 47 | &.row-baseline 48 | align-items: baseline 49 | 50 | .column 51 | display: block 52 | // IE 11 required specifying the flex-basis otherwise it breaks mobile 53 | flex: 1 1 auto 54 | margin-left: 0 55 | max-width: 100% 56 | width: 100% 57 | 58 | // Column Offsets 59 | &.column-offset-10 60 | margin-left: 10% 61 | 62 | &.column-offset-20 63 | margin-left: 20% 64 | 65 | &.column-offset-25 66 | margin-left: 25% 67 | 68 | &.column-offset-33, 69 | &.column-offset-34 70 | margin-left: 33.3333% 71 | 72 | &.column-offset-50 73 | margin-left: 50% 74 | 75 | &.column-offset-66, 76 | &.column-offset-67 77 | margin-left: 66.6666% 78 | 79 | &.column-offset-75 80 | margin-left: 75% 81 | 82 | &.column-offset-80 83 | margin-left: 80% 84 | 85 | &.column-offset-90 86 | margin-left: 90% 87 | 88 | // Explicit Column Percent Sizes 89 | // By default each grid column will evenly distribute 90 | // across the grid. However, you can specify individual 91 | // columns to take up a certain size of the available area 92 | &.column-10 93 | flex: 0 0 10% 94 | max-width: 10% 95 | 96 | &.column-20 97 | flex: 0 0 20% 98 | max-width: 20% 99 | 100 | &.column-25 101 | flex: 0 0 25% 102 | max-width: 25% 103 | 104 | &.column-33, 105 | &.column-34 106 | flex: 0 0 33.3333% 107 | max-width: 33.3333% 108 | 109 | &.column-40 110 | flex: 0 0 40% 111 | max-width: 40% 112 | 113 | &.column-50 114 | flex: 0 0 50% 115 | max-width: 50% 116 | 117 | &.column-60 118 | flex: 0 0 60% 119 | max-width: 60% 120 | 121 | &.column-66, 122 | &.column-67 123 | flex: 0 0 66.6666% 124 | max-width: 66.6666% 125 | 126 | &.column-75 127 | flex: 0 0 75% 128 | max-width: 75% 129 | 130 | &.column-80 131 | flex: 0 0 80% 132 | max-width: 80% 133 | 134 | &.column-90 135 | flex: 0 0 90% 136 | max-width: 90% 137 | 138 | // .column-* vertically aligns an individual .column 139 | .column-top 140 | align-self: flex-start 141 | 142 | .column-bottom 143 | align-self: flex-end 144 | 145 | .column-center 146 | align-self: center 147 | 148 | // Larger than mobile screen 149 | @media (min-width: 40.0rem) // Safari desktop has a bug using `rem`, but Safari mobile works 150 | 151 | .row 152 | flex-direction: row 153 | margin-left: -1.0rem 154 | width: calc(100% + 2.0rem) 155 | 156 | .column 157 | margin-bottom: inherit 158 | padding: 0 1.0rem 159 | -------------------------------------------------------------------------------- /src/core/scheduler_light.py: -------------------------------------------------------------------------------- 1 | """Job scheduler - light version. 2 | 3 | Inspiration: https://github.com/rguillon/schedule 4 | 5 | Torbjørn Pettersen, 12.01.2020. 6 | """ 7 | import utime 8 | import machine 9 | import utime 10 | import config 11 | import logfile 12 | 13 | log = logfile.LogFile("scheduler", "scheduler.log", config.log_sizelimit, 14 | config.log_level, config.log_print) 15 | 16 | 17 | class Job: 18 | """Job class. 19 | 20 | :param name: job name 21 | :type name: str 22 | :param job: function called to do job 23 | :type job: fun 24 | :param repeat: number of times to repeat job 25 | :type repeat: int 26 | :param interval: number of seconds between repeated job runs 27 | :type interval: int 28 | :returns: Job object to be used by Scheduler 29 | :rtype: object 30 | """ 31 | 32 | def __init__(self, name, job, interval, repeat): 33 | self.name = name # string 34 | self.job = job # function to be evaluated 35 | self.interval = interval # seconds 36 | self.repeat = repeat # number of times to repeat 37 | self.repeated = 0 38 | self.next_run = utime.time() + int(interval) # time for next run 39 | 40 | def set_next_run(self, **kwargs): 41 | """Set next time Job should run. 42 | 43 | :param yyyy, mmm, dd: optional year, month, day 44 | :type yyyy, mmm, dd: int 45 | :param hh, mm: optional hour and minutes 46 | :type hh, mm: int 47 | 48 | Usage: 49 | ----- 50 | Next run today at 23:59: job.set_next_run(hh=23, mm=59) 51 | Next run April 3rd @ current time: job.set_next_run(mmm=4, dd=3) 52 | """ 53 | now = machine.RTC().datetime() 54 | yyyy, mmm, dd, wday, hh, mm, sec, ms = now 55 | yyyy = kwargs.get("yyyy", int(yyyy)) 56 | mmm = kwargs.get("mmm", int(mmm)) 57 | dd = kwargs.get("dd", int(dd)) 58 | hh = kwargs.get("hh", int(hh)) 59 | mm = kwargs.get("mm", int(mm)) 60 | future = (yyyy, mmm, dd, wday, hh, mm, sec, ms) 61 | log.debug("Set next_run to: %s" % str(future)) 62 | machine.RTC().datetime(future) 63 | self.next_run = utime.time() 64 | machine.RTC().datetime(now) 65 | 66 | def run(self): 67 | now = utime.time() 68 | if now < self.next_run: 69 | return False 70 | 71 | if self.repeat == -1: # repeat = -1 => Run forever.. 72 | self.job() 73 | self.repeated += 1 74 | self.next_run = now + int(self.interval) 75 | return True 76 | 77 | elif self.repeated >= self.repeat: 78 | log.info("Skipped job %s, repeated %s times." % ( 79 | self.name, self.repeated)) 80 | return -1 81 | 82 | else: 83 | self.job() 84 | self.repeated += 1 85 | self.next_run = now + int(self.interval) 86 | return True 87 | 88 | 89 | class Scheduler: 90 | """Job scheduler""" 91 | 92 | def __init__(self, name, *args): 93 | self.name = name 94 | self.jobs = [job for job in args] 95 | 96 | def follow_up_jobs(self): 97 | """Check if jobs are due to execute""" 98 | for i, job in enumerate(self.jobs): 99 | status = job.run() 100 | if status == -1: # Job has been repeated enough. 101 | log.info("Job %s completed and removed from job list." % 102 | (job.name)) 103 | if i == 0: 104 | self.jobs = self.jobs[1:] 105 | else: 106 | self.jobs = self.jobs[:i-1] + self.jobs[i:] 107 | else: 108 | if status: 109 | log.debug("Job %s executed." % (job.name)) 110 | if len(self.jobs) == 0: 111 | log.debug("Scheduler %s has no jobs." % (self.name)) 112 | 113 | def add_jobs(self, *args): 114 | """Add jobs to list of jobs.""" 115 | self.jobs += [job for job in args] 116 | -------------------------------------------------------------------------------- /css/build/nanogram.css: -------------------------------------------------------------------------------- 1 | *, 2 | *:after, 3 | *:before { 4 | box-sizing: inherit; 5 | } 6 | 7 | html { 8 | box-sizing: border-box; 9 | font-size: 62.5%; 10 | } 11 | 12 | body { 13 | color: "lime"; 14 | font-family: "Roboto", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 15 | font-size: 1.6em; 16 | font-weight: 300; 17 | letter-spacing: 0.01em; 18 | line-height: 1.6; 19 | } 20 | 21 | input[type=password], 22 | input[type=text], 23 | textarea, 24 | select { 25 | appearance: none; 26 | background-color: transparent; 27 | border: 0.1rem solid "lime"; 28 | border-radius: 0.4rem; 29 | box-shadow: none; 30 | box-sizing: inherit; 31 | height: 3.8rem; 32 | padding: 0.6rem 1rem; 33 | width: 100%; 34 | } 35 | input[type=password]:focus, 36 | input[type=text]:focus, 37 | textarea:focus, 38 | select:focus { 39 | border-color: "yellow"; 40 | outline: 0; 41 | } 42 | 43 | select { 44 | background: url('data:image/svg+xml;utf8,') center right no-repeat; 45 | padding-right: 3rem; 46 | } 47 | select:focus { 48 | background-image: url('data:image/svg+xml;utf8,'); 49 | } 50 | 51 | textarea { 52 | min-height: 6.5rem; 53 | } 54 | 55 | label, 56 | legend { 57 | display: block; 58 | font-size: 1.6rem; 59 | font-weight: 700; 60 | margin-bottom: 0.5rem; 61 | } 62 | 63 | fieldset { 64 | border-width: 0; 65 | padding: 0; 66 | } 67 | 68 | input[type=checkbox], 69 | input[type=radio] { 70 | display: inline; 71 | } 72 | 73 | .label-inline { 74 | display: inline-block; 75 | font-weight: normal; 76 | margin-left: 0.5rem; 77 | } 78 | 79 | .container { 80 | margin: 0 auto; 81 | max-width: 112rem; 82 | padding: 0 2rem; 83 | position: relative; 84 | width: 100%; 85 | } 86 | 87 | .row { 88 | display: flex; 89 | flex-direction: column; 90 | padding: 0; 91 | width: 100%; 92 | } 93 | .row.row-no-padding { 94 | padding: 0; 95 | } 96 | .row.row-no-padding > .column { 97 | padding: 0; 98 | } 99 | .row.row-wrap { 100 | flex-wrap: wrap; 101 | } 102 | .row.row-top { 103 | align-items: flex-start; 104 | } 105 | .row.row-bottom { 106 | align-items: flex-end; 107 | } 108 | .row.row-center { 109 | align-items: center; 110 | } 111 | .row.row-stretch { 112 | align-items: stretch; 113 | } 114 | .row.row-baseline { 115 | align-items: baseline; 116 | } 117 | .row .column { 118 | display: block; 119 | flex: 1 1 auto; 120 | margin-left: 0; 121 | max-width: 100%; 122 | width: 100%; 123 | } 124 | .row .column.column-10 { 125 | flex: 0 0 10%; 126 | max-width: 10%; 127 | } 128 | .row .column.column-20 { 129 | flex: 0 0 20%; 130 | max-width: 20%; 131 | } 132 | .row .column.column-25 { 133 | flex: 0 0 25%; 134 | max-width: 25%; 135 | } 136 | .row .column.column-33, .row .column.column-34 { 137 | flex: 0 0 33.3333%; 138 | max-width: 33.3333%; 139 | } 140 | .row .column.column-40 { 141 | flex: 0 0 40%; 142 | max-width: 40%; 143 | } 144 | .row .column.column-50 { 145 | flex: 0 0 50%; 146 | max-width: 50%; 147 | } 148 | .row .column.column-60 { 149 | flex: 0 0 60%; 150 | max-width: 60%; 151 | } 152 | .row .column.column-66, .row .column.column-67 { 153 | flex: 0 0 66.6666%; 154 | max-width: 66.6666%; 155 | } 156 | .row .column.column-75 { 157 | flex: 0 0 75%; 158 | max-width: 75%; 159 | } 160 | .row .column.column-80 { 161 | flex: 0 0 80%; 162 | max-width: 80%; 163 | } 164 | .row .column.column-90 { 165 | flex: 0 0 90%; 166 | max-width: 90%; 167 | } 168 | .row .column .column-top { 169 | align-self: flex-start; 170 | } 171 | .row .column .column-bottom { 172 | align-self: flex-end; 173 | } 174 | .row .column .column-center { 175 | align-self: center; 176 | } 177 | 178 | table { 179 | border-spacing: 0; 180 | width: 100%; 181 | } 182 | 183 | td, 184 | th { 185 | border-bottom: 0.1rem solid "lime"; 186 | padding: 1.2rem 1.5rem; 187 | text-align: left; 188 | } 189 | td:first-child, 190 | th:first-child { 191 | padding-left: 0; 192 | } 193 | td:last-child, 194 | th:last-child { 195 | padding-right: 0; 196 | } 197 | 198 | /*# sourceMappingURL=nanogram.css.map */ 199 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Micropython ESP framework main loop. 3 | 4 | Authors: Aslak Einbu and Torbjørn Pettersen, Jan 2020 5 | """ 6 | 7 | # ------------------------- HOOKUP ------------------------------------------------------------------ 8 | import config 9 | import utime 10 | import wifi 11 | import webserver 12 | import gc 13 | import esp 14 | 15 | utime.sleep(2) # Digesting time for imports 16 | 17 | gc.enable() # Enable RAM management. 18 | gc.collect() # Collecting garbage RAM. 19 | esp.osdebug(config.esp_osdebug) # Setting ESP debug setting from config. 20 | 21 | 22 | # Appending provisioned SSIDs with password to stored networks and attempts to connect. 23 | wifi.add_provisioned_networks() 24 | 25 | if not wifi.wlan_sta.isconnected(): 26 | wifi.scan_networks() # Scanning for wifi networks. 27 | 28 | # Attempts connection to stored wifi networks. Broadcasts accesspoint webserver if no success. 29 | if not wifi.connect_to_known_network(): 30 | server_socket = webserver.ap_webserver_start() 31 | while not wifi.wlan_sta.isconnected(): 32 | webserver.ap_webserver_loop(server_socket) 33 | webserver.ap_webserver_stop(server_socket) 34 | 35 | 36 | # ------------------------- SETUP ------------------------------------------------------------------- 37 | import umqttsimple 38 | import logfile 39 | from scheduler_light import Scheduler, Job 40 | 41 | utime.sleep(1) # Digesting time, imports. 42 | 43 | 44 | log = logfile.LogFile("Mainloop", config.log_filename, config.log_sizelimit, 45 | config.log_level, config.log_print) 46 | 47 | 48 | # Establish MQTT client: 49 | mqtt_client = umqttsimple.MQTTClient(config.client_id, 50 | config.mqtt_broker, 51 | config.mqtt_port, 52 | config.mqtt_user, 53 | config.mqtt_password) 54 | 55 | 56 | # MQTT callbacks: 57 | def callback_mqtt_subscription(topic, msg): 58 | """ Callback upon MQTT publication of subscribed topics. """ 59 | log.debug("MQTT message recieved:") 60 | log.debug('%s,%s' % (topic.decode('utt-8'), msg.decode('utf-8'))) 61 | 62 | 63 | def mqtt_subscriptions(): 64 | """ Subscribe to MQTT topics. """ 65 | log.info("Subscribing to topic 'test'") 66 | if mqtt_client.subscribe("test") == "Error": 67 | log.error("Could not subscribe - broker down?") 68 | 69 | 70 | def mqtt_connect(): 71 | """ Connect to MQTT broker """ 72 | mqtt_client.set_callback(callback_mqtt_subscription) 73 | if mqtt_client.connect() == "Error": 74 | log.error("Could not connect - broker down?") 75 | else: 76 | log.info('Connected to %s MQTT broker ' % config.mqtt_broker) 77 | mqtt_subscriptions() 78 | return mqtt_client 79 | 80 | 81 | def mqtt_publish(topic, message): 82 | if mqtt_client.publish(topic,message) == "Error": 83 | log.error("Could not publish - broker down?") 84 | 85 | 86 | 87 | # Prepares for main loop: 88 | log.debug("Connection to MQTT broker.") 89 | mqtt_connect() 90 | 91 | 92 | # Jobs: 93 | def melding1(): 94 | mqtt_publish("test", "%s : Sender melding!" % str(logfile._timestamp()) ) 95 | 96 | def heartbeat(): 97 | mqtt_publish("/esp8266/%s/heartbeat" % config.device_name, config.heartbeat_message) 98 | 99 | 100 | # Setting scheduled jobs 101 | schedule = Scheduler("Test program", 102 | Job("Testmelding", melding1, 1, -1), 103 | Job("Heartbeat", heartbeat, 5, -1)) 104 | 105 | 106 | # Hardware IO in here: 107 | 108 | # Hardware IO routines... 109 | 110 | # ------------------------- MAIN LOOP -------------------------------------------------------------- 111 | while True: 112 | if wifi.wlan_sta.isconnected(): 113 | # Loop events when connected to wifi: 114 | if mqtt_client.check_msg() == "Error": 115 | log.error("No connection to MQTT broker - attempting reconnect...") 116 | mqtt_connect() 117 | utime.sleep(0.1) 118 | schedule.follow_up_jobs() 119 | else: 120 | # Actions upon lost connection to wifi: 121 | log.warning("No connection to wifi...") 122 | while not wifi.wlan_sta.isconnected(): 123 | log.debug("Attempting wifi reconnect.") 124 | wifi.connect_to_known_network() # Attempt reconnect to wifi upon lost network connection. 125 | log.info("Wifi connection re-established!") 126 | mqtt_connect() 127 | -------------------------------------------------------------------------------- /examples/relayapp/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Micropython ESP MiPy-ESP framework. 3 | Authors: Aslak Einbu & Torbjørn Pettersen 4 | 5 | Application: Legarage MQTT Power relay app. 6 | Author: Aslak Einbu, Jan 2020 7 | """ 8 | 9 | # ------------------------- HOOKUP ------------------------------------------------------------------ 10 | import config 11 | import utime 12 | import wifi 13 | import webserver 14 | import gc 15 | import esp 16 | 17 | utime.sleep(2) # Digesting time for imports 18 | 19 | gc.enable() # Enable RAM management. 20 | gc.collect() # Collecting garbage RAM. 21 | esp.osdebug(config.esp_osdebug) # Setting ESP debug setting from config. 22 | 23 | 24 | # Appending provisioned SSIDs with password to stored networks and attempts to connect. 25 | wifi.add_provisioned_networks() 26 | 27 | if not wifi.wlan_sta.isconnected(): 28 | wifi.scan_networks() # Scanning for wifi networks. 29 | 30 | # Attempts connection to stored wifi networks. Broadcasts accesspoint webserver if no success. 31 | if not wifi.connect_to_known_network(): 32 | server_socket = webserver.ap_webserver_start() 33 | while not wifi.wlan_sta.isconnected(): 34 | webserver.ap_webserver_loop(server_socket) 35 | webserver.ap_webserver_stop(server_socket) 36 | 37 | 38 | # ------------------------- SETUP ------------------------------------------------------------------- 39 | import umqttsimple 40 | import logfile 41 | from scheduler_light import Scheduler, Job 42 | from machine import Pin 43 | from timers import Countdown 44 | 45 | mqtt_timer = Countdown(5) # Timer for checking recieved mqtt message last 5 seconds. 46 | 47 | relay = Pin(14, Pin.OUT) # Relay pin. 48 | relay.off() # Relay off at start. 49 | 50 | utime.sleep(1) # Digesting time, imports. 51 | 52 | 53 | log = logfile.LogFile("Relay-1", config.log_filename, config.log_sizelimit, 54 | config.log_level, config.log_print) 55 | 56 | 57 | # Establish MQTT client: 58 | mqtt_client = umqttsimple.MQTTClient(config.client_id, 59 | config.mqtt_broker, 60 | config.mqtt_port, 61 | config.mqtt_user, 62 | config.mqtt_password) 63 | 64 | 65 | # MQTT callbacks: 66 | def callback_mqtt_subscription(topic, msg): 67 | """ Callback upon MQTT publication of subscribed topics. """ 68 | if topic == b'switches/relay-1/state/read': 69 | mqtt_timer.start() 70 | message = msg.decode('utf-8') 71 | log.debug(message) 72 | if message == "ON": 73 | relay.on() 74 | if message == "OFF": 75 | relay.off() 76 | 77 | 78 | def mqtt_subscriptions(): 79 | """ Subscribe to MQTT topics. """ 80 | log.info("Subscribing to topic 'test'") 81 | if mqtt_client.subscribe("test") == "Error": 82 | log.error("Could not subscribe - broker down?") 83 | 84 | 85 | def mqtt_connect(): 86 | """ Connect to MQTT broker """ 87 | mqtt_client.set_callback(callback_mqtt_subscription) 88 | if mqtt_client.connect() == "Error": 89 | log.error("Could not connect - broker down?") 90 | else: 91 | log.info('Connected to %s MQTT broker ' % config.mqtt_broker) 92 | mqtt_subscriptions() 93 | return mqtt_client 94 | 95 | 96 | def mqtt_publish(topic, message): 97 | if mqtt_client.publish(topic,message) == "Error": 98 | log.error("Could not publish - broker down?") 99 | 100 | 101 | 102 | # Prepares for main loop: 103 | log.debug("Connection to MQTT broker.") 104 | mqtt_connect() 105 | 106 | 107 | 108 | def heartbeat(): 109 | mqtt_publish("/esp8266/%s/heartbeat" % config.device_name, config.heartbeat_message) 110 | 111 | 112 | # Setting scheduled jobs 113 | schedule = Scheduler("Test program", 114 | Job("Heartbeat", heartbeat, 5, -1)) 115 | 116 | 117 | 118 | # ------------------------- MAIN LOOP -------------------------------------------------------------- 119 | while True: 120 | if wifi.wlan_sta.isconnected(): 121 | # Loop events when connected to wifi: 122 | if mqtt_client.check_msg() == "Error": 123 | log.error("No connection to MQTT broker - attempting reconnect...") 124 | mqtt_connect() 125 | utime.sleep(0.1) 126 | schedule.follow_up_jobs() 127 | if mqtt_timer.is_overdue(): 128 | log.debug("No MQTT message in the last 5 secs.") 129 | else: 130 | # Actions upon lost connection to wifi: 131 | log.warning("No connection to wifi...") 132 | while not wifi.wlan_sta.isconnected(): 133 | log.debug("Attempting wifi reconnect.") 134 | wifi.connect_to_known_network() # Attempt reconnect to wifi upon lost network connection. 135 | log.info("Wifi connection re-established!") 136 | mqtt_connect() 137 | -------------------------------------------------------------------------------- /src/core/wifi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ESP microcontroller wifi connection related fuctions. 3 | 4 | Aslak Einbu and Torbjørn Pettersen, Jan 2020. 5 | """ 6 | 7 | import network 8 | import ujson 9 | import os 10 | import config 11 | import machine 12 | import ntptime 13 | import crypt 14 | import utime 15 | import logfile 16 | 17 | log = logfile.LogFile("Wifi-connection", config.log_filename, config.log_sizelimit, 18 | config.log_level, config.log_print) 19 | 20 | networks_stored = "networks_stored.json" # File for storage of applied SSIDs and their passwords 21 | networks_detected = "networks_detected.json" # File for storage of currently present Wifi networks 22 | networks_provisioned = "wifi.json" # File for provisioning of known SSIDs and passwords 23 | 24 | crypt_seed = config.crypt_seed 25 | TypeX = crypt.Crypt(crypt_seed) 26 | 27 | wlan_ap = network.WLAN(network.AP_IF) 28 | wlan_ap.active(False) 29 | 30 | wlan_sta = network.WLAN(network.STA_IF) 31 | wlan_sta.active(False) 32 | 33 | 34 | def read_stored_networks(): 35 | """ Read stored wifi SSIDs and passwords. """ 36 | log.debug("Function - read_stored_networks()") 37 | stored = '{}' 38 | try: 39 | with open(networks_stored, 'r') as f: 40 | stored = f.read() 41 | except: 42 | with open(networks_stored, "w") as f: 43 | f.write(stored) 44 | return ujson.loads(stored) 45 | 46 | 47 | def append_stored_networks(ssid, password): 48 | """ Add SSID and password to stored networks. """ 49 | log.debug("Function - append_stored_networks(%s, password)" % ssid) 50 | stored = read_stored_networks() 51 | stored[ssid] = TypeX.encrypt(password) 52 | log.debug("Adds SSID %s to file with stored networks." % ssid) 53 | with open(networks_stored, "w") as f: 54 | f.write(ujson.dumps(stored)) 55 | 56 | 57 | def write_networks_detected(nettworks): 58 | """ Write wifi-networks detected to file. """ 59 | log.debug("Function - write_networks_detected(networks)") 60 | with open(networks_detected, "w") as f: 61 | f.write(ujson.dumps(nettworks)) 62 | 63 | 64 | def scan_networks(): 65 | """ Scan for wifi networks present. """ 66 | log.debug("Function - scan_networks()") 67 | wlan_sta.active(True) 68 | detected_networks = wlan_sta.scan() 69 | networks = {} 70 | AUTHMODE = {0: "open", 1: "WEP", 2: "WPA-PSK", 71 | 3: "WPA2-PSK", 4: "WPA/WPA2-PSK"} 72 | present = 0 73 | for ssid, bssid, channel, rssi, authmode, hidden in sorted(detected_networks, key=lambda x: x[3], reverse=True): 74 | present += 1 75 | ssid = ssid.decode('utf-8') 76 | networks[ssid] = [channel, rssi, AUTHMODE.get(authmode, '?')] 77 | log.debug("Number of networks present: %s" % str(present)) 78 | write_networks_detected(networks) 79 | wlan_sta.active(False) 80 | return networks 81 | 82 | 83 | def connect_to_wifi(ssid, password, utc_delta=1): 84 | """ Connect to wifi unless already connected. """ 85 | log.debug("Function - connect_to_stored_networks(%s, password)" % ssid) 86 | wlan_sta.active(True) 87 | if wlan_sta.isconnected(): 88 | return True 89 | log.debug('Connecting to %s...' % ssid) 90 | wlan_sta.connect(ssid, password) 91 | for retry in range(100): 92 | connected = wlan_sta.isconnected() 93 | if connected: 94 | break 95 | utime.sleep(0.1) 96 | print('.', end='') 97 | if connected: 98 | log.info("Connected! Network config: %s" % str(wlan_sta.ifconfig())) 99 | append_stored_networks(ssid, password) 100 | ntptime.settime() 101 | yr, mnt, day, wday, hh, mm, sec, ms = machine.RTC().datetime() 102 | hh += utc_delta 103 | machine.RTC().datetime((yr, mnt, day, wday, hh, mm, sec, ms)) 104 | log.info("Local time set to: {}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}". 105 | format(yr, mnt, day, hh, mm, sec)) 106 | else: 107 | log.warning('Unable to connect to ' + ssid) 108 | return connected 109 | 110 | 111 | def connect_to_known_network(): 112 | """ Attempts connects to stored networks. """ 113 | log.debug("Function - connect_to_known_network()") 114 | if not wlan_sta.isconnected(): 115 | stored = read_stored_networks() 116 | for ssid in stored.keys(): 117 | try: 118 | password = TypeX.decrypt(stored[ssid]) 119 | log.debug("Attempting connection to SSID: %s." % ssid) 120 | connect_to_wifi(ssid, password) 121 | if wlan_sta.isconnected(): 122 | return True 123 | except: 124 | pass 125 | log.info("No known nettworks available at the moment!") 126 | return False 127 | return True 128 | 129 | 130 | def add_provisioned_networks(): 131 | """ Adds provisioned networks and deletes the file. """ 132 | log.debug("Function - add_provisioned_networks()") 133 | try: 134 | networks = '{}' 135 | with open(networks_provisioned, 'r') as f: 136 | networks = ujson.loads(f.read()) 137 | for ssid, password in networks.items(): 138 | append_stored_networks(ssid, password) 139 | for ssid, password in networks.items(): 140 | connect_to_wifi(ssid, password) 141 | os.remove(networks_provisioned) 142 | except: 143 | pass 144 | 145 | -------------------------------------------------------------------------------- /src/core/webserver.py: -------------------------------------------------------------------------------- 1 | """ 2 | ESP access point webserver. 3 | 4 | Aslak Einbu, Jan 2020. 5 | """ 6 | 7 | import os 8 | import gc 9 | import socket 10 | import ure 11 | import utime 12 | import config 13 | import logfile 14 | import wifi 15 | 16 | log = logfile.LogFile("Webserver", config.log_filename, config.log_sizelimit, 17 | config.log_level, config.log_print) 18 | 19 | gc.enable() 20 | 21 | files_on_chip = os.listdir() 22 | 23 | mimetypes = {'.html': 'text/html', 24 | '.css': 'text/css', 25 | '.js': 'application/javascript', 26 | '.png': 'image/png', 27 | '.svg': 'image/svg', 28 | '.ico': 'image/vnd.microsoft.icon', 29 | '.py': 'text/html', 30 | '.txt': 'text/css', 31 | '.json': 'application/json'} 32 | 33 | binary = ['.svg', '.png', '.ico'] 34 | 35 | 36 | def send_header(client, status_code=200, content_type='text/html', content_length=None): 37 | """ Send header to socket. """ 38 | client.sendall("HTTP/1.0 {} OK\r\n".format(status_code)) 39 | client.sendall("Content-Type: {}\r\n".format(content_type)) 40 | if content_length is not None: 41 | client.sendall("Content-Length: {}\r\n".format(content_length)) 42 | client.sendall("\r\n") 43 | 44 | 45 | def send_response(client, payload, content_type, status_code=200): 46 | """ Send header + payload to socket. """ 47 | content_length = len(payload) 48 | send_header(client, status_code, content_type, content_length) 49 | if content_length > 0: 50 | client.sendall(payload) 51 | client.close() 52 | gc.collect() 53 | 54 | 55 | def handle_file(client, url): 56 | """ Sends file to socket. """ 57 | log.debug("Handling file {}".format(url)) 58 | for key in mimetypes.keys(): 59 | if url.endswith(key): 60 | content_type = mimetypes[key] 61 | if url[-4:] in binary: 62 | readtype = 'rb' 63 | else: 64 | readtype = 'r' 65 | with open(url, readtype) as f: 66 | fil = f.read() 67 | send_response(client, fil, content_type, status_code=200) 68 | gc.collect() 69 | 70 | 71 | def handle_not_found(client, url): 72 | """ Sends 404 message upon path not found. """ 73 | send_response(client, "Path not found: {}".format(url), 74 | content_type="text/html", status_code=404) 75 | 76 | def url_parse(url): 77 | """ Extracts query params password and ssid from url. """ 78 | query_params = ure.search("\?(.*?) HTTP", url.decode('utf-8')).group(1) 79 | parameters = {} 80 | ampersandsplit = query_params.split("&") 81 | for element in ampersandsplit: 82 | equalsplit = element.split("=") 83 | if equalsplit[1] != "": 84 | parameters[equalsplit[0]] = equalsplit[1] 85 | 86 | codelist = {"%20" : " ", 87 | "%21" : "!", 88 | "%23" : "#", 89 | "%24" : "$", 90 | "%2F" : "/"} 91 | 92 | for key, value in parameters.items(): 93 | for code in codelist.keys(): 94 | value = value.replace(code, codelist[code]).rstrip("/") 95 | parameters[key] = value.replace(code, codelist[code]) 96 | if "password" in parameters and "ssid" in parameters: 97 | return parameters["ssid"], parameters["password"] 98 | else: 99 | return None, None 100 | 101 | 102 | def ap_webserver_start(): 103 | """ Starting access point webserver for serving of files on chip. """ 104 | gc.collect() 105 | 106 | log.debug("Function - ap_webserver_start()") 107 | wifi.wlan_ap.active(True) 108 | wifi.wlan_ap.config(essid=config.ap_ssid, 109 | password=config.ap_password, authmode=3) 110 | 111 | addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] 112 | server_socket = socket.socket() 113 | server_socket.bind(addr) 114 | server_socket.listen(3) 115 | 116 | log.debug('Connect to ssid ' + config.ap_ssid + 117 | ', with password: ' + config.ap_password) 118 | log.debug('Contact access point webserver at 192.168.4.1.') 119 | 120 | utime.sleep(2) 121 | 122 | log.debug("Starting AP webserver!") 123 | return server_socket 124 | 125 | 126 | def ap_webserver_loop(server_socket): 127 | """ Looping access point webserver. """ 128 | log.debug("Function - ap_webserver_loop()") 129 | 130 | client, addr = server_socket.accept() 131 | log.debug('Client connected from {}'.format(addr)) 132 | 133 | try: 134 | client.settimeout(5.0) 135 | request = b"" 136 | try: 137 | while "\r\n\r\n" not in request: 138 | request += client.recv(512) 139 | except OSError: 140 | pass 141 | 142 | log.debug("Request is: {}".format(request)) 143 | if "HTTP" in request: # skip invalid requests 144 | 145 | url = ure.search("(?:GET|POST) /(.*?)(?:\\?.*?)? HTTP", 146 | request).group(1).decode("utf-8").rstrip("/") 147 | 148 | log.debug("URL is {}".format(url)) 149 | 150 | if url == "": 151 | handle_file(client, "index.html") 152 | 153 | elif url == "rescan": 154 | wifi.scan_networks() 155 | handle_file(client, "index.html") 156 | 157 | elif url == "kobletil": 158 | log.debug("Will connect to network...") 159 | ssid, passord = url_parse(request) 160 | wifi.connect_to_wifi(ssid, passord) 161 | 162 | elif url in files_on_chip: 163 | handle_file(client, url) 164 | 165 | else: 166 | handle_not_found(client, url) 167 | except: 168 | pass 169 | client.close() 170 | 171 | 172 | def ap_webserver_stop(server_socket): 173 | """ Shutting down access point webserver. """ 174 | log.debug("Function - ap_webserver_stop()") 175 | log.debug("Shutting down ESP acces point webserver!") 176 | if server_socket: 177 | server_socket.close() 178 | server_socket = None 179 | wifi.wlan_ap.active(False) 180 | return server_socket 181 | -------------------------------------------------------------------------------- /src/core/umqttsimple.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic MicroPython MQTT module. 3 | 4 | Stolen from Rui Santos - randomnerdtutorials.com 5 | Source: https://raw.githubusercontent.com/RuiSantosdotme/ESP-MicroPython/master/code/MQTT/umqttsimple.py 6 | 7 | Edit - Aslak Einbu Jan 2020: 8 | Code edited with hardening of MQTT broker loss by adding several try/excepts 9 | with return of "Error" in order to indentify and handle broker loss without crashing 10 | microcontroller code loop. 11 | 12 | """ 13 | import usocket as socket 14 | import ustruct as struct 15 | # from ubinascii import hexlify 16 | 17 | 18 | class MQTTException(Exception): 19 | pass 20 | 21 | 22 | class MQTTClient: 23 | def __init__(self, client_id, server, port=0, user=None, password=None, 24 | keepalive=0, ssl=False, ssl_params={}): 25 | if port == 0: 26 | port = 8883 if ssl else 1883 27 | self.client_id = client_id 28 | self.sock = None 29 | self.server = server 30 | self.port = port 31 | self.ssl = ssl 32 | self.ssl_params = ssl_params 33 | self.pid = 0 34 | self.cb = None 35 | self.user = user 36 | self.pswd = password 37 | self.keepalive = keepalive 38 | self.lw_topic = None 39 | self.lw_msg = None 40 | self.lw_qos = 0 41 | self.lw_retain = False 42 | 43 | def _send_str(self, s): 44 | self.sock.write(struct.pack("!H", len(s))) 45 | self.sock.write(s) 46 | 47 | def _recv_len(self): 48 | n = 0 49 | sh = 0 50 | while 1: 51 | b = self.sock.read(1)[0] 52 | n |= (b & 0x7f) << sh 53 | if not b & 0x80: 54 | return n 55 | sh += 7 56 | 57 | def set_callback(self, f): 58 | self.cb = f 59 | 60 | def set_last_will(self, topic, msg, retain=False, qos=0): 61 | assert 0 <= qos <= 2 62 | assert topic 63 | self.lw_topic = topic 64 | self.lw_msg = msg 65 | self.lw_qos = qos 66 | self.lw_retain = retain 67 | 68 | def connect(self, clean_session=True): 69 | try: 70 | self.sock = socket.socket() 71 | addr = socket.getaddrinfo(self.server, self.port)[0][-1] 72 | self.sock.connect(addr) 73 | if self.ssl: 74 | import ussl 75 | self.sock = ussl.wrap_socket(self.sock, **self.ssl_params) 76 | premsg = bytearray(b"\x10\0\0\0\0\0") 77 | msg = bytearray(b"\x04MQTT\x04\x02\0\0") 78 | 79 | sz = 10 + 2 + len(self.client_id) 80 | msg[6] = clean_session << 1 81 | if self.user is not None: 82 | sz += 2 + len(self.user) + 2 + len(self.pswd) 83 | msg[6] |= 0xC0 84 | if self.keepalive: 85 | assert self.keepalive < 65536 86 | msg[7] |= self.keepalive >> 8 87 | msg[8] |= self.keepalive & 0x00FF 88 | if self.lw_topic: 89 | sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) 90 | msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 91 | msg[6] |= self.lw_retain << 5 92 | 93 | i = 1 94 | while sz > 0x7f: 95 | premsg[i] = (sz & 0x7f) | 0x80 96 | sz >>= 7 97 | i += 1 98 | premsg[i] = sz 99 | 100 | self.sock.write(premsg, i + 2) 101 | self.sock.write(msg) 102 | #print(hex(len(msg)), hexlify(msg, ":")) 103 | self._send_str(self.client_id) 104 | if self.lw_topic: 105 | self._send_str(self.lw_topic) 106 | self._send_str(self.lw_msg) 107 | if self.user is not None: 108 | self._send_str(self.user) 109 | self._send_str(self.pswd) 110 | resp = self.sock.read(4) 111 | assert resp[0] == 0x20 and resp[1] == 0x02 112 | if resp[3] != 0: 113 | raise MQTTException(resp[3]) 114 | return resp[2] & 1 115 | except: 116 | return "Error" 117 | 118 | def disconnect(self): 119 | self.sock.write(b"\xe0\0") 120 | self.sock.close() 121 | 122 | def ping(self): 123 | self.sock.write(b"\xc0\0") 124 | 125 | def publish(self, topic, msg, retain=False, qos=0): 126 | try: 127 | pkt = bytearray(b"\x30\0\0\0") 128 | pkt[0] |= qos << 1 | retain 129 | sz = 2 + len(topic) + len(msg) 130 | if qos > 0: 131 | sz += 2 132 | assert sz < 2097152 133 | i = 1 134 | while sz > 0x7f: 135 | pkt[i] = (sz & 0x7f) | 0x80 136 | sz >>= 7 137 | i += 1 138 | pkt[i] = sz 139 | #print(hex(len(pkt)), hexlify(pkt, ":")) 140 | self.sock.write(pkt, i + 1) 141 | self._send_str(topic) 142 | if qos > 0: 143 | self.pid += 1 144 | pid = self.pid 145 | struct.pack_into("!H", pkt, 0, pid) 146 | self.sock.write(pkt, 2) 147 | self.sock.write(msg) 148 | if qos == 1: 149 | while 1: 150 | op = self.wait_msg() 151 | if op == 0x40: 152 | sz = self.sock.read(1) 153 | assert sz == b"\x02" 154 | rcv_pid = self.sock.read(2) 155 | rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] 156 | if pid == rcv_pid: 157 | return 158 | elif qos == 2: 159 | assert 0 160 | except: 161 | return "Error" 162 | 163 | def subscribe(self, topic, qos=0): 164 | try: 165 | assert self.cb is not None, "Subscribe callback is not set" 166 | pkt = bytearray(b"\x82\0\0\0") 167 | self.pid += 1 168 | struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) 169 | #print(hex(len(pkt)), hexlify(pkt, ":")) 170 | self.sock.write(pkt) 171 | self._send_str(topic) 172 | self.sock.write(qos.to_bytes(1, "little")) 173 | while 1: 174 | op = self.wait_msg() 175 | if op == 0x90: 176 | resp = self.sock.read(4) 177 | # print(resp) 178 | assert resp[1] == pkt[2] and resp[2] == pkt[3] 179 | if resp[3] == 0x80: 180 | raise MQTTException(resp[3]) 181 | return 182 | except: 183 | return "Error" 184 | 185 | # Wait for a single incoming MQTT message and process it. 186 | # Subscribed messages are delivered to a callback previously 187 | # set by .set_callback() method. Other (internal) MQTT 188 | # messages processed internally. 189 | def wait_msg(self): 190 | res = self.sock.read(1) 191 | self.sock.setblocking(True) 192 | if res is None: 193 | return None 194 | if res == b"": 195 | raise OSError(-1) 196 | if res == b"\xd0": # PINGRESP 197 | sz = self.sock.read(1)[0] 198 | assert sz == 0 199 | return None 200 | op = res[0] 201 | if op & 0xf0 != 0x30: 202 | return op 203 | sz = self._recv_len() 204 | topic_len = self.sock.read(2) 205 | topic_len = (topic_len[0] << 8) | topic_len[1] 206 | topic = self.sock.read(topic_len) 207 | sz -= topic_len + 2 208 | if op & 6: 209 | pid = self.sock.read(2) 210 | pid = pid[0] << 8 | pid[1] 211 | sz -= 2 212 | msg = self.sock.read(sz) 213 | self.cb(topic, msg) 214 | if op & 6 == 2: 215 | pkt = bytearray(b"\x40\x02\0\0") 216 | struct.pack_into("!H", pkt, 2, pid) 217 | self.sock.write(pkt) 218 | elif op & 6 == 4: 219 | assert 0 220 | 221 | # Checks whether a pending message from server is available. 222 | # If not, returns immediately with None. Otherwise, does 223 | # the same processing as wait_msg. 224 | def check_msg(self): 225 | try: 226 | self.sock.setblocking(False) 227 | return self.wait_msg() 228 | except: 229 | return "Error" 230 | -------------------------------------------------------------------------------- /src/drivers/tm1637.py: -------------------------------------------------------------------------------- 1 | """ 2 | MicroPython TM1637 quad 7-segment LED display driver 3 | https://github.com/mcauser/micropython-tm1637 4 | 5 | MIT License 6 | Copyright (c) 2016 Mike Causer 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | from micropython import const 28 | from machine import Pin 29 | from utime import sleep_us, sleep_ms 30 | 31 | TM1637_CMD1 = const(64) # 0x40 data command 32 | TM1637_CMD2 = const(192) # 0xC0 address command 33 | TM1637_CMD3 = const(128) # 0x80 display control command 34 | TM1637_DSP_ON = const(8) # 0x08 display on 35 | TM1637_DELAY = const(10) # 10us delay between clk/dio pulses 36 | TM1637_MSB = const(128) # msb is the decimal point or the colon depending on your display 37 | 38 | # 0-9, a-z, blank, dash, star 39 | _SEGMENTS = bytearray(b'\x3F\x06\x5B\x4F\x66\x6D\x7D\x07\x7F\x6F\x77\x7C\x39\x5E\x79\x71\x3D\x76\x06\x1E\x76\x38\x55\x54\x3F\x73\x67\x50\x6D\x78\x3E\x1C\x2A\x76\x6E\x5B\x00\x40\x63') 40 | 41 | class TM1637(object): 42 | """Library for quad 7-segment LED modules based on the TM1637 LED driver.""" 43 | def __init__(self, clk, dio, brightness=7): 44 | self.clk = clk 45 | self.dio = dio 46 | 47 | if not 0 <= brightness <= 7: 48 | raise ValueError("Brightness out of range") 49 | self._brightness = brightness 50 | 51 | self.clk.init(Pin.OUT, value=0) 52 | self.dio.init(Pin.OUT, value=0) 53 | sleep_us(TM1637_DELAY) 54 | 55 | self._write_data_cmd() 56 | self._write_dsp_ctrl() 57 | 58 | def _start(self): 59 | self.dio(0) 60 | sleep_us(TM1637_DELAY) 61 | self.clk(0) 62 | sleep_us(TM1637_DELAY) 63 | 64 | def _stop(self): 65 | self.dio(0) 66 | sleep_us(TM1637_DELAY) 67 | self.clk(1) 68 | sleep_us(TM1637_DELAY) 69 | self.dio(1) 70 | 71 | def _write_data_cmd(self): 72 | # automatic address increment, normal mode 73 | self._start() 74 | self._write_byte(TM1637_CMD1) 75 | self._stop() 76 | 77 | def _write_dsp_ctrl(self): 78 | # display on, set brightness 79 | self._start() 80 | self._write_byte(TM1637_CMD3 | TM1637_DSP_ON | self._brightness) 81 | self._stop() 82 | 83 | def _write_byte(self, b): 84 | for i in range(8): 85 | self.dio((b >> i) & 1) 86 | sleep_us(TM1637_DELAY) 87 | self.clk(1) 88 | sleep_us(TM1637_DELAY) 89 | self.clk(0) 90 | sleep_us(TM1637_DELAY) 91 | self.clk(0) 92 | sleep_us(TM1637_DELAY) 93 | self.clk(1) 94 | sleep_us(TM1637_DELAY) 95 | self.clk(0) 96 | sleep_us(TM1637_DELAY) 97 | 98 | def brightness(self, val=None): 99 | """Set the display brightness 0-7.""" 100 | # brightness 0 = 1/16th pulse width 101 | # brightness 7 = 14/16th pulse width 102 | if val is None: 103 | return self._brightness 104 | if not 0 <= val <= 7: 105 | raise ValueError("Brightness out of range") 106 | 107 | self._brightness = val 108 | self._write_data_cmd() 109 | self._write_dsp_ctrl() 110 | 111 | def write(self, segments, pos=0): 112 | """Display up to 6 segments moving right from a given position. 113 | The MSB in the 2nd segment controls the colon between the 2nd 114 | and 3rd segments.""" 115 | if not 0 <= pos <= 5: 116 | raise ValueError("Position out of range") 117 | self._write_data_cmd() 118 | self._start() 119 | 120 | self._write_byte(TM1637_CMD2 | pos) 121 | for seg in segments: 122 | self._write_byte(seg) 123 | self._stop() 124 | self._write_dsp_ctrl() 125 | 126 | def encode_digit(self, digit): 127 | """Convert a character 0-9, a-f to a segment.""" 128 | return _SEGMENTS[digit & 0x0f] 129 | 130 | def encode_string(self, string): 131 | """Convert an up to 4 character length string containing 0-9, a-z, 132 | space, dash, star to an array of segments, matching the length of the 133 | source string.""" 134 | segments = bytearray(len(string)) 135 | for i in range(len(string)): 136 | segments[i] = self.encode_char(string[i]) 137 | return segments 138 | 139 | def encode_char(self, char): 140 | """Convert a character 0-9, a-z, space, dash or star to a segment.""" 141 | o = ord(char) 142 | if o == 32: 143 | return _SEGMENTS[36] # space 144 | if o == 42: 145 | return _SEGMENTS[38] # star/degrees 146 | if o == 45: 147 | return _SEGMENTS[37] # dash 148 | if o >= 65 and o <= 90: 149 | return _SEGMENTS[o-55] # uppercase A-Z 150 | if o >= 97 and o <= 122: 151 | return _SEGMENTS[o-87] # lowercase a-z 152 | if o >= 48 and o <= 57: 153 | return _SEGMENTS[o-48] # 0-9 154 | raise ValueError("Character out of range: {:d} '{:s}'".format(o, chr(o))) 155 | 156 | def hex(self, val): 157 | """Display a hex value 0x0000 through 0xffff, right aligned.""" 158 | string = '{:04x}'.format(val & 0xffff) 159 | self.write(self.encode_string(string)) 160 | 161 | def number(self, num): 162 | """Display a numeric value -999 through 9999, right aligned.""" 163 | # limit to range -999 to 9999 164 | num = max(-999, min(num, 9999)) 165 | string = '{0: >4d}'.format(num) 166 | self.write(self.encode_string(string)) 167 | 168 | def numbers(self, num1, num2, colon=True): 169 | """Display two numeric values -9 through 99, with leading zeros 170 | and separated by a colon.""" 171 | num1 = max(-9, min(num1, 99)) 172 | num2 = max(-9, min(num2, 99)) 173 | segments = self.encode_string('{0:0>2d}{1:0>2d}'.format(num1, num2)) 174 | if colon: 175 | segments[1] |= 0x80 # colon on 176 | self.write(segments) 177 | 178 | def temperature(self, num): 179 | if num < -9: 180 | self.show('lo') # low 181 | elif num > 99: 182 | self.show('hi') # high 183 | else: 184 | string = '{0: >2d}'.format(num) 185 | self.write(self.encode_string(string)) 186 | self.write([_SEGMENTS[38], _SEGMENTS[12]], 2) # degrees C 187 | 188 | def show(self, string, colon=False): 189 | segments = self.encode_string(string) 190 | if len(segments) > 1 and colon: 191 | segments[1] |= 128 192 | self.write(segments[:4]) 193 | 194 | def scroll(self, string, delay=250): 195 | segments = string if isinstance(string, list) else self.encode_string(string) 196 | data = [0] * 8 197 | data[4:0] = list(segments) 198 | for i in range(len(segments) + 5): 199 | self.write(data[0+i:4+i]) 200 | sleep_ms(delay) 201 | 202 | 203 | class TM1637Decimal(TM1637): 204 | """Library for quad 7-segment LED modules based on the TM1637 LED driver. 205 | 206 | This class is meant to be used with decimal display modules (modules 207 | that have a decimal point after each 7-segment LED). 208 | """ 209 | 210 | def encode_string(self, string): 211 | """Convert a string to LED segments. 212 | 213 | Convert an up to 4 character length string containing 0-9, a-z, 214 | space, dash, star and '.' to an array of segments, matching the length of 215 | the source string.""" 216 | segments = bytearray(len(string.replace('.',''))) 217 | j = 0 218 | for i in range(len(string)): 219 | if string[i] == '.' and j > 0: 220 | segments[j-1] |= TM1637_MSB 221 | continue 222 | segments[j] = self.encode_char(string[i]) 223 | j += 1 224 | return segments 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](./images/mipyesp_logo.svg "AP webserver screenshot" ) 2 | 3 | ##### - a basic MicroPython framework for microcontroller IoT projects! 4 | 5 | [![Followers](https://img.shields.io/badge/Version-v.1.05-silver)](https://bitbucket.org/Legarage/micropython_webserver/src/master/) 6 | [![Developers](https://img.shields.io/badge/Dev_Team-Bluebell_Buggers-blue)](https://www.yr.no/sted/Norge/Tr%C3%B8ndelag/Trondheim/Bl%C3%A5klokkevegen/time_for_time.html) 7 | [![Developers](https://img.shields.io/github/languages/code-size/aslake/mipy_esp)]() 8 | [![Framework](https://img.shields.io/badge/Micropython-v.1.12-darkgreen)](https://github.com/micropython/micropython) 9 | [![Platform](https://img.shields.io/badge/Tested_on-ESP8266-darkgreen)](https://www.espressif.com/en/products/hardware/esp8266ex/overview) 10 | [![Platform](https://img.shields.io/badge/Tested_on-ESP32-darkgreen)](https://www.espressif.com/en/products/hardware/esp32/overview) 11 | [![Platform](https://img.shields.io/github/issues/aslake/mipy_esp)](https://github.com/aslake/mipy_esp/issues) 12 | [![Followers](https://img.shields.io/github/followers/aslake?style=social)](https://en.wikipedia.org/wiki/Fan_club) 13 | 14 | 15 | ### Background 16 | 17 | [MicroPython](http://docs.micropython.org/en/latest/) is a lean and efficient implementation of 18 | the Python programming language that includes a small subset of the Python standard library 19 | and is optimised to run on microcontrollers. 20 | 21 | The [ESP8266](https://www.espressif.com/en/products/hardware/esp8266ex/overview) and 22 | [ESP32](https://www.espressif.com/en/products/hardware/esp32/overview) are a low-cost Wi-Fi 23 | microchips with full TCP/IP stack and microcontroller capability. 24 | 25 | 26 | ### The project 27 | 28 | The **MiPy-ESP** code repository is a flexible framework for full-stack Python 29 | IoT projects on the ESP-family microcontrollers. 30 | 31 | The framework is developed by the **LeGarage Technical Comittee Software Developer Team** 32 | ([LG-TC-SWDT-01](https://legarage.wordpress.com/)) aiming at replacing already established 33 | C++ based code for our microcontroller applications. 34 | 35 | The project provides basic features such as network connection procedures, 36 | access point webserver, MQTT functionalities, logging/debugging, event scheduling, 37 | hardware I/O and global configuration. More detailed documentation can be found in [this](https://www.instructables.com/id/Getting-Started-With-Python-for-ESP8266-ESP32/) post on www.instructables.com. 38 | 39 | This software is gradually becoming the backbone of all our hobby electronics IoT projects 40 | involving ESP-family microcontrollers. The main development and testing was done on esp8266 boards. 41 | Preliminary testing indicate that the code runs on esp32 boards without any modifications. 42 | 43 | 44 | ### Repository structure 45 | 46 | All Python framework modules are found in the /src folder. 47 | The _/src/main.py_ and _/src/config.py_ files contain the application-specific code in the framework. 48 | Upon chip boot, _main.py_ runs and imports all the project-dependency modules with the given inputs from _config.py_. 49 | Examples of project-specific code can be found in the _/examples_ folder. 50 | 51 | For code deployment, a [MicroPython cross compiler](https://github.com/micropython/micropython/tree/master/mpy-cross) 52 | compiles the .py scripts to a binary container file format .mpy prior to chip upload. 53 | 54 | The provided Makefile can be applied for preparing files for transfer to chip by the following procedure with 55 | preparing a project staging area in /src: 56 | 57 | - Edit _src/main.py_ and _src/config.py_ for your specific project. 58 | - Copy the wanted project utilities from _src/utilities_ to _src_ 59 | - Copy the needed hardware drivers (leds.py, buttons.py etc.) of your project from _src/drivers_ to _src/_ 60 | 61 | compilation of framework modules and transfers of the needed files to the build/ folder for upload, is performed by the command: 62 | ``` 63 | make build 64 | ``` 65 | File generated in build/ are ready for transfer to microcontroller. 66 | The command: 67 | ``` 68 | make clean 69 | ``` 70 | Deletes the build/ folder and its contents. 71 | 72 | The repository folder structure below lists the current modules of the framework. Most available MicroPython hardware libraries can 73 | go into the _drivers/_ folder without any modifications. All present drivers are tested with the MiPy-ESP framework. Regarding modules in 74 | the _utilities/_ folder, more will be added as they come to life. 75 | 76 | ``` 77 | src 78 | │ 79 | ├── main.py # Main code loop 80 | ├── config.py # Global config file 81 | │ 82 | ├── core # CORE FRAMEWORK MODULES 83 | │ ├── boot.py 84 | │ ├── crypt.py # Encryption for stored SSID passwords 85 | │ ├── index.html # Main page for wifi login 86 | │ ├── logfile.py # Logging module 87 | │ ├── microajax.js # Lightweight Javascript AJAX 88 | │ ├── nanogram.min.css # Lightweight CSS framework 89 | │ ├── scheduler_light.py # Lightweight event scheduler 90 | │ ├── umqttsimple.py # MQTT module 91 | │ ├── webserver.py # ESP access point webserver 92 | │ └── wifi.py # Network connection module 93 | │ 94 | ├── utilities # OPTIONAL UTILITY MODULES 95 | │ └─── timers.py # Various timer utilities 96 | │ 97 | └── drivers # PROJECT SPECIFIC HARDWARE MODULES 98 | │ 99 | ├── bmp180.py # Module for bmp180 T/P sensor 100 | ├── buttons.py # Module for button interaction 101 | ├── leds.py # Module for led interaction 102 | └── tm1637.py # Module for TM1637 4 digit led display 103 | 104 | . 105 | └── build # TOTAL PROJECT CODE FOR CHIP UPLOAD 106 | 107 | . 108 | └── examples # SPECIFIC PROJECT CODE FOR CHIP UPLOAD [to be added to /build] 109 | 110 | ``` 111 | 112 | ### Framework architecture 113 | 114 | The flowchart below describes the layout of the _main.py_ module: 115 | Upon chip startup, the code attempts connection to wifi. 116 | When a connection is established, callbacks and jobs are set up and 117 | MQTT-client connects to a remote broker. The code then goes into a main loop, which 118 | serves hardware I/O, job events, MQTT subscriptions 119 | while maintaining chip network and MQTT broker connections. 120 | 121 | ![alt text](./images/architecture.svg "Code flowchart") 122 | 123 | The microcontroller code attempts connection with known networks upon chip boot. 124 | All previously connected network SSIDs are stored together with their encrypted passwords 125 | in chip flash memory. Networks and passwords can also be provisioned to the chip by uploading 126 | _wifi.json_ in the format {"SSID1":"PASSWORD1", "SSID2":"PASSWORD2"} to the chip. 127 | These networks are stored and passwords encrypted before the file is deleted from memory. 128 | Note - information to decrypt the passwords is available on the chip, so at least with physical 129 | access to the chip the stored wifi passwords should be considered compromised. 130 | 131 | If no known networks are detected by the chip upon startup, an access point is broadcasted from the chip 132 | with a webserver for configuring wifi-settings. The access point and webserver shuts down upon 133 | successful connection of the chip to wifi. 134 | 135 | 136 | ### ESP access point webserver 137 | 138 | The access point webserver module enables generic access point serving of files 139 | uploaded to the chip. 140 | Static .html files apply tailored CSS style for nice web interfaces; 141 | The Nanogram CSS is a lightweight module based on the 142 | [Milligram](https://milligram.io/) CSS framework. 143 | Custom versions of minified Nanogram can be compiled from .sass source in the _/css_ folder. 144 | Webpages apply Microajax, a lightweight javascript for asynchronous interaction 145 | with backend files in chip flash memory and for smooth updating of webpages. 146 | Below is an exampe screenshot of a webpage served from the MiPy-ESP chip webserver. 147 | 148 | ![alt text](./images/skjermskudd.png "AP webserver screenshot" ) 149 | 150 | 151 | ### Configuration 152 | A project specific configuration file _config.py_ belongs to the _main.py_ script. 153 | In the config file, global application parameters are set: level of debugging and 154 | logger settings, access point name and password, MQTT client and broker settings, 155 | device name and framework version (git commit id) applied amongst other. 156 | It is generally reccommended to upload the _main.py_ and _config.py_ in readable 157 | .py format to the chip, in order to enable inspection of the main code and its 158 | configuration in case of software upgrade or debugging of deployed hardware. 159 | 160 | 161 | ### Getting started 162 | Using [esptool.py](https://github.com/espressif/esptool) you can erase 163 | the ESP flash memory with the command: 164 | 165 | ``` 166 | esptool.py --port /dev/ttyUSB0 erase_flash 167 | ``` 168 | 169 | Current project master branch of the repo has been tested and is operational with Micropython 170 | v.1.12 171 | 172 | Download file 173 | '[esp8266-20191220-v1.12.bin](https://micropython.org/resources/firmware/esp8266-20191220-v1.12.bin)' 174 | from [Micropython](https://micropython.org/) website and write to chip using : 175 | 176 | ``` 177 | esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect 0 esp8266-20191220-v1.12.bin 178 | ``` 179 | 180 | Upload all files in /build directory to ESP chip using for example 181 | [rshell](https://github.com/dhylands/rshell) - and the chip is ready 182 | to launch... 183 | 184 | Use rshell to transfer files to the microcontroller and run the files on the chip: 185 | 186 | ``` 187 | rshell -p /dev/ttyUSB0 188 | cp *.mpy /pyboard # to easy transfer files to the microcontroller board 189 | ls /pyboard # to list files on the microcontroller board 190 | repl # to start the interactive interpreter mode and test code on the chip 191 | ``` 192 | 193 | ### Ongoing developments 194 | The project is work in progress. The software is provided "as is", without warranty of any kind. 195 | No deadlines exist for the development, but plenty of IoT deployments are in the pipeline... 196 | 197 | Complete repository [issue tracker](https://bitbucket.org/Legarage/micropython_webserver/issues) 198 | can be found in the GitHub repository. 199 | 200 | 201 | ### Resources, credits and inspiration 202 | 203 | - https://micropython.org/ 204 | - https://github.com/dhylands/rshell 205 | - https://randomnerdtutorials.com/ : src/umqttsimple.py 206 | - https://github.com/rguillon/schedule/ : inspiration for src/scheduler_light.py 207 | - https://github.com/mcauser/micropython-tm1637 : src/tm1637.py 208 | 209 | 210 | ### Authors: 211 | 212 | Aslak Einbu & Torbjørn Pettersen 213 | 214 | 215 | ### Contributing 216 | 217 | All members and potential prospects of LeGarage Technical Committee are invited to contribute 218 | to the MiPy-ESP project. 219 | Contributors are encouraged to establish development branches and to 220 | perform successful testing prior to submitting pull requests to master. 221 | -------------------------------------------------------------------------------- /images/micropython_logo_black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 28 | 35 | 36 | 39 | 42 | 49 | 50 | 51 | 54 | 61 | 62 | 63 | 81 | 83 | 84 | 86 | image/svg+xml 87 | 89 | 90 | 91 | 92 | 93 | 98 | 106 | 116 | 117 | 118 | --------------------------------------------------------------------------------