├── www └── index.html ├── letters.py ├── pymakr.conf ├── boot.py ├── config.py.sample ├── .gitignore ├── README.md ├── steppers.py ├── main.py ├── LICENSE ├── ina219.py └── microWebSrv.py /www/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /letters.py: -------------------------------------------------------------------------------- 1 | characters = {".": [[0, 0, 0, 0, 0], [0, 0, 1, 0, 0], [0, 1, 1, 1, 0], [0, 0, 1, 0, 0], [0, 0, 0, 0, 0]], 2 | " ": [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]} -------------------------------------------------------------------------------- /pymakr.conf: -------------------------------------------------------------------------------- 1 | { 2 | "address": "/dev/ttyUSB0", 3 | "username": "micro", 4 | "password": "python", 5 | "open_on_start": true, 6 | "safe_boot_on_upload": false, 7 | "sync_file_types":"py, html", 8 | "py_ignore": [ 9 | "pymakr.conf", 10 | ".vscode", 11 | ".gitignore", 12 | ".git", 13 | "project.pymakr", 14 | "env", 15 | "venv", 16 | ".python-version", 17 | ".micropy/", 18 | "micropy.json" 19 | ], 20 | "fast_upload": false 21 | } -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | # boot.py 2 | import config 3 | import network 4 | import utime 5 | import ntptime 6 | import machine 7 | 8 | ## ftp access 9 | #from ftp import ftpserver 10 | 11 | machine.freq(240000000) 12 | 13 | def do_connect(): 14 | sta_if = network.WLAN(network.STA_IF) 15 | start = utime.time() 16 | timed_out = False 17 | 18 | if not sta_if.isconnected(): 19 | print('connecting to network...') 20 | sta_if.active(True) 21 | sta_if.connect(config.wifi["ssid"], config.wifi["password"]) 22 | while not sta_if.isconnected() and \ 23 | not timed_out: 24 | if utime.time() - start >= 20: 25 | timed_out = True 26 | else: 27 | pass 28 | 29 | if sta_if.isconnected(): 30 | ntptime.settime() 31 | print('network config:', sta_if.ifconfig()) 32 | else: 33 | print('internet not available') 34 | 35 | do_connect() -------------------------------------------------------------------------------- /config.py.sample: -------------------------------------------------------------------------------- 1 | # Copyright 2020 LeMaRiva|tech lemariva.com 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | app = { 16 | 17 | } 18 | 19 | device = { 20 | 'ina_scl': 26, 21 | 'ina_sda': 32, 22 | 'shunt_ohms': 0.1, 23 | 'max_ma_focus': 2, 24 | 'max_ma_aperture': 2, 25 | 'm1_enable': 25, 26 | 'm1_step': 33, 27 | 'm1_dir': 23, 28 | 'm2_enable': 21, 29 | 'm2_step': 19, 30 | 'm2_dir': 22, 31 | 'margin': 50, 32 | 'pwm_freq': 500 33 | } 34 | 35 | 36 | wifi = { 37 | 'ssid':'', 38 | 'password':'' 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # configuration 133 | config.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autofocus for the 16mm telephoto lens mounted on a Raspberry Pi HQ Camera. 2 | As you may have already noticed, the Raspberry Pi HQ Camera lenses don't have any autofocus functionality. This project includes the hardware design, firmware and software to add autofocus functionality to those lenses. In this case, I use the 16mm telephoto lens. 3 | The project is divided into two repositories. This repository includes the code of the M5Stack firmware whereas [lemariva/rPIFocus](https://github.com/lemariva/rPIFocus) includes the code for the Raspberry Pi, in which the Microservices application runs. 4 | 5 | A detailed article about the application can be found on [Raspberry Pi HQ Camera: Autofocus for the Telephoto Lens (JiJi)](https://lemariva.com/blog/2020/12/raspberry-pi-hq-camera-autofocus-telephoto-lens). 6 | 7 | ## Video 8 | [![Autofocus for the Raspberry HQ Camera](https://img.youtube.com/vi/PrbyPmq_Z7Q/0.jpg)](https://www.youtube.com/watch?v=PrbyPmq_Z7Q) 9 | 10 | ## Photo examples 11 | | | | | | 12 | |:--------:|:--------:|:--------:|:--------:| 13 | |Focus Type: Box - Background focused|Focus Type: Box - Nanoblock bird focused|Focus Type: Box - Nanoblock bird focused. Diff. illum & cam. aperture|Focus Type: Object detector - Teddy bear focused| 14 | |Focus Type: Box
Background focused (download)|Focus Type: Box
Nanoblock bird focused (download)|Focus Type: Box
Nanoblock bird focused.
Diff. illum & cam. aperture (download)|Focus Type: Object detector
Teddy bear focused (download)| 15 | 16 | ## Simple PCB schematic 17 | Inside the folder [`pcb`](https://github.com/lemariva/rPIFocus/tree/main/pcb), you'll find the board and schematic files (Eagle), to order your PCB. I also added the Gerber files that I used by jlcpcb. 18 | 19 | ## M5Stack Application 20 | The M5Stack ATOM Matrix controls the motors and offers a RestAPI to receive the commands. The M5Stack application is programmed in MicroPython. If you haven't heard about MicroPython, you can check this tutorial: [Getting Started with MicroPython on ESP32, M5Stack, and ESP8266](https://lemariva.com/blog/2020/03/tutorial-getting-started-micropython-v20). MicroPython is a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library and is optimized to run on microcontrollers and in "constrained environments". 21 | The application is located [lemariva/uPyFocus](https://github.com/lemariva/uPyFocus). 22 | 23 | So, follow these steps to upload the application to the M5Stack: 24 | 1. Flash MicroPython to the M5Stack as described in [this tutorial](https://lemariva.com/blog/2020/03/tutorial-getting-started-micropython-v20). 25 | 2. Clone the [lemariva/uPyFocus](https://github.com/lemariva/uPyFocus) repository: 26 | ```sh 27 | git clone https://github.com/lemariva/uPyFocus.git 28 | ``` 29 | 3. Open the folder `uPyFocus` with VSCode and rename the `config.py.sample` to `config.py`. 30 | 4. Open the file and add your Wi-Fi credentials in this section: 31 | ```python 32 | wifi = { 33 | 'ssid':'', 34 | 'password':'' 35 | } 36 | ``` 37 | The M5Stack needs to connect to your Wi-Fi so that the Raspberry Pi (also connected to your Wi-Fi/LAN) can find it and sends the commands to control the steppers. 38 | 5. Upload the application to the M5Stack. 39 | 40 | After uploading the code, the M5Stack resets and starts with the calibration routine. Take note of the IP that the M5Stack reports while connecting to your Wi-Fi. You'll need that to configure the Microservices Application on the Raspberry Pi. -------------------------------------------------------------------------------- /steppers.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | from machine import PWM 3 | from machine import Timer 4 | import utime 5 | 6 | class Stepper: 7 | """ 8 | Handles A4988 hardware driver for bipolar stepper motors 9 | """ 10 | 11 | def __init__(self, motor_id, dir_pin, step_pin, enable_pin, freq=1000, full_steps=1910): 12 | self.step_pin = step_pin 13 | self.dir_pin = dir_pin 14 | self.enable_pin = enable_pin 15 | self.freq = freq 16 | self.full_steps = full_steps 17 | 18 | self.step_pin.init(Pin.OUT) 19 | self.dir_pin.init(Pin.OUT) 20 | self.enable_pin.init(Pin.OUT) 21 | self.enable_pin.on() 22 | 23 | self.dir = 0 24 | self.steps = 0 25 | self.count = 0 26 | 27 | self.pwm = PWM(self.step_pin, freq=self.freq, duty=1024) 28 | self.tim = Timer(motor_id) 29 | 30 | self.tim.init(period=1, mode=Timer.PERIODIC, callback=self.do_step) 31 | self.done = False 32 | 33 | def do_step(self, t): # called by timer interrupt every (freq/1000)ms 34 | if self.count == 0: 35 | self.pwm.duty(512) 36 | elif self.count >= self.steps: 37 | self.set_off() 38 | self.done = True 39 | 40 | if self.count != -1: 41 | self.count = self.count + self.freq/1000 42 | 43 | def set_motion(self, steps): 44 | self.set_on() 45 | self.done = False 46 | # set direction 47 | if steps > 0: 48 | self.dir = 1 49 | self.dir_pin.on() 50 | self.enable_pin.off() 51 | elif steps<0: 52 | self.dir = -1 53 | self.dir_pin.off() 54 | self.enable_pin.off() 55 | else: 56 | self.dir = 0 57 | # set steps 58 | self.count = 0 59 | self.steps = abs(steps) 60 | 61 | def set_on(self): 62 | self.enable_pin.off() 63 | 64 | def set_off(self): 65 | self.enable_pin.on() 66 | self.pwm.duty(1024) 67 | self.count = -1 68 | self.steps = 0 69 | self.done = True 70 | 71 | def get_step(self): 72 | return self.count 73 | 74 | def get_status(self): 75 | return self.done 76 | 77 | 78 | class Axis: 79 | def __init__(self, axis, ina, max_current, margin=50): 80 | self.axes = axis 81 | self.ina = ina 82 | self.margin = margin 83 | self.max_steps = 0 84 | self.max_current = max_current 85 | self.actual_position = 0 86 | self.calibrated = False 87 | 88 | def calibration(self): 89 | rotation = self.axes.full_steps * 2 90 | 91 | self.axes.set_on() 92 | utime.sleep_ms(2000) 93 | # avoid -> maximum recursion depth exceeded 94 | #current = self.ina.current() 95 | self.ina._handle_current_overflow() 96 | self.current_mean = self.ina._current_register() * self.ina._current_lsb * 1000 97 | 98 | # detection homing 99 | self.axes.set_motion(rotation) 100 | while not self.axes.get_status(): 101 | # avoid -> maximum recursion depth exceeded 102 | #current = self.ina.current() 103 | self.ina._handle_current_overflow() 104 | current = self.ina._current_register() * self.ina._current_lsb * 1000 105 | if current > self.current_mean + self.max_current: 106 | self.axes.set_off() 107 | utime.sleep_ms(100) 108 | # detection maximum 109 | self.axes.set_motion(-rotation) 110 | while not self.axes.get_status(): 111 | # avoid -> maximum recursion depth exceeded 112 | #current = self.ina.current() 113 | self.ina._handle_current_overflow() 114 | current = self.ina._current_register() * self.ina._current_lsb * 1000 115 | if current > self.current_mean + self.max_current: 116 | self.max_steps = self.axes.get_step() 117 | self.axes.set_off() 118 | 119 | self.axes.set_motion(self.margin) 120 | 121 | self.actual_position = self.max_steps - self.margin 122 | 123 | self.calibrated = True 124 | 125 | return self.max_steps 126 | 127 | def move(self, steps, block=False): 128 | wait = 0 129 | if not self.calibrated: 130 | return False 131 | 132 | position = self.actual_position + steps 133 | 134 | if position > self.max_steps - self.margin: 135 | return False 136 | if position < self.margin: 137 | return False 138 | else: 139 | if self.axes.done: 140 | self.axes.set_motion(-steps) 141 | self.actual_position = position 142 | if block: 143 | while not self.axes.done: 144 | wait = wait + 1 145 | return True 146 | return False 147 | 148 | def move_max(self): 149 | position = self.max_steps - self.actual_position 150 | self.move(position) 151 | 152 | def move_min(self): 153 | position = self.actual_position 154 | self.move(-position) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import config 2 | import gc 3 | import json 4 | import utime 5 | import neopixel 6 | from machine import Pin, I2C 7 | from ina219 import INA219 8 | from steppers import Stepper, Axis 9 | from logging import ERROR 10 | from letters import characters 11 | from microWebSrv import MicroWebSrv 12 | 13 | # lock 14 | lock1 = False 15 | 16 | # ina initialization 17 | i2c = I2C(-1, Pin(config.device['ina_scl']), Pin(config.device['ina_sda'])) 18 | ina = INA219(config.device['shunt_ohms'], i2c, log_level=ERROR) 19 | ina.configure() 20 | 21 | # leds 22 | np = neopixel.NeoPixel(machine.Pin(27), 25) 23 | 24 | # steppers initialization 25 | m1 = Stepper(0, Pin(config.device['m1_dir']), 26 | Pin(config.device['m1_step']), 27 | Pin(config.device['m1_enable']), 28 | config.device['pwm_freq']) 29 | 30 | m0 = Stepper(1, Pin(config.device['m2_dir']), 31 | Pin(config.device['m2_step']), 32 | Pin(config.device['m2_enable']), 33 | config.device['pwm_freq']) 34 | 35 | # axis initialization 36 | aperture = Axis(m0, ina, 37 | config.device['max_ma_aperture'], config.device['margin']) 38 | focus = Axis(m1, ina, 39 | config.device['max_ma_focus'], config.device['margin']) 40 | 41 | 42 | def write_2leds(letter, color): 43 | rgb = color 44 | char_matrix = characters.get(letter) 45 | led_counter = 0 46 | for row in char_matrix: 47 | for led in row: 48 | if(led): 49 | np[led_counter] = rgb 50 | else: 51 | np[led_counter] = (0, 0, 0) 52 | led_counter += 1 53 | np.write() 54 | 55 | # axis calibration 56 | write_2leds(".", (3, 0, 0)) 57 | current = ina.current() 58 | write_2leds(".", (0, 0, 5)) 59 | aperture.calibration() 60 | utime.sleep_ms(1000) 61 | write_2leds(".", (0, 3, 0)) 62 | focus.calibration() 63 | write_2leds(" ", (0, 0, 0)) 64 | 65 | # webserver functions 66 | def _httpHandlerMemory(httpClient, httpResponse, routeArgs): 67 | print("In Memory HTTP variable route :") 68 | query = str(routeArgs['query']) 69 | 70 | if 'gc' in query or 'collect' in query: 71 | gc.collect() 72 | 73 | content = """\ 74 | {} 75 | """.format(gc.mem_free()) 76 | httpResponse.WriteResponseOk(headers=None, 77 | contentType="text/html", 78 | contentCharset="UTF-8", 79 | content=content) 80 | 81 | 82 | def _httpHandlerGetStatus(httpClient, httpResponse, routeArgs): 83 | global focus, aperture 84 | 85 | mtype = routeArgs['mtype'] 86 | 87 | if 'focus' in mtype: 88 | max_steps = focus.max_steps 89 | calibrated = focus.calibrated 90 | actual_position = focus.actual_position 91 | elif 'aperture' in mtype: 92 | max_steps = aperture.max_steps 93 | calibrated = aperture.calibrated 94 | actual_position = aperture.actual_position 95 | 96 | data = { 97 | 'mtype': mtype, 98 | 'max_steps': max_steps, 99 | 'calibrated': calibrated, 100 | 'position': actual_position 101 | } 102 | 103 | httpResponse.WriteResponseOk(headers=None, 104 | contentType="text/html", 105 | contentCharset="UTF-8", 106 | content=json.dumps(data)) 107 | gc.collect() 108 | 109 | 110 | def _httpHandlerSetCalibration(httpClient, httpResponse, routeArgs): 111 | global focus, aperture 112 | 113 | mtype = routeArgs['mtype'] 114 | 115 | if 'focus' in mtype: 116 | max_steps = focus.calibration() 117 | position = focus.actual_position 118 | calibrated = focus.calibrated 119 | elif 'aperture' in mtype: 120 | max_steps = aperture.calibration() 121 | position = aperture.actual_position 122 | calibrated = aperture.calibrated 123 | 124 | data = { 125 | 'mtype': mtype, 126 | 'max_steps': max_steps, 127 | 'calibrated': calibrated, 128 | 'position': position 129 | } 130 | 131 | httpResponse.WriteResponseOk(headers=None, 132 | contentType="text/html", 133 | contentCharset="UTF-8", 134 | content=json.dumps(data)) 135 | gc.collect() 136 | 137 | def _httpHandlerSetMove(httpClient, httpResponse, routeArgs): 138 | global focus, aperture, lock1 139 | 140 | mtype = routeArgs['mtype'] 141 | steps = int(routeArgs['steps']) 142 | clockwise = -1 if int(routeArgs['clockwise']) == 0 else 1 143 | status = 0 144 | position = 0 145 | 146 | if 'focus' in mtype: 147 | write_2leds(".", (0, 3, 0)) 148 | status = focus.move(clockwise * steps, 1) 149 | position = focus.actual_position 150 | elif 'aperture' in mtype: 151 | write_2leds(".", (0, 0, 5)) 152 | status = aperture.move(clockwise * steps, 1) 153 | position = aperture.actual_position 154 | write_2leds(" ", (0, 0, 0)) 155 | 156 | data = { 157 | 'mtype': mtype, 158 | 'steps': steps, 159 | 'status': status, 160 | 'clockwise': clockwise, 161 | 'position': position 162 | } 163 | 164 | httpResponse.WriteResponseOk(headers=None, 165 | contentType="text/html", 166 | contentCharset="UTF-8", 167 | content=json.dumps(data)) 168 | 169 | gc.collect() 170 | 171 | routeHandlers = [ 172 | ("/memory/", "GET", _httpHandlerMemory), 173 | ("/status/", "GET", _httpHandlerGetStatus), 174 | ("/calibration/", "GET", _httpHandlerSetCalibration), 175 | ("/move///", "GET", _httpHandlerSetMove) 176 | ] 177 | 178 | mws = MicroWebSrv(routeHandlers=routeHandlers, webPath="www/") 179 | mws.Start(threaded=True) 180 | gc.collect() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ina219.py: -------------------------------------------------------------------------------- 1 | """MicroPython library for the INA219 sensor. 2 | 3 | This library supports the INA219 sensor from Texas Instruments with 4 | MicroPython using the I2C bus. 5 | """ 6 | import logging 7 | import utime 8 | from math import trunc 9 | from micropython import const 10 | 11 | 12 | class INA219: 13 | """Provides all the functionality to interact with the INA219 sensor.""" 14 | 15 | RANGE_16V = const(0) # Range 0-16 volts 16 | RANGE_32V = const(1) # Range 0-32 volts 17 | 18 | GAIN_1_40MV = const(0) # Maximum shunt voltage 40mV 19 | GAIN_2_80MV = const(1) # Maximum shunt voltage 80mV 20 | GAIN_4_160MV = const(2) # Maximum shunt voltage 160mV 21 | GAIN_8_320MV = const(3) # Maximum shunt voltage 320mV 22 | GAIN_AUTO = const(-1) # Determine gain automatically 23 | 24 | ADC_9BIT = const(0) # 9-bit conversion time 84us. 25 | ADC_10BIT = const(1) # 10-bit conversion time 148us. 26 | ADC_11BIT = const(2) # 11-bit conversion time 2766us. 27 | ADC_12BIT = const(3) # 12-bit conversion time 532us. 28 | ADC_2SAMP = const(9) # 2 samples at 12-bit, conversion time 1.06ms. 29 | ADC_4SAMP = const(10) # 4 samples at 12-bit, conversion time 2.13ms. 30 | ADC_8SAMP = const(11) # 8 samples at 12-bit, conversion time 4.26ms. 31 | ADC_16SAMP = const(12) # 16 samples at 12-bit,conversion time 8.51ms 32 | ADC_32SAMP = const(13) # 32 samples at 12-bit, conversion time 17.02ms. 33 | ADC_64SAMP = const(14) # 64 samples at 12-bit, conversion time 34.05ms. 34 | ADC_128SAMP = const(15) # 128 samples at 12-bit, conversion time 68.10ms. 35 | 36 | __ADC_CONVERSION = { 37 | ADC_9BIT: "9-bit", 38 | ADC_10BIT: "10-bit", 39 | ADC_11BIT: "11-bit", 40 | ADC_12BIT: "12-bit", 41 | ADC_2SAMP: "12-bit, 2 samples", 42 | ADC_4SAMP: "12-bit, 4 samples", 43 | ADC_8SAMP: "12-bit, 8 samples", 44 | ADC_16SAMP: "12-bit, 16 samples", 45 | ADC_32SAMP: "12-bit, 32 samples", 46 | ADC_64SAMP: "12-bit, 64 samples", 47 | ADC_128SAMP: "12-bit, 128 samples" 48 | } 49 | 50 | __ADDRESS = 0x40 51 | 52 | __REG_CONFIG = 0x00 53 | __REG_SHUNTVOLTAGE = 0x01 54 | __REG_BUSVOLTAGE = 0x02 55 | __REG_POWER = 0x03 56 | __REG_CURRENT = 0x04 57 | __REG_CALIBRATION = 0x05 58 | 59 | __RST = 15 60 | __BRNG = 13 61 | __PG1 = 12 62 | __PG0 = 11 63 | __BADC4 = 10 64 | __BADC3 = 9 65 | __BADC2 = 8 66 | __BADC1 = 7 67 | __SADC4 = 6 68 | __SADC3 = 5 69 | __SADC2 = 4 70 | __SADC1 = 3 71 | __MODE3 = 2 72 | __MODE2 = 1 73 | __MODE1 = 0 74 | 75 | __OVF = 1 76 | __CNVR = 2 77 | 78 | __BUS_RANGE = [16, 32] 79 | __GAIN_VOLTS = [0.04, 0.08, 0.16, 0.32] 80 | 81 | __CONT_SH_BUS = 7 82 | 83 | __AMP_ERR_MSG = ('Expected current %.3fA is greater ' 84 | 'than max possible current %.3fA') 85 | __RNG_ERR_MSG = ('Expected amps %.2fA, out of range, use a lower ' 86 | 'value shunt resistor') 87 | __VOLT_ERR_MSG = ('Invalid voltage range, must be one of: ' 88 | 'RANGE_16V, RANGE_32V') 89 | 90 | __LOG_FORMAT = '%(asctime)s - %(levelname)s - INA219 %(message)s' 91 | __LOG_MSG_1 = ('shunt ohms: %.3f, bus max volts: %d, ' 92 | 'shunt volts max: %.2f%s, ' 93 | 'bus ADC: %s, shunt ADC: %s') 94 | __LOG_MSG_2 = ('calibrate called with: bus max volts: %dV, ' 95 | 'max shunt volts: %.2fV%s') 96 | __LOG_MSG_3 = ('Current overflow detected - ' 97 | 'attempting to increase gain') 98 | 99 | __SHUNT_MILLIVOLTS_LSB = 0.01 # 10uV 100 | __BUS_MILLIVOLTS_LSB = 4 # 4mV 101 | __CALIBRATION_FACTOR = 0.04096 102 | # Max value supported value (65534 decimal) of the calibration register 103 | # (D0 bit is always zero, p31 of spec) 104 | __MAX_CALIBRATION_VALUE = 0xFFFE 105 | # In the spec (p17) the current LSB factor for the minimum LSB is 106 | # documented as 32767, but a larger value (100.1% of 32767) is used 107 | # to guarantee that current overflow can always be detected. 108 | __CURRENT_LSB_FACTOR = 32800 109 | 110 | def __init__(self, shunt_ohms, i2c, max_expected_amps=None, 111 | address=__ADDRESS, log_level=logging.ERROR): 112 | """Construct the class. 113 | 114 | At a minimum pass in the resistance of the shunt resistor and I2C 115 | interface to which the sensor is connected. 116 | 117 | Arguments: 118 | shunt_ohms -- value of shunt resistor in Ohms (mandatory). 119 | i2c -- an instance of the I2C class from the *machine* module, either 120 | I2C(1) or I2C(2) (mandatory). 121 | max_expected_amps -- the maximum expected current in Amps (optional). 122 | address -- the I2C address of the INA219, defaults to 123 | *0x40* (optional). 124 | log_level -- set to logging.DEBUG to see detailed calibration 125 | calculations (optional). 126 | """ 127 | logging.basicConfig(level=log_level) 128 | self._log = logging.getLogger("ina219") 129 | self._i2c = i2c 130 | self._address = address 131 | self._shunt_ohms = shunt_ohms 132 | self._max_expected_amps = max_expected_amps 133 | self._min_device_current_lsb = self._calculate_min_current_lsb() 134 | self._gain = None 135 | self._auto_gain_enabled = False 136 | 137 | def configure(self, voltage_range=RANGE_32V, gain=GAIN_AUTO, 138 | bus_adc=ADC_12BIT, shunt_adc=ADC_12BIT): 139 | """Configure and calibrate how the INA219 will take measurements. 140 | 141 | Arguments: 142 | voltage_range -- The full scale voltage range, this is either 16V 143 | or 32V represented by one of the following constants; 144 | RANGE_16V, RANGE_32V (default). 145 | gain -- The gain which controls the maximum range of the shunt 146 | voltage represented by one of the following constants; 147 | GAIN_1_40MV, GAIN_2_80MV, GAIN_4_160MV, 148 | GAIN_8_320MV, GAIN_AUTO (default). 149 | bus_adc -- The bus ADC resolution (9, 10, 11, or 12-bit) or 150 | set the number of samples used when averaging results 151 | represent by one of the following constants; ADC_9BIT, 152 | ADC_10BIT, ADC_11BIT, ADC_12BIT (default), 153 | ADC_2SAMP, ADC_4SAMP, ADC_8SAMP, ADC_16SAMP, 154 | ADC_32SAMP, ADC_64SAMP, ADC_128SAMP 155 | shunt_adc -- The shunt ADC resolution (9, 10, 11, or 12-bit) or 156 | set the number of samples used when averaging results 157 | represent by one of the following constants; ADC_9BIT, 158 | ADC_10BIT, ADC_11BIT, ADC_12BIT (default), 159 | ADC_2SAMP, ADC_4SAMP, ADC_8SAMP, ADC_16SAMP, 160 | ADC_32SAMP, ADC_64SAMP, ADC_128SAMP 161 | """ 162 | self.__validate_voltage_range(voltage_range) 163 | self._voltage_range = voltage_range 164 | 165 | if self._max_expected_amps is not None: 166 | if gain == self.GAIN_AUTO: 167 | self._auto_gain_enabled = True 168 | self._gain = self._determine_gain(self._max_expected_amps) 169 | else: 170 | self._gain = gain 171 | else: 172 | if gain != self.GAIN_AUTO: 173 | self._gain = gain 174 | else: 175 | self._auto_gain_enabled = True 176 | self._gain = self.GAIN_1_40MV 177 | 178 | self._log.info('gain set to %.2fV', self.__GAIN_VOLTS[self._gain]) 179 | 180 | self._log.debug( 181 | self.__LOG_MSG_1, 182 | self._shunt_ohms, self.__BUS_RANGE[voltage_range], 183 | self.__GAIN_VOLTS[self._gain], 184 | self.__max_expected_amps_to_string(self._max_expected_amps), 185 | self.__ADC_CONVERSION[bus_adc], self.__ADC_CONVERSION[shunt_adc]) 186 | 187 | self._calibrate( 188 | self.__BUS_RANGE[voltage_range], self.__GAIN_VOLTS[self._gain], 189 | self._max_expected_amps) 190 | self._configure(voltage_range, self._gain, bus_adc, shunt_adc) 191 | 192 | def voltage(self): 193 | """Return the bus voltage in volts.""" 194 | value = self._voltage_register() 195 | return float(value) * self.__BUS_MILLIVOLTS_LSB / 1000 196 | 197 | def supply_voltage(self): 198 | """Return the bus supply voltage in volts. 199 | 200 | This is the sum of the bus voltage and shunt voltage. A 201 | DeviceRangeError exception is thrown if current overflow occurs. 202 | """ 203 | return self.voltage() + (float(self.shunt_voltage()) / 1000) 204 | 205 | def current(self): 206 | """Return the bus current in milliamps. 207 | 208 | A DeviceRangeError exception is thrown if current overflow occurs. 209 | """ 210 | self._handle_current_overflow() 211 | return self._current_register() * self._current_lsb * 1000 212 | 213 | def power(self): 214 | """Return the bus power consumption in milliwatts. 215 | 216 | A DeviceRangeError exception is thrown if current overflow occurs. 217 | """ 218 | self._handle_current_overflow() 219 | return self._power_register() * self._power_lsb * 1000 220 | 221 | def shunt_voltage(self): 222 | """Return the shunt voltage in millivolts. 223 | 224 | A DeviceRangeError exception is thrown if current overflow occurs. 225 | """ 226 | self._handle_current_overflow() 227 | return self._shunt_voltage_register() * self.__SHUNT_MILLIVOLTS_LSB 228 | 229 | def sleep(self): 230 | """Put the INA219 into power down mode.""" 231 | configuration = self._read_configuration() 232 | self._configuration_register(configuration & 0xFFF8) 233 | 234 | def wake(self): 235 | """Wake the INA219 from power down mode.""" 236 | configuration = self._read_configuration() 237 | self._configuration_register(configuration | 0x0007) 238 | # 40us delay to recover from powerdown (p14 of spec) 239 | utime.sleep_us(40) 240 | 241 | def current_overflow(self): 242 | """Return true if the sensor has detect current overflow. 243 | 244 | In this case the current and power values are invalid. 245 | """ 246 | return self._has_current_overflow() 247 | 248 | def reset(self): 249 | """Reset the INA219 to its default configuration.""" 250 | self._configuration_register(1 << self.__RST) 251 | 252 | def _handle_current_overflow(self): 253 | if self._auto_gain_enabled: 254 | while self._has_current_overflow(): 255 | self._increase_gain() 256 | else: 257 | if self._has_current_overflow(): 258 | raise DeviceRangeError(self.__GAIN_VOLTS[self._gain]) 259 | 260 | def _determine_gain(self, max_expected_amps): 261 | shunt_v = max_expected_amps * self._shunt_ohms 262 | if shunt_v > self.__GAIN_VOLTS[3]: 263 | raise ValueError(self.__RNG_ERR_MSG % max_expected_amps) 264 | gain = min(v for v in self.__GAIN_VOLTS if v > shunt_v) 265 | return self.__GAIN_VOLTS.index(gain) 266 | 267 | def _increase_gain(self): 268 | self._log.info(self.__LOG_MSG_3) 269 | gain = self._read_gain() 270 | if gain < len(self.__GAIN_VOLTS) - 1: 271 | gain = gain + 1 272 | self._calibrate(self.__BUS_RANGE[self._voltage_range], 273 | self.__GAIN_VOLTS[gain]) 274 | self._configure_gain(gain) 275 | # 1ms delay required for new configuration to take effect, 276 | # otherwise invalid current/power readings can occur. 277 | utime.sleep_ms(1) 278 | else: 279 | self._log.info('Device limit reach, gain cannot be increased') 280 | raise DeviceRangeError(self.__GAIN_VOLTS[gain], True) 281 | 282 | def _configure(self, voltage_range, gain, bus_adc, shunt_adc): 283 | configuration = ( 284 | voltage_range << self.__BRNG | gain << self.__PG0 | 285 | bus_adc << self.__BADC1 | shunt_adc << self.__SADC1 | 286 | self.__CONT_SH_BUS) 287 | self._configuration_register(configuration) 288 | 289 | def _calibrate(self, bus_volts_max, shunt_volts_max, 290 | max_expected_amps=None): 291 | self._log.info(self.__LOG_MSG_2, 292 | bus_volts_max, shunt_volts_max, 293 | self.__max_expected_amps_to_string(max_expected_amps)) 294 | 295 | max_possible_amps = shunt_volts_max / self._shunt_ohms 296 | 297 | self._log.info("max possible current: %.3fA", max_possible_amps) 298 | 299 | self._current_lsb = \ 300 | self._determine_current_lsb(max_expected_amps, max_possible_amps) 301 | self._log.info("current LSB: %.3e A/bit", self._current_lsb) 302 | 303 | self._power_lsb = self._current_lsb * 20 304 | self._log.info("power LSB: %.3e W/bit", self._power_lsb) 305 | 306 | max_current = self._current_lsb * 32767 307 | self._log.info("max current before overflow: %.4fA", max_current) 308 | 309 | max_shunt_voltage = max_current * self._shunt_ohms 310 | self._log.info("max shunt voltage before overflow: %.4fmV", 311 | max_shunt_voltage * 1000) 312 | 313 | calibration = trunc(self.__CALIBRATION_FACTOR / 314 | (self._current_lsb * self._shunt_ohms)) 315 | self._log.info("calibration: 0x%04x (%d)", calibration, calibration) 316 | self._calibration_register(calibration) 317 | 318 | def _determine_current_lsb(self, max_expected_amps, max_possible_amps): 319 | if max_expected_amps is not None: 320 | if max_expected_amps > round(max_possible_amps, 3): 321 | raise ValueError(self.__AMP_ERR_MSG % 322 | (max_expected_amps, max_possible_amps)) 323 | self._log.info("max expected current: %.3fA", max_expected_amps) 324 | if max_expected_amps < max_possible_amps: 325 | current_lsb = max_expected_amps / self.__CURRENT_LSB_FACTOR 326 | else: 327 | current_lsb = max_possible_amps / self.__CURRENT_LSB_FACTOR 328 | else: 329 | current_lsb = max_possible_amps / self.__CURRENT_LSB_FACTOR 330 | 331 | if current_lsb < self._min_device_current_lsb: 332 | current_lsb = self._min_device_current_lsb 333 | return current_lsb 334 | 335 | def _configuration_register(self, register_value): 336 | self._log.debug("configuration: 0x%04x", register_value) 337 | self.__write_register(self.__REG_CONFIG, register_value) 338 | 339 | def _read_configuration(self): 340 | return self.__read_register(self.__REG_CONFIG) 341 | 342 | def _calculate_min_current_lsb(self): 343 | return self.__CALIBRATION_FACTOR / \ 344 | (self._shunt_ohms * self.__MAX_CALIBRATION_VALUE) 345 | 346 | def _read_gain(self): 347 | configuration = self._read_configuration() 348 | gain = (configuration & 0x1800) >> self.__PG0 349 | self._log.info("gain is currently: %.2fV", self.__GAIN_VOLTS[gain]) 350 | return gain 351 | 352 | def _configure_gain(self, gain): 353 | configuration = self._read_configuration() 354 | configuration = configuration & 0xE7FF 355 | self._configuration_register(configuration | (gain << self.__PG0)) 356 | self._gain = gain 357 | self._log.info("gain set to: %.2fV" % self.__GAIN_VOLTS[gain]) 358 | 359 | def _calibration_register(self, register_value): 360 | self._log.debug("calibration: 0x%04x" % register_value) 361 | self.__write_register(self.__REG_CALIBRATION, register_value) 362 | 363 | def _has_current_overflow(self): 364 | ovf = self._read_voltage_register() & self.__OVF 365 | return (ovf == 1) 366 | 367 | def _voltage_register(self): 368 | register_value = self._read_voltage_register() 369 | return register_value >> 3 370 | 371 | def _read_voltage_register(self): 372 | return self.__read_register(self.__REG_BUSVOLTAGE) 373 | 374 | def _current_register(self): 375 | return self.__read_register(self.__REG_CURRENT, True) 376 | 377 | def _shunt_voltage_register(self): 378 | return self.__read_register(self.__REG_SHUNTVOLTAGE, True) 379 | 380 | def _power_register(self): 381 | return self.__read_register(self.__REG_POWER) 382 | 383 | def __validate_voltage_range(self, voltage_range): 384 | if voltage_range > len(self.__BUS_RANGE) - 1: 385 | raise ValueError(self.__VOLT_ERR_MSG) 386 | 387 | def __write_register(self, register, register_value): 388 | self.__log_register_operation("write", register, register_value) 389 | 390 | register_bytes = self.__to_bytes(register_value) 391 | self._i2c.writeto_mem(self._address, register, register_bytes) 392 | 393 | def __to_bytes(self, register_value): 394 | return bytearray([(register_value >> 8) & 0xFF, register_value & 0xFF]) 395 | 396 | def __read_register(self, register, negative_value_supported=False): 397 | register_bytes = self._i2c.readfrom_mem(self._address, register, 2) 398 | register_value = int.from_bytes(register_bytes, 'big') 399 | if negative_value_supported: 400 | # Two's compliment 401 | if register_value > 32767: 402 | register_value -= 65536 403 | 404 | self.__log_register_operation("read", register, register_value) 405 | return register_value 406 | 407 | def __log_register_operation(self, msg, register, value): 408 | # performance optimisation 409 | if logging._level == logging.DEBUG: 410 | binary = '{0:#018b}'.format(value) 411 | self._log.debug("%s register 0x%02x: 0x%04x %s", 412 | msg, register, value, binary) 413 | 414 | def __max_expected_amps_to_string(self, max_expected_amps): 415 | if max_expected_amps is None: 416 | return '' 417 | else: 418 | return ', max expected amps: %.3fA' % max_expected_amps 419 | 420 | 421 | class DeviceRangeError(Exception): 422 | """This exception is throw to prevent invalid readings. 423 | 424 | Invalid readings occur When the current is greater than allowed given 425 | calibration of the device. 426 | """ 427 | 428 | __DEV_RNG_ERR = ('Current out of range (overflow), ' 429 | 'for gain %.2fV') 430 | 431 | def __init__(self, gain_volts, device_max=False): 432 | """Construct the class.""" 433 | msg = self.__DEV_RNG_ERR % gain_volts 434 | if device_max: 435 | msg = msg + ', device limit reached' 436 | super(DeviceRangeError, self).__init__(msg) 437 | self.gain_volts = gain_volts 438 | self.device_limit_reached = device_max 439 | -------------------------------------------------------------------------------- /microWebSrv.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License (MIT) 3 | Copyright © 2018 Jean-Christophe Bos & HC² (www.hc2.fr) 4 | """ 5 | 6 | 7 | from json import loads, dumps 8 | from os import stat 9 | from _thread import start_new_thread 10 | import socket 11 | import gc 12 | import re 13 | 14 | try : 15 | from microWebTemplate import MicroWebTemplate 16 | except : 17 | pass 18 | 19 | try : 20 | from microWebSocket import MicroWebSocket 21 | except : 22 | pass 23 | 24 | class MicroWebSrvRoute : 25 | def __init__(self, route, method, func, routeArgNames, routeRegex) : 26 | self.route = route 27 | self.method = method 28 | self.func = func 29 | self.routeArgNames = routeArgNames 30 | self.routeRegex = routeRegex 31 | 32 | 33 | class MicroWebSrv : 34 | 35 | # ============================================================================ 36 | # ===( Constants )============================================================ 37 | # ============================================================================ 38 | 39 | _indexPages = [ 40 | "index.pyhtml", 41 | "index.html", 42 | "index.htm", 43 | "default.pyhtml", 44 | "default.html", 45 | "default.htm" 46 | ] 47 | 48 | _mimeTypes = { 49 | ".txt" : "text/plain", 50 | ".htm" : "text/html", 51 | ".html" : "text/html", 52 | ".css" : "text/css", 53 | ".csv" : "text/csv", 54 | ".js" : "application/javascript", 55 | ".xml" : "application/xml", 56 | ".xhtml" : "application/xhtml+xml", 57 | ".json" : "application/json", 58 | ".zip" : "application/zip", 59 | ".pdf" : "application/pdf", 60 | ".ts" : "application/typescript", 61 | ".woff" : "font/woff", 62 | ".woff2" : "font/woff2", 63 | ".ttf" : "font/ttf", 64 | ".otf" : "font/otf", 65 | ".jpg" : "image/jpeg", 66 | ".jpeg" : "image/jpeg", 67 | ".png" : "image/png", 68 | ".gif" : "image/gif", 69 | ".svg" : "image/svg+xml", 70 | ".ico" : "image/x-icon" 71 | } 72 | 73 | _html_escape_chars = { 74 | "&" : "&", 75 | '"' : """, 76 | "'" : "'", 77 | ">" : ">", 78 | "<" : "<" 79 | } 80 | 81 | _pyhtmlPagesExt = '.pyhtml' 82 | 83 | # ============================================================================ 84 | # ===( Class globals )======================================================= 85 | # ============================================================================ 86 | 87 | _docoratedRouteHandlers = [] 88 | 89 | # ============================================================================ 90 | # ===( Utils )=============================================================== 91 | # ============================================================================ 92 | 93 | @classmethod 94 | def route(cls, url, method='GET'): 95 | """ Adds a route handler function to the routing list """ 96 | def route_decorator(func): 97 | item = (url, method, func) 98 | cls._docoratedRouteHandlers.append(item) 99 | return func 100 | return route_decorator 101 | 102 | # ---------------------------------------------------------------------------- 103 | 104 | @staticmethod 105 | def HTMLEscape(s) : 106 | return ''.join(MicroWebSrv._html_escape_chars.get(c, c) for c in s) 107 | 108 | # ---------------------------------------------------------------------------- 109 | 110 | @staticmethod 111 | def _startThread(func, args=()) : 112 | try : 113 | start_new_thread(func, args) 114 | except : 115 | global _mwsrv_thread_id 116 | try : 117 | _mwsrv_thread_id += 1 118 | except : 119 | _mwsrv_thread_id = 0 120 | try : 121 | start_new_thread('MWSRV_THREAD_%s' % _mwsrv_thread_id, func, args) 122 | except : 123 | return False 124 | return True 125 | 126 | # ---------------------------------------------------------------------------- 127 | 128 | @staticmethod 129 | def _unquote(s) : 130 | r = str(s).split('%') 131 | try : 132 | b = r[0].encode() 133 | for i in range(1, len(r)) : 134 | try : 135 | b += bytes([int(r[i][:2], 16)]) + r[i][2:].encode() 136 | except : 137 | b += b'%' + r[i].encode() 138 | return b.decode('UTF-8') 139 | except : 140 | return str(s) 141 | 142 | # ------------------------------------------------------------------------------ 143 | 144 | @staticmethod 145 | def _unquote_plus(s) : 146 | return MicroWebSrv._unquote(s.replace('+', ' ')) 147 | 148 | # ------------------------------------------------------------------------------ 149 | 150 | @staticmethod 151 | def _fileExists(path) : 152 | try : 153 | stat(path) 154 | return True 155 | except : 156 | return False 157 | 158 | # ---------------------------------------------------------------------------- 159 | 160 | @staticmethod 161 | def _isPyHTMLFile(filename) : 162 | return filename.lower().endswith(MicroWebSrv._pyhtmlPagesExt) 163 | 164 | # ============================================================================ 165 | # ===( Constructor )========================================================== 166 | # ============================================================================ 167 | 168 | def __init__( self, 169 | routeHandlers = [], 170 | port = 80, 171 | bindIP = '0.0.0.0', 172 | webPath = "/flash/www" ) : 173 | 174 | self._srvAddr = (bindIP, port) 175 | self._webPath = webPath 176 | self._notFoundUrl = None 177 | self._started = False 178 | 179 | self.MaxWebSocketRecvLen = 1024 180 | self.WebSocketThreaded = True 181 | self.AcceptWebSocketCallback = None 182 | self.LetCacheStaticContentLevel = 2 183 | 184 | self._routeHandlers = [] 185 | routeHandlers += self._docoratedRouteHandlers 186 | for route, method, func in routeHandlers : 187 | routeParts = route.split('/') 188 | # -> ['', 'users', '', 'addresses', '', 'test', ''] 189 | routeArgNames = [] 190 | routeRegex = '' 191 | for s in routeParts : 192 | if s.startswith('<') and s.endswith('>') : 193 | routeArgNames.append(s[1:-1]) 194 | routeRegex += '/(\\w*)' 195 | elif s : 196 | routeRegex += '/' + s 197 | routeRegex += '$' 198 | # -> '/users/(\w*)/addresses/(\w*)/test/(\w*)$' 199 | routeRegex = re.compile(routeRegex) 200 | 201 | self._routeHandlers.append(MicroWebSrvRoute(route, method, func, routeArgNames, routeRegex)) 202 | 203 | # ============================================================================ 204 | # ===( Server Process )======================================================= 205 | # ============================================================================ 206 | 207 | def _serverProcess(self) : 208 | self._started = True 209 | while True : 210 | try : 211 | client, cliAddr = self._server.accept() 212 | except Exception as ex : 213 | if ex.args and ex.args[0] == 113 : 214 | break 215 | continue 216 | self._client(self, client, cliAddr) 217 | self._started = False 218 | 219 | # ============================================================================ 220 | # ===( Functions )============================================================ 221 | # ============================================================================ 222 | 223 | def Start(self, threaded=False) : 224 | if not self._started : 225 | self._server = socket.socket() 226 | self._server.setsockopt( socket.SOL_SOCKET, 227 | socket.SO_REUSEADDR, 228 | 1 ) 229 | self._server.bind(self._srvAddr) 230 | self._server.listen(16) 231 | if threaded : 232 | MicroWebSrv._startThread(self._serverProcess) 233 | else : 234 | self._serverProcess() 235 | 236 | # ---------------------------------------------------------------------------- 237 | 238 | def Stop(self) : 239 | if self._started : 240 | self._server.close() 241 | 242 | # ---------------------------------------------------------------------------- 243 | 244 | def IsStarted(self) : 245 | return self._started 246 | 247 | # ---------------------------------------------------------------------------- 248 | 249 | def SetNotFoundPageUrl(self, url=None) : 250 | self._notFoundUrl = url 251 | 252 | # ---------------------------------------------------------------------------- 253 | 254 | def GetMimeTypeFromFilename(self, filename) : 255 | filename = filename.lower() 256 | for ext in self._mimeTypes : 257 | if filename.endswith(ext) : 258 | return self._mimeTypes[ext] 259 | return None 260 | 261 | # ---------------------------------------------------------------------------- 262 | 263 | def GetRouteHandler(self, resUrl, method) : 264 | if self._routeHandlers : 265 | #resUrl = resUrl.upper() 266 | if resUrl.endswith('/') : 267 | resUrl = resUrl[:-1] 268 | method = method.upper() 269 | for rh in self._routeHandlers : 270 | if rh.method == method : 271 | m = rh.routeRegex.match(resUrl) 272 | if m : # found matching route? 273 | if rh.routeArgNames : 274 | routeArgs = {} 275 | for i, name in enumerate(rh.routeArgNames) : 276 | value = m.group(i+1) 277 | try : 278 | value = int(value) 279 | except : 280 | pass 281 | routeArgs[name] = value 282 | return (rh.func, routeArgs) 283 | else : 284 | return (rh.func, None) 285 | return (None, None) 286 | 287 | # ---------------------------------------------------------------------------- 288 | 289 | def _physPathFromURLPath(self, urlPath) : 290 | if urlPath == '/' : 291 | for idxPage in self._indexPages : 292 | physPath = self._webPath + '/' + idxPage 293 | if MicroWebSrv._fileExists(physPath) : 294 | return physPath 295 | else : 296 | physPath = self._webPath + urlPath.replace('../', '/') 297 | if MicroWebSrv._fileExists(physPath) : 298 | return physPath 299 | return None 300 | 301 | # ============================================================================ 302 | # ===( Class Client )======================================================== 303 | # ============================================================================ 304 | 305 | class _client : 306 | 307 | # ------------------------------------------------------------------------ 308 | 309 | def __init__(self, microWebSrv, socket, addr) : 310 | socket.settimeout(2) 311 | self._microWebSrv = microWebSrv 312 | self._socket = socket 313 | self._addr = addr 314 | self._method = None 315 | self._path = None 316 | self._httpVer = None 317 | self._resPath = "/" 318 | self._queryString = "" 319 | self._queryParams = { } 320 | self._headers = { } 321 | self._contentType = None 322 | self._contentLength = 0 323 | 324 | if hasattr(socket, 'readline'): # MicroPython 325 | self._socketfile = self._socket 326 | else: # CPython 327 | self._socketfile = self._socket.makefile('rwb') 328 | 329 | self._processRequest() 330 | 331 | # ------------------------------------------------------------------------ 332 | 333 | def _processRequest(self) : 334 | try : 335 | response = MicroWebSrv._response(self) 336 | if self._parseFirstLine(response) : 337 | if self._parseHeader(response) : 338 | upg = self._getConnUpgrade() 339 | if not upg : 340 | routeHandler, routeArgs = self._microWebSrv.GetRouteHandler(self._resPath, self._method) 341 | if routeHandler : 342 | try : 343 | if routeArgs is not None: 344 | routeHandler(self, response, routeArgs) 345 | else : 346 | routeHandler(self, response) 347 | except Exception as ex : 348 | print('MicroWebSrv handler exception:\r\n - In route %s %s\r\n - %s' % (self._method, self._resPath, ex)) 349 | raise ex 350 | elif self._method.upper() == "GET" : 351 | filepath = self._microWebSrv._physPathFromURLPath(self._resPath) 352 | if filepath : 353 | if MicroWebSrv._isPyHTMLFile(filepath) : 354 | response.WriteResponsePyHTMLFile(filepath) 355 | else : 356 | contentType = self._microWebSrv.GetMimeTypeFromFilename(filepath) 357 | if contentType : 358 | if self._microWebSrv.LetCacheStaticContentLevel > 0 : 359 | if self._microWebSrv.LetCacheStaticContentLevel > 1 and \ 360 | 'if-modified-since' in self._headers : 361 | response.WriteResponseNotModified() 362 | else: 363 | headers = { 'Last-Modified' : 'Fri, 1 Jan 2018 23:42:00 GMT', \ 364 | 'Cache-Control' : 'max-age=315360000' } 365 | response.WriteResponseFile(filepath, contentType, headers) 366 | else : 367 | response.WriteResponseFile(filepath, contentType) 368 | else : 369 | response.WriteResponseForbidden() 370 | else : 371 | response.WriteResponseNotFound() 372 | else : 373 | response.WriteResponseMethodNotAllowed() 374 | elif upg == 'websocket' and 'MicroWebSocket' in globals() \ 375 | and self._microWebSrv.AcceptWebSocketCallback : 376 | MicroWebSocket( socket = self._socket, 377 | httpClient = self, 378 | httpResponse = response, 379 | maxRecvLen = self._microWebSrv.MaxWebSocketRecvLen, 380 | threaded = self._microWebSrv.WebSocketThreaded, 381 | acceptCallback = self._microWebSrv.AcceptWebSocketCallback ) 382 | return 383 | else : 384 | response.WriteResponseNotImplemented() 385 | else : 386 | response.WriteResponseBadRequest() 387 | except : 388 | response.WriteResponseInternalServerError() 389 | try : 390 | if self._socketfile is not self._socket: 391 | self._socketfile.close() 392 | self._socket.close() 393 | except : 394 | pass 395 | 396 | # ------------------------------------------------------------------------ 397 | 398 | def _parseFirstLine(self, response) : 399 | try : 400 | elements = self._socketfile.readline().decode().strip().split() 401 | if len(elements) == 3 : 402 | self._method = elements[0].upper() 403 | self._path = elements[1] 404 | self._httpVer = elements[2].upper() 405 | elements = self._path.split('?', 1) 406 | if len(elements) > 0 : 407 | self._resPath = MicroWebSrv._unquote_plus(elements[0]) 408 | if len(elements) > 1 : 409 | self._queryString = elements[1] 410 | elements = self._queryString.split('&') 411 | for s in elements : 412 | param = s.split('=', 1) 413 | if len(param) > 0 : 414 | value = MicroWebSrv._unquote(param[1]) if len(param) > 1 else '' 415 | self._queryParams[MicroWebSrv._unquote(param[0])] = value 416 | return True 417 | except : 418 | pass 419 | return False 420 | 421 | # ------------------------------------------------------------------------ 422 | 423 | def _parseHeader(self, response) : 424 | while True : 425 | elements = self._socketfile.readline().decode().strip().split(':', 1) 426 | if len(elements) == 2 : 427 | self._headers[elements[0].strip().lower()] = elements[1].strip() 428 | elif len(elements) == 1 and len(elements[0]) == 0 : 429 | if self._method == 'POST' or self._method == 'PUT' : 430 | self._contentType = self._headers.get("content-type", None) 431 | self._contentLength = int(self._headers.get("content-length", 0)) 432 | return True 433 | else : 434 | return False 435 | 436 | # ------------------------------------------------------------------------ 437 | 438 | def _getConnUpgrade(self) : 439 | if 'upgrade' in self._headers.get('connection', '').lower() : 440 | return self._headers.get('upgrade', '').lower() 441 | return None 442 | 443 | # ------------------------------------------------------------------------ 444 | 445 | def GetServer(self) : 446 | return self._microWebSrv 447 | 448 | # ------------------------------------------------------------------------ 449 | 450 | def GetAddr(self) : 451 | return self._addr 452 | 453 | # ------------------------------------------------------------------------ 454 | 455 | def GetIPAddr(self) : 456 | return self._addr[0] 457 | 458 | # ------------------------------------------------------------------------ 459 | 460 | def GetPort(self) : 461 | return self._addr[1] 462 | 463 | # ------------------------------------------------------------------------ 464 | 465 | def GetRequestMethod(self) : 466 | return self._method 467 | 468 | # ------------------------------------------------------------------------ 469 | 470 | def GetRequestTotalPath(self) : 471 | return self._path 472 | 473 | # ------------------------------------------------------------------------ 474 | 475 | def GetRequestPath(self) : 476 | return self._resPath 477 | 478 | # ------------------------------------------------------------------------ 479 | 480 | def GetRequestQueryString(self) : 481 | return self._queryString 482 | 483 | # ------------------------------------------------------------------------ 484 | 485 | def GetRequestQueryParams(self) : 486 | return self._queryParams 487 | 488 | # ------------------------------------------------------------------------ 489 | 490 | def GetRequestHeaders(self) : 491 | return self._headers 492 | 493 | # ------------------------------------------------------------------------ 494 | 495 | def GetRequestContentType(self) : 496 | return self._contentType 497 | 498 | # ------------------------------------------------------------------------ 499 | 500 | def GetRequestContentLength(self) : 501 | return self._contentLength 502 | 503 | # ------------------------------------------------------------------------ 504 | 505 | def ReadRequestContent(self, size=None) : 506 | if size is None : 507 | size = self._contentLength 508 | if size > 0 : 509 | try : 510 | return self._socketfile.read(size) 511 | except : 512 | pass 513 | return b'' 514 | 515 | # ------------------------------------------------------------------------ 516 | 517 | def ReadRequestPostedFormData(self) : 518 | res = { } 519 | data = self.ReadRequestContent() 520 | if data : 521 | elements = data.decode().split('&') 522 | for s in elements : 523 | param = s.split('=', 1) 524 | if len(param) > 0 : 525 | value = MicroWebSrv._unquote_plus(param[1]) if len(param) > 1 else '' 526 | res[MicroWebSrv._unquote_plus(param[0])] = value 527 | return res 528 | 529 | # ------------------------------------------------------------------------ 530 | 531 | def ReadRequestContentAsJSON(self) : 532 | data = self.ReadRequestContent() 533 | if data : 534 | try : 535 | return loads(data.decode()) 536 | except : 537 | pass 538 | return None 539 | 540 | # ============================================================================ 541 | # ===( Class Response )====================================================== 542 | # ============================================================================ 543 | 544 | class _response : 545 | 546 | # ------------------------------------------------------------------------ 547 | 548 | def __init__(self, client) : 549 | self._client = client 550 | 551 | # ------------------------------------------------------------------------ 552 | 553 | def _write(self, data, strEncoding='ISO-8859-1') : 554 | if data : 555 | if type(data) == str : 556 | data = data.encode(strEncoding) 557 | data = memoryview(data) 558 | while data : 559 | n = self._client._socketfile.write(data) 560 | if n is None : 561 | return False 562 | data = data[n:] 563 | return True 564 | return False 565 | 566 | # ------------------------------------------------------------------------ 567 | 568 | def _writeFirstLine(self, code) : 569 | reason = self._responseCodes.get(code, ('Unknown reason', ))[0] 570 | return self._write("HTTP/1.1 %s %s\r\n" % (code, reason)) 571 | 572 | # ------------------------------------------------------------------------ 573 | 574 | def _writeHeader(self, name, value) : 575 | return self._write("%s: %s\r\n" % (name, value)) 576 | 577 | # ------------------------------------------------------------------------ 578 | 579 | def _writeContentTypeHeader(self, contentType, charset=None) : 580 | if contentType : 581 | ct = contentType \ 582 | + (("; charset=%s" % charset) if charset else "") 583 | else : 584 | ct = "application/octet-stream" 585 | self._writeHeader("Content-Type", ct) 586 | 587 | # ------------------------------------------------------------------------ 588 | 589 | def _writeServerHeader(self) : 590 | self._writeHeader("Server", "MicroWebSrv by JC`zic") 591 | 592 | # ------------------------------------------------------------------------ 593 | 594 | def _writeEndHeader(self) : 595 | return self._write("\r\n") 596 | 597 | # ------------------------------------------------------------------------ 598 | 599 | def _writeBeforeContent(self, code, headers, contentType, contentCharset, contentLength) : 600 | self._writeFirstLine(code) 601 | if isinstance(headers, dict) : 602 | for header in headers : 603 | self._writeHeader(header, headers[header]) 604 | if contentLength > 0 : 605 | self._writeContentTypeHeader(contentType, contentCharset) 606 | self._writeHeader("Content-Length", contentLength) 607 | self._writeServerHeader() 608 | self._writeHeader("Connection", "close") 609 | self._writeEndHeader() 610 | 611 | # ------------------------------------------------------------------------ 612 | 613 | def WriteSwitchProto(self, upgrade, headers=None) : 614 | self._writeFirstLine(101) 615 | self._writeHeader("Connection", "Upgrade") 616 | self._writeHeader("Upgrade", upgrade) 617 | if isinstance(headers, dict) : 618 | for header in headers : 619 | self._writeHeader(header, headers[header]) 620 | self._writeServerHeader() 621 | self._writeEndHeader() 622 | if self._client._socketfile is not self._client._socket : 623 | self._client._socketfile.flush() # CPython needs flush to continue protocol 624 | 625 | # ------------------------------------------------------------------------ 626 | 627 | def WriteResponse(self, code, headers, contentType, contentCharset, content) : 628 | try : 629 | if content : 630 | if type(content) == str : 631 | content = content.encode(contentCharset) 632 | contentLength = len(content) 633 | else : 634 | contentLength = 0 635 | self._writeBeforeContent(code, headers, contentType, contentCharset, contentLength) 636 | if content : 637 | return self._write(content) 638 | return True 639 | except : 640 | return False 641 | 642 | # ------------------------------------------------------------------------ 643 | 644 | def WriteResponsePyHTMLFile(self, filepath, headers=None, vars=None) : 645 | if 'MicroWebTemplate' in globals() : 646 | with open(filepath, 'r') as file : 647 | code = file.read() 648 | mWebTmpl = MicroWebTemplate(code, escapeStrFunc=MicroWebSrv.HTMLEscape, filepath=filepath) 649 | try : 650 | tmplResult = mWebTmpl.Execute(None, vars) 651 | return self.WriteResponse(200, headers, "text/html", "UTF-8", tmplResult) 652 | except Exception as ex : 653 | return self.WriteResponse( 500, 654 | None, 655 | "text/html", 656 | "UTF-8", 657 | self._execErrCtnTmpl % { 658 | 'module' : 'PyHTML', 659 | 'message' : str(ex) 660 | } ) 661 | return self.WriteResponseNotImplemented() 662 | 663 | # ------------------------------------------------------------------------ 664 | 665 | def WriteResponseFile(self, filepath, contentType=None, headers=None) : 666 | try : 667 | size = stat(filepath)[6] 668 | if size > 0 : 669 | with open(filepath, 'rb') as file : 670 | self._writeBeforeContent(200, headers, contentType, None, size) 671 | try : 672 | buf = bytearray(1024) 673 | while size > 0 : 674 | x = file.readinto(buf) 675 | if x < len(buf) : 676 | buf = memoryview(buf)[:x] 677 | if not self._write(buf) : 678 | return False 679 | size -= x 680 | return True 681 | except : 682 | self.WriteResponseInternalServerError() 683 | return False 684 | except : 685 | pass 686 | self.WriteResponseNotFound() 687 | return False 688 | 689 | # ------------------------------------------------------------------------ 690 | 691 | def WriteResponseFileAttachment(self, filepath, attachmentName, headers=None) : 692 | if not isinstance(headers, dict) : 693 | headers = { } 694 | headers["Content-Disposition"] = "attachment; filename=\"%s\"" % attachmentName 695 | return self.WriteResponseFile(filepath, None, headers) 696 | 697 | # ------------------------------------------------------------------------ 698 | 699 | def WriteResponseOk(self, headers=None, contentType=None, contentCharset=None, content=None) : 700 | return self.WriteResponse(200, headers, contentType, contentCharset, content) 701 | 702 | # ------------------------------------------------------------------------ 703 | 704 | def WriteResponseJSONOk(self, obj=None, headers=None) : 705 | return self.WriteResponse(200, headers, "application/json", "UTF-8", dumps(obj)) 706 | 707 | # ------------------------------------------------------------------------ 708 | 709 | def WriteResponseRedirect(self, location) : 710 | headers = { "Location" : location } 711 | return self.WriteResponse(302, headers, None, None, None) 712 | 713 | # ------------------------------------------------------------------------ 714 | 715 | def WriteResponseError(self, code) : 716 | responseCode = self._responseCodes.get(code, ('Unknown reason', '')) 717 | return self.WriteResponse( code, 718 | None, 719 | "text/html", 720 | "UTF-8", 721 | self._errCtnTmpl % { 722 | 'code' : code, 723 | 'reason' : responseCode[0], 724 | 'message' : responseCode[1] 725 | } ) 726 | 727 | # ------------------------------------------------------------------------ 728 | 729 | def WriteResponseJSONError(self, code, obj=None) : 730 | return self.WriteResponse( code, 731 | None, 732 | "application/json", 733 | "UTF-8", 734 | dumps(obj if obj else { }) ) 735 | 736 | # ------------------------------------------------------------------------ 737 | 738 | def WriteResponseNotModified(self) : 739 | return self.WriteResponseError(304) 740 | 741 | # ------------------------------------------------------------------------ 742 | 743 | def WriteResponseBadRequest(self) : 744 | return self.WriteResponseError(400) 745 | 746 | # ------------------------------------------------------------------------ 747 | 748 | def WriteResponseForbidden(self) : 749 | return self.WriteResponseError(403) 750 | 751 | # ------------------------------------------------------------------------ 752 | 753 | def WriteResponseNotFound(self) : 754 | if self._client._microWebSrv._notFoundUrl : 755 | self.WriteResponseRedirect(self._client._microWebSrv._notFoundUrl) 756 | else : 757 | return self.WriteResponseError(404) 758 | 759 | # ------------------------------------------------------------------------ 760 | 761 | def WriteResponseMethodNotAllowed(self) : 762 | return self.WriteResponseError(405) 763 | 764 | # ------------------------------------------------------------------------ 765 | 766 | def WriteResponseInternalServerError(self) : 767 | return self.WriteResponseError(500) 768 | 769 | # ------------------------------------------------------------------------ 770 | 771 | def WriteResponseNotImplemented(self) : 772 | return self.WriteResponseError(501) 773 | 774 | # ------------------------------------------------------------------------ 775 | 776 | def FlashMessage(self, messageText, messageStyle='') : 777 | if 'MicroWebTemplate' in globals() : 778 | MicroWebTemplate.MESSAGE_TEXT = messageText 779 | MicroWebTemplate.MESSAGE_STYLE = messageStyle 780 | 781 | # ------------------------------------------------------------------------ 782 | 783 | _errCtnTmpl = """\ 784 | 785 | 786 | Error 787 | 788 | 789 |

