├── LICENSE.md ├── README.md ├── startup.py ├── ufirebase.py └── usseclient.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vishal Dubey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firebase-micropython-esp32 2 | Firebase implementation for Micropython on ESP WROOM 32. Not tested on other platforms running micropython but should work. 3 | 4 | This is a very basic and ported version of https://github.com/shariq/firebase-python for micropython. This is WIP. All the basic features like: 5 | ``` 6 | 1. GET 7 | 2. POST 8 | 3. PUSH 9 | 3. PATCH 10 | ``` 11 | work fine. 12 | 13 | 14 | Requires `urequests` and `usseclient`. `urequests` is a part of micropython already. I have added a ported version of `sseclient` removing other dependencies not available in micropython. No other dependencies have to be added. 15 | 16 | ## Connect to wifi 17 | 18 | ``` 19 | run the startup.py file to connect to WiFi. Pass ssid and password as arguments. Put ssid under single/double quotes 20 | if it contains spaces. 21 | 22 | >>> execfile('startup.py ssid password') 23 | ``` 24 | 25 | Now you are good to go. 26 | 27 | ## get and put 28 | 29 | `get` gets the value of a Firebase at some URL, `put` writes or replaces data at a Firebase path. 30 | 31 | ```python 32 | >>> import ufirebase as firebase 33 | >>> URL = 'lucid-lychee' # see note on URLs at the bottom of documentation 34 | >>> print firebase.get(URL) # this is an empty Firebase 35 | None 36 | 37 | >>> firebase.put(URL, 'tell me everything') # can take a string 38 | >>> print firebase.get(URL) 39 | tell me everything 40 | 41 | >>> firebase.put(URL, {'lucidity': 9001}) # or a dictionary 42 | >>> print firebase.get(URL) 43 | {u'lucidity': 9001} 44 | 45 | >>> firebase.put(URL, {'color': 'red'}) # replaces old value 46 | >>> print firebase.get(URL) 47 | {u'color': u'red'} 48 | 49 | >>> print firebase.get(URL + '/color') 50 | red 51 | ``` 52 | 53 | 54 | 55 | ## push 56 | 57 | `push` pushes data to a list on a Firebase path. This is the same as `patch`ing with an incrementing key, with Firebase taking care of concurrency issues. 58 | 59 | ```python 60 | >>> import ufirebase as firebase 61 | >>> URL = 'bickering-blancmanges' 62 | >>> print firebase.get(URL) 63 | None 64 | 65 | >>> firebase.push(URL, {'color': 'pink', 'jiggliness': 'high'}) 66 | >>> firebase.get(URL) 67 | { 68 | u'-JyAXHX9ZNBh7tPPja4w': {u'color': u'pink', u'jiggliness': u'high'} 69 | } 70 | 71 | >>> firebase.push(URL, {'color': 'white', 'jiggliness': 'extreme'}) 72 | >>> firebase.get(URL) 73 | { 74 | u'-JyAXHX9ZNBh7tPPja4w': {u'color': u'pink', u'jiggliness': u'high'}, 75 | u'-JyAXHX9ZNBh7tPPjasd': {u'color': u'white', u'jiggliness': u'extreme'} 76 | } 77 | ``` 78 | 79 | 80 | 81 | ## patch 82 | 83 | `patch` adds new key value pairs to an existing Firebase, without deleting the old key value pairs. 84 | 85 | ```python 86 | >>> import ufirebase as firebase 87 | >>> URL = 'tibetan-tumbleweed' 88 | >>> print firebase.get(URL) 89 | None 90 | 91 | >>> firebase.patch(URL, {'taste': 'tibetan'}) 92 | >>> print firebase.get(URL) 93 | {u'taste': u'tibetan'} 94 | 95 | >>> firebase.patch(URL, {'size': 'tumbly}) # patching does not overwrite 96 | >>> print firebase.get(URL) 97 | {u'taste': u'tibetan', u'size': u'tumbly'} 98 | ``` 99 | 100 | 101 | 102 | ## subscriber (WIP. This won't work) 103 | 104 | `subscriber` takes a URL and callback function and calls the callback on every update of the Firebase at URL. 105 | 106 | ```python 107 | >>> import firebase 108 | >>> from pprint import pprint # function which pretty prints objects 109 | >>> URL = 'clumsy-clementine' 110 | >>> S = firebase.subscriber(URL, pprint) # pprint will be called on all Firebase updates 111 | >>> S.start() # will get called with initial value of URL, which is empty 112 | (u'put', {u'data': None, u'path': u'/'}) 113 | 114 | >>> firebase.put(URL, ';-)') # will make S print something 115 | (u'put', {u'data': u';-)', u'path': u'/'}) 116 | 117 | >>> firebase.put(URL, {'status': 'mortified'}) # continuing from above 118 | (u'put', {u'data': {u'status': u'mortified'}, u'path': u'/'}) 119 | >>> firebase.patch(URL, {'reason': 'blushing'}) # same data, different method 120 | (u'patch', {u'data': {u'reason': u'blushing'}, u'path': u'/'}) 121 | 122 | >>> firebase.put(URL + '/color', 'red') 123 | (u'put', {u'data': u'red', u'path': u'/color'}) 124 | 125 | >>> S.stop() 126 | ``` 127 | 128 | 129 | 130 | ## URLs (WILL BE REMOVED in the upcoming versions for reducing any code latency) 131 | All URLs are internally converted to the apparent Firebase URL. This is done by the `firebaseURL` method. 132 | 133 | ```python 134 | >>> import firebase 135 | 136 | >>> print firebase.firebaseURL('bony-badger') 137 | https://bony-badger.firebaseio.com/.json 138 | 139 | >>> print firebase.firebaseURL('bony-badger/bones/humerus') 140 | https://bony-badger.firebaseio.com/bones/humerus.json 141 | 142 | >>> print firebase.firebaseURL('bony-badger.firebaseio.com/') 143 | https://bony-badger.firebaseio.com/.json 144 | ``` 145 | 146 | ## TO-DO: 147 | 148 | 1. Remove whatever latency possible. 149 | 2. Async Subscription to changes is WIP 150 | -------------------------------------------------------------------------------- /startup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def wlan_connect(ssid=sys.argv[1], password=sys.argv[2]): 4 | import network 5 | wlan = network.WLAN(network.STA_IF) 6 | if not wlan.active() or not wlan.isconnected(): 7 | wlan.active(True) 8 | print('connecting to:', ssid) 9 | wlan.connect(ssid, password) 10 | while not wlan.isconnected(): 11 | pass 12 | print('network config:', wlan.ifconfig()) 13 | 14 | wlan_connect() 15 | -------------------------------------------------------------------------------- /ufirebase.py: -------------------------------------------------------------------------------- 1 | # adapted from firebase/EventSource-Examples/python/chat.py by Shariq Hashme 2 | 3 | from usseclient import SSEClient 4 | import urequests as requests 5 | 6 | import ujson as json 7 | import _thread as thread 8 | import usocket as socket 9 | 10 | 11 | class ClosableSSEClient(SSEClient): 12 | def __init__(self, *args, **kwargs): 13 | self.should_connect = True 14 | super(ClosableSSEClient, self).__init__(*args, **kwargs) 15 | 16 | def _connect(self): 17 | if self.should_connect: 18 | super(ClosableSSEClient, self)._connect() 19 | else: 20 | raise StopIteration() 21 | 22 | 23 | def close(self): 24 | self.should_connect = False 25 | self.retry = 0 26 | try: 27 | self.resp.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR) 28 | self.resp.raw._fp.fp._sock.close() 29 | except AttributeError: 30 | pass 31 | 32 | 33 | class RemoteThread(): 34 | def __init__(self, parent, URL, function): 35 | self.function = function 36 | self.URL = URL 37 | self.parent = parent 38 | super(RemoteThread, self).__init__() 39 | 40 | def run(self): 41 | try: 42 | self.sse = ClosableSSEClient(self.URL) 43 | for msg in self.sse: 44 | msg_data = json.loads(msg.data) 45 | if msg_data is None: # keep-alives 46 | continue 47 | msg_event = msg.event 48 | # TODO: update parent cache here 49 | self.function((msg.event, msg_data)) 50 | except socket.error: 51 | pass # this can happen when we close the stream 52 | except KeyboardInterrupt: 53 | self.close() 54 | 55 | 56 | def start(self, run): 57 | thread.start_new_thread(run) 58 | 59 | def stop(self): 60 | thread.exit() 61 | 62 | 63 | def close(self): 64 | if self.sse: 65 | self.sse.close() 66 | 67 | 68 | def firebaseURL(URL): 69 | if '.firebaseio.com' not in URL.lower(): 70 | if '.json' == URL[-5:]: 71 | URL = URL[:-5] 72 | if '/' in URL: 73 | if '/' == URL[-1]: 74 | URL = URL[:-1] 75 | URL = 'https://' + \ 76 | URL.split('/')[0] + '.firebaseio.com/' + URL.split('/', 1)[1] + '.json' 77 | else: 78 | URL = 'https://' + URL + '.firebaseio.com/.json' 79 | return URL 80 | 81 | if 'http://' in URL: 82 | URL = URL.replace('http://', 'https://') 83 | if 'https://' not in URL: 84 | URL = 'https://' + URL 85 | if '.json' not in URL.lower(): 86 | if '/' != URL[-1]: 87 | URL = URL + '/.json' 88 | else: 89 | URL = URL + '.json' 90 | return URL 91 | 92 | 93 | class subscriber: 94 | def __init__(self, URL, function): 95 | self.cache = {} 96 | self.remote_thread = RemoteThread(self, firebaseURL(URL), function) 97 | 98 | def start(self): 99 | self.remote_thread.start() 100 | 101 | def stop(self): 102 | self.remote_thread.stop() 103 | 104 | 105 | class FirebaseException(Exception): 106 | pass 107 | 108 | 109 | def put(URL, msg): 110 | to_post = json.dumps(msg) 111 | response = requests.put(firebaseURL(URL), data=to_post) 112 | if response.status_code != 200: 113 | raise FirebaseException(response.text) 114 | 115 | 116 | def patch(URL, msg): 117 | to_post = json.dumps(msg) 118 | response = requests.patch(firebaseURL(URL), data=to_post) 119 | if response.status_code != 200: 120 | raise FirebaseException(response.text) 121 | 122 | 123 | def get(URL): 124 | response = requests.get(firebaseURL(URL)) 125 | if response.status_code != 200: 126 | raise FirebaseException(response.text) 127 | return json.loads(response.text) 128 | 129 | 130 | def push(URL, msg): 131 | to_post = json.dumps(msg) 132 | response = requests.post(firebaseURL(URL), data=to_post) 133 | if response.status_code != 200: 134 | raise Exception(response.text) 135 | -------------------------------------------------------------------------------- /usseclient.py: -------------------------------------------------------------------------------- 1 | """ 2 | Server Side Events (SSE) client for Python. 3 | Provides a generator of SSE received through an existing HTTP response. 4 | """ 5 | 6 | # Copyright (C) 2016 SignalFx, Inc. All rights reserved. 7 | 8 | 9 | __author__ = 'Maxime Petazzoni ' 10 | __email__ = 'maxime.petazzoni@bulix.org' 11 | __copyright__ = 'Copyright (C) 2016 SignalFx, Inc. All rights reserved.' 12 | __all__ = ['SSEClient'] 13 | 14 | _FIELD_SEPARATOR = ':' 15 | 16 | 17 | class SSEClient(object): 18 | """Implementation of a SSE client. 19 | See http://www.w3.org/TR/2009/WD-eventsource-20091029/ for the 20 | specification. 21 | """ 22 | 23 | def __init__(self, event_source, char_enc='utf-8'): 24 | """Initialize the SSE client over an existing, ready to consume 25 | event source. 26 | The event source is expected to provide a stream() generator method and 27 | a close() method. 28 | """ 29 | self._event_source = event_source 30 | self._char_enc = char_enc 31 | 32 | def _read(self): 33 | """Read the incoming event source stream and yield event chunks. 34 | Unfortunately it is possible for some servers to decide to break an 35 | event into multiple HTTP chunks in the response. It is thus necessary 36 | to correctly stitch together consecutive response chunks and find the 37 | SSE delimiter (empty new line) to yield full, correct event chunks.""" 38 | data = '' 39 | for chunk in self._event_source: 40 | for line in chunk.splitlines(True): 41 | if not line.strip(): 42 | yield data 43 | data = '' 44 | data += line.decode(self._char_enc) 45 | if data: 46 | yield data 47 | 48 | 49 | def events(self): 50 | for chunk in self._read(): 51 | event = Event() 52 | for line in chunk.splitlines(): 53 | # Lines starting with a separator are comments and are to be 54 | # ignored. 55 | if not line.strip() or line.startswith(_FIELD_SEPARATOR): 56 | continue 57 | 58 | data = line.split(_FIELD_SEPARATOR, 1) 59 | field = data[0] 60 | 61 | # Ignore unknown fields. 62 | if field not in event.__dict__: 63 | continue 64 | 65 | # Spaces may occur before the value; strip them. If no value is 66 | # present after the separator, assume an empty value. 67 | value = data[1].lstrip() if len(data) > 1 else '' 68 | 69 | # The data field may come over multiple lines and their values 70 | # are concatenated with each other. 71 | if field == 'data': 72 | event.__dict__[field] += value + '\n' 73 | else: 74 | event.__dict__[field] = value 75 | 76 | 77 | # Events with no data are not dispatched. 78 | if not event.data: 79 | continue 80 | 81 | # If the data field ends with a newline, remove it. 82 | if event.data.endswith('\n'): 83 | event.data = event.data[0:-1] 84 | 85 | # Dispatch the event 86 | yield event 87 | 88 | 89 | def close(self): 90 | """Manually close the event source stream.""" 91 | self._event_source.close() 92 | 93 | 94 | class Event(object): 95 | """Representation of an event from the event stream.""" 96 | 97 | def __init__(self, id=None, event='message', data='', retry=None): 98 | self.id = id 99 | self.event = event 100 | self.data = data 101 | self.retry = retry 102 | 103 | def __str__(self): 104 | s = '{0} event'.format(self.event) 105 | if self.id: 106 | s += ' #{0}'.format(self.id) 107 | if self.data: 108 | s += ', {0} byte{1}'.format(len(self.data), 109 | 's' if len(self.data) else '') 110 | else: 111 | s += ', no data' 112 | if self.retry: 113 | s += ', retry in {0}ms'.format(self.retry) 114 | return s 115 | --------------------------------------------------------------------------------