diff --git a/GW-custom/uPyLoRaWAN/.vscode/extensions.json b/GW-custom/uPyLoRaWAN/.vscode/extensions.json
deleted file mode 100644
index 2e8d346480b49a2f5651b00a5a3ca12c25b89f11..0000000000000000000000000000000000000000
--- a/GW-custom/uPyLoRaWAN/.vscode/extensions.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
-    // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
-    // List of extensions which should be recommended for users of this workspace.
-    "recommendations": [
-        "ms-python.python", // micropy-cli: required for vscode micropython integrations
-        "VisualStudioExptTeam.vscodeintellicode" // micropy-cli: optional for advanced intellisense
-    ]
-}
\ No newline at end of file
diff --git a/GW-custom/uPyLoRaWAN/.vscode/settings.json b/GW-custom/uPyLoRaWAN/.vscode/settings.json
deleted file mode 100644
index 292a63e1f79f9f016a2d07191fdd0d194d0742fd..0000000000000000000000000000000000000000
--- a/GW-custom/uPyLoRaWAN/.vscode/settings.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "python.linting.enabled": true,
-    "python.jediEnabled": false,
-
-    // Loaded Stubs:  esp32-micropython-1.12.0 
-    "python.autoComplete.extraPaths": [".micropy/BradenM-micropy-stubs-9569403/frozen", ".micropy/BradenM-micropy-stubs-4c2702f/frozen", ".micropy/BradenM-micropy-stubs-9569403/stubs", ".micropy/uPyLora"],
-    "python.autoComplete.typeshedPaths":  [".micropy/BradenM-micropy-stubs-9569403/frozen", ".micropy/BradenM-micropy-stubs-4c2702f/frozen", ".micropy/BradenM-micropy-stubs-9569403/stubs", ".micropy/uPyLora"],
-    "python.analysis.typeshedPaths":  [".micropy/BradenM-micropy-stubs-9569403/frozen", ".micropy/BradenM-micropy-stubs-4c2702f/frozen", ".micropy/BradenM-micropy-stubs-9569403/stubs", ".micropy/uPyLora"],
-
-    "python.linting.pylintEnabled": true
-}
\ No newline at end of file
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/LICENSE b/GW-custom/uPyLoRaWAN/uPySensors/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/README.md b/GW-custom/uPyLoRaWAN/uPySensors/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6847f8148028945379da7a10d5f6ac6b257e4100
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/README.md
@@ -0,0 +1,22 @@
+# uPySensors
+MicroPython sensor/actuator libraries
+
+
+# Added Drivers
+
+|   |   |   |
+|:--|:--|:--|
+|`hcsr04.py`|Ultrasonic Sensor - HC-SR04 | [about](https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf) - [buy](https://www.banggood.com/de/Wholesale-Ultrasonic-Module-HC-SR04-Distance-Measuring-Ranging-Transducer-Sensor-p-40313.html?p=QW0903761303201409LG) - [blog article](https://lemariva.com/blog/2018/06/tutorial-getting-started-with-micropython-sensors)|
+|`imu.py & vector3d.py`| MPU6055 - Triple Axis Accelerometer and Gyro |[about](https://www.invensense.com/products/motion-tracking/6-axis/mpu-6500/) - [buy](https://www.banggood.com/de/5pcs-GY-6500-MPU6500-6DOF-6-Axis-Attitude-Acceleration-Gyroscope-Sensor-Module-SPI-Interface-p-1291399.html?p=QW0903761303201409LG) - [blog article](https://lemariva.com/blog/2018/06/micropython-camera-stabilisation-application)||
+|`imu.py, vector3d.py & mpu9250.py`| MPU9250/5 - 9-axis Motion Processing Unit |[about](https://www.invensense.com/products/motion-tracking/9-axis/mpu-9250/) - [buy](https://www.banggood.com/de/GY-91-MPU9250-BMP280-10DOF-Acceleration-Gyroscope-Compass-Nine-Shaft-Sensor-Module-p-1129541.html?p=QW0903761303201409LG) - [blog article](https://lemariva.com/blog/2018/06/micropython-camera-stabilisation-application)|
+|`ublox_gps.py` | NEO-M8N/6M - GPS Modules |[about](https://www.u-blox.com/de/product/neo-m8-series) - [buy](https://rover.ebay.com/rover/1/707-53477-19255-0/1?toolid=20001&campid=5338002758&customid=link&mpre=http%3A%2F%2Fwww.ebay.de%2Fitm%2F272470133068%3F_trksid%3Dp2060353.m2749.l2649%26ssPageName%3DSTRK%253AMEBIDX%253AIT) - [blog article](https://lemariva.com/blog/2017/04/wipy-2-0-weather-report-neo-m8n)|
+|`vl53l0x.py` |  Vl53l0X - laser-ranging module |[about](http://www.st.com/en/imaging-and-photonics-solutions/vl53l0x.html) - [buy](https://www.banggood.com/de/GY-530-VL53L0X-Laser-Ranging-Sensor-Module-IIC-Communication-Ranging-Module-p-1201341.html?p=QW0903761303201409LG) - [blog article](https://lemariva.com/blog/2018/12/happy-new-year-2019)|
+|`ssd1306_i2c.py` `ssd1306.py` | SSD1306 - display drivers | [blog article](https://lemariva.com/blog/2018/10/micropython-esp32-sending-data-using-lora) |
+|`ST7735.py` `sysfont.py` | ST7735 - display drivers for ESP32/ESP8266 | [blog article](https://lemariva.com/blog/2019/01/micropython-programming-an-esp-using-jupyter-notebook) |
+|`ili934xnew.py` `fonts/*` | ILI934 - display drivers for ESP32/M5Stack | [blog article](https://lemariva.com/blog/2020/02/m5stack-micropython-and-bluetooth-ble) |
+|`bmx280.py`  |  BME280 / BMP280 - temperature, pressure and humidity (BME) sensor| [about](https://www.bosch-sensortec.com/products/environmental-sensors/humidity-sensors-bme280/) - [blog article soon]   |
+|`bmx680.py`  | BME680  - gas, temperature, pressure and humidity sensor|  [about](https://www.bosch-sensortec.com/products/environmental-sensors/gas-sensors-bme680/) - [blog article](https://lemariva.com/blog/default/default/micropython-google-cloud-platform-getting-data-m5stack-atom-sensing-air-quality)   |
+|`pmsa003.py`  | PMSA003(a) particle/dust sensor| [about](https://lemariva.com/storage/app/media/uploaded-files/PMSA003.pdf) - [blog article](https://lemariva.com/blog/2020/04/micropython-google-cloud-platform-getting-data-m5stack-atom-sensing-air-quality)   |
+|`stepper.py`  |  6-wire steppers using LN298|  [blog article soon]  |
+# Licenses
+* check files
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/ST7735.py b/GW-custom/uPyLoRaWAN/uPySensors/ST7735.py
new file mode 100644
index 0000000000000000000000000000000000000000..8ab4c5766d6b1eb40a07faae993116e5a833cebf
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/ST7735.py
@@ -0,0 +1,890 @@
+#driver for Sainsmart 1.8" TFT display ST7735
+#Translated by Guy Carver from the ST7735 sample code.
+#Modirfied for micropython-esp32 by boochow
+
+import machine
+import time
+from math import sqrt
+
+#TFTRotations and TFTRGB are bits to set
+# on MADCTL to control display rotation/color layout
+#Looking at display with pins on top.
+#00 = upper left printing right
+#10 = does nothing (MADCTL_ML)
+#20 = upper left printing down (backwards) (Vertical flip)
+#40 = upper right printing left (backwards) (X Flip)
+#80 = lower left printing right (backwards) (Y Flip)
+#04 = (MADCTL_MH)
+
+#60 = 90 right rotation
+#C0 = 180 right rotation
+#A0 = 270 right rotation
+TFTRotations = [0x00, 0x60, 0xC0, 0xA0]
+TFTBGR = 0x08 #When set color is bgr else rgb.
+TFTRGB = 0x00
+
+#@micropython.native
+def clamp( aValue, aMin, aMax ) :
+  return max(aMin, min(aMax, aValue))
+
+#@micropython.native
+def TFTColor( aR, aG, aB ) :
+  '''Create a 16 bit rgb value from the given R,G,B from 0-255.
+     This assumes rgb 565 layout and will be incorrect for bgr.'''
+  return ((aR & 0xF8) << 8) | ((aG & 0xFC) << 3) | (aB >> 3)
+
+ScreenSize = (128, 160)
+
+class TFT(object) :
+  """Sainsmart TFT 7735 display driver."""
+
+  NOP = 0x0
+  SWRESET = 0x01
+  RDDID = 0x04
+  RDDST = 0x09
+
+  SLPIN  = 0x10
+  SLPOUT  = 0x11
+  PTLON  = 0x12
+  NORON  = 0x13
+
+  INVOFF = 0x20
+  INVON = 0x21
+  DISPOFF = 0x28
+  DISPON = 0x29
+  CASET = 0x2A
+  RASET = 0x2B
+  RAMWR = 0x2C
+  RAMRD = 0x2E
+
+  COLMOD = 0x3A
+  MADCTL = 0x36
+
+  FRMCTR1 = 0xB1
+  FRMCTR2 = 0xB2
+  FRMCTR3 = 0xB3
+  INVCTR = 0xB4
+  DISSET5 = 0xB6
+
+  PWCTR1 = 0xC0
+  PWCTR2 = 0xC1
+  PWCTR3 = 0xC2
+  PWCTR4 = 0xC3
+  PWCTR5 = 0xC4
+  VMCTR1 = 0xC5
+
+  RDID1 = 0xDA
+  RDID2 = 0xDB
+  RDID3 = 0xDC
+  RDID4 = 0xDD
+
+  PWCTR6 = 0xFC
+
+  GMCTRP1 = 0xE0
+  GMCTRN1 = 0xE1
+
+  BLACK = 0
+  RED = TFTColor(0xFF, 0x00, 0x00)
+  MAROON = TFTColor(0x80, 0x00, 0x00)
+  GREEN = TFTColor(0x00, 0xFF, 0x00)
+  FOREST = TFTColor(0x00, 0x80, 0x80)
+  BLUE = TFTColor(0x00, 0x00, 0xFF)
+  NAVY = TFTColor(0x00, 0x00, 0x80)
+  CYAN = TFTColor(0x00, 0xFF, 0xFF)
+  YELLOW = TFTColor(0xFF, 0xFF, 0x00)
+  PURPLE = TFTColor(0xFF, 0x00, 0xFF)
+  WHITE = TFTColor(0xFF, 0xFF, 0xFF)
+  GRAY = TFTColor(0x80, 0x80, 0x80)
+
+  @staticmethod
+  def color( aB, aG, aR ) :
+    '''Create a 565 rgb TFTColor value'''
+    return TFTColor(aR, aG, aB)
+
+  def __init__( self, spi, aDC, aReset, aCS) :
+    """aLoc SPI pin location is either 1 for 'X' or 2 for 'Y'.
+       aDC is the DC pin and aReset is the reset pin."""
+    self._size = ScreenSize
+    self._offset = bytearray([0,0])
+    self.rotate = 0                    #Vertical with top toward pins.
+    self._rgb = True                   #color order of rgb.
+    self.dc  = machine.Pin(aDC, machine.Pin.OUT, machine.Pin.PULL_DOWN)
+    self.reset = machine.Pin(aReset, machine.Pin.OUT, machine.Pin.PULL_DOWN)
+    self.cs = machine.Pin(aCS, machine.Pin.OUT, machine.Pin.PULL_DOWN)
+    self.cs(1)
+    self.spi = spi
+    self.colorData = bytearray(2)
+    self.windowLocData = bytearray(4)
+
+  def size( self ) :
+    return self._size
+
+#   @micropython.native
+  def on( self, aTF = True ) :
+    '''Turn display on or off.'''
+    self._writecommand(TFT.DISPON if aTF else TFT.DISPOFF)
+
+#   @micropython.native
+  def invertcolor( self, aBool ) :
+    '''Invert the color data IE: Black = White.'''
+    self._writecommand(TFT.INVON if aBool else TFT.INVOFF)
+
+#   @micropython.native
+  def rgb( self, aTF = True ) :
+    '''True = rgb else bgr'''
+    self._rgb = aTF
+    self._setMADCTL()
+
+#   @micropython.native
+  def rotation( self, aRot ) :
+    '''0 - 3. Starts vertical with top toward pins and rotates 90 deg
+       clockwise each step.'''
+    if (0 <= aRot < 4):
+      rotchange = self.rotate ^ aRot
+      self.rotate = aRot
+      #If switching from vertical to horizontal swap x,y
+      # (indicated by bit 0 changing).
+      if (rotchange & 1):
+        self._size =(self._size[1], self._size[0])
+      self._setMADCTL()
+
+#  @micropython.native
+  def pixel( self, aPos, aColor ) :
+    '''Draw a pixel at the given position'''
+    if 0 <= aPos[0] < self._size[0] and 0 <= aPos[1] < self._size[1]:
+      self._setwindowpoint(aPos)
+      self._pushcolor(aColor)
+
+#   @micropython.native
+  def text( self, aPos, aString, aColor, aFont, aSize = 1, nowrap = False ) :
+    '''Draw a text at the given position.  If the string reaches the end of the
+       display it is wrapped to aPos[0] on the next line.  aSize may be an integer
+       which will size the font uniformly on w,h or a or any type that may be
+       indexed with [0] or [1].'''
+
+    if aFont == None:
+      return
+
+    #Make a size either from single value or 2 elements.
+    if (type(aSize) == int) or (type(aSize) == float):
+      wh = (aSize, aSize)
+    else:
+      wh = aSize
+
+    px, py = aPos
+    width = wh[0] * aFont["Width"] + 1
+    for c in aString:
+      self.char((px, py), c, aColor, aFont, wh)
+      px += width
+      #We check > rather than >= to let the right (blank) edge of the
+      # character print off the right of the screen.
+      if px + width > self._size[0]:
+        if nowrap:
+          break
+        else:
+          py += aFont["Height"] * wh[1] + 1
+          px = aPos[0]
+
+#   @micropython.native
+  def char( self, aPos, aChar, aColor, aFont, aSizes ) :
+    '''Draw a character at the given position using the given font and color.
+       aSizes is a tuple with x, y as integer scales indicating the
+       # of pixels to draw for each pixel in the character.'''
+
+    if aFont == None:
+      return
+
+    startchar = aFont['Start']
+    endchar = aFont['End']
+
+    ci = ord(aChar)
+    if (startchar <= ci <= endchar):
+      fontw = aFont['Width']
+      fonth = aFont['Height']
+      ci = (ci - startchar) * fontw
+
+      charA = aFont["Data"][ci:ci + fontw]
+      px = aPos[0]
+      if aSizes[0] <= 1 and aSizes[1] <= 1 :
+        for c in charA :
+          py = aPos[1]
+          for r in range(fonth) :
+            if c & 0x01 :
+              self.pixel((px, py), aColor)
+            py += 1
+            c >>= 1
+          px += 1
+      else:
+        for c in charA :
+          py = aPos[1]
+          for r in range(fonth) :
+            if c & 0x01 :
+              self.fillrect((px, py), aSizes, aColor)
+            py += aSizes[1]
+            c >>= 1
+          px += aSizes[0]
+
+#   @micropython.native
+  def line( self, aStart, aEnd, aColor ) :
+    '''Draws a line from aStart to aEnd in the given color.  Vertical or horizontal
+       lines are forwarded to vline and hline.'''
+    if aStart[0] == aEnd[0]:
+      #Make sure we use the smallest y.
+      pnt = aEnd if (aEnd[1] < aStart[1]) else aStart
+      self.vline(pnt, abs(aEnd[1] - aStart[1]) + 1, aColor)
+    elif aStart[1] == aEnd[1]:
+      #Make sure we use the smallest x.
+      pnt = aEnd if aEnd[0] < aStart[0] else aStart
+      self.hline(pnt, abs(aEnd[0] - aStart[0]) + 1, aColor)
+    else:
+      px, py = aStart
+      ex, ey = aEnd
+      dx = ex - px
+      dy = ey - py
+      inx = 1 if dx > 0 else -1
+      iny = 1 if dy > 0 else -1
+
+      dx = abs(dx)
+      dy = abs(dy)
+      if (dx >= dy):
+        dy <<= 1
+        e = dy - dx
+        dx <<= 1
+        while (px != ex):
+          self.pixel((px, py), aColor)
+          if (e >= 0):
+            py += iny
+            e -= dx
+          e += dy
+          px += inx
+      else:
+        dx <<= 1
+        e = dx - dy
+        dy <<= 1
+        while (py != ey):
+          self.pixel((px, py), aColor)
+          if (e >= 0):
+            px += inx
+            e -= dy
+          e += dx
+          py += iny
+
+#   @micropython.native
+  def vline( self, aStart, aLen, aColor ) :
+    '''Draw a vertical line from aStart for aLen. aLen may be negative.'''
+    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
+    stop = (start[0], clamp(start[1] + aLen, 0, self._size[1]))
+    #Make sure smallest y 1st.
+    if (stop[1] < start[1]):
+      start, stop = stop, start
+    self._setwindowloc(start, stop)
+    self._setColor(aColor)
+    self._draw(aLen)
+
+#   @micropython.native
+  def hline( self, aStart, aLen, aColor ) :
+    '''Draw a horizontal line from aStart for aLen. aLen may be negative.'''
+    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
+    stop = (clamp(start[0] + aLen, 0, self._size[0]), start[1])
+    #Make sure smallest x 1st.
+    if (stop[0] < start[0]):
+      start, stop = stop, start
+    self._setwindowloc(start, stop)
+    self._setColor(aColor)
+    self._draw(aLen)
+
+#   @micropython.native
+  def rect( self, aStart, aSize, aColor ) :
+    '''Draw a hollow rectangle.  aStart is the smallest coordinate corner
+       and aSize is a tuple indicating width, height.'''
+    self.hline(aStart, aSize[0], aColor)
+    self.hline((aStart[0], aStart[1] + aSize[1] - 1), aSize[0], aColor)
+    self.vline(aStart, aSize[1], aColor)
+    self.vline((aStart[0] + aSize[0] - 1, aStart[1]), aSize[1], aColor)
+
+#   @micropython.native
+  def fillrect( self, aStart, aSize, aColor ) :
+    '''Draw a filled rectangle.  aStart is the smallest coordinate corner
+       and aSize is a tuple indicating width, height.'''
+    start = (clamp(aStart[0], 0, self._size[0]), clamp(aStart[1], 0, self._size[1]))
+    end = (clamp(start[0] + aSize[0] - 1, 0, self._size[0]), clamp(start[1] + aSize[1] - 1, 0, self._size[1]))
+
+    if (end[0] < start[0]):
+      tmp = end[0]
+      end = (start[0], end[1])
+      start = (tmp, start[1])
+    if (end[1] < start[1]):
+      tmp = end[1]
+      end = (end[0], start[1])
+      start = (start[0], tmp)
+
+    self._setwindowloc(start, end)
+    numPixels = (end[0] - start[0] + 1) * (end[1] - start[1] + 1)
+    self._setColor(aColor)
+    self._draw(numPixels)
+
+#   @micropython.native
+  def circle( self, aPos, aRadius, aColor ) :
+    '''Draw a hollow circle with the given radius and color with aPos as center.'''
+    self.colorData[0] = aColor >> 8
+    self.colorData[1] = aColor
+    xend = int(0.7071 * aRadius) + 1
+    rsq = aRadius * aRadius
+    for x in range(xend) :
+      y = int(sqrt(rsq - x * x))
+      xp = aPos[0] + x
+      yp = aPos[1] + y
+      xn = aPos[0] - x
+      yn = aPos[1] - y
+      xyp = aPos[0] + y
+      yxp = aPos[1] + x
+      xyn = aPos[0] - y
+      yxn = aPos[1] - x
+
+      self._setwindowpoint((xp, yp))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xp, yn))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xn, yp))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xn, yn))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xyp, yxp))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xyp, yxn))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xyn, yxp))
+      self._writedata(self.colorData)
+      self._setwindowpoint((xyn, yxn))
+      self._writedata(self.colorData)
+
+#   @micropython.native
+  def fillcircle( self, aPos, aRadius, aColor ) :
+    '''Draw a filled circle with given radius and color with aPos as center'''
+    rsq = aRadius * aRadius
+    for x in range(aRadius) :
+      y = int(sqrt(rsq - x * x))
+      y0 = aPos[1] - y
+      ey = y0 + y * 2
+      y0 = clamp(y0, 0, self._size[1])
+      ln = abs(ey - y0) + 1;
+
+      self.vline((aPos[0] + x, y0), ln, aColor)
+      self.vline((aPos[0] - x, y0), ln, aColor)
+
+  def fill( self, aColor = BLACK ) :
+    '''Fill screen with the given color.'''
+    self.fillrect((0, 0), self._size, aColor)
+
+#   @micropython.native
+  def _setColor( self, aColor ) :
+    self.colorData[0] = aColor >> 8
+    self.colorData[1] = aColor
+    self.buf = bytes(self.colorData) * 32
+
+#   @micropython.native
+  def _draw( self, aPixels ) :
+    '''Send given color to the device aPixels times.'''
+
+    self.dc(1)
+    self.cs(0)
+    for i in range(aPixels//32):
+      self.spi.write(self.buf)
+    rest = (int(aPixels) % 32)
+    if rest > 0:
+        buf2 = bytes(self.colorData) * rest
+        self.spi.write(buf2)
+    self.cs(1)
+
+#   @micropython.native
+  def _setwindowpoint( self, aPos ) :
+    '''Set a single point for drawing a color to.'''
+    x = self._offset[0] + int(aPos[0])
+    y = self._offset[1] + int(aPos[1])
+    self._writecommand(TFT.CASET)            #Column address set.
+    self.windowLocData[0] = self._offset[0]
+    self.windowLocData[1] = x
+    self.windowLocData[2] = self._offset[0]
+    self.windowLocData[3] = x
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)            #Row address set.
+    self.windowLocData[0] = self._offset[1]
+    self.windowLocData[1] = y
+    self.windowLocData[2] = self._offset[1]
+    self.windowLocData[3] = y
+    self._writedata(self.windowLocData)
+    self._writecommand(TFT.RAMWR)            #Write to RAM.
+
+#   @micropython.native
+  def _setwindowloc( self, aPos0, aPos1 ) :
+    '''Set a rectangular area for drawing a color to.'''
+    self._writecommand(TFT.CASET)            #Column address set.
+    self.windowLocData[0] = self._offset[0]
+    self.windowLocData[1] = self._offset[0] + int(aPos0[0])
+    self.windowLocData[2] = self._offset[0]
+    self.windowLocData[3] = self._offset[0] + int(aPos1[0])
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)            #Row address set.
+    self.windowLocData[0] = self._offset[1]
+    self.windowLocData[1] = self._offset[1] + int(aPos0[1])
+    self.windowLocData[2] = self._offset[1]
+    self.windowLocData[3] = self._offset[1] + int(aPos1[1])
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RAMWR)            #Write to RAM.
+
+  #@micropython.native
+  def _writecommand( self, aCommand ) :
+    '''Write given command to the device.'''
+    self.dc(0)
+    self.cs(0)
+    self.spi.write(bytearray([aCommand]))
+    self.cs(1)
+
+  #@micropython.native
+  def _writedata( self, aData ) :
+    '''Write given data to the device.  This may be
+       either a single int or a bytearray of values.'''
+    self.dc(1)
+    self.cs(0)
+    self.spi.write(aData)
+    self.cs(1)
+
+  #@micropython.native
+  def _pushcolor( self, aColor ) :
+    '''Push given color to the device.'''
+    self.colorData[0] = aColor >> 8
+    self.colorData[1] = aColor
+    self._writedata(self.colorData)
+
+  #@micropython.native
+  def _setMADCTL( self ) :
+    '''Set screen rotation and RGB/BGR format.'''
+    self._writecommand(TFT.MADCTL)
+    rgb = TFTRGB if self._rgb else TFTBGR
+    self._writedata(bytearray([TFTRotations[self.rotate] | rgb]))
+
+  #@micropython.native
+  def _reset( self ) :
+    '''Reset the device.'''
+    self.dc(0)
+    self.reset(1)
+    time.sleep_us(500)
+    self.reset(0)
+    time.sleep_us(500)
+    self.reset(1)
+    time.sleep_us(500)
+
+  def initb( self ) :
+    '''Initialize blue tab version.'''
+    self._size = (ScreenSize[0] + 2, ScreenSize[1] + 1)
+    self._reset()
+    self._writecommand(TFT.SWRESET)              #Software reset.
+    time.sleep_us(50)
+    self._writecommand(TFT.SLPOUT)               #out of sleep mode.
+    time.sleep_us(500)
+
+    data1 = bytearray(1)
+    self._writecommand(TFT.COLMOD)               #Set color mode.
+    data1[0] = 0x05                             #16 bit color.
+    self._writedata(data1)
+    time.sleep_us(10)
+
+    data3 = bytearray([0x00, 0x06, 0x03])       #fastest refresh, 6 lines front, 3 lines back.
+    self._writecommand(TFT.FRMCTR1)              #Frame rate control.
+    self._writedata(data3)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.MADCTL)
+    data1[0] = 0x08                             #row address/col address, bottom to top refresh
+    self._writedata(data1)
+
+    data2 = bytearray(2)
+    self._writecommand(TFT.DISSET5)              #Display settings
+    data2[0] = 0x15                             #1 clock cycle nonoverlap, 2 cycle gate rise, 3 cycle oscil, equalize
+    data2[1] = 0x02                             #fix on VTL
+    self._writedata(data2)
+
+    self._writecommand(TFT.INVCTR)               #Display inversion control
+    data1[0] = 0x00                             #Line inversion.
+    self._writedata(data1)
+
+    self._writecommand(TFT.PWCTR1)               #Power control
+    data2[0] = 0x02   #GVDD = 4.7V
+    data2[1] = 0x70   #1.0uA
+    self._writedata(data2)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.PWCTR2)               #Power control
+    data1[0] = 0x05                             #VGH = 14.7V, VGL = -7.35V
+    self._writedata(data1)
+
+    self._writecommand(TFT.PWCTR3)           #Power control
+    data2[0] = 0x01   #Opamp current small
+    data2[1] = 0x02   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.VMCTR1)               #Power control
+    data2[0] = 0x3C   #VCOMH = 4V
+    data2[1] = 0x38   #VCOML = -1.1V
+    self._writedata(data2)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.PWCTR6)               #Power control
+    data2[0] = 0x11
+    data2[1] = 0x15
+    self._writedata(data2)
+
+    #These different values don't seem to make a difference.
+#     dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f,
+#                             0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10])
+    dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29,
+                            0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
+    self._writecommand(TFT.GMCTRP1)
+    self._writedata(dataGMCTRP)
+
+#     dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30,
+#                             0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10])
+    dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e,
+                            0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
+    self._writecommand(TFT.GMCTRN1)
+    self._writedata(dataGMCTRN)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.CASET)                #Column address set.
+    self.windowLocData[0] = 0x00
+    self.windowLocData[1] = 2                   #Start at column 2
+    self.windowLocData[2] = 0x00
+    self.windowLocData[3] = self._size[0] - 1
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)                #Row address set.
+    self.windowLocData[1] = 1                   #Start at row 2.
+    self.windowLocData[3] = self._size[1] - 1
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.NORON)                #Normal display on.
+    time.sleep_us(10)
+
+    self._writecommand(TFT.RAMWR)
+    time.sleep_us(500)
+
+    self._writecommand(TFT.DISPON)
+    self.cs(1)
+    time.sleep_us(500)
+
+  def initr( self ) :
+    '''Initialize a red tab version.'''
+    self._reset()
+
+    self._writecommand(TFT.SWRESET)              #Software reset.
+    time.sleep_us(150)
+    self._writecommand(TFT.SLPOUT)               #out of sleep mode.
+    time.sleep_us(500)
+
+    data3 = bytearray([0x01, 0x2C, 0x2D])       #fastest refresh, 6 lines front, 3 lines back.
+    self._writecommand(TFT.FRMCTR1)              #Frame rate control.
+    self._writedata(data3)
+
+    self._writecommand(TFT.FRMCTR2)              #Frame rate control.
+    self._writedata(data3)
+
+    data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])
+    self._writecommand(TFT.FRMCTR3)              #Frame rate control.
+    self._writedata(data6)
+    time.sleep_us(10)
+
+    data1 = bytearray(1)
+    self._writecommand(TFT.INVCTR)               #Display inversion control
+    data1[0] = 0x07                             #Line inversion.
+    self._writedata(data1)
+
+    self._writecommand(TFT.PWCTR1)               #Power control
+    data3[0] = 0xA2
+    data3[1] = 0x02
+    data3[2] = 0x84
+    self._writedata(data3)
+
+    self._writecommand(TFT.PWCTR2)               #Power control
+    data1[0] = 0xC5   #VGH = 14.7V, VGL = -7.35V
+    self._writedata(data1)
+
+    data2 = bytearray(2)
+    self._writecommand(TFT.PWCTR3)               #Power control
+    data2[0] = 0x0A   #Opamp current small
+    data2[1] = 0x00   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR4)               #Power control
+    data2[0] = 0x8A   #Opamp current small
+    data2[1] = 0x2A   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR5)               #Power control
+    data2[0] = 0x8A   #Opamp current small
+    data2[1] = 0xEE   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.VMCTR1)               #Power control
+    data1[0] = 0x0E
+    self._writedata(data1)
+
+    self._writecommand(TFT.INVOFF)
+
+    self._writecommand(TFT.MADCTL)               #Power control
+    data1[0] = 0xC8
+    self._writedata(data1)
+
+    self._writecommand(TFT.COLMOD)
+    data1[0] = 0x05
+    self._writedata(data1)
+
+    self._writecommand(TFT.CASET)                #Column address set.
+    self.windowLocData[0] = 0x00
+    self.windowLocData[1] = 0x00
+    self.windowLocData[2] = 0x00
+    self.windowLocData[3] = self._size[0] - 1
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)                #Row address set.
+    self.windowLocData[3] = self._size[1] - 1
+    self._writedata(self.windowLocData)
+
+    dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f,
+                            0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10])
+    self._writecommand(TFT.GMCTRP1)
+    self._writedata(dataGMCTRP)
+
+    dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30,
+                            0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10])
+    self._writecommand(TFT.GMCTRN1)
+    self._writedata(dataGMCTRN)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.DISPON)
+    time.sleep_us(100)
+
+    self._writecommand(TFT.NORON)                #Normal display on.
+    time.sleep_us(10)
+
+    self.cs(1)
+
+  def initb2( self ) :
+    '''Initialize another blue tab version.'''
+    self._size = (ScreenSize[0] + 2, ScreenSize[1] + 1)
+    self._offset[0] = 2
+    self._offset[1] = 1
+    self._reset()
+    self._writecommand(TFT.SWRESET)              #Software reset.
+    time.sleep_us(50)
+    self._writecommand(TFT.SLPOUT)               #out of sleep mode.
+    time.sleep_us(500)
+
+    data3 = bytearray([0x01, 0x2C, 0x2D])        #
+    self._writecommand(TFT.FRMCTR1)              #Frame rate control.
+    self._writedata(data3)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.FRMCTR2)              #Frame rate control.
+    self._writedata(data3)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.FRMCTR3)              #Frame rate control.
+    self._writedata(data3)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.INVCTR)               #Display inversion control
+    data1 = bytearray(1)                         #
+    data1[0] = 0x07
+    self._writedata(data1)
+
+    self._writecommand(TFT.PWCTR1)               #Power control
+    data3[0] = 0xA2   #
+    data3[1] = 0x02   #
+    data3[2] = 0x84   #
+    self._writedata(data3)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.PWCTR2)               #Power control
+    data1[0] = 0xC5                              #
+    self._writedata(data1)
+
+    self._writecommand(TFT.PWCTR3)           #Power control
+    data2 = bytearray(2)
+    data2[0] = 0x0A   #
+    data2[1] = 0x00   #
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR4)           #Power control
+    data2[0] = 0x8A   #
+    data2[1] = 0x2A   #
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR5)           #Power control
+    data2[0] = 0x8A   #
+    data2[1] = 0xEE   #
+    self._writedata(data2)
+
+    self._writecommand(TFT.VMCTR1)               #Power control
+    data1[0] = 0x0E   #
+    self._writedata(data1)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.MADCTL)
+    data1[0] = 0xC8                             #row address/col address, bottom to top refresh
+    self._writedata(data1)
+
+#These different values don't seem to make a difference.
+#     dataGMCTRP = bytearray([0x0f, 0x1a, 0x0f, 0x18, 0x2f, 0x28, 0x20, 0x22, 0x1f,
+#                             0x1b, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10])
+    dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29,
+                            0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
+    self._writecommand(TFT.GMCTRP1)
+    self._writedata(dataGMCTRP)
+
+#     dataGMCTRN = bytearray([0x0f, 0x1b, 0x0f, 0x17, 0x33, 0x2c, 0x29, 0x2e, 0x30,
+#                             0x30, 0x39, 0x3f, 0x00, 0x07, 0x03, 0x10])
+    dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e,
+                            0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
+    self._writecommand(TFT.GMCTRN1)
+    self._writedata(dataGMCTRN)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.CASET)                #Column address set.
+    self.windowLocData[0] = 0x00
+    self.windowLocData[1] = 0x02                   #Start at column 2
+    self.windowLocData[2] = 0x00
+    self.windowLocData[3] = self._size[0] - 1
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)                #Row address set.
+    self.windowLocData[1] = 0x01                   #Start at row 2.
+    self.windowLocData[3] = self._size[1] - 1
+    self._writedata(self.windowLocData)
+
+    data1 = bytearray(1)
+    self._writecommand(TFT.COLMOD)               #Set color mode.
+    data1[0] = 0x05                             #16 bit color.
+    self._writedata(data1)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.NORON)                #Normal display on.
+    time.sleep_us(10)
+
+    self._writecommand(TFT.RAMWR)
+    time.sleep_us(500)
+
+    self._writecommand(TFT.DISPON)
+    self.cs(1)
+    time.sleep_us(500)
+
+  #@micropython.native
+  def initg( self ) :
+    '''Initialize a green tab version.'''
+    self._reset()
+
+    self._writecommand(TFT.SWRESET)              #Software reset.
+    time.sleep_us(150)
+    self._writecommand(TFT.SLPOUT)               #out of sleep mode.
+    time.sleep_us(255)
+
+    data3 = bytearray([0x01, 0x2C, 0x2D])       #fastest refresh, 6 lines front, 3 lines back.
+    self._writecommand(TFT.FRMCTR1)              #Frame rate control.
+    self._writedata(data3)
+
+    self._writecommand(TFT.FRMCTR2)              #Frame rate control.
+    self._writedata(data3)
+
+    data6 = bytearray([0x01, 0x2c, 0x2d, 0x01, 0x2c, 0x2d])
+    self._writecommand(TFT.FRMCTR3)              #Frame rate control.
+    self._writedata(data6)
+    time.sleep_us(10)
+
+    self._writecommand(TFT.INVCTR)               #Display inversion control
+    self._writedata(bytearray([0x07]))
+    self._writecommand(TFT.PWCTR1)               #Power control
+    data3[0] = 0xA2
+    data3[1] = 0x02
+    data3[2] = 0x84
+    self._writedata(data3)
+
+    self._writecommand(TFT.PWCTR2)               #Power control
+    self._writedata(bytearray([0xC5]))
+
+    data2 = bytearray(2)
+    self._writecommand(TFT.PWCTR3)               #Power control
+    data2[0] = 0x0A   #Opamp current small
+    data2[1] = 0x00   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR4)               #Power control
+    data2[0] = 0x8A   #Opamp current small
+    data2[1] = 0x2A   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.PWCTR5)               #Power control
+    data2[0] = 0x8A   #Opamp current small
+    data2[1] = 0xEE   #Boost frequency
+    self._writedata(data2)
+
+    self._writecommand(TFT.VMCTR1)               #Power control
+    self._writedata(bytearray([0x0E]))
+
+    self._writecommand(TFT.INVOFF)
+
+    self._setMADCTL()
+
+    self._writecommand(TFT.COLMOD)
+    self._writedata(bytearray([0x05]))
+
+    self._writecommand(TFT.CASET)                #Column address set.
+    self.windowLocData[0] = 0x00
+    self.windowLocData[1] = 0x01                #Start at row/column 1.
+    self.windowLocData[2] = 0x00
+    self.windowLocData[3] = self._size[0] - 1
+    self._writedata(self.windowLocData)
+
+    self._writecommand(TFT.RASET)                #Row address set.
+    self.windowLocData[3] = self._size[1] - 1
+    self._writedata(self.windowLocData)
+
+    dataGMCTRP = bytearray([0x02, 0x1c, 0x07, 0x12, 0x37, 0x32, 0x29, 0x2d, 0x29,
+                            0x25, 0x2b, 0x39, 0x00, 0x01, 0x03, 0x10])
+    self._writecommand(TFT.GMCTRP1)
+    self._writedata(dataGMCTRP)
+
+    dataGMCTRN = bytearray([0x03, 0x1d, 0x07, 0x06, 0x2e, 0x2c, 0x29, 0x2d, 0x2e,
+                            0x2e, 0x37, 0x3f, 0x00, 0x00, 0x02, 0x10])
+    self._writecommand(TFT.GMCTRN1)
+    self._writedata(dataGMCTRN)
+
+    self._writecommand(TFT.NORON)                #Normal display on.
+    time.sleep_us(10)
+
+    self._writecommand(TFT.DISPON)
+    time.sleep_us(100)
+
+    self.cs(1)
+
+def maker(  ) :
+  t = TFT(1, "X1", "X2")
+  print("Initializing")
+  t.initr()
+  t.fill(0)
+  return t
+
+def makeb(  ) :
+  t = TFT(1, "X1", "X2")
+  print("Initializing")
+  t.initb()
+  t.fill(0)
+  return t
+
+def makeg(  ) :
+  t = TFT(1, "X1", "X2")
+  print("Initializing")
+  t.initg()
+  t.fill(0)
+  return t
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/bme680.py b/GW-custom/uPyLoRaWAN/uPySensors/bme680.py
new file mode 100644
index 0000000000000000000000000000000000000000..db0347a25e037020112a7757c462598a150802e4
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/bme680.py
@@ -0,0 +1,489 @@
+'''
+'''
+# Power Modes
+from machine import I2C, Pin
+from micropython import const
+from ustruct import unpack as unp
+import utime 
+
+epoch_offset = 946684800
+
+NORMAL = const(0)
+
+# BME680 Temperature Registers
+BME680_REG_DIG_T1 = const(0xE9)
+BME680_REG_DIG_T2 = const(0x8A)
+BME680_REG_DIG_T3 = const(0x8C)
+# BME680 Pressure Registers
+BME680_REG_DIG_P1 = const(0x8E)
+BME680_REG_DIG_P2 = const(0x90)
+BME680_REG_DIG_P3 = const(0x92)
+BME680_REG_DIG_P4 = const(0x94)
+BME680_REG_DIG_P5 = const(0x96)
+BME680_REG_DIG_P6 = const(0x99)
+BME680_REG_DIG_P7 = const(0x98)
+BME680_REG_DIG_P8 = const(0x9C)
+BME680_REG_DIG_P9 = const(0x9E)
+BME680_REG_DIG_P10 = const(0xA0)
+# BME680 Humidity Registers
+BME680_REG_DIG_H1_LSB = const(0xE1)
+BME680_REG_DIG_H1_MSB = const(0xE3)
+BME680_REG_DIG_H2_LSB = const(0xE2)
+BME680_REG_DIG_H2_MSB = const(0xE1)
+BME680_REG_DIG_H3 = const(0xE4)
+BME680_REG_DIG_H4 = const(0xE5)
+BME680_REG_DIG_H5 = const(0xE6)
+BME680_REG_DIG_H6 = const(0xE7)
+BME680_REG_DIG_H7 = const(0xE8)
+# BME680 Gas Sensor
+BME680_REG_DIG_G1 = const(0xED)
+BME680_REG_DIG_G2 = const(0xE7)
+BME680_REG_DIG_G3 = const(0xEE)
+
+BME680_REG_ID = const(0xD0)
+BME680_NEW_DATA_MSK = const(0x80)
+BME680_REG_RESET = const(0xE0)
+BME680_RES_HEAT_0 = const(0x5A)
+BME680_GAS_WAIT_0 = const(0x64)
+BME680_HEAT_STAB_MSK = const(0x10)
+
+BME680_GAS_INDEX_MSK = const(0x0F)
+BME680_GAS_RANGE_MSK = const(0x0F)
+BME680_GASM_VALID_MSK = const(0x20)
+
+BME680_REG_CTRL_GAS = const(0x71)
+BME680_REG_CTRL_HUM = const(0x72)
+BME680_REG_STATUS = const(0xF3)
+BME680_REG_CTRL_MEAS = const(0x74)
+BME680_REG_CONFIG = const(0x75)  # IIR filter config
+
+BME680_REG_MEAS_STATUS = const(0x1D)
+BME680_REG_PDATA = const(0x1F)
+BME680_REG_TDATA = const(0x22)
+BME680_REG_HDATA = const(0x25)
+BME680_MAX_OVERFLOW_VAL =const(0x40000000)
+
+BMP680_I2C_ADDR = const(0x77)
+
+BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
+BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
+BME680_RUNGAS = const(0x10)
+
+_LOOKUP_TABLE_1 = (
+    2147483647.0,
+    2147483647.0,
+    2147483647.0,
+    2147483647.0,
+    2147483647.0,
+    2126008810.0,
+    2147483647.0,
+    2130303777.0,
+    2147483647.0,
+    2147483647.0,
+    2143188679.0,
+    2136746228.0,
+    2147483647.0,
+    2126008810.0,
+    2147483647.0,
+    2147483647.0,
+)
+
+_LOOKUP_TABLE_2 = (
+    4096000000.0,
+    2048000000.0,
+    1024000000.0,
+    512000000.0,
+    255744255.0,
+    127110228.0,
+    64000000.0,
+    32258064.0,
+    16016016.0,
+    8000000.0,
+    4000000.0,
+    2000000.0,
+    1000000.0,
+    500000.0,
+    250000.0,
+    125000.0,
+)
+
+class MPUException(OSError):
+    '''
+    Exception for MPU devices
+    '''
+    pass
+
+class GasSettings:
+    def __init__(self):
+        # Variable to store nb conversion
+        self.nb_conv = None
+        # Variable to store heater control
+        self.heatr_ctrl = None
+        # Run gas enable value
+        self.run_gas = None
+        # Pointer to store heater temperature
+        self.heatr_temp = None
+        # Pointer to store duration profile
+        self.heatr_dur = None
+
+class TPHSettings:
+    def __init__(self):
+        # Humidity oversampling
+        self.os_hum = None
+        # Temperature oversampling
+        self.os_temp = None
+        # Pressure oversampling
+        self.os_pres = None
+        # Filter coefficient
+        self.filter = None
+
+class BME680():
+    _I2Cerror = "I2C failure when communicating with the BMP/E"
+    # BME680 = 0x61
+    _chip_id = 0x61
+
+    _i2c_addr = BMP680_I2C_ADDR
+
+    def __init__(self, i2c, pins):
+        
+        self._pins = pins
+        
+        self._buf1 = bytearray(1)
+        self._buf2 = bytearray(2)
+
+        scl_pin = Pin(self._pins["scl"], Pin.OUT)
+        sda_pin = Pin(self._pins["sda"], Pin.IN)
+
+        self._i2c = I2C(i2c, scl=scl_pin, sda=sda_pin)
+        
+        self.chip_id
+
+        self._load_calibration()
+
+        self.power_on()
+
+        # Sensor settings
+        self.tph_settings = TPHSettings()
+        # Gas Sensor settings
+        self.gas_settings = GasSettings()
+        
+        # Default oversampling and filter register values.
+        self.tph_settings.os_pres = 0b011
+        self.tph_settings.os_temp = 0b100
+        self.tph_settings.os_hum = 0b010
+        self.tph_settings.filter = 0b010
+
+        # gas measurements enabled
+        self._write(BME680_REG_CTRL_GAS, BME680_RUNGAS)
+
+        # RAW measurements
+        self._p_raw = 0
+        self._t_raw = 0
+        self._h_raw = 0
+        self._g_raw = 0
+
+        # Calibrated measurements
+        self._t_fine = 0
+        self._t = 0
+        self._h = 0
+        self._p = 0
+        self._g = 0
+
+        self._g_range = 0
+        self._g_stable = 0
+
+        self._read_wait_ms = 100 
+        self._new_read_ms = 200 
+        self._last_read_ts = 0
+
+    def _read(self, memaddr, size=1):
+        data = self._i2c.readfrom_mem(self._i2c_addr, memaddr, size)
+        return data
+        
+    def _write(self, addr, b_arr):
+        if not type(b_arr) is bytearray:
+            b_arr = bytearray([b_arr])
+        return self._i2c.writeto_mem(self._i2c_addr, addr, b_arr)
+
+    def _load_calibration(self):
+        # read calibration data
+        # < little-endian
+        # H unsigned short
+        # h signed short
+        self._T1 = unp('<H', self._read(BME680_REG_DIG_T1, 2))[0]
+        self._T2 = unp('<h', self._read(BME680_REG_DIG_T2, 2))[0]
+        self._T3 = unp('<b', self._read(BME680_REG_DIG_T3, 1))[0]
+
+        self._P1 = unp('<H', self._read(BME680_REG_DIG_P1, 2))[0]
+        self._P2 = unp('<h', self._read(BME680_REG_DIG_P2, 2))[0]
+        self._P3 = unp('<b', self._read(BME680_REG_DIG_P3, 1))[0]
+        self._P4 = unp('<h', self._read(BME680_REG_DIG_P4, 2))[0]
+        self._P5 = unp('<h', self._read(BME680_REG_DIG_P5, 2))[0]
+        self._P6 = unp('<h', self._read(BME680_REG_DIG_P6, 2))[0]
+        self._P7 = unp('<b', self._read(BME680_REG_DIG_P7, 2))[0]
+        self._P8 = unp('<h', self._read(BME680_REG_DIG_P8, 2))[0]
+        self._P9 = unp('<h', self._read(BME680_REG_DIG_P9, 2))[0]
+        self._P10 = unp('<b', self._read(BME680_REG_DIG_P10, 1))[0]
+
+        self._H1 = ((unp('<b', self._read(BME680_REG_DIG_H1_MSB, 1))[0] << 4)
+			| (unp('<b', self._read(BME680_REG_DIG_H1_LSB, 1))[0] & 0x0F))
+        self._H2 = ((unp('<b', self._read(BME680_REG_DIG_H2_MSB, 1))[0] << 4)
+			| (unp('<b', self._read(BME680_REG_DIG_H2_LSB, 1))[0] & 0x0F))
+        self._H3 = unp('<b', self._read(BME680_REG_DIG_H3, 1))[0]
+        self._H4 = unp('<b', self._read(BME680_REG_DIG_H4, 1))[0]
+        self._H5 = unp('<b', self._read(BME680_REG_DIG_H5, 1))[0]
+        self._H6 = unp('<b', self._read(BME680_REG_DIG_H6, 1))[0]
+        self._H7 = unp('<b', self._read(BME680_REG_DIG_H7, 1))[0]
+        
+        self._G1 = unp('<b', self._read(BME680_REG_DIG_G1, 1))[0]
+        self._G2 = unp('<h', self._read(BME680_REG_DIG_G2, 2))[0]
+        self._G3 = unp('<b', self._read(BME680_REG_DIG_G3, 1))[0]
+
+        self._heat_range = (unp('<b', self._read(0x02, 1))[0] & 0x30) / 16
+        self._heat_val = unp('<b', self._read(0x00, 1))[0]
+        self._sw_err = (unp('<b', self._read(0x04, 1))[0] & 0xF0) / 16
+        
+    def print_calibration(self):
+        print("T1: {} {}".format(self._T1, type(self._T1)))
+        print("T2: {} {}".format(self._T2, type(self._T2)))
+        print("T3: {} {}".format(self._T3, type(self._T3)))
+        print("P1: {} {}".format(self._P1, type(self._P1)))
+        print("P2: {} {}".format(self._P2, type(self._P2)))
+        print("P3: {} {}".format(self._P3, type(self._P3)))
+        print("P4: {} {}".format(self._P4, type(self._P4)))
+        print("P5: {} {}".format(self._P5, type(self._P5)))
+        print("P6: {} {}".format(self._P6, type(self._P6)))
+        print("P7: {} {}".format(self._P7, type(self._P7)))
+        print("P8: {} {}".format(self._P8, type(self._P8)))
+        print("P9: {} {}".format(self._P9, type(self._P9)))
+        print("P10: {} {}".format(self._P10, type(self._P10)))
+        print("H1: {} {}".format(self._H1, type(self._H1)))
+        print("H2: {} {}".format(self._H2, type(self._H2)))
+        print("H3: {} {}".format(self._H3, type(self._H3)))
+        print("H4: {} {}".format(self._H4, type(self._H4)))
+        print("H5: {} {}".format(self._H5, type(self._H5)))
+        print("H6: {} {}".format(self._H6, type(self._H6)))
+        print("G1: {} {}".format(self._G1, type(self._G1)))
+        print("G2: {} {}".format(self._G2, type(self._G2)))
+        print("G3: {} {}".format(self._G3, type(self._G3)))
+        print("heater_range: {} {}".format(self._heat_range, type(self._heat_range)))
+        print("heat_val: {} {}".format(self._heat_val, type(self._heat_val)))
+        print("sw_err: {} {}".format(self._sw_err, type(self._sw_err)))
+
+    def power_off(self):
+        self._write(0xF4, 0)
+
+    # normal mode
+    def power_on(self):
+        self._write(0xF4, 0x2F)
+
+    def _gauge(self):
+        """Perform a single-shot reading from the sensor and fill internal data structure for
+           calculations"""
+        now = utime.ticks_ms()
+        if utime.ticks_diff(now, self._last_read_ts) > self._new_read_ms:
+            # set filter
+            self._write(BME680_REG_CONFIG, self.tph_settings.filter << 2)
+            # turn on temp oversample & pressure oversample
+            self._write(
+                BME680_REG_CTRL_MEAS,
+                (self.tph_settings.os_temp << 5) | (self.tph_settings.os_pres << 2),
+            )
+            utime.sleep_ms(100)
+
+            # turn on humidity oversample
+            self._write(BME680_REG_CTRL_HUM, self.tph_settings.os_hum)
+
+            ctrl = unp('<b', self._read(BME680_REG_CTRL_MEAS, 1))[0]
+            ctrl = (ctrl & 0xFC) | 0x01  # enable single shot!
+            self._write(BME680_REG_CTRL_MEAS, ctrl)
+            
+            data_status = False
+            while not data_status:
+                regs = self._read(BME680_REG_MEAS_STATUS, 15)
+                data_status = regs[0] & BME680_NEW_DATA_MSK != 0
+                utime.sleep_ms(5)
+            
+            self._last_read_ts = utime.ticks_ms()
+
+            self._p_raw  = (regs[2] << 12) | (regs[3] << 4) | (regs[4] >> 4)
+            self._t_raw  = (regs[5] << 12) | (regs[6] << 4) | (regs[7] >> 4)
+            self._h_raw  = (regs[8] << 8) | regs[9]
+
+            self._g_raw  = (regs[13] << 2) | (regs[14] >> 6)
+            self._g_range  = regs[14] & BME680_GAS_RANGE_MSK
+            self._g_stable = (data_status & BME680_HEAT_STAB_MSK) > 0
+
+            self._t_fine = 0
+            self._t = 0
+            self._g = 0
+            self._h = 0
+            self._p = 0
+
+    def _calc_t_fine(self):
+        # From datasheet page 22
+        self._gauge()
+        if self._t_fine == 0:
+            var1 = (((self._t_raw / 16384.0) - (self._T1 / 1024.0)) * self._T2)
+            var2 = ((((self._t_raw / 131072.0) - (self._T1 / 8192.0)) *
+                ((self._t_raw / 131072.0) - (self._T1 / 8192.0))) *
+                (self._T3 * 16.0))
+            self._t_fine = var1 + var2
+        
+        self._t = ((self._t_fine * 5) + 128) / 256 / 100
+
+
+    def set_gas_heater_profile(self, temperature, duration, nb_profile=0):
+
+        if nb_profile > 9 or nb_profile < 0:
+            raise ValueError("Profile '{}' should be between {} and {}".format(nb_profile, 0, 9)) 
+
+        # set temperature
+        self.gas_settings.heatr_temp = temperature
+        temp = int(self._calc_heater_resistance(temperature))
+        self._write(BME680_RES_HEAT_0+ nb_profile, temp)
+
+        # set duration
+        self.gas_settings.heatr_dur = duration
+        temp = self._calc_heater_duration(duration)
+        self._write(BME680_GAS_WAIT_0 + nb_profile, temp)
+
+
+    def _calc_heater_resistance(self, temperature):
+        temperature = min(max(temperature,200),400)
+
+        var1 = ((self._t * self._G3) / 1000) * 256
+        var2 = (self._G1 + 784) * (((((self._G2 + 154009) * temperature * 5) / 100) + 3276800) / 10)
+        var3 = var1 + (var2 / 2)
+        var4 = (var3 / (self._g_range + 4))
+        var5 = (131 * self._heat_val) + 65536
+        heatr_res_x100 = (((var4 / var5) - 250) * 34)
+        heatr_res = ((heatr_res_x100 + 50) / 100)
+
+        return heatr_res
+
+    def _calc_heater_duration(self, duration):
+        if duration < 0xfc0:
+            factor = 0
+
+            while duration > 0x3f:
+                duration /= 4
+                factor += 1
+
+            return int(duration + (factor * 64))
+
+        return 0xff
+
+    @property
+    def gas(self):
+        """The gas resistance in ohms"""
+        self._calc_t_fine()
+        if self._g == 0:
+            var1 = int(
+                (1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._g_range])
+            ) >> 16
+            var2 = ((self._g_raw << 15) - 16777216) + var1
+            var3 = int(_LOOKUP_TABLE_2[self._g_range] * var1) >> 9
+            self._g = (var3 + (var2 / 2)) / var2
+        
+        return self._g
+
+    @property
+    def filter_size(self):
+        """The filter size for the built in IIR filter"""
+        return BME680_FILTERSIZES[self._filter]
+
+    @property
+    def humidity(self):
+        """The relative humidity in RH %"""
+        self._calc_t_fine()
+        if self._h == 0:
+            temp_scaled = ((self._t_fine * 5) + 128) / 256
+            var1 = (self._h_raw - (self._H1 * 16)) - (
+                (temp_scaled * self._H3) / 200
+            )
+            var2 = (
+                self._H2
+                * (
+                    ((temp_scaled * self._H4) / 100)
+                    + (
+                        (
+                            (
+                                temp_scaled
+                                * ((temp_scaled * self._H5) / 100)
+                            )
+                            / 64
+                        )
+                        / 100
+                    )
+                    + 16384
+                )
+            ) / 1024
+            var3 = var1 * var2
+            var4 = self._H6 * 128
+            var4 = (var4 + ((temp_scaled * self._H7) / 100)) / 16
+            var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
+            var6 = (var4 * var5) / 2
+            calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
+            self._h = calc_hum / 1000  # get back to RH
+        return self._h
+
+    @property
+    def temperature(self):
+        self._calc_t_fine()
+        self._t = ((self._t_fine * 5) + 128) / 256 / 100
+        return self._t 
+
+    @property
+    def pressure(self):
+        self._calc_t_fine()
+        if self._p == 0:
+            var1 = (int(self._t_fine) >> 1) - 64000
+            var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * self._P6) >> 2
+            var2 = var2 + ((var1 * self._P5) << 1)
+            var2 = (var2 >> 2) + (self._P4 << 16)
+            var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * \
+                    (self._P3 << 5)) >> 3) + ((self._P2 * var1) >> 1)
+            var1 = var1 >> 18
+            var1 = ((32768 + var1) * self._P1) >> 15
+            pressure_comp = 1048576 - self._p_raw
+            pressure_comp = int((pressure_comp - (var2 >> 12)) * 3125)
+            
+            if pressure_comp >= BME680_MAX_OVERFLOW_VAL:
+                pressure_comp = (int(pressure_comp / var1) << 1)
+            else:
+                pressure_comp = (pressure_comp << 1) / var1
+            
+
+            var1 = (self._P9 * ((pressure_comp >> 3) * \
+                (pressure_comp >> 3)) >> 13) >> 12
+            var2 = ((pressure_comp >> 2) * self._P8) >> 13 
+            var3 = ((pressure_comp >> 8) * (pressure_comp >> 8) * \
+		        (pressure_comp >> 8) * self._P10) >> 17
+
+        self._p = (pressure_comp + ((var1 + var2 + var3 + (self._P7 << 7)) >> 4)) / 100
+        
+        return self._p
+
+    @property
+    def measurements(self):
+        d = {}
+        d['time'] = utime.time() + epoch_offset
+        d['temp'] = self.temperature
+        d['hum'] = self.humidity
+        d['press'] = self.pressure
+        d['gas'] = self.gas
+        return d
+        
+    @property
+    def chip_id(self):
+        '''
+        Returns Chip ID
+        '''
+        try:
+            chip_id = unp('<b',self._read(const(0xD0), 1))[0]
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        if chip_id != self._chip_id:
+            raise ValueError('Bad chip ID ({0}!={1}) retrieved: MPU communication failure'.format(chip_id, self._chip_id))
+        return chip_id
\ No newline at end of file
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/bmx280.py b/GW-custom/uPyLoRaWAN/uPySensors/bmx280.py
new file mode 100644
index 0000000000000000000000000000000000000000..450ec399bc6aeb2eafac0aa310842ba724a66c54
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/bmx280.py
@@ -0,0 +1,263 @@
+'''
+'''
+# Power Modes
+from machine import I2C, Pin
+from micropython import const
+from ustruct import unpack as unp
+import utime 
+
+NORMAL = const(0)
+
+BMX280_TEMP_OS_SKIP = const(0)
+BMX280_TEMP_OS_1 = const(1)
+BMX280_TEMP_OS_2 = const(2)
+BMX280_TEMP_OS_4 = const(3)
+BMX280_TEMP_OS_8 = const(4)
+BMX280_TEMP_OS_16 = const(5)
+
+BMX280_PRES_OS_SKIP = const(0)
+BMX280_PRES_OS_1 = const(1)
+BMX280_PRES_OS_2 = const(2)
+BMX280_PRES_OS_4 = const(3)
+BMX280_PRES_OS_8 = const(4)
+BMX280_PRES_OS_16 = const(5)
+
+# BMP280 Temperature Registers
+BMX280_REGISTER_DIG_T1 = const(0x88)
+BMX280_REGISTER_DIG_T2 = const(0x8A)
+BMX280_REGISTER_DIG_T3 = const(0x8C)
+# BMP280 Pressure Registers
+BMX280_REGISTER_DIG_P1 = const(0x8E)
+BMX280_REGISTER_DIG_P2 = const(0x90)
+BMX280_REGISTER_DIG_P3 = const(0x92)
+BMX280_REGISTER_DIG_P4 = const(0x94)
+BMX280_REGISTER_DIG_P5 = const(0x96)
+BMX280_REGISTER_DIG_P6 = const(0x98)
+BMX280_REGISTER_DIG_P7 = const(0x9A)
+BMX280_REGISTER_DIG_P8 = const(0x9C)
+BMX280_REGISTER_DIG_P9 = const(0x9E)
+
+BME280_REGISTER_DIG_H1 = const(0xA1)
+BME280_REGISTER_DIG_H2 = const(0xE1)
+BME280_REGISTER_DIG_H3 = const(0xE3)
+BME280_REGISTER_DIG_H4 = const(0xE4)
+BME280_REGISTER_DIG_H5 = const(0xE5)
+BME280_REGISTER_DIG_H6 = const(0xE6)
+BME280_REGISTER_DIG_H7 = const(0xE7)
+
+BMX280_REGISTER_ID = const(0xD0)
+BMX280_REGISTER_RESET = const(0xE0)
+BMX280_REGISTER_STATUS = const(0xF3)
+BMX280_REGISTER_CONTROL = const(0xF4)
+BMX280_REGISTER_CONFIG = const(0xF5)  # IIR filter config
+
+BMX280_REGISTER_DATA = const(0xF7)
+
+BMX280_I2C_ADDR = const(0x76)
+
+class MPUException(OSError):
+    '''
+    Exception for MPU devices
+    '''
+    pass
+
+class BMX280():
+    _I2Cerror = "I2C failure when communicating with the BMP/E"
+    # BMP280 = 0x58, BME280 = 0x60, BME680 = 0x61
+    _chip_id = 0x58
+
+    _i2c_addr = BMX280_I2C_ADDR
+
+    def __init__(self, i2c, pins):
+        
+        self._pins = pins
+        
+        self._buf1 = bytearray(1)
+        self._buf2 = bytearray(2)
+
+        scl_pin = Pin(self._pins["scl"], Pin.OUT)
+        sda_pin = Pin(self._pins["sda"], Pin.IN)
+
+        self._i2c = I2C(i2c, scl=scl_pin, sda=sda_pin)
+        
+        self.chip_id
+
+        self._load_calibration()
+
+        self._t_os = BMX280_TEMP_OS_2  # temperature oversampling
+        self._p_os = BMX280_PRES_OS_16  # pressure oversampling
+
+        self._t_raw = 0
+        self._t_fine = 0
+        self._t = 0
+
+        self._p_raw = 0
+        self._p = 0
+
+        self._read_wait_ms = 100 
+        self._new_read_ms = 200 
+        self._last_read_ts = 0
+        
+
+    def _read(self, memaddr, size=1):
+        data = self._i2c.readfrom_mem(self._i2c_addr, memaddr, size)
+        return data
+        
+    def _write(self, addr, b_arr):
+        if not type(b_arr) is bytearray:
+            b_arr = bytearray([b_arr])
+        return self._i2c.writeto_mem(self._i2c_addr, addr, b_arr)
+
+    def _load_calibration(self):
+        # read calibration data
+        # < little-endian
+        # H unsigned short
+        # h signed short
+        self._T1 = unp('<H', self._read(BMX280_REGISTER_DIG_T1, 2))[0]
+        self._T2 = unp('<h', self._read(BMX280_REGISTER_DIG_T2, 2))[0]
+        self._T3 = unp('<h', self._read(BMX280_REGISTER_DIG_T3, 2))[0]
+        self._P1 = unp('<H', self._read(BMX280_REGISTER_DIG_P1, 2))[0]
+        self._P2 = unp('<h', self._read(BMX280_REGISTER_DIG_P2, 2))[0]
+        self._P3 = unp('<h', self._read(BMX280_REGISTER_DIG_P3, 2))[0]
+        self._P4 = unp('<h', self._read(BMX280_REGISTER_DIG_P4, 2))[0]
+        self._P5 = unp('<h', self._read(BMX280_REGISTER_DIG_P5, 2))[0]
+        self._P6 = unp('<h', self._read(BMX280_REGISTER_DIG_P6, 2))[0]
+        self._P7 = unp('<h', self._read(BMX280_REGISTER_DIG_P7, 2))[0]
+        self._P8 = unp('<h', self._read(BMX280_REGISTER_DIG_P8, 2))[0]
+        self._P9 = unp('<h', self._read(BMX280_REGISTER_DIG_P9, 2))[0]
+
+        if self._chip_id != 0x58:
+            self._H1 = unp('<b', self._read(BME280_REGISTER_DIG_H1, 1))[0]
+            self._H2 = unp('<h', self._read(BME280_REGISTER_DIG_H2, 2))[0]
+            self._H3 = unp('<b', self._read(BME280_REGISTER_DIG_H3, 1))[0]
+            self._H6 = unp('<b', self._read(BME280_REGISTER_DIG_H7, 1))[0]
+
+            h4 = unp('<b', self._read(BME280_REGISTER_DIG_H4, 1))[0]
+            h4 = (h4 << 24) >> 20
+            self._H4 = h4 | (
+                unp('<b', self._read(BME280_REGISTER_DIG_H5, 1))[0] & 0x0F)
+
+            h5 = unp('<b', self._read(BME280_REGISTER_DIG_H6, 1))[0]
+            h5 = (h5 << 24) >> 20
+            self._H5 = h5 | (
+                unp('<b', self._read(BME280_REGISTER_DIG_H5, 1))[0] >> 4 & 0x0F)
+
+
+    def print_calibration(self):
+        print("T1: {} {}".format(self._T1, type(self._T1)))
+        print("T2: {} {}".format(self._T2, type(self._T2)))
+        print("T3: {} {}".format(self._T3, type(self._T3)))
+        print("P1: {} {}".format(self._P1, type(self._P1)))
+        print("P2: {} {}".format(self._P2, type(self._P2)))
+        print("P3: {} {}".format(self._P3, type(self._P3)))
+        print("P4: {} {}".format(self._P4, type(self._P4)))
+        print("P5: {} {}".format(self._P5, type(self._P5)))
+        print("P6: {} {}".format(self._P6, type(self._P6)))
+        print("P7: {} {}".format(self._P7, type(self._P7)))
+        print("P8: {} {}".format(self._P8, type(self._P8)))
+        print("P9: {} {}".format(self._P9, type(self._P9)))
+        if self._chip_id != 0x58:
+            print("H1: {} {}".format(self._H1, type(self._H1)))
+            print("H2: {} {}".format(self._H2, type(self._H2)))
+            print("H3: {} {}".format(self._H3, type(self._H3)))
+            print("H4: {} {}".format(self._H4, type(self._H4)))
+            print("H5: {} {}".format(self._H5, type(self._H5)))
+            print("H6: {} {}".format(self._H6, type(self._H6)))
+
+    def power_off(self):
+        self._write(0xF4, 0)
+
+    # normal mode
+    def power_on(self):
+        self._write(0xF4, 0x2F)
+
+    def _gauge(self):
+        now = utime.ticks_ms()
+        if utime.ticks_diff(now, self._last_read_ts) > self._new_read_ms:
+            self._last_read_ts = now
+            r = self._t_os + (self._p_os << 3) + (1 << 6)
+            self._write(BMX280_REGISTER_CONTROL, r)
+            utime.sleep_ms(100)  # TODO calc sleep
+            if self._chip_id == 0x58:
+                d = self._read(BMX280_REGISTER_DATA, 6)  # read all data at once (as by spec)
+                self._p_raw = (d[0] << 12) + (d[1] << 4) + (d[2] >> 4)
+                self._t_raw = (d[3] << 12) + (d[4] << 4) + (d[5] >> 4)
+            else:
+                d = self._read(BMX280_REGISTER_DATA, 8)  # read all data at once (as by spec)
+                self._p_raw = (d[0] << 12) + (d[1] << 4) + (d[2] >> 4)
+                self._t_raw = (d[3] << 12) + (d[4] << 4) + (d[5] >> 4)
+                self._h_raw = (d[6] << 8) + d[7]
+
+            self._t_fine = 0
+            self._t = 0
+            self._h = 0
+            self._p = 0
+
+    def _calc_t_fine(self):
+        # From datasheet page 22
+        self._gauge()
+        if self._t_fine == 0:
+            var1 = (((self._t_raw >> 3) - (self._T1 << 1)) * self._T2) >> 11
+            var2 = (((((self._t_raw >> 4) - self._T1) * ((self._t_raw >> 4) - self._T1)) >> 12) * self._T3) >> 14
+            self._t_fine = var1 + var2
+
+    @property
+    def humidity(self):
+        if self._chip_id != 0x58:
+            var1 = self._calc_t_fine - 76800
+            var1 = (((((self._h_raw << 14) - (self._H5 << 20) - (self._H5 * var1)) +
+                16384) >> 15) * (((((((var1 * self._H6) >> 10) * (((var1 *
+                                self._H3) >> 11) + 32768)) >> 10) + 2097152) *
+                                self._H2 + 8192) >> 14))
+            var1 = var1 - (((((var1 >> 15) * (var1 >> 15)) >> 7) * self._H1) >> 4)
+            var1 = 0 if var1 < 0 else var1
+            var1 = 419430400 if var1 > 419430400 else var1
+            return var1 >> 12
+        else:
+            print("This is a BMP not a BME, therefore it cannot measure humidity! :(")
+            return 0
+
+    @property
+    def temperature(self):
+        self._calc_t_fine()
+        if self._t == 0:
+            self._t = ((self._t_fine * 5 + 128) >> 8) / 100.
+        return self._t
+
+    @property
+    def pressure(self):
+        # From datasheet page 22 (BMP) /25 (BME)
+        self._calc_t_fine()
+        if self._p == 0:
+            var1 = self._t_fine - 128000
+            var2 = var1 * var1 * self._P6
+            var2 = var2 + ((var1 * self._P5) << 17)
+            var2 = var2 + (self._P4 << 35)
+            var1 = ((var1 * var1 * self._P3) >> 8) + ((var1 * self._P2) << 12)
+            var1 = (((1 << 47) + var1) * self._P1) >> 33
+
+            if var1 == 0:
+                return 0
+
+            p = 1048576 - self._p_raw
+            p = int((((p << 31) - var2) * 3125) / var1)
+            var1 = (self._P9 * (p >> 13) * (p >> 13)) >> 25
+            var2 = (self._P8 * p) >> 19
+
+            p = ((p + var1 + var2) >> 8) + (self._P7 << 4)
+            self._p = p / 256.0
+        return self._p
+
+
+    @property
+    def chip_id(self):
+        '''
+        Returns Chip ID
+        '''
+        try:
+            chip_id = unp('<b',self._read(const(0xD0), 1))[0]
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        if chip_id != self._chip_id:
+            raise ValueError('Bad chip ID ({0}!={1}) retrieved: MPU communication failure'.format(chip_id, self._chip_id))
+        return chip_id
\ No newline at end of file
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/fonts/glcdfont.py b/GW-custom/uPyLoRaWAN/uPySensors/fonts/glcdfont.py
new file mode 100644
index 0000000000000000000000000000000000000000..c76e4d6a1de114088eba68190ceda0b87f417e5e
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/fonts/glcdfont.py
@@ -0,0 +1,294 @@
+# Original Adafruit_GFX 5x7 font
+
+def height():
+    return 8
+
+def max_width():
+    return 6
+
+def hmap():
+    return False
+
+def reverse():
+    return False
+
+def monospaced():
+    return True
+
+def min_ch():
+    return 0
+
+def max_ch():
+    return 255
+
+
+_font = \
+	b'\x00\x00\x00\x00\x00'\
+	b'\x3E\x5B\x4F\x5B\x3E'\
+	b'\x3E\x6B\x4F\x6B\x3E'\
+	b'\x1C\x3E\x7C\x3E\x1C'\
+	b'\x18\x3C\x7E\x3C\x18'\
+	b'\x1C\x57\x7D\x57\x1C'\
+	b'\x1C\x5E\x7F\x5E\x1C'\
+	b'\x00\x18\x3C\x18\x00'\
+	b'\xFF\xE7\xC3\xE7\xFF'\
+	b'\x00\x18\x24\x18\x00'\
+	b'\xFF\xE7\xDB\xE7\xFF'\
+	b'\x30\x48\x3A\x06\x0E'\
+	b'\x26\x29\x79\x29\x26'\
+	b'\x40\x7F\x05\x05\x07'\
+	b'\x40\x7F\x05\x25\x3F'\
+	b'\x5A\x3C\xE7\x3C\x5A'\
+	b'\x7F\x3E\x1C\x1C\x08'\
+	b'\x08\x1C\x1C\x3E\x7F'\
+	b'\x14\x22\x7F\x22\x14'\
+	b'\x5F\x5F\x00\x5F\x5F'\
+	b'\x06\x09\x7F\x01\x7F'\
+	b'\x00\x66\x89\x95\x6A'\
+	b'\x60\x60\x60\x60\x60'\
+	b'\x94\xA2\xFF\xA2\x94'\
+	b'\x08\x04\x7E\x04\x08'\
+	b'\x10\x20\x7E\x20\x10'\
+	b'\x08\x08\x2A\x1C\x08'\
+	b'\x08\x1C\x2A\x08\x08'\
+	b'\x1E\x10\x10\x10\x10'\
+	b'\x0C\x1E\x0C\x1E\x0C'\
+	b'\x30\x38\x3E\x38\x30'\
+	b'\x06\x0E\x3E\x0E\x06'\
+	b'\x00\x00\x00\x00\x00'\
+	b'\x00\x00\x5F\x00\x00'\
+	b'\x00\x07\x00\x07\x00'\
+	b'\x14\x7F\x14\x7F\x14'\
+	b'\x24\x2A\x7F\x2A\x12'\
+	b'\x23\x13\x08\x64\x62'\
+	b'\x36\x49\x56\x20\x50'\
+	b'\x00\x08\x07\x03\x00'\
+	b'\x00\x1C\x22\x41\x00'\
+	b'\x00\x41\x22\x1C\x00'\
+	b'\x2A\x1C\x7F\x1C\x2A'\
+	b'\x08\x08\x3E\x08\x08'\
+	b'\x00\x80\x70\x30\x00'\
+	b'\x08\x08\x08\x08\x08'\
+	b'\x00\x00\x60\x60\x00'\
+	b'\x20\x10\x08\x04\x02'\
+	b'\x3E\x51\x49\x45\x3E'\
+	b'\x00\x42\x7F\x40\x00'\
+	b'\x72\x49\x49\x49\x46'\
+	b'\x21\x41\x49\x4D\x33'\
+	b'\x18\x14\x12\x7F\x10'\
+	b'\x27\x45\x45\x45\x39'\
+	b'\x3C\x4A\x49\x49\x31'\
+	b'\x41\x21\x11\x09\x07'\
+	b'\x36\x49\x49\x49\x36'\
+	b'\x46\x49\x49\x29\x1E'\
+	b'\x00\x00\x14\x00\x00'\
+	b'\x00\x40\x34\x00\x00'\
+	b'\x00\x08\x14\x22\x41'\
+	b'\x14\x14\x14\x14\x14'\
+	b'\x00\x41\x22\x14\x08'\
+	b'\x02\x01\x59\x09\x06'\
+	b'\x3E\x41\x5D\x59\x4E'\
+	b'\x7C\x12\x11\x12\x7C'\
+	b'\x7F\x49\x49\x49\x36'\
+	b'\x3E\x41\x41\x41\x22'\
+	b'\x7F\x41\x41\x41\x3E'\
+	b'\x7F\x49\x49\x49\x41'\
+	b'\x7F\x09\x09\x09\x01'\
+	b'\x3E\x41\x41\x51\x73'\
+	b'\x7F\x08\x08\x08\x7F'\
+	b'\x00\x41\x7F\x41\x00'\
+	b'\x20\x40\x41\x3F\x01'\
+	b'\x7F\x08\x14\x22\x41'\
+	b'\x7F\x40\x40\x40\x40'\
+	b'\x7F\x02\x1C\x02\x7F'\
+	b'\x7F\x04\x08\x10\x7F'\
+	b'\x3E\x41\x41\x41\x3E'\
+	b'\x7F\x09\x09\x09\x06'\
+	b'\x3E\x41\x51\x21\x5E'\
+	b'\x7F\x09\x19\x29\x46'\
+	b'\x26\x49\x49\x49\x32'\
+	b'\x03\x01\x7F\x01\x03'\
+	b'\x3F\x40\x40\x40\x3F'\
+	b'\x1F\x20\x40\x20\x1F'\
+	b'\x3F\x40\x38\x40\x3F'\
+	b'\x63\x14\x08\x14\x63'\
+	b'\x03\x04\x78\x04\x03'\
+	b'\x61\x59\x49\x4D\x43'\
+	b'\x00\x7F\x41\x41\x41'\
+	b'\x02\x04\x08\x10\x20'\
+	b'\x00\x41\x41\x41\x7F'\
+	b'\x04\x02\x01\x02\x04'\
+	b'\x40\x40\x40\x40\x40'\
+	b'\x00\x03\x07\x08\x00'\
+	b'\x20\x54\x54\x78\x40'\
+	b'\x7F\x28\x44\x44\x38'\
+	b'\x38\x44\x44\x44\x28'\
+	b'\x38\x44\x44\x28\x7F'\
+	b'\x38\x54\x54\x54\x18'\
+	b'\x00\x08\x7E\x09\x02'\
+	b'\x18\xA4\xA4\x9C\x78'\
+	b'\x7F\x08\x04\x04\x78'\
+	b'\x00\x44\x7D\x40\x00'\
+	b'\x20\x40\x40\x3D\x00'\
+	b'\x7F\x10\x28\x44\x00'\
+	b'\x00\x41\x7F\x40\x00'\
+	b'\x7C\x04\x78\x04\x78'\
+	b'\x7C\x08\x04\x04\x78'\
+	b'\x38\x44\x44\x44\x38'\
+	b'\xFC\x18\x24\x24\x18'\
+	b'\x18\x24\x24\x18\xFC'\
+	b'\x7C\x08\x04\x04\x08'\
+	b'\x48\x54\x54\x54\x24'\
+	b'\x04\x04\x3F\x44\x24'\
+	b'\x3C\x40\x40\x20\x7C'\
+	b'\x1C\x20\x40\x20\x1C'\
+	b'\x3C\x40\x30\x40\x3C'\
+	b'\x44\x28\x10\x28\x44'\
+	b'\x4C\x90\x90\x90\x7C'\
+	b'\x44\x64\x54\x4C\x44'\
+	b'\x00\x08\x36\x41\x00'\
+	b'\x00\x00\x77\x00\x00'\
+	b'\x00\x41\x36\x08\x00'\
+	b'\x02\x01\x02\x04\x02'\
+	b'\x3C\x26\x23\x26\x3C'\
+	b'\x1E\xA1\xA1\x61\x12'\
+	b'\x3A\x40\x40\x20\x7A'\
+	b'\x38\x54\x54\x55\x59'\
+	b'\x21\x55\x55\x79\x41'\
+	b'\x21\x54\x54\x78\x41'\
+	b'\x21\x55\x54\x78\x40'\
+	b'\x20\x54\x55\x79\x40'\
+	b'\x0C\x1E\x52\x72\x12'\
+	b'\x39\x55\x55\x55\x59'\
+	b'\x39\x54\x54\x54\x59'\
+	b'\x39\x55\x54\x54\x58'\
+	b'\x00\x00\x45\x7C\x41'\
+	b'\x00\x02\x45\x7D\x42'\
+	b'\x00\x01\x45\x7C\x40'\
+	b'\xF0\x29\x24\x29\xF0'\
+	b'\xF0\x28\x25\x28\xF0'\
+	b'\x7C\x54\x55\x45\x00'\
+	b'\x20\x54\x54\x7C\x54'\
+	b'\x7C\x0A\x09\x7F\x49'\
+	b'\x32\x49\x49\x49\x32'\
+	b'\x32\x48\x48\x48\x32'\
+	b'\x32\x4A\x48\x48\x30'\
+	b'\x3A\x41\x41\x21\x7A'\
+	b'\x3A\x42\x40\x20\x78'\
+	b'\x00\x9D\xA0\xA0\x7D'\
+	b'\x39\x44\x44\x44\x39'\
+	b'\x3D\x40\x40\x40\x3D'\
+	b'\x3C\x24\xFF\x24\x24'\
+	b'\x48\x7E\x49\x43\x66'\
+	b'\x2B\x2F\xFC\x2F\x2B'\
+	b'\xFF\x09\x29\xF6\x20'\
+	b'\xC0\x88\x7E\x09\x03'\
+	b'\x20\x54\x54\x79\x41'\
+	b'\x00\x00\x44\x7D\x41'\
+	b'\x30\x48\x48\x4A\x32'\
+	b'\x38\x40\x40\x22\x7A'\
+	b'\x00\x7A\x0A\x0A\x72'\
+	b'\x7D\x0D\x19\x31\x7D'\
+	b'\x26\x29\x29\x2F\x28'\
+	b'\x26\x29\x29\x29\x26'\
+	b'\x30\x48\x4D\x40\x20'\
+	b'\x38\x08\x08\x08\x08'\
+	b'\x08\x08\x08\x08\x38'\
+	b'\x2F\x10\xC8\xAC\xBA'\
+	b'\x2F\x10\x28\x34\xFA'\
+	b'\x00\x00\x7B\x00\x00'\
+	b'\x08\x14\x2A\x14\x22'\
+	b'\x22\x14\x2A\x14\x08'\
+	b'\xAA\x00\x55\x00\xAA'\
+	b'\xAA\x55\xAA\x55\xAA'\
+	b'\x00\x00\x00\xFF\x00'\
+	b'\x10\x10\x10\xFF\x00'\
+	b'\x14\x14\x14\xFF\x00'\
+	b'\x10\x10\xFF\x00\xFF'\
+	b'\x10\x10\xF0\x10\xF0'\
+	b'\x14\x14\x14\xFC\x00'\
+	b'\x14\x14\xF7\x00\xFF'\
+	b'\x00\x00\xFF\x00\xFF'\
+	b'\x14\x14\xF4\x04\xFC'\
+	b'\x14\x14\x17\x10\x1F'\
+	b'\x10\x10\x1F\x10\x1F'\
+	b'\x14\x14\x14\x1F\x00'\
+	b'\x10\x10\x10\xF0\x00'\
+	b'\x00\x00\x00\x1F\x10'\
+	b'\x10\x10\x10\x1F\x10'\
+	b'\x10\x10\x10\xF0\x10'\
+	b'\x00\x00\x00\xFF\x10'\
+	b'\x10\x10\x10\x10\x10'\
+	b'\x10\x10\x10\xFF\x10'\
+	b'\x00\x00\x00\xFF\x14'\
+	b'\x00\x00\xFF\x00\xFF'\
+	b'\x00\x00\x1F\x10\x17'\
+	b'\x00\x00\xFC\x04\xF4'\
+	b'\x14\x14\x17\x10\x17'\
+	b'\x14\x14\xF4\x04\xF4'\
+	b'\x00\x00\xFF\x00\xF7'\
+	b'\x14\x14\x14\x14\x14'\
+	b'\x14\x14\xF7\x00\xF7'\
+	b'\x14\x14\x14\x17\x14'\
+	b'\x10\x10\x1F\x10\x1F'\
+	b'\x14\x14\x14\xF4\x14'\
+	b'\x10\x10\xF0\x10\xF0'\
+	b'\x00\x00\x1F\x10\x1F'\
+	b'\x00\x00\x00\x1F\x14'\
+	b'\x00\x00\x00\xFC\x14'\
+	b'\x00\x00\xF0\x10\xF0'\
+	b'\x10\x10\xFF\x10\xFF'\
+	b'\x14\x14\x14\xFF\x14'\
+	b'\x10\x10\x10\x1F\x00'\
+	b'\x00\x00\x00\xF0\x10'\
+	b'\xFF\xFF\xFF\xFF\xFF'\
+	b'\xF0\xF0\xF0\xF0\xF0'\
+	b'\xFF\xFF\xFF\x00\x00'\
+	b'\x00\x00\x00\xFF\xFF'\
+	b'\x0F\x0F\x0F\x0F\x0F'\
+	b'\x38\x44\x44\x38\x44'\
+	b'\x7C\x2A\x2A\x3E\x14'\
+	b'\x7E\x02\x02\x06\x06'\
+	b'\x02\x7E\x02\x7E\x02'\
+	b'\x63\x55\x49\x41\x63'\
+	b'\x38\x44\x44\x3C\x04'\
+	b'\x40\x7E\x20\x1E\x20'\
+	b'\x06\x02\x7E\x02\x02'\
+	b'\x99\xA5\xE7\xA5\x99'\
+	b'\x1C\x2A\x49\x2A\x1C'\
+	b'\x4C\x72\x01\x72\x4C'\
+	b'\x30\x4A\x4D\x4D\x30'\
+	b'\x30\x48\x78\x48\x30'\
+	b'\xBC\x62\x5A\x46\x3D'\
+	b'\x3E\x49\x49\x49\x00'\
+	b'\x7E\x01\x01\x01\x7E'\
+	b'\x2A\x2A\x2A\x2A\x2A'\
+	b'\x44\x44\x5F\x44\x44'\
+	b'\x40\x51\x4A\x44\x40'\
+	b'\x40\x44\x4A\x51\x40'\
+	b'\x00\x00\xFF\x01\x03'\
+	b'\xE0\x80\xFF\x00\x00'\
+	b'\x08\x08\x6B\x6B\x08'\
+	b'\x36\x12\x36\x24\x36'\
+	b'\x06\x0F\x09\x0F\x06'\
+	b'\x00\x00\x18\x18\x00'\
+	b'\x00\x00\x10\x10\x00'\
+	b'\x30\x40\xFF\x01\x01'\
+	b'\x00\x1F\x01\x01\x1E'\
+	b'\x00\x19\x1D\x17\x12'\
+	b'\x00\x3C\x3C\x3C\x3C'\
+	b'\x00\x00\x00\x00\x00'
+
+_mvfont = memoryview(_font)
+
+def get_width(s):
+    return len(s)*6
+
+def get_ch(ch): 
+    ordch = ord(ch)
+    offset = ordch*5
+    buf = bytearray(6)
+    buf[0] = 0
+    buf[1:]=_mvfont[offset:offset+5]
+    return buf, 6
+
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt14.py b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt14.py
new file mode 100644
index 0000000000000000000000000000000000000000..60100b11fc5c4c1172a15c59d3fbde3f4bf2eec1
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt14.py
@@ -0,0 +1,159 @@
+# Code generated by font-to-py.py.
+# Font: CM Sans Serif 2012.ttf
+version = '0.2'
+
+def height():
+    return 15
+
+def max_width():
+    return 12
+
+def hmap():
+    return False
+
+def reverse():
+    return False
+
+def monospaced():
+    return False
+
+def min_ch():
+    return 32
+
+def max_ch():
+    return 126
+
+_font =\
+b'\x08\x00\x08\x00\x0c\x00\x8c\x07\xcc\x07\xcc\x00\x7c\x00\x38\x00'\
+b'\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\xfc\x06'\
+b'\xfc\x06\x00\x00\x05\x00\x1c\x00\x3c\x00\x00\x00\x3c\x00\x00\x00'\
+b'\x07\x00\x40\x03\xf0\x03\xf0\x03\x30\x03\xf0\x03\x30\x03\x30\x00'\
+b'\x07\x00\x30\x00\x78\x03\x6c\x02\xfc\x07\xd8\x03\x90\x03\x00\x00'\
+b'\x0b\x00\x18\x00\x3c\x00\x3c\x04\x3c\x03\xd8\x01\xf0\x03\xd8\x07'\
+b'\xc4\x06\xc0\x07\x80\x03\x00\x00\x09\x00\x80\x03\xf8\x07\x7c\x06'\
+b'\x4c\x06\xfc\x06\x98\x07\xc0\x07\xc0\x05\x00\x04\x02\x00\x1c\x00'\
+b'\x3c\x00\x04\x00\xf0\x03\xfc\x0f\x0e\x1c\x02\x10\x05\x00\x02\x10'\
+b'\x0e\x1c\xfc\x0f\xf0\x03\x00\x00\x05\x00\x0c\x00\x3c\x00\x18\x00'\
+b'\x2c\x00\x0c\x00\x07\x00\xc0\x00\xc0\x00\xf0\x03\xf0\x03\xc0\x00'\
+b'\xc0\x00\x00\x00\x03\x00\x00\x16\x00\x0e\x00\x00\x04\x00\xc0\x00'\
+b'\xc0\x00\xc0\x00\xc0\x00\x03\x00\x00\x06\x00\x06\x00\x00\x04\x00'\
+b'\x00\x06\xe0\x07\xfc\x00\x0e\x00\x08\x00\xf0\x01\xf8\x03\x1c\x07'\
+b'\x0c\x06\x1c\x07\xf8\x03\xf0\x01\x00\x00\x04\x00\x18\x00\xfc\x07'\
+b'\xfc\x07\x00\x00\x08\x00\x18\x04\x1c\x07\x8c\x06\x8c\x06\x4c\x06'\
+b'\x7c\x06\x38\x06\x00\x00\x07\x00\x08\x02\x0c\x06\x0c\x06\x6c\x06'\
+b'\x6c\x06\xfc\x07\xd8\x03\x07\x00\x80\x01\xe0\x01\xb0\x01\x9c\x01'\
+b'\xfc\x07\xfc\x07\x80\x01\x07\x00\x00\x02\xfc\x06\x6c\x06\x6c\x06'\
+b'\x6c\x06\xcc\x03\xc0\x03\x08\x00\xf0\x01\xf8\x03\x6c\x06\x6c\x06'\
+b'\x6c\x06\xe8\x07\xc0\x03\x00\x00\x06\x00\x0c\x00\x0c\x07\xcc\x07'\
+b'\xfc\x00\x1c\x00\x0c\x00\x07\x00\x98\x03\xfc\x07\x6c\x06\x6c\x06'\
+b'\x6c\x06\xfc\x07\x98\x03\x08\x00\x78\x02\xfc\x06\xcc\x06\xcc\x06'\
+b'\xcc\x06\xf8\x03\xf0\x01\x00\x00\x03\x00\x30\x06\x30\x06\x00\x00'\
+b'\x03\x00\x30\x16\x30\x0e\x00\x00\x06\x00\xc0\x01\xc0\x01\x60\x03'\
+b'\x60\x03\x30\x06\x00\x00\x06\x00\x60\x03\x60\x03\x60\x03\x60\x03'\
+b'\x60\x03\x60\x03\x06\x00\x30\x06\x60\x03\x60\x03\x40\x01\xc0\x01'\
+b'\x00\x00\x08\x00\x08\x00\x0c\x00\x8c\x07\xcc\x07\xcc\x00\x7c\x00'\
+b'\x38\x00\x00\x00\x0b\x00\xe0\x00\xf8\x01\x18\x03\xec\x07\xbc\x07'\
+b'\xfc\x06\xfc\x07\x9c\x03\xf8\x01\xf0\x00\x00\x00\x08\x00\x00\x06'\
+b'\x80\x07\xf0\x01\x9c\x01\x8c\x01\xf8\x01\xc0\x07\x00\x07\x08\x00'\
+b'\xfc\x07\xfc\x07\x6c\x06\x6c\x06\x6c\x06\xfc\x07\xd8\x03\x00\x00'\
+b'\x09\x00\xe0\x00\xf8\x03\x18\x03\x0c\x06\x0c\x06\x0c\x06\x1c\x07'\
+b'\x18\x03\x20\x01\x09\x00\xfc\x07\xfc\x07\x0c\x06\x0c\x06\x1c\x07'\
+b'\xf8\x03\xf0\x01\x00\x00\x00\x00\x08\x00\xfc\x07\xfc\x07\x6c\x06'\
+b'\x6c\x06\x6c\x06\x6c\x06\x0c\x06\x00\x00\x07\x00\xfc\x07\xfc\x07'\
+b'\xcc\x00\xcc\x00\xcc\x00\xcc\x00\x0c\x00\x0a\x00\xe0\x01\xf8\x03'\
+b'\x18\x07\x0c\x06\x0c\x06\xcc\x06\xdc\x02\xd8\x07\xe0\x07\x00\x00'\
+b'\x09\x00\xfc\x07\xfc\x07\x60\x00\x60\x00\x60\x00\xfc\x07\xfc\x07'\
+b'\x00\x00\x00\x00\x03\x00\xfc\x07\xfc\x07\x00\x00\x07\x00\x80\x03'\
+b'\x80\x07\x00\x06\x00\x06\xfc\x07\xfc\x03\x00\x00\x08\x00\xfc\x07'\
+b'\xfc\x07\x60\x00\xf0\x00\xd8\x01\x0c\x07\x04\x06\x04\x04\x07\x00'\
+b'\xfc\x07\xfc\x07\x00\x06\x00\x06\x00\x06\x00\x06\x00\x00\x0b\x00'\
+b'\xfc\x07\xfc\x07\x3c\x00\xe0\x03\x00\x06\xe0\x03\x3c\x00\xfc\x07'\
+b'\xfc\x07\x00\x00\x00\x00\x09\x00\xfc\x07\xfc\x07\x38\x00\xe0\x00'\
+b'\x80\x03\xfc\x07\xfc\x07\x00\x00\x00\x00\x0a\x00\xf0\x01\xf8\x03'\
+b'\x1c\x07\x0c\x06\x0c\x06\x0c\x06\x1c\x07\xf8\x03\xf0\x01\x00\x00'\
+b'\x09\x00\xfc\x07\xfc\x07\xcc\x00\xcc\x00\xcc\x00\xfc\x00\x78\x00'\
+b'\x00\x00\x00\x00\x0a\x00\xf0\x01\xf8\x03\x1c\x07\x0c\x06\x8c\x06'\
+b'\x8c\x07\x1c\x07\xf8\x07\xf0\x05\x00\x00\x09\x00\xfc\x07\xfc\x07'\
+b'\xcc\x00\xcc\x00\xcc\x03\x7c\x07\x78\x04\x00\x00\x00\x00\x09\x00'\
+b'\x38\x01\x3c\x03\x4c\x06\x4c\x06\x4c\x06\x4c\x06\x98\x07\x90\x03'\
+b'\x00\x00\x07\x00\x0c\x00\x0c\x00\xfc\x07\xfc\x07\x0c\x00\x0c\x00'\
+b'\x00\x00\x09\x00\xfc\x03\xfc\x07\x00\x06\x00\x06\x00\x06\xfc\x07'\
+b'\xfc\x01\x00\x00\x00\x00\x08\x00\x0c\x00\x7c\x00\xe0\x03\x00\x07'\
+b'\x80\x07\xf8\x01\x3c\x00\x04\x00\x0b\x00\x0c\x00\xfc\x00\xe0\x07'\
+b'\x80\x07\xf8\x03\x1c\x00\xfc\x00\xc0\x07\x80\x07\xfc\x01\x1c\x00'\
+b'\x08\x00\x04\x04\x0c\x07\xb8\x03\xf0\x00\xf0\x01\x9c\x07\x0c\x06'\
+b'\x04\x04\x08\x00\x04\x00\x1c\x00\x78\x00\xe0\x07\xe0\x07\x78\x00'\
+b'\x1c\x00\x04\x00\x07\x00\x0c\x06\x0c\x07\x8c\x07\xcc\x06\x2c\x06'\
+b'\x1c\x06\x0c\x06\x04\x00\xff\x1f\xff\x1f\x03\x18\x00\x00\x04\x00'\
+b'\x0e\x00\xfe\x00\xe0\x07\x00\x07\x04\x00\x03\x18\xff\x1f\xff\x1f'\
+b'\x00\x00\x07\x00\x80\x00\xe0\x00\x7c\x00\x0c\x00\x7c\x00\xe0\x00'\
+b'\x80\x00\x0a\x00\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18\x00\x18'\
+b'\x00\x18\x00\x18\x00\x18\x00\x18\x05\x00\x04\x00\x0c\x00\x08\x00'\
+b'\x00\x00\x00\x00\x08\x00\x00\x03\xe0\x07\xb0\x06\xb0\x06\xb0\x02'\
+b'\xf0\x07\xe0\x07\x00\x00\x09\x00\xfe\x07\xfe\x07\x30\x02\x30\x06'\
+b'\x30\x06\xe0\x07\xc0\x01\x00\x00\x00\x00\x07\x00\xc0\x01\xe0\x03'\
+b'\x30\x06\x30\x06\x30\x06\x60\x03\x40\x01\x08\x00\xc0\x01\xf0\x07'\
+b'\x30\x06\x30\x06\x30\x06\xfe\x07\xfe\x07\x00\x00\x07\x00\xc0\x01'\
+b'\xe0\x03\xb0\x07\xb0\x07\xb0\x07\xe0\x03\xc0\x01\x04\x00\x20\x00'\
+b'\xfc\x07\xfe\x07\x00\x00\x08\x00\xc0\x11\xf0\x1f\x30\x36\x30\x36'\
+b'\x20\x36\xf0\x1f\xf0\x0f\x00\x00\x08\x00\xfe\x07\xfe\x07\x30\x00'\
+b'\x30\x00\xf0\x07\xe0\x07\x00\x00\x00\x00\x03\x00\xfc\x07\xfc\x07'\
+b'\x00\x00\x03\x00\xec\x7f\xec\x3f\x00\x00\x07\x00\xfe\x07\xfe\x07'\
+b'\xc0\x00\xe0\x01\xb0\x07\x10\x06\x10\x04\x03\x00\xfe\x07\xfe\x07'\
+b'\x00\x00\x0c\x00\xf0\x07\xf0\x07\x30\x00\x30\x00\xf0\x07\xe0\x07'\
+b'\x30\x00\x30\x00\xf0\x07\xe0\x07\x00\x00\x00\x00\x08\x00\xf0\x07'\
+b'\xf0\x07\x30\x00\x30\x00\xf0\x07\xe0\x07\x00\x00\x00\x00\x08\x00'\
+b'\xc0\x01\xe0\x03\x30\x06\x30\x06\x30\x06\xe0\x03\xc0\x01\x00\x00'\
+b'\x09\x00\xf0\x3f\xf0\x3f\x30\x06\x30\x06\x30\x06\xf0\x07\xc0\x01'\
+b'\x00\x00\x00\x00\x08\x00\xc0\x01\xf0\x07\x30\x06\x30\x06\x30\x06'\
+b'\xf0\x3f\xf0\x3f\x00\x00\x04\x00\xf0\x07\xf0\x07\x30\x00\x30\x00'\
+b'\x08\x00\x60\x00\x70\x03\xb0\x07\xf0\x06\xb0\x06\xa0\x07\x00\x03'\
+b'\x00\x00\x04\x00\x30\x00\xfe\x03\xfe\x07\x00\x00\x08\x00\xf0\x03'\
+b'\xf0\x07\x00\x06\x00\x06\xf0\x07\xf0\x07\x00\x00\x00\x00\x07\x00'\
+b'\x30\x00\xf0\x00\x80\x07\x00\x06\xc0\x03\xf0\x00\x10\x00\x0a\x00'\
+b'\x30\x00\xf0\x01\x80\x07\xc0\x07\xf0\x00\xf0\x00\x80\x07\x80\x07'\
+b'\xf0\x01\x30\x00\x07\x00\x10\x04\x30\x07\xe0\x03\xc0\x01\x70\x07'\
+b'\x10\x06\x00\x04\x07\x00\x30\x00\xf0\x30\xc0\x3f\x00\x1f\xe0\x03'\
+b'\x70\x00\x10\x00\x06\x00\x30\x06\x30\x07\xb0\x07\xf0\x06\x70\x06'\
+b'\x30\x06\x05\x00\x40\x00\xbe\x0f\xbe\x0f\x00\x00\x00\x00\x04\x00'\
+b'\xfe\x07\xfe\x07\x00\x00\x00\x00\x04\x00\xbe\x0f\xbe\x0f\x40\x00'\
+b'\x00\x00\x05\x00\x04\x00\x04\x00\x04\x00\x06\x00\x04\x00'
+
+_index =\
+b'\x00\x00\x12\x00\x1c\x00\x24\x00\x30\x00\x40\x00\x50\x00\x68\x00'\
+b'\x7c\x00\x82\x00\x8c\x00\x98\x00\xa4\x00\xb4\x00\xbc\x00\xc6\x00'\
+b'\xce\x00\xd8\x00\xea\x00\xf4\x00\x06\x01\x16\x01\x26\x01\x36\x01'\
+b'\x48\x01\x56\x01\x66\x01\x78\x01\x80\x01\x88\x01\x96\x01\xa4\x01'\
+b'\xb2\x01\xc4\x01\xdc\x01\xee\x01\x00\x02\x14\x02\x28\x02\x3a\x02'\
+b'\x4a\x02\x60\x02\x74\x02\x7c\x02\x8c\x02\x9e\x02\xae\x02\xc6\x02'\
+b'\xda\x02\xf0\x02\x04\x03\x1a\x03\x2e\x03\x42\x03\x52\x03\x66\x03'\
+b'\x78\x03\x90\x03\xa2\x03\xb4\x03\xc4\x03\xce\x03\xd8\x03\xe2\x03'\
+b'\xf2\x03\x08\x04\x14\x04\x26\x04\x3a\x04\x4a\x04\x5c\x04\x6c\x04'\
+b'\x76\x04\x88\x04\x9a\x04\xa2\x04\xaa\x04\xba\x04\xc2\x04\xdc\x04'\
+b'\xee\x04\x00\x05\x14\x05\x26\x05\x30\x05\x42\x05\x4c\x05\x5e\x05'\
+b'\x6e\x05\x84\x05\x94\x05\xa4\x05\xb2\x05\xbe\x05\xc8\x05\xd2\x05'\
+b'\xde\x05'
+
+_mvfont = memoryview(_font)
+
+def _chr_addr(ordch):
+    offset = 2 * (ordch - 32)
+    return int.from_bytes(_index[offset:offset + 2], 'little')
+
+def get_width(s):
+    width = 0
+    for ch in s:
+        ordch = ord(ch)
+        ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+        offset = _chr_addr(ordch)
+        width += int.from_bytes(_font[offset:offset + 2], 'little')
+    return width
+
+def get_ch(ch):
+    ordch = ord(ch)
+    ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+    offset = _chr_addr(ordch)
+    width = int.from_bytes(_font[offset:offset + 2], 'little')
+    next_offs = _chr_addr(ordch +1)
+    return _mvfont[offset + 2:next_offs], width
+ 
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt24.py b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt24.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd6bd1372f37668816d079e18b0945e5961a203e
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt24.py
@@ -0,0 +1,286 @@
+# Code generated by font-to-py.py.
+# Font: CM Sans Serif 2012.ttf
+version = '0.2'
+
+def height():
+    return 24
+
+def max_width():
+    return 20
+
+def hmap():
+    return False
+
+def reverse():
+    return False
+
+def monospaced():
+    return False
+
+def min_ch():
+    return 32
+
+def max_ch():
+    return 126
+
+_font =\
+b'\x0c\x00\xc0\x00\x00\xf0\x00\x00\xf0\x00\x00\x78\x70\x07\x38\x78'\
+b'\x07\x38\x7c\x07\x78\x1e\x00\xf0\x0f\x00\xf0\x07\x00\xc0\x03\x00'\
+b'\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00'\
+b'\x00\x02\xf8\x3f\x07\xf8\x3f\x07\xf8\x3f\x02\x00\x00\x00\x00\x00'\
+b'\x00\x08\x00\xf8\x03\x00\xf8\x03\x00\x08\x00\x00\x00\x00\x00\xf8'\
+b'\x03\x00\xf8\x03\x00\x08\x00\x00\x00\x00\x00\x0c\x00\x00\xe0\x00'\
+b'\x80\xe3\x00\x80\xf3\x03\xe0\xff\x03\xe0\xff\x00\xe0\xe3\x00\x80'\
+b'\xe3\x03\x80\xff\x03\xe0\xff\x01\xe0\xe7\x00\x80\xe3\x00\x80\x03'\
+b'\x00\x0b\x00\x00\xe0\x00\xc0\xe3\x01\xe0\xc7\x01\x70\x8e\x03\xf8'\
+b'\xef\x07\xf8\xff\x07\x70\x8c\x03\xe0\x9d\x01\xc0\xf9\x01\x00\xf0'\
+b'\x00\x00\x00\x00\x12\x00\xe0\x01\x00\xf0\x03\x00\x38\x07\x00\x38'\
+b'\x07\x04\x38\x07\x07\x38\xc7\x07\xf0\xf3\x01\xe0\x79\x00\x00\x1e'\
+b'\x00\x80\x07\x00\xe0\xe3\x01\xf8\xf0\x03\x38\x38\x07\x08\x38\x07'\
+b'\x00\x38\x07\x00\xf0\x03\x00\xe0\x01\x00\x00\x00\x0f\x00\x00\xf0'\
+b'\x00\x00\xf8\x01\xe0\xfd\x03\xf0\x8f\x07\xf8\x07\x07\x38\x0e\x07'\
+b'\x38\x1f\x07\xf8\x3f\x07\xf0\xfb\x07\xe0\xf0\x07\x00\xe0\x03\x00'\
+b'\xf8\x07\x00\xf8\x07\x00\x78\x06\x00\x00\x04\x04\x00\xf8\x03\x00'\
+b'\xf8\x03\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x7f\x00\xe0\xff'\
+b'\x03\xf8\xff\x0f\x7c\x00\x1f\x0e\x00\x38\x02\x00\x20\x02\x00\x20'\
+b'\x07\x00\x02\x00\x20\x0e\x00\x38\x7c\x00\x1f\xf8\xff\x0f\xe0\xff'\
+b'\x03\x00\x7f\x00\x00\x00\x00\x09\x00\x60\x00\x00\x60\x00\x00\x78'\
+b'\x03\x00\xf0\x01\x00\xe0\x00\x00\xf8\x03\x00\x68\x01\x00\x60\x00'\
+b'\x00\x00\x00\x00\x0c\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\
+b'\x38\x00\x80\xff\x03\x80\xff\x03\x80\xff\x03\x00\x38\x00\x00\x38'\
+b'\x00\x00\x38\x00\x00\x38\x00\x00\x00\x00\x05\x00\x00\x00\x37\x00'\
+b'\x00\x3f\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x07\x00\x00\x38\x00'\
+b'\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\
+b'\x38\x00\x05\x00\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00\x00\x00'\
+b'\x00\x00\x00\x08\x00\x00\x00\x06\x00\xe0\x07\x00\xfc\x07\xc0\xff'\
+b'\x01\xf8\x3f\x00\xfc\x03\x00\x7c\x00\x00\x04\x00\x00\x0d\x00\x80'\
+b'\x7f\x00\xe0\xff\x01\xf0\xff\x03\xf8\xc0\x07\x38\x00\x07\x38\x00'\
+b'\x07\x38\x00\x07\xf8\xc0\x07\xf0\xff\x03\xe0\xff\x01\x80\x7f\x00'\
+b'\x00\x00\x00\x00\x00\x00\x08\x00\xc0\x01\x00\xc0\x01\x00\xe0\x01'\
+b'\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x00\x00\x00\x00\x00'\
+b'\x0d\x00\xc0\x81\x07\xe0\xe1\x07\xf0\xe1\x07\x78\x70\x07\x38\x38'\
+b'\x07\x38\x38\x07\x38\x1c\x07\x78\x1e\x07\xf0\x0f\x07\xf0\x07\x07'\
+b'\xc0\x03\x07\x00\x00\x00\x00\x00\x00\x0c\x00\xc0\xc0\x00\xf0\xc0'\
+b'\x03\xf0\xc0\x03\x78\x80\x07\x38\x00\x07\x38\x0e\x07\x38\x0e\x07'\
+b'\x78\x9e\x07\xf0\xff\x03\xf0\xff\x03\xe0\xf1\x00\x00\x00\x00\x0c'\
+b'\x00\x00\xf0\x00\x00\xf8\x00\x00\xfe\x00\x00\xef\x00\xc0\xe7\x00'\
+b'\xf0\xe1\x00\xf8\xe0\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00'\
+b'\xe0\x00\x00\xe0\x00\x0c\x00\x00\xce\x00\xf8\xcf\x03\xf8\xcf\x03'\
+b'\x38\x8e\x07\x38\x07\x07\x38\x07\x07\x38\x07\x07\x38\x8f\x07\x38'\
+b'\xfe\x03\x38\xfc\x01\x00\xf8\x00\x00\x00\x00\x0d\x00\x00\x7f\x00'\
+b'\xe0\xff\x01\xf0\xff\x03\x78\x8e\x07\x38\x07\x07\x38\x07\x07\x38'\
+b'\x07\x07\x78\x8f\x07\xf0\xfe\x03\xe0\xfc\x01\xc0\xf8\x00\x00\x00'\
+b'\x00\x00\x00\x00\x0b\x00\x38\x00\x00\x38\x00\x00\x38\xc0\x07\x38'\
+b'\xf8\x07\x38\xfe\x07\xb8\x3f\x00\xf8\x07\x00\xf8\x01\x00\xf8\x00'\
+b'\x00\x78\x00\x00\x38\x00\x00\x0c\x00\x00\xf0\x00\xe0\xf9\x01\xf0'\
+b'\xff\x03\xf0\x9f\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e'\
+b'\x07\xf0\x9f\x07\xf0\xff\x03\xe0\xf9\x01\x00\xf0\x00\x0d\x00\xc0'\
+b'\x07\x00\xe0\x0f\x01\xf0\x1f\x03\x78\x3c\x07\x38\x38\x07\x38\x38'\
+b'\x07\x38\x38\x07\x38\x38\x07\x70\x9c\x07\xf0\xff\x03\xe0\xff\x01'\
+b'\x80\x7f\x00\x00\x00\x00\x05\x00\x00\x07\x07\x00\x07\x07\x00\x07'\
+b'\x07\x00\x00\x00\x00\x00\x00\x05\x00\x00\x07\x37\x00\x07\x3f\x00'\
+b'\x07\x1f\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x38\x00\x00\x38\x00'\
+b'\x00\x7c\x00\x00\x6c\x00\x00\xee\x00\x00\xee\x00\x00\xc7\x01\x00'\
+b'\xc7\x01\x80\x83\x03\x00\x00\x00\x0b\x00\x00\xce\x01\x00\xce\x01'\
+b'\x00\xce\x01\x00\xce\x01\x00\xce\x01\x00\xce\x01\x00\xce\x01\x00'\
+b'\xce\x01\x00\xce\x01\x00\xce\x01\x00\x00\x00\x0a\x00\x80\x83\x03'\
+b'\x80\x83\x03\x00\xc7\x01\x00\xc7\x01\x00\xee\x00\x00\xee\x00\x00'\
+b'\x7c\x00\x00\x7c\x00\x00\x38\x00\x00\x00\x00\x0c\x00\xc0\x00\x00'\
+b'\xf0\x00\x00\xf0\x00\x00\x78\x70\x07\x38\x78\x07\x38\x7c\x07\x78'\
+b'\x1e\x00\xf0\x0f\x00\xf0\x07\x00\xc0\x03\x00\x00\x00\x00\x00\x00'\
+b'\x00\x11\x00\x00\x3f\x00\xc0\xff\x00\xe0\xe1\x01\xf0\xc0\x03\x70'\
+b'\xbc\x03\x78\x7e\x07\x38\x7f\x07\xb8\x73\x07\xb8\x33\x07\xb8\x3f'\
+b'\x07\x38\x7f\x07\x70\xf3\x03\x70\x70\x03\xe0\x38\x02\xe0\x3f\x00'\
+b'\x80\x0f\x00\x00\x00\x00\x0f\x00\x00\x00\x06\x00\xc0\x07\x00\xf0'\
+b'\x07\x00\xfe\x03\xc0\xff\x00\xf8\xef\x00\xf8\xe1\x00\x78\xe0\x00'\
+b'\xf8\xe3\x00\xf0\xff\x00\xc0\xff\x00\x00\xfe\x07\x00\xf0\x07\x00'\
+b'\x80\x07\x00\x00\x04\x0f\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07'\
+b'\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38'\
+b'\x9e\x07\xf8\xff\x07\xf0\xff\x03\xe0\xf1\x01\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x10\x00\x00\x3f\x00\xc0\xff\x00\xe0\xff\x01\xf0'\
+b'\xe1\x03\x70\x80\x03\x78\x80\x07\x38\x00\x07\x38\x00\x07\x38\x00'\
+b'\x07\x38\x00\x07\x78\x80\x03\xf0\xc0\x03\xe0\xc0\x01\xc0\xc0\x00'\
+b'\x80\x40\x00\x00\x00\x00\x0f\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff'\
+b'\x07\x38\x00\x07\x38\x00\x07\x38\x00\x07\x38\x00\x07\x38\x00\x07'\
+b'\x78\x80\x07\xf0\xc0\x03\xe0\xff\x01\xc0\xff\x00\x00\x3f\x00\x00'\
+b'\x00\x00\x00\x00\x00\x0e\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07'\
+b'\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38\x0e\x07\x38'\
+b'\x0e\x07\x38\x0e\x07\x38\x00\x07\x38\x00\x07\x00\x00\x00\x00\x00'\
+b'\x00\x0d\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x38\x1c\x00\x38'\
+b'\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c'\
+b'\x00\x38\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x3f\x00\xc0'\
+b'\xff\x00\xe0\xff\x01\xf0\xe1\x03\x70\x80\x03\x78\x80\x07\x38\x00'\
+b'\x07\x38\x00\x07\x38\x1c\x07\x38\x1c\x07\x78\x9c\x03\xf0\xdc\x01'\
+b'\xe0\xfc\x07\xc0\xfc\x07\x80\xfc\x07\x00\x00\x00\x00\x00\x00\x0f'\
+b'\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x0e\x00\x00\x0e\x00'\
+b'\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\x00\x0e\x00\xf8'\
+b'\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x00\x00\x00\x00\x00\x06\x00'\
+b'\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x0c\x00\x00\xe0\x00\x00\xe0\x01\x00\xe0\x03\x00\x80\x07'\
+b'\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00\x80\x07\xf8\xff\x03\xf8'\
+b'\xff\x03\xf8\xff\x00\x00\x00\x00\x0e\x00\xf8\xff\x07\xf8\xff\x07'\
+b'\xf8\xff\x07\x00\x1e\x00\x00\x0f\x00\x80\x1f\x00\xc0\x7f\x00\xe0'\
+b'\xf9\x00\xf0\xf0\x03\x78\xc0\x07\x38\x80\x07\x18\x00\x07\x08\x00'\
+b'\x04\x00\x00\x00\x0c\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00'\
+b'\x00\x07\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00\x00'\
+b'\x07\x00\x00\x07\x00\x00\x07\x00\x00\x00\x12\x00\xf8\xff\x07\xf8'\
+b'\xff\x07\xf8\xff\x07\xf8\x01\x00\xf8\x1f\x00\xc0\xff\x00\x00\xfc'\
+b'\x07\x00\xe0\x07\x00\xe0\x07\x00\xfc\x07\xc0\xff\x00\xf8\x1f\x00'\
+b'\xf8\x01\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x00\x00\x00'\
+b'\x00\x00\x0f\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\xf8\x00\x00'\
+b'\xe0\x03\x00\xc0\x0f\x00\x00\x3f\x00\x00\xfc\x00\x00\xf0\x01\x00'\
+b'\xe0\x07\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x00\x00\x00\x00\x00'\
+b'\x00\x11\x00\x00\x3f\x00\xc0\xff\x00\xe0\xff\x01\xf0\xe1\x03\x70'\
+b'\x80\x03\x78\x80\x07\x38\x00\x07\x38\x00\x07\x38\x00\x07\x78\x80'\
+b'\x07\x70\x80\x03\xf0\xe1\x03\xe0\xff\x01\xc0\xff\x00\x00\x3f\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0e\x00\xf8\xff\x07\xf8\xff\x07\xf8\xff'\
+b'\x07\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00'\
+b'\x78\x1e\x00\xf0\x0f\x00\xf0\x0f\x00\xc0\x03\x00\x00\x00\x00\x00'\
+b'\x00\x00\x11\x00\x00\x3f\x00\xc0\xff\x00\xe0\xff\x01\xf0\xe1\x03'\
+b'\x70\x80\x03\x78\x80\x07\x38\x00\x07\x38\x10\x07\x38\x38\x07\x38'\
+b'\x70\x07\x78\xf0\x07\x70\xe0\x03\xf0\xe1\x03\xe0\xff\x07\xc0\xff'\
+b'\x03\x00\x3f\x02\x00\x00\x00\x0f\x00\xf8\xff\x07\xf8\xff\x07\xf8'\
+b'\xff\x07\x38\x1c\x00\x38\x1c\x00\x38\x1c\x00\x38\x3c\x00\x38\xfc'\
+b'\x00\x38\xfc\x03\x78\xfe\x07\xf0\x9f\x07\xf0\x0f\x07\xc0\x07\x04'\
+b'\x00\x00\x00\x00\x00\x00\x0e\x00\x00\xc0\x00\xe0\xc1\x01\xf0\xe3'\
+b'\x03\xf0\x87\x03\x78\x06\x07\x38\x0e\x07\x38\x0e\x07\x38\x0c\x07'\
+b'\x38\x0c\x07\x78\x9c\x07\xf0\xf8\x03\xe0\xf8\x03\xc0\xf0\x00\x00'\
+b'\x00\x00\x0c\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\
+b'\xf8\xff\x07\xf8\xff\x07\xf8\xff\x07\x38\x00\x00\x38\x00\x00\x38'\
+b'\x00\x00\x38\x00\x00\x00\x00\x00\x0e\x00\xf8\xff\x00\xf8\xff\x01'\
+b'\xf8\xff\x03\x00\x80\x07\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00'\
+b'\x00\x07\x00\x80\x07\xf8\xff\x03\xf8\xff\x01\xf8\xff\x00\x00\x00'\
+b'\x00\x00\x00\x00\x0e\x00\x18\x00\x00\xf8\x00\x00\xf8\x07\x00\xf0'\
+b'\x3f\x00\x00\xff\x01\x00\xf8\x07\x00\x80\x07\x00\xe0\x07\x00\xfe'\
+b'\x07\xc0\x7f\x00\xf8\x0f\x00\xf8\x01\x00\x38\x00\x00\x08\x00\x00'\
+b'\x14\x00\x18\x00\x00\xf8\x01\x00\xf8\x1f\x00\xe0\xff\x00\x00\xfe'\
+b'\x07\x00\xe0\x07\x00\xf8\x07\xc0\xff\x07\xf8\x3f\x00\xf8\x03\x00'\
+b'\xf8\x01\x00\xf8\x3f\x00\xc0\xff\x07\x00\xf8\x07\x00\xc0\x07\x00'\
+b'\xfe\x07\xe0\xff\x01\xf8\x1f\x00\xf8\x01\x00\x38\x00\x00\x0e\x00'\
+b'\x08\x00\x04\x18\x00\x07\x78\x80\x07\xf8\xe0\x07\xf0\xf3\x01\xc0'\
+b'\xff\x00\x00\x3f\x00\x80\x7f\x00\xe0\xff\x00\xf0\xf1\x03\xf8\xc0'\
+b'\x07\x38\x80\x07\x18\x00\x06\x08\x00\x04\x0d\x00\x18\x00\x00\x38'\
+b'\x00\x00\xf8\x00\x00\xf0\x03\x00\xc0\x0f\x00\x00\xff\x07\x00\xfc'\
+b'\x07\x00\xff\x07\xc0\x0f\x00\xf0\x03\x00\xf8\x00\x00\x38\x00\x00'\
+b'\x18\x00\x00\x0d\x00\x00\x00\x07\x38\x80\x07\x38\xc0\x07\x38\xe0'\
+b'\x07\x38\x78\x07\x38\x3c\x07\x38\x1e\x07\x38\x0f\x07\xf8\x07\x07'\
+b'\xf8\x01\x07\xf8\x00\x07\x78\x00\x07\x38\x00\x07\x07\x00\xff\xff'\
+b'\x3f\xff\xff\x3f\xff\xff\x3f\x07\x00\x38\x07\x00\x38\x07\x00\x38'\
+b'\x00\x00\x00\x08\x00\x04\x00\x00\x3c\x00\x00\xfc\x01\x00\xfc\x1f'\
+b'\x00\xe0\xff\x00\x00\xfe\x07\x00\xf0\x07\x00\x00\x07\x07\x00\x07'\
+b'\x00\x38\x07\x00\x38\x07\x00\x38\xff\xff\x3f\xff\xff\x3f\xff\xff'\
+b'\x3f\x00\x00\x00\x0c\x00\x00\x18\x00\x00\x1f\x00\xc0\x1f\x00\xf8'\
+b'\x03\x00\xf8\x00\x00\x78\x00\x00\xf8\x03\x00\xe0\x0f\x00\x00\x1f'\
+b'\x00\x00\x1c\x00\x00\x10\x00\x00\x00\x00\x12\x00\x00\x00\x38\x00'\
+b'\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00'\
+b'\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38'\
+b'\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00\x00\x38\x00'\
+b'\x00\x38\x09\x00\x04\x00\x00\x04\x00\x00\x0c\x00\x00\x1c\x00\x00'\
+b'\x18\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0d'\
+b'\x00\x00\xc0\x01\x00\xe4\x03\x00\xe6\x07\x00\x37\x07\x80\x37\x07'\
+b'\x80\x33\x07\x80\x33\x07\x80\x33\x07\x80\x93\x03\x80\xff\x07\x00'\
+b'\xff\x07\x00\xfe\x07\x00\x00\x00\x0e\x00\xfc\xff\x07\xfc\xff\x07'\
+b'\xfc\xff\x07\x00\x87\x03\x80\x03\x07\x80\x03\x07\x80\x03\x07\x80'\
+b'\x03\x07\x80\x87\x07\x00\xff\x03\x00\xfe\x01\x00\xf8\x00\x00\x00'\
+b'\x00\x00\x00\x00\x0d\x00\x00\x78\x00\x00\xfe\x01\x00\xff\x03\x00'\
+b'\x87\x03\x80\x03\x07\x80\x03\x07\x80\x03\x07\x80\x03\x07\x80\x87'\
+b'\x07\x00\x87\x03\x00\x86\x01\x00\x88\x00\x00\x00\x00\x0e\x00\x00'\
+b'\x78\x00\x00\xfe\x01\x00\xff\x03\x00\x87\x03\x80\x87\x07\x80\x03'\
+b'\x07\x80\x03\x07\x80\x03\x07\x80\x87\x07\x00\x87\x03\xfc\xff\x07'\
+b'\xfc\xff\x07\xfc\xff\x07\x00\x00\x00\x0d\x00\x00\x30\x00\x00\xfc'\
+b'\x00\x00\xfe\x01\x00\xff\x03\x80\x77\x07\x80\x73\x07\x80\x73\x07'\
+b'\x80\x73\x07\x80\xf7\x07\x00\xff\x03\x00\x7e\x03\x00\x7c\x00\x00'\
+b'\x60\x00\x07\x00\x80\x03\x00\xf0\xff\x07\xfc\xff\x07\xfc\xff\x07'\
+b'\x9e\x03\x00\x8e\x03\x00\x00\x00\x00\x0e\x00\x00\x78\x00\x00\xfe'\
+b'\x01\x00\xff\x23\x00\x87\x73\x80\x87\xf7\x80\x03\xe7\x80\x03\xe7'\
+b'\x80\x03\xe7\x00\x87\xe7\x00\x87\xf3\x80\xff\x7f\x80\xff\x3f\x80'\
+b'\xff\x1f\x00\x00\x00\x0d\x00\xfc\xff\x07\xfc\xff\x07\xfc\xff\x07'\
+b'\x00\x07\x00\x80\x03\x00\x80\x03\x00\x80\x03\x00\x80\x07\x00\x00'\
+b'\xff\x07\x00\xff\x07\x00\xfc\x07\x00\x00\x00\x00\x00\x00\x05\x00'\
+b'\xb8\xff\x07\xb8\xff\x07\xb8\xff\x07\x00\x00\x00\x00\x00\x00\x05'\
+b'\x00\x00\x00\xe0\x00\x00\xe0\xb8\xff\xff\xb8\xff\xff\xb8\xff\x7f'\
+b'\x0c\x00\xfc\xff\x07\xfc\xff\x07\xfc\xff\x07\x00\x78\x00\x00\x3c'\
+b'\x00\x00\xfe\x00\x80\xff\x01\x80\xe7\x07\x80\x83\x07\x80\x01\x07'\
+b'\x80\x00\x04\x00\x00\x00\x05\x00\xfc\xff\x07\xfc\xff\x07\xfc\xff'\
+b'\x07\x00\x00\x00\x00\x00\x00\x14\x00\x80\xff\x07\x80\xff\x07\x80'\
+b'\xff\x07\x00\x07\x00\x80\x03\x00\x80\x03\x00\x80\x03\x00\x80\x07'\
+b'\x00\x00\xff\x07\x00\xfe\x07\x00\xff\x07\x80\x07\x00\x80\x03\x00'\
+b'\x80\x03\x00\x80\x07\x00\x00\xff\x07\x00\xff\x07\x00\xfc\x07\x00'\
+b'\x00\x00\x00\x00\x00\x0d\x00\x80\xff\x07\x80\xff\x07\x80\xff\x07'\
+b'\x00\x07\x00\x80\x03\x00\x80\x03\x00\x80\x03\x00\x80\x07\x00\x00'\
+b'\xff\x07\x00\xff\x07\x00\xfc\x07\x00\x00\x00\x00\x00\x00\x0d\x00'\
+b'\x00\x78\x00\x00\xfe\x01\x00\xff\x03\x00\x87\x03\x80\x03\x07\x80'\
+b'\x03\x07\x80\x03\x07\x80\x03\x07\x00\x87\x03\x00\xff\x03\x00\xfe'\
+b'\x01\x00\x78\x00\x00\x00\x00\x0e\x00\x80\xff\xff\x80\xff\xff\x80'\
+b'\xff\xff\x00\x87\x03\x80\x03\x07\x80\x03\x07\x80\x03\x07\x80\x03'\
+b'\x07\x80\x87\x07\x00\xff\x03\x00\xfe\x01\x00\x7c\x00\x00\x00\x00'\
+b'\x00\x00\x00\x0e\x00\x00\x78\x00\x00\xfe\x01\x00\xff\x03\x00\x87'\
+b'\x03\x80\x87\x07\x80\x03\x07\x80\x03\x07\x80\x03\x07\x80\x87\x07'\
+b'\x00\x87\x03\x80\xff\xff\x80\xff\xff\x80\xff\xff\x00\x00\x00\x08'\
+b'\x00\x80\xff\x07\x80\xff\x07\x80\xff\x07\x00\x07\x00\x80\x03\x00'\
+b'\x80\x03\x00\x80\x03\x00\x00\x00\x00\x0c\x00\x00\x80\x00\x00\x8e'\
+b'\x01\x00\x9f\x03\x80\x9f\x07\x80\x3b\x07\x80\x33\x07\x80\x33\x07'\
+b'\x80\x33\x07\x00\xf7\x07\x00\xe7\x03\x00\xc6\x01\x00\x00\x00\x07'\
+b'\x00\x80\x03\x00\xfc\xff\x00\xfc\xff\x03\xfc\xff\x07\x80\x03\x07'\
+b'\x80\x03\x07\x00\x00\x00\x0d\x00\x80\xff\x00\x80\xff\x03\x80\xff'\
+b'\x03\x00\x80\x07\x00\x00\x07\x00\x00\x07\x00\x00\x07\x00\x80\x03'\
+b'\x80\xff\x07\x80\xff\x07\x80\xff\x07\x00\x00\x00\x00\x00\x00\x0c'\
+b'\x00\x80\x01\x00\x80\x0f\x00\x80\x3f\x00\x00\xfe\x01\x00\xf0\x07'\
+b'\x00\x80\x07\x00\xc0\x07\x00\xf8\x07\x00\xff\x00\x80\x3f\x00\x80'\
+b'\x07\x00\x80\x00\x00\x12\x00\x80\x01\x00\x80\x0f\x00\x80\xff\x00'\
+b'\x00\xfe\x07\x00\xe0\x07\x00\xc0\x07\x00\xfc\x07\x80\xff\x00\x80'\
+b'\x0f\x00\x80\x3f\x00\x00\xff\x01\x00\xf0\x07\x00\x80\x07\x00\xf8'\
+b'\x07\x80\xff\x03\x80\x3f\x00\x80\x07\x00\x80\x00\x00\x0c\x00\x80'\
+b'\x00\x04\x80\x01\x07\x80\x87\x07\x80\xef\x07\x00\xfe\x01\x00\x7c'\
+b'\x00\x00\xfe\x00\x00\xff\x03\x80\xc7\x07\x80\x03\x07\x80\x00\x06'\
+b'\x00\x00\x04\x0c\x00\x80\x01\x00\x80\x07\xe0\x80\x3f\xe0\x00\xff'\
+b'\xf0\x00\xf8\xff\x00\xc0\x7f\x00\xc0\x1f\x00\xf8\x07\x00\xff\x00'\
+b'\x80\x1f\x00\x80\x07\x00\x80\x00\x00\x0b\x00\x80\x03\x07\x80\x83'\
+b'\x07\x80\xc3\x07\x80\xe3\x07\x80\xf3\x07\x80\x7b\x07\x80\x3f\x07'\
+b'\x80\x1f\x07\x80\x0f\x07\x80\x07\x07\x00\x00\x07\x08\x00\x00\x0c'\
+b'\x00\x00\x1e\x00\xfc\xff\x0f\xfe\xff\x1f\xfe\xf3\x1f\x06\x00\x38'\
+b'\x06\x00\x38\x00\x00\x00\x05\x00\xfc\xff\x07\xfc\xff\x07\xfc\xff'\
+b'\x07\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x38\xfe\xf3\x1f\xfe'\
+b'\xff\x1f\xfc\xff\x0f\x00\x1e\x00\x00\x0c\x00\x00\x00\x00\x09\x00'\
+b'\x18\x00\x00\x0c\x00\x00\x0c\x00\x00\x0c\x00\x00\x18\x00\x00\x18'\
+b'\x00\x00\x1c\x00\x00\x04\x00\x00\x00\x00\x00'
+
+_index =\
+b'\x00\x00\x26\x00\x3d\x00\x51\x00\x6b\x00\x91\x00\xb4\x00\xec\x00'\
+b'\x1b\x01\x29\x01\x40\x01\x57\x01\x74\x01\x9a\x01\xab\x01\xc2\x01'\
+b'\xd3\x01\xed\x01\x16\x02\x30\x02\x59\x02\x7f\x02\xa5\x02\xcb\x02'\
+b'\xf4\x02\x17\x03\x3d\x03\x66\x03\x77\x03\x88\x03\xa8\x03\xcb\x03'\
+b'\xeb\x03\x11\x04\x46\x04\x75\x04\xa4\x04\xd6\x04\x05\x05\x31\x05'\
+b'\x5a\x05\x8f\x05\xbe\x05\xd2\x05\xf8\x05\x24\x06\x4a\x06\x82\x06'\
+b'\xb1\x06\xe6\x06\x12\x07\x47\x07\x76\x07\xa2\x07\xc8\x07\xf4\x07'\
+b'\x20\x08\x5e\x08\x8a\x08\xb3\x08\xdc\x08\xf3\x08\x0d\x09\x24\x09'\
+b'\x4a\x09\x82\x09\x9f\x09\xc8\x09\xf4\x09\x1d\x0a\x49\x0a\x72\x0a'\
+b'\x89\x0a\xb5\x0a\xde\x0a\xef\x0a\x00\x0b\x26\x0b\x37\x0b\x75\x0b'\
+b'\x9e\x0b\xc7\x0b\xf3\x0b\x1f\x0c\x39\x0c\x5f\x0c\x76\x0c\x9f\x0c'\
+b'\xc5\x0c\xfd\x0c\x23\x0d\x49\x0d\x6c\x0d\x86\x0d\x97\x0d\xae\x0d'\
+b'\xcb\x0d'
+
+_mvfont = memoryview(_font)
+
+def _chr_addr(ordch):
+    offset = 2 * (ordch - 32)
+    return int.from_bytes(_index[offset:offset + 2], 'little')
+    
+def get_width(s):
+    width = 0
+    for ch in s:
+        ordch = ord(ch)
+        ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+        offset = _chr_addr(ordch)
+        width += int.from_bytes(_font[offset:offset + 2], 'little')
+    return width
+
+def get_ch(ch):
+    ordch = ord(ch)
+    ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+    offset = _chr_addr(ordch)
+    width = int.from_bytes(_font[offset:offset + 2], 'little')
+    next_offs = _chr_addr(ordch +1)
+    return _mvfont[offset + 2:next_offs], width
+ 
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt32.py b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt32.py
new file mode 100644
index 0000000000000000000000000000000000000000..e98667e65d6b26c11c6edb1367ac3a564c59310c
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/fonts/tt32.py
@@ -0,0 +1,430 @@
+# Code generated by font-to-py.py.
+# Font: CM Sans Serif 2012.ttf
+version = '0.2'
+
+def height():
+    return 31
+
+def max_width():
+    return 26
+
+def hmap():
+    return False
+
+def reverse():
+    return False
+
+def monospaced():
+    return False
+
+def min_ch():
+    return 32
+
+def max_ch():
+    return 126
+
+_font =\
+b'\x0f\x00\x00\x07\x00\x00\xc0\x07\x00\x00\xe0\x07\x00\x00\xe0\x07'\
+b'\x00\x00\xf0\x01\x67\x00\xf0\xc0\xf7\x00\xf0\xe0\xf7\x00\xf0\xe0'\
+b'\x67\x00\xf0\xf1\x01\x00\xe0\xff\x00\x00\xe0\x7f\x00\x00\xc0\x3f'\
+b'\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x08\x00\xf0\xff\xf3\x00\xf0\xff\xf3\x00\xf0\xff'\
+b'\xf3\x00\xf0\xff\x63\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0a\x00\xf0\x0f\x00\x00\xf0\x0f\x00\x00'\
+b'\xf0\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0\x0f\x00\x00'\
+b'\xf0\x0f\x00\x00\xf0\x0f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00'\
+b'\x0f\x00\x00\x00\x0e\x00\x00\x0e\x0e\x00\x00\x0e\x7e\x00\x00\xfe'\
+b'\x7f\x00\xe0\xff\x7f\x00\xe0\xff\x0f\x00\xe0\x0f\x0e\x00\x00\x0e'\
+b'\x0e\x00\x00\x0e\x7f\x00\x00\xfe\x7f\x00\xe0\xff\x7f\x00\xe0\xff'\
+b'\x0f\x00\xe0\x0f\x0e\x00\x00\x0e\x0e\x00\x00\x0e\x00\x00\x0e\x00'\
+b'\x00\x1f\x0e\x00\x80\x3f\x1e\x00\xc0\x3f\x3e\x00\xc0\x79\x3c\x00'\
+b'\xc0\x71\x38\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xc0\xe1\x38\x00'\
+b'\xc0\xe3\x38\x00\xc0\xe7\x3f\x00\x80\xc7\x1f\x00\x00\x86\x0f\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00\x80\x07\x00\x00\xc0\x0f'\
+b'\x00\x00\xe0\x1f\x00\x00\xf0\x3c\x00\x00\x70\x38\x80\x00\x70\x38'\
+b'\xe0\x00\x70\x38\xf8\x00\xf0\x3c\x7e\x00\xe0\x1f\x1f\x00\xc0\xcf'\
+b'\x0f\x00\x80\xf7\x03\x00\x00\xfc\x00\x00\x00\x3f\x1e\x00\x80\x8f'\
+b'\x7f\x00\xe0\x87\x7f\x00\xf0\xc1\xf3\x00\x70\xc0\xe1\x00\x10\xc0'\
+b'\xe1\x00\x00\xc0\xf3\x00\x00\x80\x7f\x00\x00\x80\x7f\x00\x00\x00'\
+b'\x1e\x00\x00\x00\x00\x00\x13\x00\x00\x00\x0f\x00\x00\x80\x3f\x00'\
+b'\x00\xc0\x3f\x00\x80\xe7\x7f\x00\xe0\x7f\xf8\x00\xe0\x3f\xf8\x00'\
+b'\xf0\x3f\xf0\x00\xf0\x78\xf0\x00\xf0\xf8\xf0\x00\xf0\xff\xf3\x00'\
+b'\xe0\xdf\xff\x00\xe0\x8f\xff\x00\x80\x07\x7e\x00\x00\xc0\xff\x00'\
+b'\x00\xc0\xff\x00\x00\xc0\xff\x00\x00\xc0\xc3\x00\x00\x00\x80\x00'\
+b'\x00\x00\x00\x00\x05\x00\xf0\x0f\x00\x00\xf0\x0f\x00\x00\xf0\x0f'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\xfc\x03\x00'\
+b'\x80\xff\x1f\x00\xe0\xff\x7f\x00\xf8\xff\xff\x01\xfc\x01\xf8\x03'\
+b'\x3e\x00\xc0\x07\x0e\x00\x00\x07\x02\x00\x00\x04\x00\x00\x00\x00'\
+b'\x09\x00\x02\x00\x00\x04\x0e\x00\x00\x07\x3e\x00\xc0\x07\xfc\x01'\
+b'\xf8\x03\xf8\xff\xff\x01\xe0\xff\x7f\x00\x80\xff\x1f\x00\x00\xfc'\
+b'\x03\x00\x00\x00\x00\x00\x0b\x00\x80\x01\x00\x00\xa0\x09\x00\x00'\
+b'\xf0\x1f\x00\x00\xe0\x0f\x00\x00\xc0\x07\x00\x00\xe0\x0f\x00\x00'\
+b'\xf0\x1d\x00\x00\xa0\x09\x00\x00\x80\x01\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x0f\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\xc0'\
+b'\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\xfe\x7f\x00\x00\xfe'\
+b'\x7f\x00\x00\xfe\x7f\x00\x00\xfe\x7f\x00\x00\xc0\x03\x00\x00\xc0'\
+b'\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\x00'\
+b'\x00\x00\x06\x00\x00\x00\x60\x0c\x00\x00\xf0\x0c\x00\x00\xf0\x07'\
+b'\x00\x00\xe0\x03\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\xc0'\
+b'\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\xc0'\
+b'\x03\x00\x00\xc0\x03\x00\x00\xc0\x03\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x06\x00\x00\x00\x60\x00\x00\x00\xf0\x00\x00\x00\xf0\x00'\
+b'\x00\x00\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0a\x00\x00\x00'\
+b'\xe0\x00\x00\x00\xfc\x00\x00\xc0\xff\x00\x00\xf8\xff\x00\x80\xff'\
+b'\x1f\x00\xf8\xff\x03\x00\xfc\x3f\x00\x00\xfc\x07\x00\x00\x7c\x00'\
+b'\x00\x00\x04\x00\x00\x00\x11\x00\x00\xfc\x03\x00\x80\xff\x1f\x00'\
+b'\xc0\xff\x3f\x00\xe0\xff\x7f\x00\xe0\x07\x7e\x00\xf0\x01\xf8\x00'\
+b'\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x01\xf8\x00'\
+b'\xe0\x07\x7e\x00\xe0\xff\x7f\x00\xc0\xff\x3f\x00\x80\xff\x1f\x00'\
+b'\x00\xfc\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x80\x03'\
+b'\x00\x00\x80\x03\x00\x00\xc0\x03\x00\x00\xe0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x10\x00\x00\x07\xf8\x00\xc0\x07\xfc\x00\xc0\x07\xfe\x00'\
+b'\xe0\x07\xff\x00\xf0\x81\xf7\x00\xf0\x80\xf3\x00\xf0\xc0\xf1\x00'\
+b'\xf0\xe0\xf1\x00\xf0\xe0\xf0\x00\xf0\xf1\xf0\x00\xe0\x7f\xf0\x00'\
+b'\xe0\x7f\xf0\x00\xc0\x3f\xf0\x00\x00\x0f\xf0\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x10\x00\x00\x00\x0e\x00\x00\x07\x3e\x00\xc0\x07'\
+b'\x3e\x00\xe0\x07\x7e\x00\xe0\x07\xf8\x00\xf0\x01\xf0\x00\xf0\xf0'\
+b'\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf9\xf0\x00\xf0\xff'\
+b'\xf9\x00\xe0\xff\x7f\x00\xc0\xdf\x7f\x00\x80\x8f\x3f\x00\x00\x00'\
+b'\x0f\x00\x00\x00\x00\x00\x10\x00\x00\x00\x0f\x00\x00\x80\x0f\x00'\
+b'\x00\xe0\x0f\x00\x00\xf8\x0f\x00\x00\x7c\x0f\x00\x00\x3f\x0f\x00'\
+b'\x80\x0f\x0f\x00\xe0\x07\x0f\x00\xf0\x01\x0f\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\x00\x00\x0f\x00'\
+b'\x00\x00\x0f\x00\x00\x00\x00\x00\x0f\x00\x00\x70\x1c\x00\xf0\x7f'\
+b'\x3c\x00\xf0\x7f\x7c\x00\xf0\xff\x7c\x00\xf0\x78\xf8\x00\xf0\x3c'\
+b'\xf0\x00\xf0\x3c\xf0\x00\xf0\x3c\xf0\x00\xf0\x3c\xf0\x00\xf0\x7c'\
+b'\x78\x00\xf0\xf8\x7f\x00\xf0\xf0\x3f\x00\xf0\xe0\x1f\x00\x00\xc0'\
+b'\x0f\x00\x00\x00\x00\x00\x10\x00\x00\xfc\x07\x00\x00\xff\x1f\x00'\
+b'\xc0\xff\x3f\x00\xe0\xff\x7f\x00\xe0\xf3\x78\x00\xf0\x79\xf0\x00'\
+b'\xf0\x78\xf0\x00\xf0\x78\xf0\x00\xf0\x78\xf0\x00\xf0\xf9\xf8\x00'\
+b'\xe0\xf3\x7f\x00\xe0\xf3\x7f\x00\xc0\xe3\x3f\x00\x00\x83\x0f\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x00\xf0\x00\x00\x00\xf0\x00'\
+b'\x00\x00\xf0\x00\x00\x00\xf0\x00\xf8\x00\xf0\x80\xff\x00\xf0\xe0'\
+b'\xff\x00\xf0\xf8\xff\x00\xf0\xfc\x07\x00\xf0\x7e\x00\x00\xf0\x1f'\
+b'\x00\x00\xf0\x07\x00\x00\xf0\x03\x00\x00\xf0\x01\x00\x00\xf0\x00'\
+b'\x00\x00\x10\x00\x00\x00\x0f\x00\x80\xc7\x3f\x00\xc0\xef\x7f\x00'\
+b'\xe0\xff\x7f\x00\xe0\xff\xf8\x00\xf0\x7d\xf0\x00\xf0\x78\xf0\x00'\
+b'\xf0\x78\xf0\x00\xf0\x78\xf0\x00\xf0\x7d\xf0\x00\xe0\xff\xf8\x00'\
+b'\xe0\xff\x7f\x00\xc0\xef\x7f\x00\x80\xc7\x3f\x00\x00\x00\x0f\x00'\
+b'\x00\x00\x00\x00\x10\x00\x00\x3f\x00\x00\x80\xff\x30\x00\xc0\xff'\
+b'\x71\x00\xe0\xff\x71\x00\xf0\xe1\xf3\x00\xf0\xc1\xf3\x00\xf0\xc0'\
+b'\xf3\x00\xf0\xc0\xf3\x00\xf0\xc0\xf3\x00\xf0\xc1\xf9\x00\xe0\xe1'\
+b'\x7d\x00\xe0\xff\x7f\x00\xc0\xff\x3f\x00\x80\xff\x1f\x00\x00\xfc'\
+b'\x03\x00\x00\x00\x00\x00\x06\x00\x00\x18\x60\x00\x00\x3c\xf0\x00'\
+b'\x00\x3c\xf0\x00\x00\x18\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x06\x00\x00\x18\x60\x0c\x00\x3c\xf0\x0c\x00\x3c\xf0\x07\x00\x18'\
+b'\xe0\x03\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\xc0\x03\x00'\
+b'\x00\xe0\x07\x00\x00\xe0\x07\x00\x00\xf0\x0f\x00\x00\xf0\x0f\x00'\
+b'\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x38\x1c\x00\x00\x3c\x3c\x00'\
+b'\x00\x1c\x38\x00\x00\x1e\x78\x00\x00\x00\x00\x00\x0e\x00\x00\x78'\
+b'\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x78'\
+b'\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x78'\
+b'\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x1e\x78\x00\x00\x3c\x3c\x00'\
+b'\x00\x3c\x3c\x00\x00\x78\x1e\x00\x00\x78\x1e\x00\x00\x70\x0e\x00'\
+b'\x00\xf0\x0f\x00\x00\xe0\x07\x00\x00\xe0\x07\x00\x00\xc0\x07\x00'\
+b'\x00\xc0\x03\x00\x00\x00\x00\x00\x0f\x00\x00\x07\x00\x00\xc0\x07'\
+b'\x00\x00\xe0\x07\x00\x00\xe0\x07\x00\x00\xf0\x01\x67\x00\xf0\xc0'\
+b'\xf7\x00\xf0\xe0\xf7\x00\xf0\xe0\x67\x00\xf0\xf1\x01\x00\xe0\xff'\
+b'\x00\x00\xe0\x7f\x00\x00\xc0\x3f\x00\x00\x00\x1f\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x16\x00\x00\xf8\x01\x00\x00\xfe\x07\x00'\
+b'\x80\xff\x1f\x00\xc0\x0f\x1e\x00\xc0\x03\x3c\x00\xe0\xe1\x79\x00'\
+b'\xe0\xf8\x73\x00\xf0\xfc\xf7\x00\x70\x1c\xe7\x00\x70\x0e\xe7\x00'\
+b'\x70\x8e\xe3\x00\x70\xfe\xe1\x00\x70\xfc\xe3\x00\xf0\xfc\xe7\x00'\
+b'\xe0\x0c\x77\x00\xe0\x01\x77\x00\xc0\xc3\x23\x00\xc0\xff\x43\x00'\
+b'\x00\xff\x01\x00\x00\x7e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x13\x00\x00\x00\xc0\x00\x00\x00\xf8\x00\x00\x00\xfe\x00\x00\xc0'\
+b'\xff\x00\x00\xf8\x3f\x00\x00\xff\x0f\x00\xe0\xff\x0f\x00\xf0\x1f'\
+b'\x0f\x00\xf0\x03\x0f\x00\xf0\x00\x0f\x00\xf0\x07\x0f\x00\xf0\x3f'\
+b'\x0f\x00\xc0\xff\x0f\x00\x00\xfe\x0f\x00\x00\xf0\x7f\x00\x00\x80'\
+b'\xff\x00\x00\x00\xfc\x00\x00\x00\xf0\x00\x00\x00\x80\x00\x12\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00'\
+b'\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf9\xf0\x00'\
+b'\xf0\xff\xf9\x00\xe0\xff\x7f\x00\xc0\xdf\x7f\x00\x80\x8f\x3f\x00'\
+b'\x00\x00\x1f\x00\x00\x00\x00\x00\x14\x00\x00\xf8\x01\x00\x00\xfe'\
+b'\x07\x00\x80\xff\x1f\x00\xc0\xff\x3f\x00\xc0\x0f\x3f\x00\xe0\x03'\
+b'\x7c\x00\xe0\x01\x78\x00\xf0\x01\xf8\x00\xf0\x00\xf0\x00\xf0\x00'\
+b'\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x01\xf8\x00\xe0\x01'\
+b'\x78\x00\xe0\x07\x7e\x00\xc0\x07\x3e\x00\x80\x07\x1e\x00\x00\x03'\
+b'\x0c\x00\x00\x04\x04\x00\x00\x00\x00\x00\x13\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\x00\xf0\x00'\
+b'\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00'\
+b'\xf0\x00\xf8\x00\xe0\x01\x78\x00\xe0\x07\x7e\x00\xc0\xff\x3f\x00'\
+b'\x80\xff\x1f\x00\x00\xff\x0f\x00\x00\xfc\x03\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\xf0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xf0\xf0\x00\xf0\xf0'\
+b'\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0'\
+b'\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\xf0\xf0\x00\xf0\x00'\
+b'\xf0\x00\xf0\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xf0\x00\x00\xf0\xf0\x00\x00\xf0\xf0\x00\x00\xf0\xf0\x00\x00'\
+b'\xf0\xf0\x00\x00\xf0\xf0\x00\x00\xf0\xf0\x00\x00\xf0\xf0\x00\x00'\
+b'\xf0\xf0\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00'\
+b'\x15\x00\x00\xf8\x01\x00\x00\xfe\x0f\x00\x80\xff\x1f\x00\xc0\xff'\
+b'\x3f\x00\xc0\x0f\x7f\x00\xe0\x03\x7c\x00\xe0\x01\xf8\x00\xf0\x01'\
+b'\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\xe0\xf1\x00\xf0\xe0'\
+b'\xf1\x00\xf0\xe1\x79\x00\xe0\xe1\x79\x00\xe0\xe3\x3d\x00\xc0\xe7'\
+b'\xff\x00\x80\xe7\xff\x00\x00\xe3\xff\x00\x00\xe4\xff\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x14\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\x00\xf0\x00\x00\x00\xf0\x00\x00'\
+b'\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00'\
+b'\x00\xf0\x00\x00\x00\xf0\x00\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\xf0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x3f\x00'\
+b'\x00\x00\x7f\x00\x00\x00\x7f\x00\x00\x00\xf8\x00\x00\x00\xf0\x00'\
+b'\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf8\x00'\
+b'\xf0\xff\x7f\x00\xf0\xff\x7f\x00\xf0\xff\x3f\x00\xf0\xff\x0f\x00'\
+b'\x00\x00\x00\x00\x12\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\xf0\xff\xff\x00\x00\xf0\x01\x00\x00\xfc\x00\x00\x00\xfe'\
+b'\x00\x00\x00\xff\x01\x00\x80\xff\x07\x00\xc0\xef\x0f\x00\xe0\x87'\
+b'\x3f\x00\xf0\x01\x7f\x00\xf0\x00\xfc\x00\x70\x00\xf8\x00\x30\x00'\
+b'\xf0\x00\x10\x00\xc0\x00\x10\x00\x80\x00\x00\x00\x00\x00\x10\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00'\
+b'\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00'\
+b'\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x18\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\xf0\x07\x00\x00\xf0\x7f\x00\x00\x80\xff\x07\x00\x00\xfc'\
+b'\x3f\x00\x00\xc0\xff\x00\x00\x00\xfe\x00\x00\x00\xfe\x00\x00\xc0'\
+b'\xff\x00\x00\xfc\x3f\x00\x80\xff\x07\x00\xf0\x7f\x00\x00\xf0\x07'\
+b'\x00\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff'\
+b'\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x14\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\x07\x00\x00\xc0\x1f\x00\x00\x00\x3f\x00\x00'\
+b'\x00\xfc\x00\x00\x00\xf0\x03\x00\x00\xc0\x0f\x00\x00\x80\x3f\x00'\
+b'\x00\x00\xfe\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x16\x00\x00\xf8\x01\x00\x00\xfe\x07\x00\x80\xff'\
+b'\x1f\x00\xc0\xff\x3f\x00\xc0\x0f\x3f\x00\xe0\x03\x7c\x00\xe0\x01'\
+b'\x78\x00\xf0\x01\xf8\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00\xf0\x00'\
+b'\xf0\x00\xf0\x00\xf0\x00\xf0\x01\xf8\x00\xe0\x01\x78\x00\xe0\x03'\
+b'\x7c\x00\xc0\x0f\x3f\x00\xc0\xff\x3f\x00\x80\xff\x1f\x00\x00\xfe'\
+b'\x07\x00\x00\xf8\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xe0\x01\x00\xf0\xe0\x01\x00\xf0\xe0\x01\x00\xf0\xe0\x01\x00'\
+b'\xf0\xe0\x01\x00\xf0\xe0\x01\x00\xf0\xf1\x01\x00\xe0\xff\x00\x00'\
+b'\xe0\xff\x00\x00\xc0\x7f\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\xf8\x01\x00\x00\xff'\
+b'\x0f\x00\x80\xff\x1f\x00\xc0\xff\x3f\x00\xe0\x0f\x7e\x00\xe0\x03'\
+b'\x7c\x00\xf0\x01\xf8\x00\xf0\x00\xf0\x00\xf0\x00\xf1\x00\xf0\x80'\
+b'\xf3\x00\xf0\xc0\xf7\x00\xf0\x80\xff\x00\xf0\x01\xff\x00\xe0\x03'\
+b'\x7e\x00\xe0\x0f\x7e\x00\xc0\xff\x7f\x00\x80\xff\xff\x00\x00\xfe'\
+b'\x77\x00\x00\xf8\x21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xe0\x01\x00\xf0\xe0\x01\x00\xf0\xe0\x01\x00\xf0\xe0\x03\x00'\
+b'\xf0\xe0\x0f\x00\xf0\xe0\x3f\x00\xf0\xf1\x7f\x00\xe0\xff\xfc\x00'\
+b'\xe0\xff\xf0\x00\xc0\x7f\xe0\x00\x00\x1f\x80\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x04\x00\x80\x0f'\
+b'\x1e\x00\xc0\x1f\x3e\x00\xe0\x3f\x7e\x00\xe0\x3f\x7c\x00\xf0\x39'\
+b'\xf8\x00\xf0\x70\xf0\x00\xf0\x70\xf0\x00\xf0\x70\xf0\x00\xf0\x70'\
+b'\xf0\x00\xf0\x60\xf0\x00\xf0\xe1\xf8\x00\xe0\xe3\x78\x00\xe0\xc7'\
+b'\x7f\x00\xc0\xc3\x7f\x00\x80\x83\x3f\x00\x00\x02\x0f\x00\x00\x00'\
+b'\x00\x00\x0f\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00'\
+b'\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf0\xff\xff\x00\xf0\x00\x00\x00\xf0\x00\x00\x00'\
+b'\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00'\
+b'\x12\x00\xf0\xff\x07\x00\xf0\xff\x1f\x00\xf0\xff\x3f\x00\xf0\xff'\
+b'\x7f\x00\x00\x00\x7c\x00\x00\x00\xf8\x00\x00\x00\xf0\x00\x00\x00'\
+b'\xf0\x00\x00\x00\xf0\x00\x00\x00\xf0\x00\x00\x00\xf8\x00\x00\x00'\
+b'\x7c\x00\xf0\xff\x7f\x00\xf0\xff\x3f\x00\xf0\xff\x1f\x00\xf0\xff'\
+b'\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x30\x00\x00\x00'\
+b'\xf0\x01\x00\x00\xf0\x0f\x00\x00\xf0\x7f\x00\x00\xc0\xff\x03\x00'\
+b'\x00\xfe\x1f\x00\x00\xe0\xff\x00\x00\x00\xff\x00\x00\x00\xf8\x00'\
+b'\x00\x00\xff\x00\x00\xe0\xff\x00\x00\xfe\x1f\x00\xc0\xff\x03\x00'\
+b'\xf0\x7f\x00\x00\xf0\x0f\x00\x00\xf0\x01\x00\x00\x30\x00\x00\x00'\
+b'\x19\x00\x70\x00\x00\x00\xf0\x03\x00\x00\xf0\x3f\x00\x00\xf0\xff'\
+b'\x01\x00\x80\xff\x1f\x00\x00\xf8\xff\x00\x00\x80\xff\x00\x00\x00'\
+b'\xfc\x00\x00\xe0\xff\x00\x00\xff\xff\x00\xf0\xff\x07\x00\xf0\x3f'\
+b'\x00\x00\xf0\x03\x00\x00\xf0\x1f\x00\x00\xf0\xff\x03\x00\x80\xff'\
+b'\x7f\x00\x00\xf8\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\xf0'\
+b'\xff\x00\x00\xff\x3f\x00\xf0\xff\x07\x00\xf0\x7f\x00\x00\xf0\x07'\
+b'\x00\x00\xf0\x00\x00\x00\x11\x00\x10\x00\x80\x00\x30\x00\xe0\x00'\
+b'\xf0\x00\xf0\x00\xf0\x01\xfc\x00\xf0\x07\xfe\x00\xc0\x9f\x3f\x00'\
+b'\x80\xff\x0f\x00\x00\xfe\x07\x00\x00\xfc\x01\x00\x00\xfe\x07\x00'\
+b'\x80\xff\x0f\x00\xc0\x9f\x3f\x00\xf0\x07\x7f\x00\xf0\x03\xfc\x00'\
+b'\xf0\x00\xf8\x00\x70\x00\xe0\x00\x10\x00\x80\x00\x12\x00\x10\x00'\
+b'\x00\x00\x30\x00\x00\x00\xf0\x00\x00\x00\xf0\x03\x00\x00\xf0\x0f'\
+b'\x00\x00\xc0\x3f\x00\x00\x00\x7f\x00\x00\x00\xfc\xff\x00\x00\xf0'\
+b'\xff\x00\x00\xf0\xff\x00\x00\xfc\xff\x00\x00\x7f\x00\x00\xc0\x3f'\
+b'\x00\x00\xf0\x0f\x00\x00\xf0\x03\x00\x00\xf0\x00\x00\x00\x30\x00'\
+b'\x00\x00\x10\x00\x00\x00\x11\x00\x00\x00\xe0\x00\xf0\x00\xf8\x00'\
+b'\xf0\x00\xfc\x00\xf0\x00\xfe\x00\xf0\x00\xff\x00\xf0\x80\xff\x00'\
+b'\xf0\xc0\xf3\x00\xf0\xf0\xf1\x00\xf0\xf8\xf0\x00\xf0\x7c\xf0\x00'\
+b'\xf0\x3e\xf0\x00\xf0\x0f\xf0\x00\xf0\x07\xf0\x00\xf0\x03\xf0\x00'\
+b'\xf0\x01\xf0\x00\xf0\x00\xf0\x00\x00\x00\x00\x00\x09\x00\xff\xff'\
+b'\xff\x0f\xff\xff\xff\x0f\xff\xff\xff\x0f\xff\xff\xff\x0f\x0f\x00'\
+b'\x00\x0f\x0f\x00\x00\x0f\x0f\x00\x00\x0f\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x0a\x00\x04\x00\x00\x00\x3c\x00\x00\x00\xfc\x03\x00\x00'\
+b'\xfc\x1f\x00\x00\xf8\xff\x01\x00\xc0\xff\x1f\x00\x00\xfc\xff\x00'\
+b'\x00\xe0\xff\x00\x00\x00\xfe\x00\x00\x00\xe0\x00\x09\x00\x0f\x00'\
+b'\x00\x0f\x0f\x00\x00\x0f\x0f\x00\x00\x0f\xff\xff\xff\x0f\xff\xff'\
+b'\xff\x0f\xff\xff\xff\x0f\xff\xff\xff\x0f\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x0f\x00\x00\x80\x01\x00\x00\xf0\x01\x00\x00\xfc\x01\x00'\
+b'\x80\xff\x01\x00\xe0\x3f\x00\x00\xf0\x07\x00\x00\xf0\x01\x00\x00'\
+b'\xf0\x03\x00\x00\xf0\x1f\x00\x00\xc0\xff\x00\x00\x00\xfe\x01\x00'\
+b'\x00\xf8\x01\x00\x00\xc0\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00'\
+b'\x17\x00\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00'\
+b'\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00'\
+b'\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00'\
+b'\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00'\
+b'\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00'\
+b'\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x00\x00\x00\x0f\x0c\x00'\
+b'\x08\x00\x00\x00\x18\x00\x00\x00\x38\x00\x00\x00\x78\x00\x00\x00'\
+b'\x70\x00\x00\x00\x60\x00\x00\x00\x40\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x11\x00\x00\x40\x3c\x00\x00\x30\x7e\x00\x00\x38\x7f\x00\x00\x7c'\
+b'\xff\x00\x00\x3e\xf3\x00\x00\x1e\xf3\x00\x00\x1e\xf3\x00\x00\x1e'\
+b'\xf1\x00\x00\x9e\x71\x00\x00\xbe\x79\x00\x00\xfc\xff\x00\x00\xfc'\
+b'\xff\x00\x00\xf8\xff\x00\x00\xf0\xff\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x12\x00\xfc\xff\xff\x00\xfc\xff\xff\x00'\
+b'\xfc\xff\xff\x00\xfc\xff\xff\x00\x00\x7c\x3c\x00\x00\x3c\x78\x00'\
+b'\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x1e\xf0\x00'\
+b'\x00\x3e\xf8\x00\x00\x7c\x7c\x00\x00\xfc\x7f\x00\x00\xf8\x3f\x00'\
+b'\x00\xf0\x1f\x00\x00\xc0\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x10\x00\x00\xc0\x07\x00\x00\xf0\x1f\x00\x00\xf8\x3f\x00\x00\xfc'\
+b'\x7f\x00\x00\x7c\x7c\x00\x00\x3e\xf8\x00\x00\x1e\xf0\x00\x00\x1e'\
+b'\xf0\x00\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x3c\xf8\x00\x00\x7c'\
+b'\x7c\x00\x00\x78\x38\x00\x00\x30\x18\x00\x00\x20\x08\x00\x00\x00'\
+b'\x00\x00\x12\x00\x00\xc0\x07\x00\x00\xf0\x1f\x00\x00\xf8\x3f\x00'\
+b'\x00\xfc\x7f\x00\x00\x7e\xfc\x00\x00\x3e\xf8\x00\x00\x1e\xf0\x00'\
+b'\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x3c\x78\x00\x00\x7c\x7c\x00'\
+b'\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff\xff\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\xe0'\
+b'\x0f\x00\x00\xf0\x1f\x00\x00\xf8\x3f\x00\x00\xfc\x7f\x00\x00\xdc'\
+b'\x7b\x00\x00\xde\xf3\x00\x00\xde\xf3\x00\x00\xde\xf3\x00\x00\xde'\
+b'\xf3\x00\x00\xde\xfb\x00\x00\xfc\x7b\x00\x00\xfc\x7b\x00\x00\xf8'\
+b'\x33\x00\x00\xf0\x03\x00\x00\x80\x03\x00\x00\x00\x00\x00\x09\x00'\
+b'\x00\x1c\x00\x00\x00\x1c\x00\x00\xc0\xff\xff\x00\xf0\xff\xff\x00'\
+b'\xf0\xff\xff\x00\xf8\xff\xff\x00\x78\x1c\x00\x00\x3c\x1c\x00\x00'\
+b'\x3c\x1c\x00\x00\x11\x00\x00\xc0\x07\x08\x00\xf0\x1f\x1c\x00\xf8'\
+b'\x3f\x3c\x00\xfc\x7f\x3c\x00\x7e\xfc\x7c\x00\x3e\xf8\x78\x00\x1e'\
+b'\xf0\x78\x00\x1e\xf0\x78\x00\x1e\xf0\x78\x00\x3c\x78\x7c\x00\x78'\
+b'\x7c\x3e\x00\xfe\xff\x3f\x00\xfe\xff\x1f\x00\xfe\xff\x0f\x00\xfe'\
+b'\xff\x03\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\xfc\xff\xff\x00'\
+b'\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff\xff\x00\x00\x3c\x00\x00'\
+b'\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00'\
+b'\x00\x3e\x00\x00\x00\xfc\xff\x00\x00\xfc\xff\x00\x00\xf8\xff\x00'\
+b'\x00\xe0\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x60\xfe'\
+b'\xff\x00\xf0\xfe\xff\x00\xf0\xfe\xff\x00\x60\xfe\xff\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x78'\
+b'\x00\x00\x00\x78\x60\xfe\xff\x7f\xf0\xfe\xff\x3f\xf0\xfe\xff\x3f'\
+b'\x60\xfe\xff\x0f\x10\x00\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff'\
+b'\xff\x00\xfc\xff\xff\x00\x00\xc0\x07\x00\x00\xe0\x03\x00\x00\xf0'\
+b'\x07\x00\x00\xfc\x0f\x00\x00\xfe\x3f\x00\x00\x3e\xff\x00\x00\x1e'\
+b'\xfc\x00\x00\x0e\xf8\x00\x00\x06\xe0\x00\x00\x02\xc0\x00\x00\x00'\
+b'\x80\x00\x00\x00\x00\x00\x07\x00\xfc\xff\xff\x00\xfc\xff\xff\x00'\
+b'\xfc\xff\xff\x00\xfc\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x00\x00\x00\x00\x1a\x00\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\xfe'\
+b'\xff\x00\x00\xfe\xff\x00\x00\x3c\x00\x00\x00\x1e\x00\x00\x00\x1e'\
+b'\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x3e\x00\x00\x00\xfc'\
+b'\xff\x00\x00\xfc\xff\x00\x00\xf8\xff\x00\x00\xfc\xff\x00\x00\x3e'\
+b'\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00\x00\x3e'\
+b'\x00\x00\x00\xfc\xff\x00\x00\xfc\xff\x00\x00\xf8\xff\x00\x00\xe0'\
+b'\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00'\
+b'\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\xfe\xff\x00'\
+b'\x00\x3c\x00\x00\x00\x1c\x00\x00\x00\x1e\x00\x00\x00\x1e\x00\x00'\
+b'\x00\x1e\x00\x00\x00\x3e\x00\x00\x00\xfc\xff\x00\x00\xfc\xff\x00'\
+b'\x00\xf8\xff\x00\x00\xe0\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x11\x00\x00\xc0\x07\x00\x00\xf0\x1f\x00\x00\xf8\x3f\x00\x00\xfc'\
+b'\x7f\x00\x00\x7c\x7c\x00\x00\x3e\xf8\x00\x00\x1e\xf0\x00\x00\x1e'\
+b'\xf0\x00\x00\x1e\xf0\x00\x00\x3e\xf8\x00\x00\x7c\x7c\x00\x00\xfc'\
+b'\x7f\x00\x00\xf8\x3f\x00\x00\xf0\x1f\x00\x00\xc0\x07\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x12\x00\x00\xfe\xff\x7f\x00\xfe\xff\x7f'\
+b'\x00\xfe\xff\x7f\x00\xfe\xff\x7f\x00\x7c\x7c\x00\x00\x3c\x78\x00'\
+b'\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x1e\xf0\x00\x00\x1e\xf0\x00'\
+b'\x00\x3e\xf8\x00\x00\x7c\x7c\x00\x00\xfc\x7f\x00\x00\xf8\x3f\x00'\
+b'\x00\xf0\x1f\x00\x00\xc0\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x11\x00\x00\xc0\x07\x00\x00\xf0\x1f\x00\x00\xf8\x3f\x00\x00\xfc'\
+b'\x7f\x00\x00\x7e\xfc\x00\x00\x3e\xf8\x00\x00\x1e\xf0\x00\x00\x1e'\
+b'\xf0\x00\x00\x1e\xf0\x00\x00\x3c\x78\x00\x00\x7c\x7c\x00\x00\xfe'\
+b'\xff\x7f\x00\xfe\xff\x7f\x00\xfe\xff\x7f\x00\xfe\xff\x7f\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0a\x00\x00\xfe\xff\x00\x00\xfe\xff\x00'\
+b'\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\x3c\x00\x00\x00\x1e\x00\x00'\
+b'\x00\x1e\x00\x00\x00\x0e\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00'\
+b'\x0f\x00\x00\x00\x08\x00\x00\x70\x18\x00\x00\xfc\x38\x00\x00\xfc'\
+b'\x7d\x00\x00\xfe\xf9\x00\x00\x9e\xf1\x00\x00\x9e\xf1\x00\x00\x9e'\
+b'\xf1\x00\x00\x9e\xf3\x00\x00\x3e\xf3\x00\x00\x3c\x7f\x00\x00\x3c'\
+b'\x7f\x00\x00\x38\x3e\x00\x00\x20\x1c\x00\x00\x00\x00\x00\x09\x00'\
+b'\x00\x0e\x00\x00\x00\x0e\x00\x00\xfc\xff\x1f\x00\xfc\xff\x3f\x00'\
+b'\xfc\xff\x7f\x00\xfc\xff\xff\x00\x00\x0e\xf0\x00\x00\x0e\xf0\x00'\
+b'\x00\x0e\xf0\x00\x10\x00\x00\xfe\x0f\x00\x00\xfe\x3f\x00\x00\xfe'\
+b'\x7f\x00\x00\xfe\x7f\x00\x00\x00\xf8\x00\x00\x00\xf0\x00\x00\x00'\
+b'\xf0\x00\x00\x00\xf0\x00\x00\x00\x70\x00\x00\x00\x78\x00\x00\xfe'\
+b'\xff\x00\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\xfe\xff\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x06\x00\x00\x00\x3e\x00\x00'\
+b'\x00\xfe\x00\x00\x00\xfe\x07\x00\x00\xf8\x3f\x00\x00\xc0\xff\x00'\
+b'\x00\x00\xfe\x00\x00\x00\xf0\x00\x00\x00\xfe\x00\x00\xc0\xff\x00'\
+b'\x00\xf8\x1f\x00\x00\xfe\x07\x00\x00\xfe\x00\x00\x00\x1e\x00\x00'\
+b'\x00\x06\x00\x00\x16\x00\x00\x06\x00\x00\x00\x7e\x00\x00\x00\xfe'\
+b'\x03\x00\x00\xfe\x1f\x00\x00\xf0\xff\x00\x00\x00\xff\x00\x00\x00'\
+b'\xf8\x00\x00\x80\xff\x00\x00\xf8\xff\x00\x00\xfe\x0f\x00\x00\xfe'\
+b'\x00\x00\x00\xfe\x00\x00\x00\xfe\x07\x00\x00\xf8\x7f\x00\x00\x80'\
+b'\xff\x00\x00\x00\xf8\x00\x00\x00\xff\x00\x00\xf0\xff\x00\x00\xfe'\
+b'\x3f\x00\x00\xfe\x03\x00\x00\x7e\x00\x00\x00\x0e\x00\x00\x0f\x00'\
+b'\x00\x02\x80\x00\x00\x06\xe0\x00\x00\x1e\xf0\x00\x00\x3e\xfc\x00'\
+b'\x00\xfe\x7e\x00\x00\xf8\x3f\x00\x00\xe0\x0f\x00\x00\xe0\x07\x00'\
+b'\x00\xf8\x1f\x00\x00\xfc\x7f\x00\x00\x7e\xfc\x00\x00\x1e\xf8\x00'\
+b'\x00\x0e\xe0\x00\x00\x02\xc0\x00\x00\x00\x80\x00\x0f\x00\x00\x06'\
+b'\x00\x00\x00\x3e\x00\x00\x00\xfe\x00\x78\x00\xfe\x07\x78\x00\xf8'\
+b'\x1f\x7c\x00\xc0\xff\x7f\x00\x00\xfe\x3f\x00\x00\xf0\x1f\x00\x00'\
+b'\xfe\x07\x00\xc0\xff\x00\x00\xf8\x1f\x00\x00\xfe\x07\x00\x00\xfe'\
+b'\x00\x00\x00\x1e\x00\x00\x00\x06\x00\x00\x0e\x00\x00\x00\xe0\x00'\
+b'\x00\x1e\xf0\x00\x00\x1e\xf8\x00\x00\x1e\xfc\x00\x00\x1e\xfe\x00'\
+b'\x00\x1e\xff\x00\x00\x9e\xf7\x00\x00\xde\xf3\x00\x00\xfe\xf1\x00'\
+b'\x00\xfe\xf0\x00\x00\x7e\xf0\x00\x00\x3e\xf0\x00\x00\x1e\xf0\x00'\
+b'\x00\x00\x00\x00\x0a\x00\x00\x60\x00\x00\x00\xf0\x00\x00\x00\xf0'\
+b'\x00\x00\xf8\xff\xff\x01\xfc\xff\xff\x03\xfe\x9f\xff\x07\xfe\x0f'\
+b'\xff\x07\x0e\x00\x00\x07\x0e\x00\x00\x07\x00\x00\x00\x00\x08\x00'\
+b'\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff\xff\x00\xfc\xff\xff\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
+b'\x09\x00\x0e\x00\x00\x07\x0e\x00\x00\x07\xfe\x0f\xff\x07\xfe\x9f'\
+b'\xff\x07\xfc\xff\xff\x03\xf8\xff\xff\x01\x00\xf0\x00\x00\x00\xf0'\
+b'\x00\x00\x00\x60\x00\x00\x0c\x00\x30\x00\x00\x00\x38\x00\x00\x00'\
+b'\x18\x00\x00\x00\x18\x00\x00\x00\x18\x00\x00\x00\x30\x00\x00\x00'\
+b'\x30\x00\x00\x00\x30\x00\x00\x00\x38\x00\x00\x00\x18\x00\x00\x00'\
+b'\x00\x00\x00\x00\x00\x00\x00\x00'
+
+_index =\
+b'\x00\x00\x3e\x00\x64\x00\x86\x00\xb0\x00\xee\x00\x28\x01\x86\x01'\
+b'\xd4\x01\xea\x01\x10\x02\x36\x02\x64\x02\xa2\x02\xbc\x02\xe2\x02'\
+b'\xfc\x02\x26\x03\x6c\x03\x92\x03\xd4\x03\x16\x04\x58\x04\x96\x04'\
+b'\xd8\x04\x12\x05\x54\x05\x96\x05\xb0\x05\xca\x05\xfc\x05\x36\x06'\
+b'\x68\x06\xa6\x06\x00\x07\x4e\x07\x98\x07\xea\x07\x38\x08\x7e\x08'\
+b'\xc0\x08\x16\x09\x68\x09\x86\x09\xc4\x09\x0e\x0a\x50\x0a\xb2\x0a'\
+b'\x04\x0b\x5e\x0b\xa8\x0b\xfe\x0b\x48\x0c\x92\x0c\xd0\x0c\x1a\x0d'\
+b'\x60\x0d\xc6\x0d\x0c\x0e\x56\x0e\x9c\x0e\xc2\x0e\xec\x0e\x12\x0f'\
+b'\x50\x0f\xae\x0f\xe0\x0f\x26\x10\x70\x10\xb2\x10\xfc\x10\x3e\x11'\
+b'\x64\x11\xaa\x11\xec\x11\x0a\x12\x24\x12\x66\x12\x84\x12\xee\x12'\
+b'\x30\x13\x76\x13\xc0\x13\x06\x14\x30\x14\x6e\x14\x94\x14\xd6\x14'\
+b'\x14\x15\x6e\x15\xac\x15\xea\x15\x24\x16\x4e\x16\x70\x16\x96\x16'\
+b'\xc8\x16'
+
+_mvfont = memoryview(_font)
+
+def _chr_addr(ordch):
+    offset = 2 * (ordch - 32)
+    return int.from_bytes(_index[offset:offset + 2], 'little')
+    
+def get_width(s):
+    width = 0
+    for ch in s:
+        ordch = ord(ch)
+        ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+        offset = _chr_addr(ordch)
+        width += int.from_bytes(_font[offset:offset + 2], 'little')
+    return width
+
+def get_ch(ch):
+    ordch = ord(ch)
+    ordch = ordch + 1 if ordch >= 32 and ordch <= 126 else 32
+    offset = _chr_addr(ordch)
+    width = int.from_bytes(_font[offset:offset + 2], 'little')
+    next_offs = _chr_addr(ordch +1)
+    return _mvfont[offset + 2:next_offs], width
+ 
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/hcsr04.py b/GW-custom/uPyLoRaWAN/uPySensors/hcsr04.py
new file mode 100644
index 0000000000000000000000000000000000000000..91df95e25733d83cd93944bca1b1ee52e2f4aaf6
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/hcsr04.py
@@ -0,0 +1,93 @@
+import time
+from uos import uname
+from machine import Pin
+
+try:
+    from machine import time_pulse_us
+except:
+    from pycom import pulses_get
+
+#__author__ = 'Roberto Sánchez'
+#__license__= "Apache License 2.0. https://www.apache.org/licenses/LICENSE-2.0"
+#__author-update__ = "Mauro Riva"
+
+class HCSR04:
+    """
+    Driver to use the untrasonic sensor HC-SR04.
+    The sensor range is between 2cm and 4m.
+
+    The timeouts received listening to echo pin are converted to OSError('Out of range')
+
+    """
+    # echo_timeout_us is based in chip range limit (400cm)
+    def __init__(self, trigger_pin, echo_pin, echo_timeout_us=500*2*30):
+        """
+        trigger_pin: Output pin to send pulses
+        echo_pin: Readonly pin to measure the distance. The pin should be protected with 1k resistor
+        echo_timeout_us: Timeout in microseconds to listen to echo pin.
+        By default is based in sensor limit range (4m)
+        """
+        self.echo_timeout_us = echo_timeout_us
+        # Init trigger pin (out)
+        self.trigger = Pin(trigger_pin, mode=Pin.OUT)
+        self.trigger.value(0)
+        # Init echo pin (in)
+        if (uname().sysname == 'WiPy'):
+            self.echo = Pin(echo_pin, mode=Pin.OPEN_DRAIN)
+        else:
+            self.echo = Pin(echo_pin, mode=Pin.IN)
+
+    def _send_pulse_and_wait(self):
+        """
+        Send the pulse to trigger and listen on echo pin.
+        We use the method `machine.time_pulse_us()` to get the microseconds until the echo is received.
+        """
+        self.trigger.value(0) # Stabilize the sensor
+        time.sleep_us(5)
+        self.trigger.value(1)
+        # Send a 10us pulse.
+        time.sleep_us(10)
+        self.trigger.value(0)
+        try:
+            if (uname().sysname == 'WiPy'):
+                pulse_list = pulses_get(self.echo, self.echo_timeout_us)
+                if(len(pulse_list) == 0):
+                    pulse_time = -1
+                else:
+                    pulse_time = pulse_list[0][1]
+            else:
+                pulse_time = time_pulse_us(self.echo, 1, self.echo_timeout_us)
+
+            return pulse_time
+        except OSError as ex:
+            if ex.args[0] == 110: # 110 = ETIMEDOUT
+                raise OSError('Out of range')
+            raise ex
+
+    def distance_mm(self):
+        """
+        Get the distance in milimeters without floating point operations.
+        """
+        pulse_time = self._send_pulse_and_wait()
+
+        # To calculate the distance we get the pulse_time and divide it by 2
+        # (the pulse walk the distance twice) and by 29.1 becasue
+        # the sound speed on air (343.2 m/s), that It's equivalent to
+        # 0.34320 mm/us that is 1mm each 2.91us
+        # pulse_time // 2 // 2.91 -> pulse_time // 5.82 -> pulse_time * 100 // 582
+        mm = pulse_time * 100 // 582
+        return mm
+
+    def distance_cm(self):
+        """
+        Get the distance in centimeters with floating point operations.
+        It returns a float
+        """
+        pulse_time = self._send_pulse_and_wait()
+
+        # To calculate the distance we get the pulse_time and divide it by 2
+        # (the pulse walk the distance twice) and by 29.1 becasue
+        # the sound speed on air (343.2 m/s), that It's equivalent to
+        # 0.034320 cm/us that is 1cm each 29.1us
+        cms = (pulse_time / 2) / 29.1
+        return cms
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/ili934xnew.py b/GW-custom/uPyLoRaWAN/uPySensors/ili934xnew.py
new file mode 100644
index 0000000000000000000000000000000000000000..44be116454469c6fa05ecef00cbe77a12c5b7681
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/ili934xnew.py
@@ -0,0 +1,326 @@
+# This is an adapted version of the ILI934X driver as below.
+# It works with multiple fonts and also works with the esp32 H/W SPI implementation
+# Also includes a word wrap print function
+# Proportional fonts are generated by Peter Hinch's Font-to-py
+# MIT License; Copyright (c) 2017 Jeffrey N. Magee
+
+# This file is part of MicroPython ILI934X driver
+# Copyright (c) 2016 - 2017 Radomir Dopieralski, Mika Tuupola
+#
+# Licensed under the MIT license:
+#   http://www.opensource.org/licenses/mit-license.php
+#
+# Project home:
+#   https://github.com/tuupola/micropython-ili934x
+
+import time
+import ustruct
+import framebuf
+from micropython import const
+
+_RDDSDR = const(0x0f) # Read Display Self-Diagnostic Result
+_SLPOUT = const(0x11) # Sleep Out
+_GAMSET = const(0x26) # Gamma Set
+_DISPOFF = const(0x28) # Display Off
+_DISPON = const(0x29) # Display On
+_CASET = const(0x2a) # Column Address Set
+_PASET = const(0x2b) # Page Address Set
+_RAMWR = const(0x2c) # Memory Write
+_RAMRD = const(0x2e) # Memory Read
+_MADCTL = const(0x36) # Memory Access Control
+_VSCRSADD = const(0x37) # Vertical Scrolling Start Address
+_PIXSET = const(0x3a) # Pixel Format Set
+_PWCTRLA = const(0xcb) # Power Control A
+_PWCRTLB = const(0xcf) # Power Control B
+_DTCTRLA = const(0xe8) # Driver Timing Control A
+_DTCTRLB = const(0xea) # Driver Timing Control B
+_PWRONCTRL = const(0xed) # Power on Sequence Control
+_PRCTRL = const(0xf7) # Pump Ratio Control
+_PWCTRL1 = const(0xc0) # Power Control 1
+_PWCTRL2 = const(0xc1) # Power Control 2
+_VMCTRL1 = const(0xc5) # VCOM Control 1
+_VMCTRL2 = const(0xc7) # VCOM Control 2
+_FRMCTR1 = const(0xb1) # Frame Rate Control 1
+_DISCTRL = const(0xb6) # Display Function Control
+_ENA3G = const(0xf2) # Enable 3G
+_PGAMCTRL = const(0xe0) # Positive Gamma Control
+_NGAMCTRL = const(0xe1) # Negative Gamma Control
+
+_CHUNK = const(1024) #maximum number of pixels per spi write
+
+def color565(r, g, b):
+    return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
+
+class ILI9341:
+
+    def __init__(self, spi, cs, dc, rst, w, h, r, font):
+        self.spi = spi
+        self.cs = cs
+        self.dc = dc
+        self.rst = rst
+        self._init_width = w
+        self._init_height = h
+        self.width = w
+        self.height = h
+        self.rotation = r
+        self.cs.init(self.cs.OUT, value=1)
+        self.dc.init(self.dc.OUT, value=0)
+        self.rst.init(self.rst.OUT, value=0)
+        self.reset()
+        self.init()
+        self._scroll = 0
+        self._buf = bytearray(_CHUNK * 2)
+        self._colormap = bytearray(b'\x00\x00\xFF\xFF') #default white foregraound, black background
+        self._x = 0
+        self._y = 0
+        self._font = font
+        self.scrolling = False
+
+    def set_color(self, fg, bg):
+        self._colormap[0] = bg>>8
+        self._colormap[1] = bg & 255
+        self._colormap[2] = fg>>8
+        self._colormap[3] = fg & 255
+
+    def set_pos(self, x, y):
+        self._x = x
+        self._y = y
+
+    def reset_scroll(self):
+        self.scrolling = False
+        self._scroll = 0
+        self.scroll(0)
+
+    def set_font(self, font):
+        self._font = font
+
+    def init(self):
+        for command, data in (
+            (_RDDSDR, b"\x03\x80\x02"),
+            (_PWCRTLB, b"\x00\xc1\x30"),
+            (_PWRONCTRL, b"\x64\x03\x12\x81"),
+            (_DTCTRLA, b"\x85\x00\x78"),
+            (_PWCTRLA, b"\x39\x2c\x00\x34\x02"),
+            (_PRCTRL, b"\x20"),
+            (_DTCTRLB, b"\x00\x00"),
+            (_PWCTRL1, b"\x23"),
+            (_PWCTRL2, b"\x10"),
+            (_VMCTRL1, b"\x3e\x28"),
+            (_VMCTRL2, b"\x86")):
+            self._write(command, data)
+
+        if self.rotation == 0:                  # 0 deg
+            self._write(_MADCTL, b"\x48")
+            self.width = self._init_height
+            self.height = self._init_width
+        elif self.rotation == 1:                # 90 deg
+            self._write(_MADCTL, b"\x28")
+            self.width = self._init_width
+            self.height = self._init_height
+        elif self.rotation == 2:                # 180 deg
+            self._write(_MADCTL, b"\x88")
+            self.width = self._init_height
+            self.height = self._init_width
+        elif self.rotation == 3:                # 270 deg
+            self._write(_MADCTL, b"\xE8")
+            self.width = self._init_width
+            self.height = self._init_height
+        elif self.rotation == 4:                # Mirrored + 0 deg
+            self._write(_MADCTL, b"\xC8")
+            self.width = self._init_height
+            self.height = self._init_width
+        elif self.rotation == 5:                # Mirrored + 90 deg
+            self._write(_MADCTL, b"\x68")
+            self.width = self._init_width
+            self.height = self._init_height
+        elif self.rotation == 6:                # Mirrored + 180 deg
+            self._write(_MADCTL, b"\x08")
+            self.width = self._init_height
+            self.height = self._init_width
+        elif self.rotation == 7:                # Mirrored + 270 deg
+            self._write(_MADCTL, b"\xA8")
+            self.width = self._init_width
+            self.height = self._init_height
+        else:
+            self._write(_MADCTL, b"\x08")
+
+        for command, data in (
+            (_PIXSET, b"\x55"),
+            (_FRMCTR1, b"\x00\x18"),
+            (_DISCTRL, b"\x08\x82\x27"),
+            (_ENA3G, b"\x00"),
+            (_GAMSET, b"\x01"),
+            (_PGAMCTRL, b"\x0f\x31\x2b\x0c\x0e\x08\x4e\xf1\x37\x07\x10\x03\x0e\x09\x00"),
+            (_NGAMCTRL, b"\x00\x0e\x14\x03\x11\x07\x31\xc1\x48\x08\x0f\x0c\x31\x36\x0f")):
+            self._write(command, data)
+        self._write(_SLPOUT)
+        time.sleep_ms(120)
+        self._write(_DISPON)
+
+    def reset(self):
+        self.rst(0)
+        time.sleep_ms(50)
+        self.rst(1)
+        time.sleep_ms(50)
+
+    def _write(self, command, data=None):
+        self.dc(0)
+        self.cs(0)
+        self.spi.write(bytearray([command]))
+        self.cs(1)
+        if data is not None:
+            self._data(data)
+
+    def _data(self, data):
+        self.dc(1)
+        self.cs(0)
+        self.spi.write(data)
+        self.cs(1)
+
+    def _writeblock(self, x0, y0, x1, y1, data=None):
+        self._write(_CASET, ustruct.pack(">HH", x0, x1))
+        self._write(_PASET, ustruct.pack(">HH", y0, y1))
+        self._write(_RAMWR, data)
+
+    def _readblock(self, x0, y0, x1, y1):
+        self._write(_CASET, ustruct.pack(">HH", x0, x1))
+        self._write(_PASET, ustruct.pack(">HH", y0, y1))
+        if data is None:
+            return self._read(_RAMRD, (x1 - x0 + 1) * (y1 - y0 + 1) * 3)
+
+    def _read(self, command, count):
+        self.dc(0)
+        self.cs(0)
+        self.spi.write(bytearray([command]))
+        data = self.spi.read(count)
+        self.cs(1)
+        return data
+
+    def pixel(self, x, y, color=None):
+        if color is None:
+            r, b, g = self._readblock(x, y, x, y)
+            return color565(r, g, b)
+        if not 0 <= x < self.width or not 0 <= y < self.height:
+            return
+        self._writeblock(x, y, x, y, ustruct.pack(">H", color))
+
+    def fill_rectangle(self, x, y, w, h, color=None):
+        x = min(self.width - 1, max(0, x))
+        y = min(self.height - 1, max(0, y))
+        w = min(self.width - x, max(1, w))
+        h = min(self.height - y, max(1, h))
+        if color:
+            color = ustruct.pack(">H", color)
+        else:
+            color = self._colormap[0:2] #background
+        for i in range(_CHUNK):
+            self._buf[2*i]=color[0]; self._buf[2*i+1]=color[1]
+        chunks, rest = divmod(w * h, _CHUNK)
+        self._writeblock(x, y, x + w - 1, y + h - 1, None)
+        if chunks:
+            for count in range(chunks):
+                self._data(self._buf)
+        if rest != 0:
+            mv = memoryview(self._buf)
+            self._data(mv[:rest*2])
+
+    def erase(self):
+        self.fill_rectangle(0, 0, self.width, self.height)
+
+    def blit(self, bitbuff, x, y, w, h):
+        x = min(self.width - 1, max(0, x))
+        y = min(self.height - 1, max(0, y))
+        w = min(self.width - x, max(1, w))
+        h = min(self.height - y, max(1, h))
+        chunks, rest = divmod(w * h, _CHUNK)
+        self._writeblock(x, y, x + w - 1, y + h - 1, None)
+        written = 0
+        for iy in range(h):
+            for ix in range(w):
+                index = ix+iy*w - written
+                if index >=_CHUNK:
+                    self._data(self._buf)
+                    written += _CHUNK
+                    index   -= _CHUNK
+                c = bitbuff.pixel(ix,iy)
+                self._buf[index*2] = self._colormap[c*2]
+                self._buf[index*2+1] = self._colormap[c*2+1]
+        rest = w*h - written
+        if rest != 0:
+            mv = memoryview(self._buf)
+            self._data(mv[:rest*2])
+
+    def chars(self, str, x, y):
+        str_w  = self._font.get_width(str)
+        div, rem = divmod(self._font.height(),8)
+        nbytes = div+1 if rem else div
+        buf = bytearray(str_w * nbytes)
+        pos = 0
+        for ch in str:
+            glyph, char_w = self._font.get_ch(ch)
+            for row in range(nbytes):
+                index = row*str_w + pos
+                for i in range(char_w):
+                    buf[index+i] = glyph[nbytes*i+row]
+            pos += char_w
+        fb = framebuf.FrameBuffer(buf,str_w, self._font.height(), framebuf.MONO_VLSB)
+        self.blit(fb,x,y,str_w,self._font.height())
+        return x+str_w
+
+    def scroll(self, dy):
+        self._scroll = (self._scroll + dy) % self.height
+        self._write(_VSCRSADD, ustruct.pack(">H", self._scroll))
+
+    def next_line(self, cury, char_h):
+        global scrolling
+        if not self.scrolling:
+            res = cury + char_h
+            self.scrolling = (res >= self.height)
+        if self.scrolling:
+            self.scroll(char_h)
+            res = (self.height - char_h + self._scroll)%self.height
+            self.fill_rectangle(0, res, self.width, self._font.height())
+        return res
+
+    def write(self, text): #does character wrap, compatible with stream output
+        curx = self._x; cury = self._y
+        char_h = self._font.height()
+        width = 0
+        written = 0
+        for pos, ch in enumerate(text):
+            if ch == '\n':
+                if pos>0:
+                    self.chars(text[written:pos],curx,cury)
+                curx = 0; written = pos+1; width = 0
+                cury = self.next_line(cury,char_h)
+            else:
+                char_w = self._font.get_width(ch)
+                if curx + width + char_w >= self.width:
+                    self.chars(text[written:pos], curx,cury)
+                    curx = 0 ; written = pos; width = char_h
+                    cury = self.next_line(cury,char_h)
+                else:
+                    width += char_w
+        if written<len(text):
+            curx = self.chars(text[written:], curx,cury)
+        self._x = curx; self._y = cury
+
+
+    def print(self, text): #does word wrap, leaves self._x unchanged
+        cury = self._y; curx = self._x
+        char_h = self._font.height()
+        char_w = self._font.max_width()
+        lines = text.split('\n')
+        for line in lines:
+            words = line.split(' ')
+            for word in words:
+                if curx + self._font.get_width(word) >= self.width:
+                    curx = self._x; cury = self.next_line(cury,char_h)
+                    while self._font.get_width(word) > self.width:
+                        self.chars(word[:self.width//char_w],curx,cury)
+                        word = word[self.width//char_w:]
+                        cury = self.next_line(cury,char_h)
+                if len(word)>0:
+                    curx = self.chars(word+' ', curx,cury)
+            curx = self._x; cury = self.next_line(cury,char_h)
+        self._y = cury
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/imu.py b/GW-custom/uPyLoRaWAN/uPySensors/imu.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1f99b21a058015d08219b017ce2ef5897bd14b5
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/imu.py
@@ -0,0 +1,409 @@
+# -*- coding: utf-8 -*-
+# imu.py MicroPython driver for the InvenSense inertial measurement units
+# This is the base class
+# Adapted from Sebastian Plamauer's MPU9150 driver:
+# https://github.com/micropython-IMU/micropython-mpu9150.git
+# Authors Peter Hinch, Sebastian Plamauer
+# V0.2 17th May 2017 Platform independent: utime and machine replace pyb
+
+'''
+mpu9250 is a micropython module for the InvenSense MPU9250 sensor.
+It measures acceleration, turn rate and the magnetic field in three axis.
+mpu9150 driver modified for the MPU9250 by Peter Hinch
+
+The MIT License (MIT)
+Copyright (c) 2014 Sebastian Plamauer, oeplse@gmail.com, Peter Hinch
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+'''
+
+# User access is now by properties e.g.
+# myimu = MPU9250('X')
+# magx = myimu.mag.x
+# accelxyz = myimu.accel.xyz
+# Error handling: on code used for initialisation, abort with message
+# At runtime try to continue returning last good data value. We don't want aircraft
+# crashing. However if the I2C has crashed we're probably stuffed.
+
+from utime import sleep_ms
+from machine import I2C, Pin
+from .vector3d import Vector3d
+from os import uname
+
+
+class MPUException(OSError):
+    '''
+    Exception for MPU devices
+    '''
+    pass
+
+
+def bytes_toint(msb, lsb):
+    '''
+    Convert two bytes to signed integer (big endian)
+    for little endian reverse msb, lsb arguments
+    Can be used in an interrupt handler
+    '''
+    if not msb & 0x80:
+        return msb << 8 | lsb  # +ve
+    return - (((msb ^ 255) << 8) | (lsb ^ 255) + 1)
+
+
+class MPU6050(object):
+    '''
+    Module for InvenSense IMUs. Base class implements MPU6050 6DOF sensor, with
+    features common to MPU9150 and MPU9250 9DOF sensors.
+    '''
+
+    _I2Cerror = "I2C failure when communicating with IMU"
+    _mpu_addr = (104, 105)  # addresses of MPU9150/MPU6050. There can be two devices
+    _chip_id = 104  # MPU6050
+
+    def __init__(self, side_str, dev_pin=(15, 4), device_addr=None, transposition=(0, 1, 2), scaling=(1, 1, 1)):
+
+        self._accel = Vector3d(transposition, scaling, self._accel_callback)
+        self._gyro = Vector3d(transposition, scaling, self._gyro_callback)
+        self.buf1 = bytearray(1)                # Pre-allocated buffers for reads: allows reads to
+        self.buf2 = bytearray(2)                # be done in interrupt handlers
+        self.buf3 = bytearray(3)
+        self.buf6 = bytearray(6)
+
+        sleep_ms(200)                           # Ensure PSU and device have settled
+
+        #if(uname().sysname == 'WiPy'):
+        #    self._mpu_i2c = I2C(side_str)
+        if isinstance(side_str, str):           # Non-pyb targets may use other than X or Y
+            self._mpu_i2c = I2C(side_str)
+        elif isinstance(side_str, int):         # WiPY &ESP32 targets
+            if (side_str == -1):
+                self._mpu_i2c = I2C(side_str, scl=Pin(dev_pin[0], mode=Pin.OUT), sda=Pin(dev_pin[1], mode=Pin.IN))   # ESP32
+            else:
+                self._mpu_i2c = I2C(side_str)       # WiPy
+        elif hasattr(side_str, 'readfrom'):     # Soft or hard I2C instance. See issue #3097
+            self._mpu_i2c = side_str
+        else:
+            raise ValueError("Invalid I2C instance")
+
+        if device_addr is None:
+            devices = set(self._mpu_i2c.scan())
+            mpus = devices.intersection(set(self._mpu_addr))
+            number_of_mpus = len(mpus)
+            if number_of_mpus == 0:
+                raise MPUException("No MPU's detected")
+            elif number_of_mpus == 1:
+                self.mpu_addr = mpus.pop()
+            else:
+                raise ValueError("Two MPU's detected: must specify a device address")
+        else:
+            if device_addr not in (0, 1):
+                raise ValueError('Device address must be 0 or 1')
+            self.mpu_addr = self._mpu_addr[device_addr]
+
+        self.chip_id                            # Test communication by reading chip_id: throws exception on error
+                                                # Can communicate with chip. Set it up.
+        self.wake()                             # wake it up
+        self.passthrough = True                 # Enable mag access from main I2C bus
+        self.accel_range = 0                    # default to highest sensitivity
+        self.gyro_range = 0                     # Likewise for gyro
+
+    # read from device
+    def _read(self, buf, memaddr, addr):        # addr = I2C device address, memaddr = memory location within the I2C device
+        '''
+        Read bytes to pre-allocated buffer Caller traps OSError.
+        '''
+        self._mpu_i2c.readfrom_mem_into(addr, memaddr, buf)
+
+    # write to device
+    def _write(self, data, memaddr, addr):
+        '''
+        Perform a memory write. Caller should trap OSError.
+        '''
+        self.buf1[0] = data
+        self._mpu_i2c.writeto_mem(addr, memaddr, self.buf1)
+
+    # wake
+    def wake(self):
+        '''
+        Wakes the device.
+        '''
+        try:
+            self._write(0x01, 0x6B, self.mpu_addr)  # Use best clock source
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return 'awake'
+
+    # mode
+    def sleep(self):
+        '''
+        Sets the device to sleep mode.
+        '''
+        try:
+            self._write(0x40, 0x6B, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return 'asleep'
+
+    # chip_id
+    @property
+    def chip_id(self):
+        '''
+        Returns Chip ID
+        '''
+        try:
+            self._read(self.buf1, 0x75, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        chip_id = int(self.buf1[0])
+        if chip_id != self._chip_id:
+            raise ValueError('Bad chip ID ({0}!={1}) retrieved: MPU communication failure'.format(chip_id, self._chip_id))
+        return chip_id
+
+    @property
+    def sensors(self):
+        '''
+        returns sensor objects accel, gyro
+        '''
+        return self._accel, self._gyro
+
+    # get temperature
+    @property
+    def temperature(self):
+        '''
+        Returns the temperature in degree C.
+        '''
+        try:
+            self._read(self.buf2, 0x41, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return bytes_toint(self.buf2[0], self.buf2[1])/340 + 35  # I think
+
+    # passthrough
+    @property
+    def passthrough(self):
+        '''
+        Returns passthrough mode True or False
+        '''
+        try:
+            self._read(self.buf1, 0x37, self.mpu_addr)
+            return self.buf1[0] & 0x02 > 0
+        except OSError:
+            raise MPUException(self._I2Cerror)
+
+    @passthrough.setter
+    def passthrough(self, mode):
+        '''
+        Sets passthrough mode True or False
+        '''
+        if type(mode) is bool:
+            val = 2 if mode else 0
+            try:
+                self._write(val, 0x37, self.mpu_addr)  # I think this is right.
+                self._write(0x00, 0x6A, self.mpu_addr)
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('pass either True or False')
+
+    # sample rate. Not sure why you'd ever want to reduce this from the default.
+    @property
+    def sample_rate(self):
+        '''
+        Get sample rate as per Register Map document section 4.4
+        SAMPLE_RATE= Internal_Sample_Rate / (1 + rate)
+        default rate is zero i.e. sample at internal rate.
+        '''
+        try:
+            self._read(self.buf1, 0x19, self.mpu_addr)
+            return self.buf1[0]
+        except OSError:
+            raise MPUException(self._I2Cerror)
+
+    @sample_rate.setter
+    def sample_rate(self, rate):
+        '''
+        Set sample rate as per Register Map document section 4.4
+        '''
+        if rate < 0 or rate > 255:
+            raise ValueError("Rate must be in range 0-255")
+        try:
+            self._write(rate, 0x19, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+
+    # Low pass filters. Using the filter_range property of the MPU9250 is
+    # harmless but gyro_filter_range is preferred and offers an extra setting.
+    @property
+    def filter_range(self):
+        '''
+        Returns the gyro and temperature sensor low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6
+        Cutoff (Hz):        250 184 92  41  20  10  5
+        Sample rate (KHz):  8   1   1   1   1   1   1
+        '''
+        try:
+            self._read(self.buf1, 0x1A, self.mpu_addr)
+            res = self.buf1[0] & 7
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return res
+
+    @filter_range.setter
+    def filter_range(self, filt):
+        '''
+        Sets the gyro and temperature sensor low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6
+        Cutoff (Hz):        250 184 92  41  20  10  5
+        Sample rate (KHz):  8   1   1   1   1   1   1
+        '''
+        # set range
+        if filt in range(7):
+            try:
+                self._write(filt, 0x1A, self.mpu_addr)
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('Filter coefficient must be between 0 and 6')
+
+    # accelerometer range
+    @property
+    def accel_range(self):
+        '''
+        Accelerometer range
+        Value:              0   1   2   3
+        for range +/-:      2   4   8   16  g
+        '''
+        try:
+            self._read(self.buf1, 0x1C, self.mpu_addr)
+            ari = self.buf1[0]//8
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return ari
+
+    @accel_range.setter
+    def accel_range(self, accel_range):
+        '''
+        Set accelerometer range
+        Pass:               0   1   2   3
+        for range +/-:      2   4   8   16  g
+        '''
+        ar_bytes = (0x00, 0x08, 0x10, 0x18)
+        if accel_range in range(len(ar_bytes)):
+            try:
+                self._write(ar_bytes[accel_range], 0x1C, self.mpu_addr)
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('accel_range can only be 0, 1, 2 or 3')
+
+    # gyroscope range
+    @property
+    def gyro_range(self):
+        '''
+        Gyroscope range
+        Value:              0   1   2    3
+        for range +/-:      250 500 1000 2000  degrees/second
+        '''
+        # set range
+        try:
+            self._read(self.buf1, 0x1B, self.mpu_addr)
+            gri = self.buf1[0]//8
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return gri
+
+    @gyro_range.setter
+    def gyro_range(self, gyro_range):
+        '''
+        Set gyroscope range
+        Pass:               0   1   2    3
+        for range +/-:      250 500 1000 2000  degrees/second
+        '''
+        gr_bytes = (0x00, 0x08, 0x10, 0x18)
+        if gyro_range in range(len(gr_bytes)):
+            try:
+                self._write(gr_bytes[gyro_range], 0x1B, self.mpu_addr)  # Sets fchoice = b11 which enables filter
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('gyro_range can only be 0, 1, 2 or 3')
+
+    # Accelerometer
+    @property
+    def accel(self):
+        '''
+        Acceleremoter object
+        '''
+        return self._accel
+
+    def _accel_callback(self):
+        '''
+        Update accelerometer Vector3d object
+        '''
+        try:
+            self._read(self.buf6, 0x3B, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        self._accel._ivector[0] = bytes_toint(self.buf6[0], self.buf6[1])
+        self._accel._ivector[1] = bytes_toint(self.buf6[2], self.buf6[3])
+        self._accel._ivector[2] = bytes_toint(self.buf6[4], self.buf6[5])
+        scale = (16384, 8192, 4096, 2048)
+        self._accel._vector[0] = self._accel._ivector[0]/scale[self.accel_range]
+        self._accel._vector[1] = self._accel._ivector[1]/scale[self.accel_range]
+        self._accel._vector[2] = self._accel._ivector[2]/scale[self.accel_range]
+
+    def get_accel_irq(self):
+        '''
+        For use in interrupt handlers. Sets self._accel._ivector[] to signed
+        unscaled integer accelerometer values
+        '''
+        self._read(self.buf6, 0x3B, self.mpu_addr)
+        self._accel._ivector[0] = bytes_toint(self.buf6[0], self.buf6[1])
+        self._accel._ivector[1] = bytes_toint(self.buf6[2], self.buf6[3])
+        self._accel._ivector[2] = bytes_toint(self.buf6[4], self.buf6[5])
+
+    # Gyro
+    @property
+    def gyro(self):
+        '''
+        Gyroscope object
+        '''
+        return self._gyro
+
+    def _gyro_callback(self):
+        '''
+        Update gyroscope Vector3d object
+        '''
+        try:
+            self._read(self.buf6, 0x43, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        self._gyro._ivector[0] = bytes_toint(self.buf6[0], self.buf6[1])
+        self._gyro._ivector[1] = bytes_toint(self.buf6[2], self.buf6[3])
+        self._gyro._ivector[2] = bytes_toint(self.buf6[4], self.buf6[5])
+        scale = (131, 65.5, 32.8, 16.4)
+        self._gyro._vector[0] = self._gyro._ivector[0]/scale[self.gyro_range]
+        self._gyro._vector[1] = self._gyro._ivector[1]/scale[self.gyro_range]
+        self._gyro._vector[2] = self._gyro._ivector[2]/scale[self.gyro_range]
+
+    def get_gyro_irq(self):
+        '''
+        For use in interrupt handlers. Sets self._gyro._ivector[] to signed
+        unscaled integer gyro values. Error trapping disallowed.
+        '''
+        self._read(self.buf6, 0x43, self.mpu_addr)
+        self._gyro._ivector[0] = bytes_toint(self.buf6[0], self.buf6[1])
+        self._gyro._ivector[1] = bytes_toint(self.buf6[2], self.buf6[3])
+        self._gyro._ivector[2] = bytes_toint(self.buf6[4], self.buf6[5])
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/mpu9250.py b/GW-custom/uPyLoRaWAN/uPySensors/mpu9250.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ad8c29e8af42f25fb986b8e5e8ac1bbed46ac58
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/mpu9250.py
@@ -0,0 +1,218 @@
+# mpu9250.py MicroPython driver for the InvenSense MPU9250 inertial measurement unit
+# Authors Peter Hinch, Sebastian Plamauer
+# V0.5 17th June 2015
+
+'''
+mpu9250 is a micropython module for the InvenSense MPU9250 sensor.
+It measures acceleration, turn rate and the magnetic field in three axis.
+
+The MIT License (MIT)
+Copyright (c) 2014 Sebastian Plamauer, oeplse@gmail.com, Peter Hinch
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+'''
+
+from .imu import MPU6050, bytes_toint, MPUException
+from .vector3d import Vector3d
+
+
+class MPU9250(MPU6050):
+    '''
+    MPU9250 constructor arguments
+    1. side_str 'X' or 'Y' depending on the Pyboard I2C interface being used
+    2. optional device_addr 0, 1 depending on the voltage applied to pin AD0 (Drotek default is 1)
+       if None driver will scan for a device (if one device only is on bus)
+    3, 4. transposition, scaling optional 3-tuples allowing for outputs to be based on vehicle
+          coordinates rather than those of the sensor itself. See readme.
+    '''
+
+    _mag_addr = 12          # Magnetometer address
+    _chip_id = 113
+
+    def __init__(self, side_str, dev_pin=(15, 4), device_addr=None, transposition=(0, 1, 2), scaling=(1, 1, 1)):
+
+        super().__init__(side_str, dev_pin, device_addr, transposition, scaling)
+        self._mag = Vector3d(transposition, scaling, self._mag_callback)
+        self.accel_filter_range = 0             # fast filtered response
+        self.gyro_filter_range = 0
+        self._mag_stale_count = 0               # MPU9250 count of consecutive reads where old data was returned
+        self.mag_correction = self._magsetup()  # 16 bit, 100Hz update.Return correction factors.
+        self._mag_callback()  # Seems neccessary to kick the mag off else 1st reading is zero (?)
+
+    def enable_irq_mode(self, level=0x22, freq=0x03):
+        self.filter_range = 1       # set accel lpf to 184Hz
+        self._write(0x40, 0x38, self.mpu_addr)     # enable motion interrupt
+        self._write(0xC0, 0x69, self.mpu_addr)     # enable accel hardware intelligence
+        self._write(level, 0x1F, self.mpu_addr)    # motion threshold: 1~255 LSBs (0~1020mg)
+        self._write(freq, 0x1E, self.mpu_addr)     # motion frequency: [3:0] = 0.24Hz ~ 500Hz
+        self._write(0x21, 0x6B, self.mpu_addr)     # enable cycle mode (Accel Low Power Mode)
+
+    def disable_irq_mode(self):
+        self._write(0x00, 0x69, self.mpu_addr)     # disable accel hardware intelligence
+        self._write(0x01, 0x6B, self.mpu_addr)     # enable sensor
+
+
+    @property
+    def sensors(self):
+        '''
+        returns sensor objects accel, gyro and mag
+        '''
+        return self._accel, self._gyro, self._mag
+
+    # get temperature
+    @property
+    def temperature(self):
+        '''
+        Returns the temperature in degree C.
+        '''
+        try:
+            self._read(self.buf2, 0x41, self.mpu_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return bytes_toint(self.buf2[0], self.buf2[1])/333.87 + 21  # I think
+
+    # Low pass filters
+    @property
+    def gyro_filter_range(self):
+        '''
+        Returns the gyro and temperature sensor low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6   7
+        Cutoff (Hz):        250 184 92  41  20  10  5   3600
+        Sample rate (KHz):  8   1   1   1   1   1   1   8
+        '''
+        try:
+            self._read(self.buf1, 0x1A, self.mpu_addr)
+            res = self.buf1[0] & 7
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return res
+
+    @gyro_filter_range.setter
+    def gyro_filter_range(self, filt):
+        '''
+        Sets the gyro and temperature sensor low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6   7
+        Cutoff (Hz):        250 184 92  41  20  10  5   3600
+        Sample rate (KHz):  8   1   1   1   1   1   1   8
+        '''
+        if filt in range(8):
+            try:
+                self._write(filt, 0x1A, self.mpu_addr)
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('Filter coefficient must be between 0 and 7')
+
+    @property
+    def accel_filter_range(self):
+        '''
+        Returns the accel low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6   7 BEWARE 7 doesn't work on device I tried.
+        Cutoff (Hz):        460 184 92  41  20  10  5   460
+        Sample rate (KHz):  1   1   1   1   1   1   1   1
+        '''
+        try:
+            self._read(self.buf1, 0x1D, self.mpu_addr)
+            res = self.buf1[0] & 7
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        return res
+
+    @accel_filter_range.setter
+    def accel_filter_range(self, filt):
+        '''
+        Sets the accel low pass filter cutoff frequency
+        Pass:               0   1   2   3   4   5   6   7 BEWARE 7 doesn't work on device I tried.
+        Cutoff (Hz):        460 184 92  41  20  10  5   460
+        Sample rate (KHz):  1   1   1   1   1   1   1   1
+        '''
+        if filt in range(8):
+            try:
+                self._write(filt, 0x1D, self.mpu_addr)
+            except OSError:
+                raise MPUException(self._I2Cerror)
+        else:
+            raise ValueError('Filter coefficient must be between 0 and 7')
+
+    def _magsetup(self):                        # mode 2 100Hz continuous reads, 16 bit
+        '''
+        Magnetometer initialisation: use 16 bit continuous mode.
+        Mode 1 is 8Hz mode 2 is 100Hz repetition
+        returns correction values
+        '''
+        try:
+            self._write(0x0F, 0x0A, self._mag_addr)      # fuse ROM access mode
+            self._read(self.buf3, 0x10, self._mag_addr)  # Correction values
+            self._write(0, 0x0A, self._mag_addr)         # Power down mode (AK8963 manual 6.4.6)
+            self._write(0x16, 0x0A, self._mag_addr)      # 16 bit (0.15uT/LSB not 0.015), mode 2
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        mag_x = (0.5*(self.buf3[0] - 128))/128 + 1
+        mag_y = (0.5*(self.buf3[1] - 128))/128 + 1
+        mag_z = (0.5*(self.buf3[2] - 128))/128 + 1
+        return (mag_x, mag_y, mag_z)
+
+    @property
+    def mag(self):
+        '''
+        Magnetomerte object
+        '''
+        return self._mag
+
+    def _mag_callback(self):
+        '''
+        Update magnetometer Vector3d object (if data available)
+        '''
+        try:                                    # If read fails, returns last valid data and
+            self._read(self.buf1, 0x02, self._mag_addr)  # increments mag_stale_count
+            if self.buf1[0] & 1 == 0:
+                return self._mag                # Data not ready: return last value
+            self._read(self.buf6, 0x03, self._mag_addr)
+            self._read(self.buf1, 0x09, self._mag_addr)
+        except OSError:
+            raise MPUException(self._I2Cerror)
+        if self.buf1[0] & 0x08 > 0:             # An overflow has occurred
+            self._mag_stale_count += 1          # Error conditions retain last good value
+            return                              # user should check for ever increasing stale_counts
+        self._mag._ivector[1] = bytes_toint(self.buf6[1], self.buf6[0])  # Note axis twiddling and little endian
+        self._mag._ivector[0] = bytes_toint(self.buf6[3], self.buf6[2])
+        self._mag._ivector[2] = -bytes_toint(self.buf6[5], self.buf6[4])
+        scale = 0.15                            # scale is 0.15uT/LSB
+        self._mag._vector[0] = self._mag._ivector[0]*self.mag_correction[0]*scale
+        self._mag._vector[1] = self._mag._ivector[1]*self.mag_correction[1]*scale
+        self._mag._vector[2] = self._mag._ivector[2]*self.mag_correction[2]*scale
+        self._mag_stale_count = 0
+
+    @property
+    def mag_stale_count(self):
+        '''
+        Number of consecutive times where old data was returned
+        '''
+        return self._mag_stale_count
+
+    def get_mag_irq(self):
+        '''
+        Uncorrected values because floating point uses heap
+        '''
+        self._read(self.buf1, 0x02, self._mag_addr)
+        if self.buf1[0] == 1:                   # Data is ready
+            self._read(self.buf6, 0x03, self._mag_addr)
+            self._read(self.buf1, 0x09, self._mag_addr)    # Mandatory status2 read
+            self._mag._ivector[1] = 0
+            if self.buf1[0] & 0x08 == 0:        # No overflow has occurred
+                self._mag._ivector[1] = bytes_toint(self.buf6[1], self.buf6[0])
+                self._mag._ivector[0] = bytes_toint(self.buf6[3], self.buf6[2])
+                self._mag._ivector[2] = -bytes_toint(self.buf6[5], self.buf6[4])
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/pmsa003.py b/GW-custom/uPyLoRaWAN/uPySensors/pmsa003.py
new file mode 100644
index 0000000000000000000000000000000000000000..0972a7fe620c0d5dc90592f6e53fa836b9876ac4
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/pmsa003.py
@@ -0,0 +1,99 @@
+
+import machine
+import utime
+
+epoch_offset = 946684800
+
+class PMSA003:
+    def __init__(self, uart, pins):
+        
+        self._uart = uart
+        self._pins = pins
+        
+        self._set = machine.Pin(self._pins["set"], machine.Pin.OUT, value=0)
+        self._rst = machine.Pin(self._pins["rst"], machine.Pin.OUT, value=0)
+        self._uart.init(tx=self._pins["tx"], rx=self._pins["rx"])
+        self.power_off()
+
+    def wake_up(self):
+        self._set(True)
+        self._rst(True)
+        # sleep for 7 seconds to initialize the sensor properly
+        self._set_normal()
+        utime.sleep_ms(7000)
+        # warning init
+        for idx in range(10):
+            data = self.measurements
+            utime.sleep_ms(500)
+
+    def _set_idle(self):
+        idelcmd = b'\x42\x4d\xe4\x00\x00\x01\x73'
+        ary = bytearray(idelcmd)
+        self._uart.write(ary)
+
+    def _set_normal(self):
+        normalcmd = b'\x42\x4d\xe4\x00\x01\x01\x74'
+        ary = bytearray(normalcmd)
+        self._uart.write(ary)
+
+    def power_off(self):
+        self._set_idle()
+        self._set(False)
+        self._rst(False)
+
+    def reset(self):
+        self._rst(False)
+        utime.sleep_ms(2000)
+        self._rst(True)
+
+    @property
+    def measurements(self):
+        # flush the buffer to read fresh data
+        ret_data = None
+        self._wait_for_data(32)
+
+        while self._uart.read(1) != b'\x42':
+            machine.idle()
+
+        if self._uart.read(1) == b'\x4D':
+            self._wait_for_data(30)
+            try:
+                self._data = self._uart.read(30)
+                if self._data:
+                    ret_data = self._PMdata()
+            except ValueError as e:
+                print('error reading frame: {}'.format(e.message))
+                pass
+                
+        return ret_data
+
+    def _wait_for_data(self, byte_count):
+        u = self._uart.any()
+        while u < byte_count:
+            u = self._uart.any()
+            # 32*8*1000/9600 (32 bytes @9600kbps)
+            # but let's assume byte is 10 bits to skip complex math
+            utime.sleep_ms(10)
+
+    def _PMdata(self):
+        d = {}
+        check = False
+        # check data
+        control_sum = 0x42 + 0x4d
+        for b in range(len(self._data)-2):
+            control_sum += self._data[b]
+
+        control_sum_data = self._data[28] * 256 + self._data[29]
+        print()
+        if control_sum == control_sum_data:
+            check = True
+
+        d['time'] = utime.time() + epoch_offset
+        d['cpm10'] = self._data[4] * 256 + self._data[5]
+        d['cpm25'] = self._data[6] * 256 + self._data[7]
+        d['cpm100'] = self._data[8] * 256 + self._data[9]
+        d['apm10'] = self._data[10] * 256 + self._data[11]
+        d['apm25'] = self._data[12] * 256 + self._data[13]
+        d['apm100'] = self._data[14] * 256 + self._data[15]
+
+        return [check, d]
\ No newline at end of file
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/sdcard.py b/GW-custom/uPyLoRaWAN/uPySensors/sdcard.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0786e8af745e5886628dc0853cc3ea4455fe0be
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/sdcard.py
@@ -0,0 +1,279 @@
+"""
+MicroPython driver for SD cards using SPI bus.
+
+Requires an SPI bus and a CS pin.  Provides readblocks and writeblocks
+methods so the device can be mounted as a filesystem.
+
+Example usage on pyboard:
+
+    import pyb, sdcard, os
+    sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
+    pyb.mount(sd, '/sd2')
+    os.listdir('/')
+
+Example usage on ESP8266:
+
+    import machine, sdcard, os
+    sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15))
+    os.mount(sd, '/sd')
+    os.listdir('/')
+
+"""
+
+from micropython import const
+from machine import Pin
+import time
+
+
+_CMD_TIMEOUT = const(100)
+
+_R1_IDLE_STATE = const(1 << 0)
+#R1_ERASE_RESET = const(1 << 1)
+_R1_ILLEGAL_COMMAND = const(1 << 2)
+#R1_COM_CRC_ERROR = const(1 << 3)
+#R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
+#R1_ADDRESS_ERROR = const(1 << 5)
+#R1_PARAMETER_ERROR = const(1 << 6)
+_TOKEN_CMD25 = const(0xfc)
+_TOKEN_STOP_TRAN = const(0xfd)
+_TOKEN_DATA = const(0xfe)
+
+
+class SDCard:
+    def __init__(self, spi, cs):
+        self.spi = spi
+        self.cs = cs
+
+        self.cmdbuf = bytearray(6)
+        self.dummybuf = bytearray(512)
+        self.tokenbuf = bytearray(1)
+        for i in range(512):
+            self.dummybuf[i] = 0xff
+        self.dummybuf_memoryview = memoryview(self.dummybuf)
+
+        # initialise the card
+        self.init_card()
+
+    def init_spi(self, baudrate):
+        try:
+            master = self.spi.MASTER
+        except AttributeError:
+            # on ESP8266
+            self.spi.init(baudrate=baudrate, phase=0, polarity=0)
+        else:
+            # on pyboard
+            self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)
+
+    def init_card(self):
+        # init CS pin
+        self.cs.init(self.cs.OUT, value=1)
+
+        # init SPI bus; use low data rate for initialisation
+        self.init_spi(10000000)
+
+        # clock card at least 100 cycles with cs high
+        for i in range(16):
+            self.spi.write(b'\xff')
+
+        # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
+        for _ in range(5):
+            if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
+                break
+        else:
+            raise OSError("no SD card")
+
+        # CMD8: determine card version
+        r = self.cmd(8, 0x01aa, 0x87, 4)
+        if r == _R1_IDLE_STATE:
+            self.init_card_v2()
+        elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
+            self.init_card_v1()
+        else:
+            raise OSError("couldn't determine SD card version")
+
+        # get the number of sectors
+        # CMD9: response R2 (R1 byte + 16-byte block read)
+        if self.cmd(9, 0, 0, 0, False) != 0:
+            raise OSError("no response from SD card")
+        csd = bytearray(16)
+        self.readinto(csd)
+        if csd[0] & 0xc0 == 0x40: # CSD version 2.0
+            self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024
+        elif csd[0] & 0xc0 == 0x00: # CSD version 1.0 (old, <=2GB)
+            c_size = csd[6] & 0b11 | csd[7] << 2 | (csd[8] & 0b11000000) << 4
+            c_size_mult = ((csd[9] & 0b11) << 1) | csd[10] >> 7
+            self.sectors = (c_size + 1) * (2 ** (c_size_mult + 2))
+        else:
+            raise OSError("SD card CSD format not supported")
+        #print('sectors', self.sectors)
+
+        # CMD16: set block length to 512 bytes
+        if self.cmd(16, 512, 0) != 0:
+            raise OSError("can't set 512 block size")
+
+        # set to high data rate now that it's initialised
+        self.init_spi(1320000)
+
+    def init_card_v1(self):
+        for i in range(_CMD_TIMEOUT):
+            self.cmd(55, 0, 0)
+            if self.cmd(41, 0, 0) == 0:
+                self.cdv = 512
+                #print("[SDCard] v1 card")
+                return
+        raise OSError("timeout waiting for v1 card")
+
+    def init_card_v2(self):
+        for i in range(_CMD_TIMEOUT):
+            time.sleep_ms(50)
+            self.cmd(58, 0, 0, 4)
+            self.cmd(55, 0, 0)
+            if self.cmd(41, 0x40000000, 0) == 0:
+                self.cmd(58, 0, 0, 4)
+                self.cdv = 1
+                #print("[SDCard] v2 card")
+                return
+        raise OSError("timeout waiting for v2 card")
+
+    def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False):
+        self.cs(0)
+
+        # create and send the command
+        buf = self.cmdbuf
+        buf[0] = 0x40 | cmd
+        buf[1] = arg >> 24
+        buf[2] = arg >> 16
+        buf[3] = arg >> 8
+        buf[4] = arg
+        buf[5] = crc
+        self.spi.write(buf)
+
+        if skip1:
+            self.spi.readinto(self.tokenbuf, 0xff)
+
+        # wait for the response (response[7] == 0)
+        for i in range(_CMD_TIMEOUT):
+            self.spi.readinto(self.tokenbuf, 0xff)
+            response = self.tokenbuf[0]
+            if not (response & 0x80):
+                # this could be a big-endian integer that we are getting here
+                for j in range(final):
+                    self.spi.write(b'\xff')
+                if release:
+                    self.cs(1)
+                    self.spi.write(b'\xff')
+                return response
+
+        # timeout
+        self.cs(1)
+        self.spi.write(b'\xff')
+        return -1
+
+    def readinto(self, buf):
+        self.cs(0)
+
+        # read until start byte (0xff)
+        while True:
+            self.spi.readinto(self.tokenbuf, 0xff)
+            if self.tokenbuf[0] == _TOKEN_DATA:
+                break
+
+        # read data
+        mv = self.dummybuf_memoryview
+        if len(buf) != len(mv):
+            mv = mv[:len(buf)]
+        self.spi.write_readinto(mv, buf)
+
+        # read checksum
+        self.spi.write(b'\xff')
+        self.spi.write(b'\xff')
+
+        self.cs(1)
+        self.spi.write(b'\xff')
+
+    def write(self, token, buf):
+        self.cs(0)
+
+        # send: start of block, data, checksum
+        self.spi.read(1, token)
+        self.spi.write(buf)
+        self.spi.write(b'\xff')
+        self.spi.write(b'\xff')
+
+        # check the response
+        if (self.spi.read(1, 0xff)[0] & 0x1f) != 0x05:
+            self.cs(1)
+            self.spi.write(b'\xff')
+            return
+
+        # wait for write to finish
+        while self.spi.read(1, 0xff)[0] == 0:
+            pass
+
+        self.cs(1)
+        self.spi.write(b'\xff')
+
+    def write_token(self, token):
+        self.cs(0)
+        self.spi.read(1, token)
+        self.spi.write(b'\xff')
+        # wait for write to finish
+        while self.spi.read(1, 0xff)[0] == 0x00:
+            pass
+
+        self.cs(1)
+        self.spi.write(b'\xff')
+
+    def readblocks(self, block_num, buf):
+        nblocks = len(buf) // 512
+        assert nblocks and not len(buf) % 512, 'Buffer length is invalid'
+        if nblocks == 1:
+            # CMD17: set read address for single block
+            if self.cmd(17, block_num * self.cdv, 0, release=False) != 0:
+                # release the card
+                self.cs(1)
+                raise OSError(5) # EIO
+            # receive the data and release card
+            self.readinto(buf)
+        else:
+            # CMD18: set read address for multiple blocks
+            if self.cmd(18, block_num * self.cdv, 0, release=False) != 0:
+                # release the card
+                self.cs(1)
+                raise OSError(5) # EIO
+            offset = 0
+            mv = memoryview(buf)
+            while nblocks:
+                # receive the data and release card
+                self.readinto(mv[offset : offset + 512])
+                offset += 512
+                nblocks -= 1
+            if self.cmd(12, 0, 0xff, skip1=True):
+                raise OSError(5) # EIO
+
+    def writeblocks(self, block_num, buf):
+        nblocks, err = divmod(len(buf), 512)
+        assert nblocks and not err, 'Buffer length is invalid'
+        if nblocks == 1:
+            # CMD24: set write address for single block
+            if self.cmd(24, block_num * self.cdv, 0) != 0:
+                raise OSError(5) # EIO
+
+            # send the data
+            self.write(_TOKEN_DATA, buf)
+        else:
+            # CMD25: set write address for first block
+            if self.cmd(25, block_num * self.cdv, 0) != 0:
+                raise OSError(5) # EIO
+            # send the data
+            offset = 0
+            mv = memoryview(buf)
+            while nblocks:
+                self.write(_TOKEN_CMD25, mv[offset : offset + 512])
+                offset += 512
+                nblocks -= 1
+            self.write_token(_TOKEN_STOP_TRAN)
+
+    def ioctl(self, op, arg):
+        if op == 4: # get number of blocks
+            return self.sectors
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/ssd1306.py b/GW-custom/uPyLoRaWAN/uPySensors/ssd1306.py
new file mode 100644
index 0000000000000000000000000000000000000000..a666a2d144841cf6a15a0edf942afb9835924849
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/ssd1306.py
@@ -0,0 +1,162 @@
+# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
+from micropython import const
+import time
+import framebuf
+
+
+# register definitions
+SET_CONTRAST        = const(0x81)
+SET_ENTIRE_ON       = const(0xa4)
+SET_NORM_INV        = const(0xa6)
+SET_DISP            = const(0xae)
+SET_MEM_ADDR        = const(0x20)
+SET_COL_ADDR        = const(0x21)
+SET_PAGE_ADDR       = const(0x22)
+SET_DISP_START_LINE = const(0x40)
+SET_SEG_REMAP       = const(0xa0)
+SET_MUX_RATIO       = const(0xa8)
+SET_COM_OUT_DIR     = const(0xc0)
+SET_DISP_OFFSET     = const(0xd3)
+SET_COM_PIN_CFG     = const(0xda)
+SET_DISP_CLK_DIV    = const(0xd5)
+SET_PRECHARGE       = const(0xd9)
+SET_VCOM_DESEL      = const(0xdb)
+SET_CHARGE_PUMP     = const(0x8d)
+
+
+class SSD1306:
+    def __init__(self, width, height, external_vcc):
+        self.width = width
+        self.height = height
+        self.external_vcc = external_vcc
+        self.pages = self.height // 8
+        self.buffer = bytearray(self.pages * self.width)
+        fb = framebuf.FrameBuffer(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
+        self.framebuf = fb
+        # Provide methods for accessing FrameBuffer graphics primitives. This is a
+        # workround because inheritance from a native class is currently unsupported.
+        # http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
+        self.fill = fb.fill
+        self.pixel = fb.pixel
+        self.hline = fb.hline
+        self.vline = fb.vline
+        self.line = fb.line
+        self.rect = fb.rect
+        self.fill_rect = fb.fill_rect
+        self.text = fb.text
+        self.scroll = fb.scroll
+        self.blit = fb.blit
+        self.poweron()
+        self.init_display()
+
+    def init_display(self):
+        for cmd in (
+            SET_DISP | 0x00, # off
+            # address setting
+            SET_MEM_ADDR, 0x00, # horizontal
+            # resolution and layout
+            SET_DISP_START_LINE | 0x00,
+            SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
+            SET_MUX_RATIO, self.height - 1,
+            SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
+            SET_DISP_OFFSET, 0x00,
+            SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
+            # timing and driving scheme
+            SET_DISP_CLK_DIV, 0x80,
+            SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
+            SET_VCOM_DESEL, 0x30, # 0.83*Vcc
+            # display
+            SET_CONTRAST, 0xff, # maximum
+            SET_ENTIRE_ON, # output follows RAM contents
+            SET_NORM_INV, # not inverted
+            # charge pump
+            SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
+            SET_DISP | 0x01): # on
+            self.write_cmd(cmd)
+        self.fill(0)
+        self.show()
+
+    def poweroff(self):
+        self.write_cmd(SET_DISP | 0x00)
+
+    def contrast(self, contrast):
+        self.write_cmd(SET_CONTRAST)
+        self.write_cmd(contrast)
+
+    def invert(self, invert):
+        self.write_cmd(SET_NORM_INV | (invert & 1))
+
+    def show(self):
+        x0 = 0
+        x1 = self.width - 1
+        if self.width == 64:
+            # displays with width of 64 pixels are shifted by 32
+            x0 += 32
+            x1 += 32
+        self.write_cmd(SET_COL_ADDR)
+        self.write_cmd(x0)
+        self.write_cmd(x1)
+        self.write_cmd(SET_PAGE_ADDR)
+        self.write_cmd(0)
+        self.write_cmd(self.pages - 1)
+        self.write_data(self.buffer)
+
+
+class SSD1306_I2C(SSD1306):
+    def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
+        self.i2c = i2c
+        self.addr = addr
+        self.temp = bytearray(2)
+        super().__init__(width, height, external_vcc)
+
+    def write_cmd(self, cmd):
+        self.temp[0] = 0x80 # Co=1, D/C#=0
+        self.temp[1] = cmd
+        self.i2c.writeto(self.addr, self.temp)
+
+    def write_data(self, buf):
+        self.temp[0] = self.addr << 1
+        self.temp[1] = 0x40 # Co=0, D/C#=1
+        self.i2c.start()
+        self.i2c.write(self.temp)
+        self.i2c.write(buf)
+        self.i2c.stop()
+
+    def poweron(self):
+        pass
+
+
+class SSD1306_SPI(SSD1306):
+    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
+        self.rate = 10 * 1024 * 1024
+        dc.init(dc.OUT, value=0)
+        res.init(res.OUT, value=0)
+        cs.init(cs.OUT, value=1)
+        self.spi = spi
+        self.dc = dc
+        self.res = res
+        self.cs = cs
+        super().__init__(width, height, external_vcc)
+
+    def write_cmd(self, cmd):
+        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
+        self.cs(1)
+        self.dc(0)
+        self.cs(0)
+        self.spi.write(bytearray([cmd]))
+        self.cs(1)
+
+    def write_data(self, buf):
+        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
+        self.cs(1)
+        self.dc(1)
+        self.cs(0)
+        self.spi.write(buf)
+        self.cs(1)
+
+    def poweron(self):
+        self.res(1)
+        time.sleep_ms(1)
+        self.res(0)
+        time.sleep_ms(10)
+        self.res(1)
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/ssd1306_i2c.py b/GW-custom/uPyLoRaWAN/uPySensors/ssd1306_i2c.py
new file mode 100644
index 0000000000000000000000000000000000000000..26bae2c7da93fbf52bb68bf2455defcbe7f92310
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/ssd1306_i2c.py
@@ -0,0 +1,82 @@
+# https://learn.adafruit.com/micropython-hardware-ssd1306-oled-display/software
+import time
+from machine import I2C, Pin
+from .ssd1306 import SSD1306_I2C
+
+class Display:
+
+    def __init__(self,
+                 width = 128, height = 64,
+                 scl_pin_id = 15, sda_pin_id = 4,
+                 freq = 400000):
+
+        self.width = width
+        self.height = height
+        self.poweron()
+        self.i2c = I2C(scl = Pin(scl_pin_id, Pin.OUT),
+                               sda = Pin(sda_pin_id),
+                               freq = freq)
+        self.display = SSD1306_I2C(width, height, self.i2c)
+        self.show = self.display.show
+
+    def poweron(self, pin=16):
+        pin_reset = Pin(pin, mode=Pin.OUT)
+        pin_reset.value(0)
+        time.sleep_ms(50)
+        pin_reset.value(1)
+
+    def poweroff(self, pin=16):
+        pin_reset = Pin(pin, mode=Pin.OUT)
+        pin_reset.value(0)
+
+    def clear(self):
+        self.display.fill(0)
+        self.display.show()
+
+
+    def show_text(self, text, x = 0, y = 0, clear_first = True, show_now = True, hold_seconds = 0):
+        if clear_first: self.display.fill(0)
+        self.display.text(text, x, y)
+        if show_now:
+            self.display.show()
+            if hold_seconds > 0: time.sleep(hold_seconds)
+
+
+    def wrap(self, text, start_line = 0,
+             height_per_line = 8, width_per_char = 8,
+             start_pixel_each_line = 0):
+
+        chars_per_line = self.width//width_per_char
+        max_lines = self.height//height_per_line - start_line
+        lines = [(text[chars_per_line*line: chars_per_line*(line+1)], start_pixel_each_line, height_per_line*(line+start_line))
+                 for line in range(max_lines)]
+
+        return lines
+
+
+    def show_text_wrap(self, text,
+                       start_line = 0, height_per_line = 8, width_per_char = 8, start_pixel_each_line = 0,
+                       clear_first = True, show_now = True, hold_seconds = 0):
+
+        if clear_first: self.clear()
+
+        for line, x, y in self.wrap(text, start_line, height_per_line, width_per_char, start_pixel_each_line):
+            self.show_text(line, x, y, clear_first = False, show_now = False)
+
+        if show_now:
+            self.display.show()
+            if hold_seconds > 0: time.sleep(hold_seconds)
+
+
+    def show_datetime(self, year, month, day, hour, minute, second):
+        datetime = [year, month, day, hour, minute, second]
+        datetime_str = ["{0:0>2}".format(d) for d in datetime]
+
+        self.show_text(text = '-'.join(datetime_str[:3]),
+                        x = 0, y = 0, clear_first = True, show_now = False)
+        self.show_text(text = ':'.join(datetime_str[3:6]),
+                        x = 0, y = 10, clear_first = False, show_now = True)
+
+
+    def show_time(self, year, month, day, hour, minute, second):
+        self.show_datetime(year, month, day, hour, minute, second)
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/stepper.py b/GW-custom/uPyLoRaWAN/uPySensors/stepper.py
new file mode 100644
index 0000000000000000000000000000000000000000..463272c0a86f08a9134b50d8437c8b91e30378e3
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/stepper.py
@@ -0,0 +1,94 @@
+"""
+Copyright 2020 LeMaRiva|Tech (Mauro Riva) info@lemariva.com
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import utime
+from machine import Pin
+
+class STEPPER:
+
+    def __init__(self, device_config):
+        self._In1 = Pin(device_config["In1"], Pin.OUT)
+        self._In2 = Pin(device_config["In2"], Pin.OUT)
+        self._In3 = Pin(device_config["In3"], Pin.OUT)
+        self._In4 = Pin(device_config["In4"], Pin.OUT)
+        self._number_of_steps = device_config["number_of_steps"] + 1
+        self._max_speed = 60 * 1000 * 1000 / self._number_of_steps / device_config["max_speed"]
+        self._step_number = 0
+        self._last_step_time = 0
+        self.set_speed(device_config["max_speed"]/2)
+
+    def set_speed(self, speed):
+        self._step_delay = 60 * 1000 * 1000 / self._number_of_steps / speed
+        if self._step_delay < self._max_speed:
+            self._step_delay = self._max_speed
+
+    def step_motor(self, step):
+        if step == 0:  #1010
+            self._In1.value(1)
+            self._In2.value(0)
+            self._In3.value(1)
+            self._In4.value(0)
+        elif step == 1: #0110
+            self._In1.value(0)
+            self._In2.value(1)
+            self._In3.value(1)
+            self._In4.value(0)
+        elif step == 2: #0101
+            self._In1.value(0)
+            self._In2.value(1)
+            self._In3.value(0)
+            self._In4.value(1)
+        elif step == 3: #1001
+            self._In1.value(1)
+            self._In2.value(0)
+            self._In3.value(0)
+            self._In4.value(1)
+
+    def release(self):
+        self._In1.value(0)
+        self._In2.value(0)
+        self._In3.value(0)
+        self._In4.value(0)
+
+    def step(self, steps_to_move, speed=None, hold=True):
+        if speed is not None:
+            self.set_speed(speed)
+
+        steps_left = abs(steps_to_move)
+        
+        if steps_to_move > 0:
+            direction = 1
+        else:
+            direction = 0
+
+        while steps_left > 0:
+            now = utime.ticks_us()
+            if now - self._last_step_time >= self._step_delay:
+                self._last_step_time = now
+                if direction == 1:
+                    self._step_number += 1
+
+                if direction == 0:
+                    if self._step_number == 0:
+                        self._step_number == steps_left
+
+                    self._step_number -= 1
+
+                self.step_motor(self._step_number % 4)
+                steps_left -= 1
+
+        if self._step_number == steps_left:
+            self._step_number = 0
+
+        if steps_left == 0 and not hold:
+            self.release()
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/sysfont.py b/GW-custom/uPyLoRaWAN/uPySensors/sysfont.py
new file mode 100644
index 0000000000000000000000000000000000000000..b1fa2d69b10862ea87e30c208e54d394aff7474a
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/sysfont.py
@@ -0,0 +1,265 @@
+
+#Font used for ST7735 display.
+
+#Each character uses 5 bytes.
+#index using ASCII value * 5.
+#Each byte contains a column of pixels.
+#The character may be 8 pixels high and 5 wide.
+
+sysfont = {"Width": 5, "Height": 8, "Start": 0, "End": 254, "Data": bytearray([
+  0x00, 0x00, 0x00, 0x00, 0x00,
+  0x3E, 0x5B, 0x4F, 0x5B, 0x3E,
+  0x3E, 0x6B, 0x4F, 0x6B, 0x3E,
+  0x1C, 0x3E, 0x7C, 0x3E, 0x1C,
+  0x18, 0x3C, 0x7E, 0x3C, 0x18,
+  0x1C, 0x57, 0x7D, 0x57, 0x1C,
+  0x1C, 0x5E, 0x7F, 0x5E, 0x1C,
+  0x00, 0x18, 0x3C, 0x18, 0x00,
+  0xFF, 0xE7, 0xC3, 0xE7, 0xFF,
+  0x00, 0x18, 0x24, 0x18, 0x00,
+  0xFF, 0xE7, 0xDB, 0xE7, 0xFF,
+  0x30, 0x48, 0x3A, 0x06, 0x0E,
+  0x26, 0x29, 0x79, 0x29, 0x26,
+  0x40, 0x7F, 0x05, 0x05, 0x07,
+  0x40, 0x7F, 0x05, 0x25, 0x3F,
+  0x5A, 0x3C, 0xE7, 0x3C, 0x5A,
+  0x7F, 0x3E, 0x1C, 0x1C, 0x08,
+  0x08, 0x1C, 0x1C, 0x3E, 0x7F,
+  0x14, 0x22, 0x7F, 0x22, 0x14,
+  0x5F, 0x5F, 0x00, 0x5F, 0x5F,
+  0x06, 0x09, 0x7F, 0x01, 0x7F,
+  0x00, 0x66, 0x89, 0x95, 0x6A,
+  0x60, 0x60, 0x60, 0x60, 0x60,
+  0x94, 0xA2, 0xFF, 0xA2, 0x94,
+  0x08, 0x04, 0x7E, 0x04, 0x08,
+  0x10, 0x20, 0x7E, 0x20, 0x10,
+  0x08, 0x08, 0x2A, 0x1C, 0x08,
+  0x08, 0x1C, 0x2A, 0x08, 0x08,
+  0x1E, 0x10, 0x10, 0x10, 0x10,
+  0x0C, 0x1E, 0x0C, 0x1E, 0x0C,
+  0x30, 0x38, 0x3E, 0x38, 0x30,
+  0x06, 0x0E, 0x3E, 0x0E, 0x06,
+  0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x5F, 0x00, 0x00,
+  0x00, 0x07, 0x00, 0x07, 0x00,
+  0x14, 0x7F, 0x14, 0x7F, 0x14,
+  0x24, 0x2A, 0x7F, 0x2A, 0x12,
+  0x23, 0x13, 0x08, 0x64, 0x62,
+  0x36, 0x49, 0x56, 0x20, 0x50,
+  0x00, 0x08, 0x07, 0x03, 0x00,
+  0x00, 0x1C, 0x22, 0x41, 0x00,
+  0x00, 0x41, 0x22, 0x1C, 0x00,
+  0x2A, 0x1C, 0x7F, 0x1C, 0x2A,
+  0x08, 0x08, 0x3E, 0x08, 0x08,
+  0x00, 0x80, 0x70, 0x30, 0x00,
+  0x08, 0x08, 0x08, 0x08, 0x08,
+  0x00, 0x00, 0x60, 0x60, 0x00,
+  0x20, 0x10, 0x08, 0x04, 0x02,
+  0x3E, 0x51, 0x49, 0x45, 0x3E,
+  0x00, 0x42, 0x7F, 0x40, 0x00,
+  0x72, 0x49, 0x49, 0x49, 0x46,
+  0x21, 0x41, 0x49, 0x4D, 0x33,
+  0x18, 0x14, 0x12, 0x7F, 0x10,
+  0x27, 0x45, 0x45, 0x45, 0x39,
+  0x3C, 0x4A, 0x49, 0x49, 0x31,
+  0x41, 0x21, 0x11, 0x09, 0x07,
+  0x36, 0x49, 0x49, 0x49, 0x36,
+  0x46, 0x49, 0x49, 0x29, 0x1E,
+  0x00, 0x00, 0x14, 0x00, 0x00,
+  0x00, 0x40, 0x34, 0x00, 0x00,
+  0x00, 0x08, 0x14, 0x22, 0x41,
+  0x14, 0x14, 0x14, 0x14, 0x14,
+  0x00, 0x41, 0x22, 0x14, 0x08,
+  0x02, 0x01, 0x59, 0x09, 0x06,
+  0x3E, 0x41, 0x5D, 0x59, 0x4E,
+  0x7C, 0x12, 0x11, 0x12, 0x7C,
+  0x7F, 0x49, 0x49, 0x49, 0x36,
+  0x3E, 0x41, 0x41, 0x41, 0x22,
+  0x7F, 0x41, 0x41, 0x41, 0x3E,
+  0x7F, 0x49, 0x49, 0x49, 0x41,
+  0x7F, 0x09, 0x09, 0x09, 0x01,
+  0x3E, 0x41, 0x41, 0x51, 0x73,
+  0x7F, 0x08, 0x08, 0x08, 0x7F,
+  0x00, 0x41, 0x7F, 0x41, 0x00,
+  0x20, 0x40, 0x41, 0x3F, 0x01,
+  0x7F, 0x08, 0x14, 0x22, 0x41,
+  0x7F, 0x40, 0x40, 0x40, 0x40,
+  0x7F, 0x02, 0x1C, 0x02, 0x7F,
+  0x7F, 0x04, 0x08, 0x10, 0x7F,
+  0x3E, 0x41, 0x41, 0x41, 0x3E,
+  0x7F, 0x09, 0x09, 0x09, 0x06,
+  0x3E, 0x41, 0x51, 0x21, 0x5E,
+  0x7F, 0x09, 0x19, 0x29, 0x46,
+  0x26, 0x49, 0x49, 0x49, 0x32,
+  0x03, 0x01, 0x7F, 0x01, 0x03,
+  0x3F, 0x40, 0x40, 0x40, 0x3F,
+  0x1F, 0x20, 0x40, 0x20, 0x1F,
+  0x3F, 0x40, 0x38, 0x40, 0x3F,
+  0x63, 0x14, 0x08, 0x14, 0x63,
+  0x03, 0x04, 0x78, 0x04, 0x03,
+  0x61, 0x59, 0x49, 0x4D, 0x43,
+  0x00, 0x7F, 0x41, 0x41, 0x41,
+  0x02, 0x04, 0x08, 0x10, 0x20,
+  0x00, 0x41, 0x41, 0x41, 0x7F,
+  0x04, 0x02, 0x01, 0x02, 0x04,
+  0x40, 0x40, 0x40, 0x40, 0x40,
+  0x00, 0x03, 0x07, 0x08, 0x00,
+  0x20, 0x54, 0x54, 0x78, 0x40,
+  0x7F, 0x28, 0x44, 0x44, 0x38,
+  0x38, 0x44, 0x44, 0x44, 0x28,
+  0x38, 0x44, 0x44, 0x28, 0x7F,
+  0x38, 0x54, 0x54, 0x54, 0x18,
+  0x00, 0x08, 0x7E, 0x09, 0x02,
+  0x18, 0xA4, 0xA4, 0x9C, 0x78,
+  0x7F, 0x08, 0x04, 0x04, 0x78,
+  0x00, 0x44, 0x7D, 0x40, 0x00,
+  0x20, 0x40, 0x40, 0x3D, 0x00,
+  0x7F, 0x10, 0x28, 0x44, 0x00,
+  0x00, 0x41, 0x7F, 0x40, 0x00,
+  0x7C, 0x04, 0x78, 0x04, 0x78,
+  0x7C, 0x08, 0x04, 0x04, 0x78,
+  0x38, 0x44, 0x44, 0x44, 0x38,
+  0xFC, 0x18, 0x24, 0x24, 0x18,
+  0x18, 0x24, 0x24, 0x18, 0xFC,
+  0x7C, 0x08, 0x04, 0x04, 0x08,
+  0x48, 0x54, 0x54, 0x54, 0x24,
+  0x04, 0x04, 0x3F, 0x44, 0x24,
+  0x3C, 0x40, 0x40, 0x20, 0x7C,
+  0x1C, 0x20, 0x40, 0x20, 0x1C,
+  0x3C, 0x40, 0x30, 0x40, 0x3C,
+  0x44, 0x28, 0x10, 0x28, 0x44,
+  0x4C, 0x90, 0x90, 0x90, 0x7C,
+  0x44, 0x64, 0x54, 0x4C, 0x44,
+  0x00, 0x08, 0x36, 0x41, 0x00,
+  0x00, 0x00, 0x77, 0x00, 0x00,
+  0x00, 0x41, 0x36, 0x08, 0x00,
+  0x02, 0x01, 0x02, 0x04, 0x02,
+  0x3C, 0x26, 0x23, 0x26, 0x3C,
+  0x1E, 0xA1, 0xA1, 0x61, 0x12,
+  0x3A, 0x40, 0x40, 0x20, 0x7A,
+  0x38, 0x54, 0x54, 0x55, 0x59,
+  0x21, 0x55, 0x55, 0x79, 0x41,
+  0x21, 0x54, 0x54, 0x78, 0x41,
+  0x21, 0x55, 0x54, 0x78, 0x40,
+  0x20, 0x54, 0x55, 0x79, 0x40,
+  0x0C, 0x1E, 0x52, 0x72, 0x12,
+  0x39, 0x55, 0x55, 0x55, 0x59,
+  0x39, 0x54, 0x54, 0x54, 0x59,
+  0x39, 0x55, 0x54, 0x54, 0x58,
+  0x00, 0x00, 0x45, 0x7C, 0x41,
+  0x00, 0x02, 0x45, 0x7D, 0x42,
+  0x00, 0x01, 0x45, 0x7C, 0x40,
+  0xF0, 0x29, 0x24, 0x29, 0xF0,
+  0xF0, 0x28, 0x25, 0x28, 0xF0,
+  0x7C, 0x54, 0x55, 0x45, 0x00,
+  0x20, 0x54, 0x54, 0x7C, 0x54,
+  0x7C, 0x0A, 0x09, 0x7F, 0x49,
+  0x32, 0x49, 0x49, 0x49, 0x32,
+  0x32, 0x48, 0x48, 0x48, 0x32,
+  0x32, 0x4A, 0x48, 0x48, 0x30,
+  0x3A, 0x41, 0x41, 0x21, 0x7A,
+  0x3A, 0x42, 0x40, 0x20, 0x78,
+  0x00, 0x9D, 0xA0, 0xA0, 0x7D,
+  0x39, 0x44, 0x44, 0x44, 0x39,
+  0x3D, 0x40, 0x40, 0x40, 0x3D,
+  0x3C, 0x24, 0xFF, 0x24, 0x24,
+  0x48, 0x7E, 0x49, 0x43, 0x66,
+  0x2B, 0x2F, 0xFC, 0x2F, 0x2B,
+  0xFF, 0x09, 0x29, 0xF6, 0x20,
+  0xC0, 0x88, 0x7E, 0x09, 0x03,
+  0x20, 0x54, 0x54, 0x79, 0x41,
+  0x00, 0x00, 0x44, 0x7D, 0x41,
+  0x30, 0x48, 0x48, 0x4A, 0x32,
+  0x38, 0x40, 0x40, 0x22, 0x7A,
+  0x00, 0x7A, 0x0A, 0x0A, 0x72,
+  0x7D, 0x0D, 0x19, 0x31, 0x7D,
+  0x26, 0x29, 0x29, 0x2F, 0x28,
+  0x26, 0x29, 0x29, 0x29, 0x26,
+  0x30, 0x48, 0x4D, 0x40, 0x20,
+  0x38, 0x08, 0x08, 0x08, 0x08,
+  0x08, 0x08, 0x08, 0x08, 0x38,
+  0x2F, 0x10, 0xC8, 0xAC, 0xBA,
+  0x2F, 0x10, 0x28, 0x34, 0xFA,
+  0x00, 0x00, 0x7B, 0x00, 0x00,
+  0x08, 0x14, 0x2A, 0x14, 0x22,
+  0x22, 0x14, 0x2A, 0x14, 0x08,
+  0xAA, 0x00, 0x55, 0x00, 0xAA,
+  0xAA, 0x55, 0xAA, 0x55, 0xAA,
+  0x00, 0x00, 0x00, 0xFF, 0x00,
+  0x10, 0x10, 0x10, 0xFF, 0x00,
+  0x14, 0x14, 0x14, 0xFF, 0x00,
+  0x10, 0x10, 0xFF, 0x00, 0xFF,
+  0x10, 0x10, 0xF0, 0x10, 0xF0,
+  0x14, 0x14, 0x14, 0xFC, 0x00,
+  0x14, 0x14, 0xF7, 0x00, 0xFF,
+  0x00, 0x00, 0xFF, 0x00, 0xFF,
+  0x14, 0x14, 0xF4, 0x04, 0xFC,
+  0x14, 0x14, 0x17, 0x10, 0x1F,
+  0x10, 0x10, 0x1F, 0x10, 0x1F,
+  0x14, 0x14, 0x14, 0x1F, 0x00,
+  0x10, 0x10, 0x10, 0xF0, 0x00,
+  0x00, 0x00, 0x00, 0x1F, 0x10,
+  0x10, 0x10, 0x10, 0x1F, 0x10,
+  0x10, 0x10, 0x10, 0xF0, 0x10,
+  0x00, 0x00, 0x00, 0xFF, 0x10,
+  0x10, 0x10, 0x10, 0x10, 0x10,
+  0x10, 0x10, 0x10, 0xFF, 0x10,
+  0x00, 0x00, 0x00, 0xFF, 0x14,
+  0x00, 0x00, 0xFF, 0x00, 0xFF,
+  0x00, 0x00, 0x1F, 0x10, 0x17,
+  0x00, 0x00, 0xFC, 0x04, 0xF4,
+  0x14, 0x14, 0x17, 0x10, 0x17,
+  0x14, 0x14, 0xF4, 0x04, 0xF4,
+  0x00, 0x00, 0xFF, 0x00, 0xF7,
+  0x14, 0x14, 0x14, 0x14, 0x14,
+  0x14, 0x14, 0xF7, 0x00, 0xF7,
+  0x14, 0x14, 0x14, 0x17, 0x14,
+  0x10, 0x10, 0x1F, 0x10, 0x1F,
+  0x14, 0x14, 0x14, 0xF4, 0x14,
+  0x10, 0x10, 0xF0, 0x10, 0xF0,
+  0x00, 0x00, 0x1F, 0x10, 0x1F,
+  0x00, 0x00, 0x00, 0x1F, 0x14,
+  0x00, 0x00, 0x00, 0xFC, 0x14,
+  0x00, 0x00, 0xF0, 0x10, 0xF0,
+  0x10, 0x10, 0xFF, 0x10, 0xFF,
+  0x14, 0x14, 0x14, 0xFF, 0x14,
+  0x10, 0x10, 0x10, 0x1F, 0x00,
+  0x00, 0x00, 0x00, 0xF0, 0x10,
+  0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+  0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
+  0xFF, 0xFF, 0xFF, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0xFF, 0xFF,
+  0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
+  0x38, 0x44, 0x44, 0x38, 0x44,
+  0x7C, 0x2A, 0x2A, 0x3E, 0x14,
+  0x7E, 0x02, 0x02, 0x06, 0x06,
+  0x02, 0x7E, 0x02, 0x7E, 0x02,
+  0x63, 0x55, 0x49, 0x41, 0x63,
+  0x38, 0x44, 0x44, 0x3C, 0x04,
+  0x40, 0x7E, 0x20, 0x1E, 0x20,
+  0x06, 0x02, 0x7E, 0x02, 0x02,
+  0x99, 0xA5, 0xE7, 0xA5, 0x99,
+  0x1C, 0x2A, 0x49, 0x2A, 0x1C,
+  0x4C, 0x72, 0x01, 0x72, 0x4C,
+  0x30, 0x4A, 0x4D, 0x4D, 0x30,
+  0x30, 0x48, 0x78, 0x48, 0x30,
+  0xBC, 0x62, 0x5A, 0x46, 0x3D,
+  0x3E, 0x49, 0x49, 0x49, 0x00,
+  0x7E, 0x01, 0x01, 0x01, 0x7E,
+  0x2A, 0x2A, 0x2A, 0x2A, 0x2A,
+  0x44, 0x44, 0x5F, 0x44, 0x44,
+  0x40, 0x51, 0x4A, 0x44, 0x40,
+  0x40, 0x44, 0x4A, 0x51, 0x40,
+  0x00, 0x00, 0xFF, 0x01, 0x03,
+  0xE0, 0x80, 0xFF, 0x00, 0x00,
+  0x08, 0x08, 0x6B, 0x6B, 0x08,
+  0x36, 0x12, 0x36, 0x24, 0x36,
+  0x06, 0x0F, 0x09, 0x0F, 0x06,
+  0x00, 0x00, 0x18, 0x18, 0x00,
+  0x00, 0x00, 0x10, 0x10, 0x00,
+  0x30, 0x40, 0xFF, 0x01, 0x01,
+  0x00, 0x1F, 0x01, 0x01, 0x1E,
+  0x00, 0x19, 0x1D, 0x17, 0x12,
+  0x00, 0x3C, 0x3C, 0x3C, 0x3C
+])}
+
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/ublox_gps.py b/GW-custom/uPyLoRaWAN/uPySensors/ublox_gps.py
new file mode 100644
index 0000000000000000000000000000000000000000..61108799cffa374f2feb99a214a4091c03c95be9
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/ublox_gps.py
@@ -0,0 +1,808 @@
+# -*- coding: utf-8 -*-
+# MicropyGPS - a GPS NMEA sentence parser for Micropython/Python 3.X
+
+#
+# The MIT License (MIT)
+
+# Copyright (c) 2014 Michael Calvin McCoy (calvin.mccoy@gmail.com)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+# TODO:
+# Time Since First Fix
+# Distance/Time to Target
+# Logging
+# More Helper Functions
+# Dynamically limit sentences types to parse
+
+#
+# Modified by [Mauro Riva <lemariva@mail.com> <lemariva.com>] for Wipy 2.0
+# * reduce size because of heap size of the Wipy 2.0
+# * added updateall, latitude_decimal, longitude_decimal methods
+# * added support for GLONASS, and GLONASS + GPS
+
+import time
+import gc
+
+class MicropyGPS(object):
+    """GPS NMEA Sentence Parser. Creates object that stores all relevant GPS data and statistics.
+    Parses sentences one character at a time using update(). """
+
+    # Max Number of Characters a valid sentence can be (based on GGA sentence)
+    SENTENCE_LIMIT = 76
+    __HEMISPHERES = ('N', 'S', 'E', 'W')
+    __NO_FIX = 1
+    __FIX_2D = 2
+    __FIX_3D = 3
+    __DIRECTIONS = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
+    __MONTHS = ('January', 'February', 'March', 'April', 'May',
+                'June', 'July', 'August', 'September', 'October',
+                'November', 'December')
+
+    def __init__(self, local_offset=0):
+        """Setup GPS Object Status Flags, Internal Data Registers, etc"""
+
+        #####################
+        # Object Status Flags
+        self.sentence_active = False
+        self.active_segment = 0
+        self.process_crc = False
+        self.gps_segments = []
+        self.crc_xor = 0
+        self.char_count = 0
+        self.fix_time = 0
+
+        #####################
+        # Sentence Statistics
+        self.crc_fails = 0
+        self.clean_sentences = 0
+        self.parsed_sentences = 0
+
+        #####################
+        # Logging Related
+        self.log_handle = None
+        self.log_en = False
+
+        #####################
+        # Data From Sentences
+        # Time
+        self.timestamp = (0, 0, 0)
+        self.date = (0, 0, 0)
+        self.local_offset = local_offset
+
+        # Position/Motion
+        self.latitude = (0, 0.0, 'N')
+        self.longitude = (0, 0.0, 'W')
+        self.speed = (0.0, 0.0, 0.0)
+        self.course = 0.0
+        self.altitude = 0.0
+        self.geoid_height = 0.0
+
+        # GPS Info
+        self.satellites_in_view = 0
+        self.satellites_in_use = 0
+        self.satellites_used = []
+        self.last_sv_sentence = 0
+        self.total_sv_sentences = 0
+        self.satellite_data = dict()
+        self.hdop = 0.0
+        self.pdop = 0.0
+        self.vdop = 0.0
+        self.valid = False
+        self.fix_stat = 0
+        self.fix_type = 1
+
+        # UART readall
+        self.oldstring = bytes()
+
+    ########################################
+    # Logging Related Functions
+    ########################################
+    def start_logging(self, target_file, mode="append"):
+        """
+        Create GPS data log object
+        """
+        if mode == 'new':
+            mode_code = 'w'
+        else:
+            mode_code = 'a'
+        try:
+            self.log_handle = open(target_file, mode_code)
+        except AttributeError:
+            print("Invalid FileName")
+            return False
+
+        self.log_en = True
+        return True
+
+    def stop_logging(self):
+        """
+        Closes the log file handler and disables further logging
+        """
+        try:
+            self.log_handle.close()
+        except AttributeError:
+            print("Invalid Handle")
+            return False
+
+        self.log_en = False
+        return True
+
+    def write_log(self, log_string):
+        """Attempts to write the last valid NMEA sentence character to the active file handler
+        """
+        try:
+            self.log_handle.write(log_string)
+        except TypeError:
+            return False
+        return True
+
+    ########################################
+    # Sentence Parsers
+    ########################################
+    def gprmc(self):
+        """Parse Recommended Minimum Specific GPS/Transit data (RMC)Sentence. Updates UTC timestamp, latitude,
+        longitude, Course, Speed, Date, and fix status"""
+
+        # UTC Timestamp
+        try:
+            utc_string = self.gps_segments[1]
+
+            if utc_string:  # Possible timestamp found
+                hours = int(utc_string[0:2]) + self.local_offset
+                minutes = int(utc_string[2:4])
+                seconds = float(utc_string[4:])
+                self.timestamp = (hours, minutes, seconds)
+            else:  # No Time stamp yet
+                self.timestamp = (0, 0, 0)
+
+        except ValueError:  # Bad Timestamp value present
+            return False
+
+        # Date stamp
+        try:
+            date_string = self.gps_segments[9]
+
+            # Date string printer function assumes to be year >=2000,
+            # date_string() must be supplied with the correct century argument to display correctly
+            if date_string:  # Possible date stamp found
+                day = int(date_string[0:2])
+                month = int(date_string[2:4])
+                year = int(date_string[4:6])
+                self.date = (day, month, year)
+            else:  # No Date stamp yet
+                self.date = (0, 0, 0)
+
+        except ValueError:  # Bad Date stamp value present
+            return False
+
+        # Check Receiver Data Valid Flag
+        if self.gps_segments[2] == 'A':  # Data from Receiver is Valid/Has Fix
+
+            # Longitude / Latitude
+            try:
+                # Latitude
+                l_string = self.gps_segments[3]
+                lat_degs = int(l_string[0:2])
+                lat_mins = float(l_string[2:])
+                lat_hemi = self.gps_segments[4]
+
+                # Longitude
+                l_string = self.gps_segments[5]
+                lon_degs = int(l_string[0:3])
+                lon_mins = float(l_string[3:])
+                lon_hemi = self.gps_segments[6]
+            except ValueError:
+                return False
+
+            if lat_hemi not in self.__HEMISPHERES:
+                return False
+
+            if lon_hemi not in self.__HEMISPHERES:
+                return False
+
+            # Speed
+            try:
+                spd_knt = float(self.gps_segments[7])
+            except ValueError:
+                return False
+
+            # Course
+            try:
+                course = float(self.gps_segments[8])
+            except ValueError:
+                return False
+
+            # TODO - Add Magnetic Variation
+
+            # Update Object Data
+            self.latitude = (lat_degs, lat_mins, lat_hemi)
+            self.longitude = (lon_degs, lon_mins, lon_hemi)
+            # Include mph and hm/h
+            self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852)
+            self.course = course
+            self.valid = True
+
+            # Update Last Fix Time
+            self.new_fix_time()
+
+        else:  # Clear Position Data if Sentence is 'Invalid'
+            self.latitude = (0, 0.0, 'N')
+            self.longitude = (0, 0.0, 'W')
+            self.speed = (0.0, 0.0, 0.0)
+            self.course = 0.0
+            self.date = (0, 0, 0)
+            self.valid = False
+
+        return True
+
+    def gpgll(self):
+        """Parse Geographic Latitude and Longitude (GLL)Sentence. Updates UTC timestamp, latitude,
+        longitude, and fix status"""
+
+        # UTC Timestamp
+        try:
+            utc_string = self.gps_segments[5]
+
+            if utc_string:  # Possible timestamp found
+                hours = int(utc_string[0:2]) + self.local_offset
+                minutes = int(utc_string[2:4])
+                seconds = float(utc_string[4:])
+                self.timestamp = (hours, minutes, seconds)
+            else:  # No Time stamp yet
+                self.timestamp = (0, 0, 0)
+
+        except ValueError:  # Bad Timestamp value present
+            return False
+
+        # Check Receiver Data Valid Flag
+        if self.gps_segments[6] == 'A':  # Data from Receiver is Valid/Has Fix
+
+            # Longitude / Latitude
+            try:
+                # Latitude
+                l_string = self.gps_segments[1]
+                lat_degs = int(l_string[0:2])
+                lat_mins = float(l_string[2:])
+                lat_hemi = self.gps_segments[2]
+
+                # Longitude
+                l_string = self.gps_segments[3]
+                lon_degs = int(l_string[0:3])
+                lon_mins = float(l_string[3:])
+                lon_hemi = self.gps_segments[4]
+            except ValueError:
+                return False
+
+            if lat_hemi not in self.__HEMISPHERES:
+                return False
+
+            if lon_hemi not in self.__HEMISPHERES:
+                return False
+
+            # Update Object Data
+            self.latitude = (lat_degs, lat_mins, lat_hemi)
+            self.longitude = (lon_degs, lon_mins, lon_hemi)
+            self.valid = True
+
+            # Update Last Fix Time
+            self.new_fix_time()
+
+        else:  # Clear Position Data if Sentence is 'Invalid'
+            self.latitude = (0, 0.0, 'N')
+            self.longitude = (0, 0.0, 'W')
+            self.valid = False
+
+        return True
+
+    def gpvtg(self):
+        """Parse Track Made Good and Ground Speed (VTG) Sentence. Updates speed and course"""
+        try:
+            course = float(self.gps_segments[1])
+            spd_knt = float(self.gps_segments[5])
+        except ValueError:
+            return False
+
+        # Include mph and km/h
+        self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852)
+        self.course = course
+        return True
+
+    def gpgga(self):
+        """Parse Global Positioning System Fix Data (GGA) Sentence. Updates UTC timestamp, latitude, longitude,
+        fix status, satellites in use, Horizontal Dilution of Precision (HDOP), altitude, geoid height and fix status"""
+
+        try:
+            # UTC Timestamp
+            utc_string = self.gps_segments[1]
+
+            # Skip timestamp if receiver doesn't have on yet
+            if utc_string:
+                hours = int(utc_string[0:2]) + self.local_offset
+                minutes = int(utc_string[2:4])
+                seconds = float(utc_string[4:])
+            else:
+                hours = 0
+                minutes = 0
+                seconds = 0.0
+
+            # Number of Satellites in Use
+            satellites_in_use = int(self.gps_segments[7])
+
+            # Horizontal Dilution of Precision
+            hdop = float(self.gps_segments[8])
+
+            # Get Fix Status
+            fix_stat = int(self.gps_segments[6])
+
+        except ValueError:
+            return False
+
+        # Process Location and Speed Data if Fix is GOOD
+        if fix_stat:
+
+            # Longitude / Latitude
+            try:
+                # Latitude
+                l_string = self.gps_segments[2]
+                lat_degs = int(l_string[0:2])
+                lat_mins = float(l_string[2:])
+                lat_hemi = self.gps_segments[3]
+
+                # Longitude
+                l_string = self.gps_segments[4]
+                lon_degs = int(l_string[0:3])
+                lon_mins = float(l_string[3:])
+                lon_hemi = self.gps_segments[5]
+            except ValueError:
+                return False
+
+            if lat_hemi not in self.__HEMISPHERES:
+                return False
+
+            if lon_hemi not in self.__HEMISPHERES:
+                return False
+
+            # Altitude / Height Above Geoid
+            try:
+                altitude = float(self.gps_segments[9])
+                geoid_height = float(self.gps_segments[11])
+            except ValueError:
+                return False
+
+            # Update Object Data
+            self.latitude = (lat_degs, lat_mins, lat_hemi)
+            self.longitude = (lon_degs, lon_mins, lon_hemi)
+            self.altitude = altitude
+            self.geoid_height = geoid_height
+
+        # Update Object Data
+        self.timestamp = (hours, minutes, seconds)
+        self.satellites_in_use = satellites_in_use
+        self.hdop = hdop
+        self.fix_stat = fix_stat
+
+        # If Fix is GOOD, update fix timestamp
+        if fix_stat:
+            self.new_fix_time()
+
+        return True
+
+    def gpgsa(self):
+        """Parse GNSS DOP and Active Satellites (GSA) sentence. Updates GPS fix type, list of satellites used in
+        fix calculation, Position Dilution of Precision (PDOP), Horizontal Dilution of Precision (HDOP), Vertical
+        Dilution of Precision, and fix status"""
+
+        # Fix Type (None,2D or 3D)
+        try:
+            fix_type = int(self.gps_segments[2])
+        except ValueError:
+            return False
+
+        # Read All (up to 12) Available PRN Satellite Numbers
+        sats_used = []
+        for sats in range(12):
+            sat_number_str = self.gps_segments[3 + sats]
+            if sat_number_str:
+                try:
+                    sat_number = int(sat_number_str)
+                    sats_used.append(sat_number)
+                except ValueError:
+                    return False
+            else:
+                break
+
+        # PDOP,HDOP,VDOP
+        try:
+            pdop = float(self.gps_segments[15])
+            hdop = float(self.gps_segments[16])
+            vdop = float(self.gps_segments[17])
+        except ValueError:
+            return False
+
+        # Update Object Data
+        self.fix_type = fix_type
+
+        # If Fix is GOOD, update fix timestamp
+        if fix_type > self.__NO_FIX:
+            self.new_fix_time()
+
+        self.satellites_used = sats_used
+        self.hdop = hdop
+        self.vdop = vdop
+        self.pdop = pdop
+
+        return True
+
+    def gpgsv(self):
+        """Parse Satellites in View (GSV) sentence. Updates number of SV Sentences,the number of the last SV sentence
+        parsed, and data on each satellite present in the sentence"""
+        try:
+            num_sv_sentences = int(self.gps_segments[1])
+            current_sv_sentence = int(self.gps_segments[2])
+            sats_in_view = int(self.gps_segments[3])
+        except ValueError:
+            return False
+
+        # Create a blank dict to store all the satellite data from this sentence in:
+        # satellite PRN is key, tuple containing telemetry is value
+        satellite_dict = dict()
+
+        # Calculate  Number of Satelites to pull data for and thus how many segment positions to read
+        if num_sv_sentences == current_sv_sentence:
+            sat_segment_limit = ((sats_in_view % 4) * 4) + 4  # Last sentence may have 1-4 satellites
+        else:
+            sat_segment_limit = 20  # Non-last sentences have 4 satellites and thus read up to position 20
+
+        # Try to recover data for up to 4 satellites in sentence
+        for sats in range(4, sat_segment_limit, 4):
+
+            # If a PRN is present, grab satellite data
+            if self.gps_segments[sats]:
+                try:
+                    sat_id = int(self.gps_segments[sats])
+                except ValueError:
+                    return False
+
+                try:  # elevation can be null (no value) when not tracking
+                    elevation = int(self.gps_segments[sats+1])
+                except ValueError:
+                    elevation = None
+
+                try:  # azimuth can be null (no value) when not tracking
+                    azimuth = int(self.gps_segments[sats+2])
+                except ValueError:
+                    azimuth = None
+
+                try:  # SNR can be null (no value) when not tracking
+                    snr = int(self.gps_segments[sats+3])
+                except ValueError:
+                    snr = None
+
+            # If no PRN is found, then the sentence has no more satellites to read
+            else:
+                break
+
+            # Add Satellite Data to Sentence Dict
+            satellite_dict[sat_id] = (elevation, azimuth, snr)
+
+        # Update Object Data
+        self.total_sv_sentences = num_sv_sentences
+        self.last_sv_sentence = current_sv_sentence
+        self.satellites_in_view = sats_in_view
+
+        # For a new set of sentences, we either clear out the existing sat data or
+        # update it as additional SV sentences are parsed
+        if current_sv_sentence == 1:
+            self.satellite_data = satellite_dict
+        else:
+            self.satellite_data.update(satellite_dict)
+
+        return True
+
+    ##########################################
+    # Data Stream Handler Functions
+    ##########################################
+
+    def new_sentence(self):
+        """Adjust Object Flags in Preparation for a New Sentence"""
+        self.gps_segments = ['']
+        self.active_segment = 0
+        self.crc_xor = 0
+        self.sentence_active = True
+        self.process_crc = True
+        self.char_count = 0
+
+    def updateall(self, string, fastmode=False):
+        try:
+            idx = 0
+            string_tmp = self.oldstring + string
+
+            for c in string_tmp:
+                idx = idx + 1
+                stat = self.update(chr(c))
+                if(stat != None):
+                    self.oldstring = string_tmp[idx:]
+                    if(fastmode):
+                        self.stringclean()
+                        return stat
+            return stat
+
+            gc.collect()
+        except OSError:
+            print('Error')
+
+
+    def stringclean(self):
+        self.oldstring = bytes()
+
+    def update(self, new_char):
+        """Process a new input char and updates GPS object if necessary based on special characters ('$', ',', '*')
+        Function builds a list of received string that are validate by CRC prior to parsing by the  appropriate
+        sentence function. Returns sentence type on successful parse, None otherwise"""
+
+        valid_sentence = False
+
+        # Validate new_char is a printable char
+        ascii_char = ord(new_char)
+
+        if 10 <= ascii_char <= 126:
+            self.char_count += 1
+
+            # Write Character to log file if enabled
+            if self.log_en:
+                self.write_log(new_char)
+
+            # Check if a new string is starting ($)
+            if new_char == '$':
+                self.new_sentence()
+                return None
+
+            elif self.sentence_active:
+
+                # Check if sentence is ending (*)
+                if new_char == '*':
+                    self.process_crc = False
+                    self.active_segment += 1
+                    self.gps_segments.append('')
+                    return None
+
+                # Check if a section is ended (,), Create a new substring to feed
+                # characters to
+                elif new_char == ',':
+                    self.active_segment += 1
+                    self.gps_segments.append('')
+
+                # Store All Other printable character and check CRC when ready
+                else:
+                    self.gps_segments[self.active_segment] += new_char
+
+                    # When CRC input is disabled, sentence is nearly complete
+                    if not self.process_crc:
+
+                        if len(self.gps_segments[self.active_segment]) == 2:
+                            try:
+                                final_crc = int(self.gps_segments[self.active_segment], 16)
+                                if self.crc_xor == final_crc:
+                                    valid_sentence = True
+                                else:
+                                    self.crc_fails += 1
+                            except ValueError:
+                                pass  # CRC Value was deformed and could not have been correct
+
+                # Update CRC
+                if self.process_crc:
+                    self.crc_xor ^= ascii_char
+
+                # If a Valid Sentence Was received and it's a supported sentence, then parse it!!
+                if valid_sentence:
+                    self.clean_sentences += 1  # Increment clean sentences received
+                    self.sentence_active = False  # Clear Active Processing Flag
+
+                    if self.gps_segments[0][2:] in self.supported_sentences:
+                        # parse the Sentence Based on the message type, return True if parse is clean
+                        if self.supported_sentences[self.gps_segments[0][2:]](self):
+                            # Let host know that the GPS object was updated by returning parsed sentence type
+                            self.parsed_sentences += 1
+                            return self.gps_segments[0]
+
+                # Check that the sentence buffer isn't filling up with Garage waiting for the sentence to complete
+                if self.char_count > self.SENTENCE_LIMIT:
+                    self.sentence_active = False
+
+        # Tell Host no new sentence was parsed
+        return None
+
+    def new_fix_time(self):
+        """Updates a high resolution counter with current time when fix is updated. Currently only triggered from
+        GGA, GSA and RMC sentences"""
+        try:
+            self.fix_time = pyb.millis()
+        except NameError:
+            self.fix_time = time.time()
+
+    #########################################
+    # User Helper Functions
+    # These functions make working with the GPS object data easier
+    #########################################
+
+    def satellite_data_updated(self):
+        """
+        Checks if the all the GSV sentences in a group have been read, making satellite data complete
+        :return: boolean
+        """
+        if self.total_sv_sentences > 0 and self.total_sv_sentences == self.last_sv_sentence:
+            return True
+        else:
+            return False
+
+    def satellites_visible(self):
+        """
+        Returns a list of of the satellite PRNs currently visible to the receiver
+        :return: list
+        """
+        return list(self.satellite_data.keys())
+
+    def time_since_fix(self):
+        """Returns number of millisecond since the last sentence with a valid fix was parsed. Returns 0 if
+        no fix has been found"""
+
+        # Test if a Fix has been found
+        if self.fix_time == 0:
+            return -1
+
+        # Try calculating fix time assuming using millis on a pyboard; default to seconds if not
+        try:
+            current = pyb.elapsed_millis(self.fix_time)
+        except NameError:
+            current = time.time() - self.fix_time
+
+        return current
+
+    def compass_direction(self):
+        """
+        Determine a cardinal or inter-cardinal direction based on current course.
+        :return: string
+        """
+        # Calculate the offset for a rotated compass
+        if self.course >= 348.75:
+            offset_course = 360 - self.course
+        else:
+            offset_course = self.course + 11.25
+
+        # Each compass point is separated by 22.5 degrees, divide to find lookup value
+        dir_index = offset_course // 22.5   #dir_index = floor(offset_course / 22.5)
+
+        final_dir = self.__DIRECTIONS[dir_index]
+
+        return final_dir
+
+    def latitude_string(self):
+        """
+        Create a readable string of the current latitude data
+        :return: string
+        """
+        lat_string = str(self.latitude[0]) + '° ' + str(self.latitude[1]) + "' " + str(self.latitude[2])
+        return lat_string
+
+    def latitude_decimal(self):
+        """
+        Create a decimal value of the current latitude data
+        :return: decimal
+        """
+        lat_decimal = float(self.latitude[0]) + float(self.latitude[1]) / 60
+        return lat_decimal
+
+    def longitude_string(self):
+        """
+        Create a readable string of the current longitude data
+        :return: string
+        """
+        long_string = str(self.longitude[0]) + '° ' + str(self.longitude[1]) + "' " + str(self.longitude[2])
+        return long_string
+
+
+    def longitude_decimal(self):
+        """
+        Create a decimal value of the current longitude data
+        :return: decimal
+        """
+        long_decimal = float(self.longitude[0]) + float(self.longitude[1]) / 60
+        return long_decimal
+
+    def speed_string(self, unit='kph'):
+        """
+        Creates a readable string of the current speed data in one of three units
+        :param unit: string of 'kph','mph, or 'knot'
+        :return:
+        """
+        if unit == 'mph':
+            speed_string = str(self.speed[1]) + ' mph'
+
+        elif unit == 'knot':
+            if self.speed[0] == 1:
+                unit_str = ' knot'
+            else:
+                unit_str = ' knots'
+            speed_string = str(self.speed[0]) + unit_str
+
+        else:
+            speed_string = str(self.speed[2]) + ' km/h'
+
+        return speed_string
+
+    def date_string(self, formatting='s_mdy', century='20'):
+        """
+        Creates a readable string of the current date.
+        Can select between long format: Januray 1st, 2014
+        or two short formats:
+        11/01/2014 (MM/DD/YYYY)
+        01/11/2014 (DD/MM/YYYY)
+        :param formatting: string 's_mdy', 's_dmy', or 'long'
+        :param century: int delineating the century the GPS data is from (19 for 19XX, 20 for 20XX)
+        :return: date_string  string with long or short format date
+        """
+
+        # Long Format Januray 1st, 2014
+        if formatting == 'long':
+            # Retrieve Month string from private set
+            month = self.__MONTHS[self.date[1] - 1]
+
+            # Determine Date Suffix
+            if self.date[0] in (1, 21, 31):
+                suffix = 'st'
+            elif self.date[0] in (2, 22):
+                suffix = 'nd'
+            elif self.date[0] == 3:
+                suffix = 'rd'
+            else:
+                suffix = 'th'
+
+            day = str(self.date[0]) + suffix  # Create Day String
+
+            year = century + str(self.date[2])  # Create Year String
+
+            date_string = month + ' ' + day + ', ' + year  # Put it all together
+
+        else:
+            # Add leading zeros to day string if necessary
+            if self.date[0] < 10:
+                day = '0' + str(self.date[0])
+            else:
+                day = str(self.date[0])
+
+            # Add leading zeros to month string if necessary
+            if self.date[1] < 10:
+                month = '0' + str(self.date[1])
+            else:
+                month = str(self.date[1])
+
+            # Add leading zeros to year string if necessary
+            if self.date[2] < 10:
+                year = '0' + str(self.date[2])
+            else:
+                year = str(self.date[2])
+
+            # Build final string based on desired formatting
+            if formatting == 's_dmy':
+                date_string = day + '/' + month + '/' + year
+
+            else:  # Default date format
+                date_string = month + '/' + day + '/' + year
+
+        return date_string
+
+    # All the currently supported NMEA sentences
+    supported_sentences = {'RMC': gprmc, 'GGA': gpgga, 'VTG': gpvtg, 'GSA': gpgsa, 'GSV': gpgsv, 'GLL': gpgll} # GPS + GLONASS
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/vector3d.py b/GW-custom/uPyLoRaWAN/uPySensors/vector3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e757167f7af415e8865cf1403e9cf470e7a110f
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/vector3d.py
@@ -0,0 +1,148 @@
+# vector3d.py 3D vector class for use in inertial measurement unit drivers
+# Authors Peter Hinch, Sebastian Plamauer
+
+# V0.7 17th May 2017 pyb replaced with utime
+# V0.6 18th June 2015
+
+'''
+The MIT License (MIT)
+Copyright (c) 2014 Sebastian Plamauer, oeplse@gmail.com, Peter Hinch
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+'''
+
+from utime import sleep_ms
+from math import sqrt, degrees, acos, atan2
+
+
+def default_wait():
+    '''
+    delay of 50 ms
+    '''
+    sleep_ms(50)
+
+
+class Vector3d(object):
+    '''
+    Represents a vector in a 3D space using Cartesian coordinates.
+    Internally uses sensor relative coordinates.
+    Returns vehicle-relative x, y and z values.
+    '''
+    def __init__(self, transposition, scaling, update_function):
+        self._vector = [0, 0, 0]
+        self._ivector = [0, 0, 0]
+        self.cal = (0, 0, 0)
+        self.argcheck(transposition, "Transposition")
+        self.argcheck(scaling, "Scaling")
+        if set(transposition) != {0, 1, 2}:
+            raise ValueError('Transpose indices must be unique and in range 0-2')
+        self._scale = scaling
+        self._transpose = transposition
+        self.update = update_function
+
+    def argcheck(self, arg, name):
+        '''
+        checks if arguments are of correct length
+        '''
+        if len(arg) != 3 or not (type(arg) is list or type(arg) is tuple):
+            raise ValueError(name + ' must be a 3 element list or tuple')
+
+    def calibrate(self, stopfunc, waitfunc=default_wait):
+        '''
+        calibration routine, sets cal
+        '''
+        self.update()
+        maxvec = self._vector[:]                # Initialise max and min lists with current values
+        minvec = self._vector[:]
+        while not stopfunc():
+            waitfunc()
+            self.update()
+            maxvec = list(map(max, maxvec, self._vector))
+            minvec = list(map(min, minvec, self._vector))
+        self.cal = tuple(map(lambda a, b: (a + b)/2, maxvec, minvec))
+
+    @property
+    def _calvector(self):
+        '''
+        Vector adjusted for calibration offsets
+        '''
+        return list(map(lambda val, offset: val - offset, self._vector, self.cal))
+
+    @property
+    def x(self):                                # Corrected, vehicle relative floating point values
+        self.update()
+        return self._calvector[self._transpose[0]] * self._scale[0]
+
+    @property
+    def y(self):
+        self.update()
+        return self._calvector[self._transpose[1]] * self._scale[1]
+
+    @property
+    def z(self):
+        self.update()
+        return self._calvector[self._transpose[2]] * self._scale[2]
+
+    @property
+    def xyz(self):
+        self.update()
+        return (self._calvector[self._transpose[0]] * self._scale[0],
+                self._calvector[self._transpose[1]] * self._scale[1],
+                self._calvector[self._transpose[2]] * self._scale[2])
+
+    @property
+    def magnitude(self):
+        x, y, z = self.xyz  # All measurements must correspond to the same instant
+        return sqrt(x**2 + y**2 + z**2)
+
+    @property
+    def inclination(self):
+        x, y, z = self.xyz
+        return degrees(acos(z / sqrt(x**2 + y**2 + z**2)))
+
+    @property
+    def elevation(self):
+        return 90 - self.inclination
+
+    @property
+    def azimuth(self):
+        x, y, z = self.xyz
+        return degrees(atan2(y, x))
+
+    # Raw uncorrected integer values from sensor
+    @property
+    def ix(self):
+        return self._ivector[0]
+
+    @property
+    def iy(self):
+        return self._ivector[1]
+
+    @property
+    def iz(self):
+        return self._ivector[2]
+
+    @property
+    def ixyz(self):
+        return self._ivector
+
+    @property
+    def transpose(self):
+        return tuple(self._transpose)
+
+    @property
+    def scale(self):
+        return tuple(self._scale)
diff --git a/GW-custom/uPyLoRaWAN/uPySensors/vl53l0x.py b/GW-custom/uPyLoRaWAN/uPySensors/vl53l0x.py
new file mode 100644
index 0000000000000000000000000000000000000000..65aa8d26128835bcd938d6540c7c1a0b4a414dbe
--- /dev/null
+++ b/GW-custom/uPyLoRaWAN/uPySensors/vl53l0x.py
@@ -0,0 +1,344 @@
+from micropython import const
+import ustruct
+import utime
+from machine import I2C
+
+_IO_TIMEOUT = 1000
+_SYSRANGE_START = const(0x00)
+_EXTSUP_HV = const(0x89)
+_MSRC_CONFIG = const(0x60)
+_FINAL_RATE_RTN_LIMIT = const(0x44)
+_SYSTEM_SEQUENCE = const(0x01)
+_SPAD_REF_START = const(0x4f)
+_SPAD_ENABLES = const(0xb0)
+_REF_EN_START_SELECT = const(0xb6)
+_SPAD_NUM_REQUESTED = const(0x4e)
+_INTERRUPT_GPIO = const(0x0a)
+_INTERRUPT_CLEAR = const(0x0b)
+_GPIO_MUX_ACTIVE_HIGH = const(0x84)
+_RESULT_INTERRUPT_STATUS = const(0x13)
+_RESULT_RANGE_STATUS = const(0x14)
+_OSC_CALIBRATE = const(0xf8)
+_MEASURE_PERIOD = const(0x04)
+
+class TimeoutError(RuntimeError):
+    pass
+
+class VL53L0X:
+    def __init__(self, i2c, address=0x29):
+        if isinstance(i2c, str):           # Non-pyb targets may use other than X or Y
+            self.i2c = I2C(i2c)
+        elif isinstance(i2c, int):         # WiPY targets
+            self.i2c = I2C(i2c)
+        elif hasattr(i2c, 'readfrom'):     # Soft or hard I2C instance. See issue #3097
+            self.i2c = i2c
+        else:
+            raise ValueError("Invalid I2C instance")
+
+        self.address = address
+        self.init()
+        self._started = False
+
+    def _registers(self, register, values=None, struct='B'):
+        if values is None:
+            size = ustruct.calcsize(struct)
+            data = self.i2c.readfrom_mem(self.address, register, size)
+            values = ustruct.unpack(struct, data)
+            return values
+        data = ustruct.pack(struct, *values)
+        self.i2c.writeto_mem(self.address, register, data)
+
+    def _register(self, register, value=None, struct='B'):
+        if value is None:
+            return self._registers(register, struct=struct)[0]
+        self._registers(register, (value,), struct=struct)
+
+    def _flag(self, register=0x00, bit=0, value=None):
+        data = self._register(register)
+        mask = 1 << bit
+        if value is None:
+            return bool(data & mask)
+        elif value:
+            data |= mask
+        else:
+            data &= ~mask
+        self._register(register, data)
+
+    def _config(self, *config):
+        for register, value in config:
+            self._register(register, value)
+
+    def init(self, power2v8=True):
+        self._flag(_EXTSUP_HV, 0, power2v8)
+
+        # I2C standard mode
+        self._config(
+            (0x88, 0x00),
+
+            (0x80, 0x01),
+            (0xff, 0x01),
+            (0x00, 0x00),
+        )
+        self._stop_variable = self._register(0x91)
+        self._config(
+            (0x00, 0x01),
+            (0xff, 0x00),
+            (0x80, 0x00),
+        )
+
+        # disable signal_rate_msrc and signal_rate_pre_range limit checks
+        self._flag(_MSRC_CONFIG, 1, True)
+        self._flag(_MSRC_CONFIG, 4, True)
+
+        # rate_limit = 0.25
+        self._register(_FINAL_RATE_RTN_LIMIT, int(0.25 * (1 << 7)),
+                       struct='>H')
+
+        self._register(_SYSTEM_SEQUENCE, 0xff)
+
+        spad_count, is_aperture = self._spad_info()
+        spad_map = bytearray(self._registers(_SPAD_ENABLES, struct='6B'))
+
+        # set reference spads
+        self._config(
+            (0xff, 0x01),
+            (_SPAD_REF_START, 0x00),
+            (_SPAD_NUM_REQUESTED, 0x2c),
+            (0xff, 0x00),
+            (_REF_EN_START_SELECT, 0xb4),
+        )
+
+        spads_enabled = 0
+        for i in range(48):
+            if i < 12 and is_aperture or spads_enabled >= spad_count:
+                spad_map[i // 8] &= ~(1 << (i >> 2))
+            elif spad_map[i // 8] & (1 << (i >> 2)):
+                spads_enabled += 1
+
+        self._registers(_SPAD_ENABLES, spad_map, struct='6B')
+
+        self._config(
+            (0xff, 0x01),
+            (0x00, 0x00),
+
+            (0xff, 0x00),
+            (0x09, 0x00),
+            (0x10, 0x00),
+            (0x11, 0x00),
+
+            (0x24, 0x01),
+            (0x25, 0xFF),
+            (0x75, 0x00),
+
+            (0xFF, 0x01),
+            (0x4E, 0x2C),
+            (0x48, 0x00),
+            (0x30, 0x20),
+
+            (0xFF, 0x00),
+            (0x30, 0x09),
+            (0x54, 0x00),
+            (0x31, 0x04),
+            (0x32, 0x03),
+            (0x40, 0x83),
+            (0x46, 0x25),
+            (0x60, 0x00),
+            (0x27, 0x00),
+            (0x50, 0x06),
+            (0x51, 0x00),
+            (0x52, 0x96),
+            (0x56, 0x08),
+            (0x57, 0x30),
+            (0x61, 0x00),
+            (0x62, 0x00),
+            (0x64, 0x00),
+            (0x65, 0x00),
+            (0x66, 0xA0),
+
+            (0xFF, 0x01),
+            (0x22, 0x32),
+            (0x47, 0x14),
+            (0x49, 0xFF),
+            (0x4A, 0x00),
+
+            (0xFF, 0x00),
+            (0x7A, 0x0A),
+            (0x7B, 0x00),
+            (0x78, 0x21),
+
+            (0xFF, 0x01),
+            (0x23, 0x34),
+            (0x42, 0x00),
+            (0x44, 0xFF),
+            (0x45, 0x26),
+            (0x46, 0x05),
+            (0x40, 0x40),
+            (0x0E, 0x06),
+            (0x20, 0x1A),
+            (0x43, 0x40),
+
+            (0xFF, 0x00),
+            (0x34, 0x03),
+            (0x35, 0x44),
+
+            (0xFF, 0x01),
+            (0x31, 0x04),
+            (0x4B, 0x09),
+            (0x4C, 0x05),
+            (0x4D, 0x04),
+
+            (0xFF, 0x00),
+            (0x44, 0x00),
+            (0x45, 0x20),
+            (0x47, 0x08),
+            (0x48, 0x28),
+            (0x67, 0x00),
+            (0x70, 0x04),
+            (0x71, 0x01),
+            (0x72, 0xFE),
+            (0x76, 0x00),
+            (0x77, 0x00),
+
+            (0xFF, 0x01),
+            (0x0D, 0x01),
+
+            (0xFF, 0x00),
+            (0x80, 0x01),
+            (0x01, 0xF8),
+
+            (0xFF, 0x01),
+            (0x8E, 0x01),
+            (0x00, 0x01),
+            (0xFF, 0x00),
+            (0x80, 0x00),
+        )
+
+        self._register(_INTERRUPT_GPIO, 0x04)
+        self._flag(_GPIO_MUX_ACTIVE_HIGH, 4, False)
+        self._register(_INTERRUPT_CLEAR, 0x01)
+
+        # XXX Need to implement this.
+        #budget = self._timing_budget()
+        #self._register(_SYSTEM_SEQUENCE, 0xe8)
+        #self._timing_budget(budget)
+
+        self._register(_SYSTEM_SEQUENCE, 0x01)
+        self._calibrate(0x40)
+        self._register(_SYSTEM_SEQUENCE, 0x02)
+        self._calibrate(0x00)
+
+        self._register(_SYSTEM_SEQUENCE, 0xe8)
+
+    def _spad_info(self):
+        self._config(
+            (0x80, 0x01),
+            (0xff, 0x01),
+            (0x00, 0x00),
+
+            (0xff, 0x06),
+        )
+        self._flag(0x83, 3, True)
+        self._config(
+            (0xff, 0x07),
+            (0x81, 0x01),
+
+            (0x80, 0x01),
+
+            (0x94, 0x6b),
+            (0x83, 0x00),
+        )
+        for timeout in range(_IO_TIMEOUT):
+            if self._register(0x83):
+                break
+            utime.sleep_ms(1)
+        else:
+            raise TimeoutError()
+        self._config(
+            (0x83, 0x01),
+        )
+        value = self._register(0x92)
+        self._config(
+            (0x81, 0x00),
+            (0xff, 0x06),
+        )
+        self._flag(0x83, 3, False)
+        self._config(
+            (0xff, 0x01),
+            (0x00, 0x01),
+
+            (0xff, 0x00),
+            (0x80, 0x00),
+        )
+        count = value & 0x7f
+        is_aperture = bool(value & 0b10000000)
+        return count, is_aperture
+
+    def _calibrate(self, vhv_init_byte):
+        self._register(_SYSRANGE_START, 0x01 | vhv_init_byte)
+        for timeout in range(_IO_TIMEOUT):
+            if self._register(_RESULT_INTERRUPT_STATUS) & 0x07:
+                break
+            utime.sleep_ms(1)
+        else:
+            raise TimeoutError()
+        self._register(_INTERRUPT_CLEAR, 0x01)
+        self._register(_SYSRANGE_START, 0x00)
+
+    def start(self, period=0):
+        self._config(
+          (0x80, 0x01),
+          (0xFF, 0x01),
+          (0x00, 0x00),
+          (0x91, self._stop_variable),
+          (0x00, 0x01),
+          (0xFF, 0x00),
+          (0x80, 0x00),
+        )
+        if period:
+            oscilator = self._register(_OSC_CALIBRATE, struct='>H')
+            if oscilator:
+                period *= oscilator
+            self._register(_MEASURE_PERIOD, period, struct='>H')
+            self._register(_SYSRANGE_START, 0x04)
+        else:
+            self._register(_SYSRANGE_START, 0x02)
+        self._started = True
+
+    def stop(self):
+        self._register(_SYSRANGE_START, 0x01)
+        self._config(
+          (0xFF, 0x01),
+          (0x00, 0x00),
+          (0x91, self._stop_variable),
+          (0x00, 0x01),
+          (0xFF, 0x00),
+        )
+        self._started = False
+
+    def read(self):
+        if not self._started:
+            self._config(
+              (0x80, 0x01),
+              (0xFF, 0x01),
+              (0x00, 0x00),
+              (0x91, self._stop_variable),
+              (0x00, 0x01),
+              (0xFF, 0x00),
+              (0x80, 0x00),
+              (_SYSRANGE_START, 0x01),
+            )
+            for timeout in range(_IO_TIMEOUT):
+                if not self._register(_SYSRANGE_START) & 0x01:
+                    break
+                utime.sleep_ms(1)
+            else:
+                raise TimeoutError()
+        for timeout in range(_IO_TIMEOUT):
+            if self._register(_RESULT_INTERRUPT_STATUS) & 0x07:
+                break
+            utime.sleep_ms(1)
+        else:
+            raise TimeoutError()
+        value = self._register(_RESULT_RANGE_STATUS + 10, struct='>H')
+        self._register(_INTERRUPT_CLEAR, 0x01)
+        return value