%(code)d %(reason)s

790 | %(message)s 791 | 792 | 793 | """ 794 | 795 | # ------------------------------------------------------------------------ 796 | 797 | _execErrCtnTmpl = """\ 798 | 799 | 800 | Page execution error 801 | 802 | 803 |

%(module)s page execution error

804 | %(message)s 805 | 806 | 807 | """ 808 | 809 | # ------------------------------------------------------------------------ 810 | 811 | _responseCodes = { 812 | 100: ('Continue', 'Request received, please continue'), 813 | 101: ('Switching Protocols', 814 | 'Switching to new protocol; obey Upgrade header'), 815 | 816 | 200: ('OK', 'Request fulfilled, document follows'), 817 | 201: ('Created', 'Document created, URL follows'), 818 | 202: ('Accepted', 819 | 'Request accepted, processing continues off-line'), 820 | 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), 821 | 204: ('No Content', 'Request fulfilled, nothing follows'), 822 | 205: ('Reset Content', 'Clear input form for further input.'), 823 | 206: ('Partial Content', 'Partial content follows.'), 824 | 825 | 300: ('Multiple Choices', 826 | 'Object has several resources -- see URI list'), 827 | 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), 828 | 302: ('Found', 'Object moved temporarily -- see URI list'), 829 | 303: ('See Other', 'Object moved -- see Method and URL list'), 830 | 304: ('Not Modified', 831 | 'Document has not changed since given time'), 832 | 305: ('Use Proxy', 833 | 'You must use proxy specified in Location to access this ' 834 | 'resource.'), 835 | 307: ('Temporary Redirect', 836 | 'Object moved temporarily -- see URI list'), 837 | 838 | 400: ('Bad Request', 839 | 'Bad request syntax or unsupported method'), 840 | 401: ('Unauthorized', 841 | 'No permission -- see authorization schemes'), 842 | 402: ('Payment Required', 843 | 'No payment -- see charging schemes'), 844 | 403: ('Forbidden', 845 | 'Request forbidden -- authorization will not help'), 846 | 404: ('Not Found', 'Nothing matches the given URI'), 847 | 405: ('Method Not Allowed', 848 | 'Specified method is invalid for this resource.'), 849 | 406: ('Not Acceptable', 'URI not available in preferred format.'), 850 | 407: ('Proxy Authentication Required', 'You must authenticate with ' 851 | 'this proxy before proceeding.'), 852 | 408: ('Request Timeout', 'Request timed out; try again later.'), 853 | 409: ('Conflict', 'Request conflict.'), 854 | 410: ('Gone', 855 | 'URI no longer exists and has been permanently removed.'), 856 | 411: ('Length Required', 'Client must specify Content-Length.'), 857 | 412: ('Precondition Failed', 'Precondition in headers is false.'), 858 | 413: ('Request Entity Too Large', 'Entity is too large.'), 859 | 414: ('Request-URI Too Long', 'URI is too long.'), 860 | 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), 861 | 416: ('Requested Range Not Satisfiable', 862 | 'Cannot satisfy request range.'), 863 | 417: ('Expectation Failed', 864 | 'Expect condition could not be satisfied.'), 865 | 866 | 500: ('Internal Server Error', 'Server got itself in trouble'), 867 | 501: ('Not Implemented', 868 | 'Server does not support this operation'), 869 | 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), 870 | 503: ('Service Unavailable', 871 | 'The server cannot process the request due to a high load'), 872 | 504: ('Gateway Timeout', 873 | 'The gateway server did not receive a timely response'), 874 | 505: ('HTTP Version Not Supported', 'Cannot fulfill request.'), 875 | } 876 | 877 | # ============================================================================ 878 | # ============================================================================ 879 | # ============================================================================ 880 | 881 | --------------------------------------------------------------------------------