├── 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 | [](https://www.youtube.com/watch?v=PrbyPmq_Z7Q)
9 |
10 | ## Photo examples
11 | | | | | |
12 | |:--------:|:--------:|:--------:|:--------:|
13 | |
|
|
|
|
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 |
--------------------------------------------------------------------------------