subDesTagesMitExtraKaese 3 years ago
commit
8e3c5d1531
11 changed files with 543 additions and 0 deletions
  1. 149 0
      .gitignore
  2. 9 0
      .vscode/extensions.json
  3. 14 0
      .vscode/settings.json
  4. 17 0
      README.md
  5. 5 0
      boot.py
  6. 9 0
      config.py
  7. 1 0
      dev-requirements.txt
  8. 119 0
      main.py
  9. 13 0
      micropy.json
  10. 0 0
      requirements.txt
  11. 207 0
      umqttsimple.py

+ 149 - 0
.gitignore

@@ -0,0 +1,149 @@
+
+# Created by https://www.gitignore.io/api/python,visualstudiocode
+# Edit at https://www.gitignore.io/?templates=python,visualstudiocode
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+
+# End of https://www.gitignore.io/api/python,visualstudiocode
+
+### Micropy Cli ###
+.micropy/
+!micropy.json
+!src/lib
+
+wifi_config.py

+ 9 - 0
.vscode/extensions.json

@@ -0,0 +1,9 @@
+{
+    // 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
+    ]
+}

+ 14 - 0
.vscode/settings.json

@@ -0,0 +1,14 @@
+{
+    "python.linting.enabled": true,
+    "python.jediEnabled": false,
+
+    // Loaded Stubs:  esp8266-micropython-1.11.0 
+    "python.autoComplete.extraPaths": [".micropy/BradenM-micropy-stubs-70f5531/frozen", ".micropy/BradenM-micropy-stubs-4f5a52a/frozen", ".micropy/BradenM-micropy-stubs-70f5531/stubs", ".micropy/micropython-rgbww-bulb"],
+    "python.autoComplete.typeshedPaths":  [".micropy/BradenM-micropy-stubs-70f5531/frozen", ".micropy/BradenM-micropy-stubs-4f5a52a/frozen", ".micropy/BradenM-micropy-stubs-70f5531/stubs", ".micropy/micropython-rgbww-bulb"],
+    "python.analysis.typeshedPaths":  [".micropy/BradenM-micropy-stubs-70f5531/frozen", ".micropy/BradenM-micropy-stubs-4f5a52a/frozen", ".micropy/BradenM-micropy-stubs-70f5531/stubs", ".micropy/micropython-rgbww-bulb"],
+
+    "python.linting.pylintEnabled": true,
+    "python.analysis.extraPaths": [
+      "./.micropy/BradenM-micropy-stubs-70f5531/stubs"
+    ]
+}

+ 17 - 0
README.md

@@ -0,0 +1,17 @@
+# MicroPython RGBWW Smart Bulb
+tested on a Tuya WinnerMicro W600 Smart Bulb
+
+## [VS Code Development guide](https://lemariva.com/blog/2019/08/micropython-vsc-ide-intellisense)
+
+## Installation
+1. Copy python files to board.
+2. Reset the uC
+
+## How to upload code remotely (WM uPython SDK)
+
+1.  Create `wifi_config.py` with following content:
+    ```python
+    WIFI_SSID="<SSID>"
+    WIFI_PASSWD="<PASSWORD>"
+    ```
+2.  Reboot the M600 and connect to bulb via FTP without username and password

+ 5 - 0
boot.py

@@ -0,0 +1,5 @@
+# boot.py -- run on boot-up
+# can run arbitrary Python, but best to keep it minimal
+print("")
+print("    WinnerMicro W600")
+print("")

+ 9 - 0
config.py

@@ -0,0 +1,9 @@
+import ubinascii
+import machine
+
+# Default MQTT server to connect to
+mqtt_server = "192.168.1.1"
+
+client_id = ubinascii.hexlify(machine.unique_id())
+topic_sub = b"Room/rgbBulb/1/command"
+topic_pub = b"Room/rgbBulb/1/state"

+ 1 - 0
dev-requirements.txt

@@ -0,0 +1 @@
+micropy-cli

+ 119 - 0
main.py

@@ -0,0 +1,119 @@
+# main.py -- put your code here!
+
+import time
+import machine
+from machine import Pin, PWM
+import ujson
+import network
+
+from umqttsimple import MQTTClient
+from config import mqtt_server, client_id, topic_sub, topic_pub
+
+import gc
+gc.collect()
+
+station = network.WLAN(network.STA_IF)
+
+vals = {
+  'C': 0,
+  'G': 0,
+  'R': 0,
+  'W': 0,
+  'B': 0
+}
+duration = 50
+pwm = {}
+def initPWM():
+  global pwm
+  pwm = {
+    'C': PWM(Pin(Pin.PB_16), channel=2, freq=800, duty=vals['C']),
+    'G': PWM(Pin(Pin.PB_13), channel=1, freq=800, duty=vals['G']),
+    'R': PWM(Pin(Pin.PA_05), channel=0, freq=800, duty=vals['R']),
+    'W': PWM(Pin(Pin.PB_08), channel=4, freq=800, duty=vals['W']),
+    'B': PWM(Pin(Pin.PB_15), channel=3, freq=800, duty=vals['B'])
+  }
+initPWM()
+
+def fade():
+  global pwm, vals, duration
+  if duration > 2:
+    diff = {}
+    for color in pwm:
+      diff[color] = float(vals[color] - pwm[color].duty()) / (duration-1)
+    for i in range(duration-1, 1, -1):
+      for color in pwm:
+        pwm[color].duty(int(vals[color] - diff[color] * i))
+      time.sleep(0.02)
+  
+  for color in pwm:
+    pwm[color].duty(vals[color])
+
+def sub_cb(topic, msg):
+  global pwm, vals, duration
+  try:
+    cmd = ujson.loads(str(msg, 'utf-8'))
+    if 'state' in cmd:
+      if cmd['state'] == "OFF":
+        for color in pwm:
+          pwm[color].deinit()
+        pwm = {}
+      else:
+        initPWM()
+    if 'speed' in cmd:
+      duration = int(cmd['fade'])
+    if 'color' in cmd:
+      for color in vals:
+        if color in cmd['color']:
+          val = int(cmd['color'][color])
+          if val >= 0 and val <= 255:
+            vals[color] = val
+      fade()
+    if 'reset' in cmd:
+      machine.reset()
+    
+  except Exception as e:
+    client.publish(topic_pub, b"error")
+
+
+def connect_and_subscribe():
+  global client_id, mqtt_server, topic_sub
+  client = MQTTClient(client_id, mqtt_server)
+  client.set_callback(sub_cb)
+  client.connect()
+  client.subscribe(topic_sub)
+  print('Connected to %s MQTT broker, subscribed to %s topic' % (mqtt_server, topic_sub))
+  return client
+
+def restart_and_reconnect():
+  print('Failed to connect to MQTT broker. Reconnecting...')
+  time.sleep(10)
+  machine.reset()
+
+try:
+  client = connect_and_subscribe()
+  client.set_last_will(topic_pub, b"offline", retain=False, qos=0)
+  client.sock.settimeout(10)
+except OSError as e:
+  print(e)
+  restart_and_reconnect()
+
+last_message = 0
+message_interval = 10
+counter = 0
+
+while True:
+  try:
+    client.wait_msg()
+    if (time.time() - last_message) > message_interval:
+      msg = b'online #%d' % counter
+      client.publish(topic_pub, msg)
+      last_message = time.time()
+      gc.collect()
+      counter += 1
+  except OSError as e:
+    client.publish(topic_pub, b"OSError")
+    restart_and_reconnect()
+  if station.isconnected() == False:
+    if 'R' in pwm:
+      pwm['R'].duty(60)
+    

+ 13 - 0
micropy.json

@@ -0,0 +1,13 @@
+{
+    "name": "micropython-rgbww-bulb",
+    "stubs": {
+        "esp8266-micropython-1.11.0": "1.2.0"
+    },
+    "dev-packages": {
+        "micropy-cli": "*"
+    },
+    "packages": {},
+    "config": {
+        "vscode": true
+    }
+}

+ 0 - 0
requirements.txt


+ 207 - 0
umqttsimple.py

@@ -0,0 +1,207 @@
+try:
+    import usocket as socket
+except:
+    import socket
+import ustruct as struct
+from ubinascii import hexlify
+
+class MQTTException(Exception):
+    pass
+
+class MQTTClient:
+
+    def __init__(self, client_id, server, port=0, user=None, password=None, keepalive=0,
+                 ssl=False, ssl_params={}):
+        if port == 0:
+            port = 8883 if ssl else 1883
+        self.client_id = client_id
+        self.sock = None
+        self.server = server
+        self.port = port
+        self.ssl = ssl
+        self.ssl_params = ssl_params
+        self.pid = 0
+        self.cb = None
+        self.user = user
+        self.pswd = password
+        self.keepalive = keepalive
+        self.lw_topic = None
+        self.lw_msg = None
+        self.lw_qos = 0
+        self.lw_retain = False
+
+    def _send_str(self, s):
+        self.sock.write(struct.pack("!H", len(s)))
+        self.sock.write(s)
+
+    def _recv_len(self):
+        n = 0
+        sh = 0
+        while 1:
+            b = self.sock.read(1)[0]
+            n |= (b & 0x7f) << sh
+            if not b & 0x80:
+                return n
+            sh += 7
+
+    def set_callback(self, f):
+        self.cb = f
+
+    def set_last_will(self, topic, msg, retain=False, qos=0):
+        assert 0 <= qos <= 2
+        assert topic
+        self.lw_topic = topic
+        self.lw_msg = msg
+        self.lw_qos = qos
+        self.lw_retain = retain
+
+    def connect(self, clean_session=True):
+        self.sock = socket.socket()
+        addr = socket.getaddrinfo(self.server, self.port)[0][-1]
+        self.sock.connect(addr)
+        if self.ssl:
+            import ussl
+            self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
+        premsg = bytearray(b"\x10\0\0\0\0\0")
+        msg = bytearray(b"\x04MQTT\x04\x02\0\0")
+
+        sz = 10 + 2 + len(self.client_id)
+        msg[6] = clean_session << 1
+        if self.user is not None:
+            sz += 2 + len(self.user) + 2 + len(self.pswd)
+            msg[6] |= 0xC0
+        if self.keepalive:
+            assert self.keepalive < 65536
+            msg[7] |= self.keepalive >> 8
+            msg[8] |= self.keepalive & 0x00FF
+        if self.lw_topic:
+            sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
+            msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
+            msg[6] |= self.lw_retain << 5
+
+        i = 1
+        while sz > 0x7f:
+            premsg[i] = (sz & 0x7f) | 0x80
+            sz >>= 7
+            i += 1
+        premsg[i] = sz
+
+        self.sock.write(premsg, i + 2)
+        self.sock.write(msg)
+        #print(hex(len(msg)), hexlify(msg, ":"))
+        self._send_str(self.client_id)
+        if self.lw_topic:
+            self._send_str(self.lw_topic)
+            self._send_str(self.lw_msg)
+        if self.user is not None:
+            self._send_str(self.user)
+            self._send_str(self.pswd)
+        resp = self.sock.read(4)
+        assert resp[0] == 0x20 and resp[1] == 0x02
+        if resp[3] != 0:
+            raise MQTTException(resp[3])
+        return resp[2] & 1
+
+    def disconnect(self):
+        self.sock.write(b"\xe0\0")
+        self.sock.close()
+
+    def ping(self):
+        self.sock.write(b"\xc0\0")
+
+    def publish(self, topic, msg, retain=False, qos=0):
+        pkt = bytearray(b"\x30\0\0\0")
+        pkt[0] |= qos << 1 | retain
+        sz = 2 + len(topic) + len(msg)
+        if qos > 0:
+            sz += 2
+        assert sz < 2097152
+        i = 1
+        while sz > 0x7f:
+            pkt[i] = (sz & 0x7f) | 0x80
+            sz >>= 7
+            i += 1
+        pkt[i] = sz
+        #print(hex(len(pkt)), hexlify(pkt, ":"))
+        self.sock.write(pkt, i + 1)
+        self._send_str(topic)
+        if qos > 0:
+            self.pid += 1
+            pid = self.pid
+            struct.pack_into("!H", pkt, 0, pid)
+            self.sock.write(pkt, 2)
+        self.sock.write(msg)
+        if qos == 1:
+            while 1:
+                op = self.wait_msg()
+                if op == 0x40:
+                    sz = self.sock.read(1)
+                    assert sz == b"\x02"
+                    rcv_pid = self.sock.read(2)
+                    rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
+                    if pid == rcv_pid:
+                        return
+        elif qos == 2:
+            assert 0
+
+    def subscribe(self, topic, qos=0):
+        assert self.cb is not None, "Subscribe callback is not set"
+        pkt = bytearray(b"\x82\0\0\0")
+        self.pid += 1
+        struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
+        #print(hex(len(pkt)), hexlify(pkt, ":"))
+        self.sock.write(pkt)
+        self._send_str(topic)
+        self.sock.write(qos.to_bytes(1, "little"))
+        while 1:
+            op = self.wait_msg()
+            if op == 0x90:
+                resp = self.sock.read(4)
+                #print(resp)
+                assert resp[1] == pkt[2] and resp[2] == pkt[3]
+                if resp[3] == 0x80:
+                    raise MQTTException(resp[3])
+                return
+
+    # Wait for a single incoming MQTT message and process it.
+    # Subscribed messages are delivered to a callback previously
+    # set by .set_callback() method. Other (internal) MQTT
+    # messages processed internally.
+    def wait_msg(self):
+        res = self.sock.read(1)
+        #self.sock.setblocking(True)
+        if res is None:
+            return None
+        if res == b"":
+            raise OSError(-1)
+        if res == b"\xd0":  # PINGRESP
+            sz = self.sock.read(1)[0]
+            assert sz == 0
+            return None
+        op = res[0]
+        if op & 0xf0 != 0x30:
+            return op
+        sz = self._recv_len()
+        topic_len = self.sock.read(2)
+        topic_len = (topic_len[0] << 8) | topic_len[1]
+        topic = self.sock.read(topic_len)
+        sz -= topic_len + 2
+        if op & 6:
+            pid = self.sock.read(2)
+            pid = pid[0] << 8 | pid[1]
+            sz -= 2
+        msg = self.sock.read(sz)
+        self.cb(topic, msg)
+        if op & 6 == 2:
+            pkt = bytearray(b"\x40\x02\0\0")
+            struct.pack_into("!H", pkt, 2, pid)
+            self.sock.write(pkt)
+        elif op & 6 == 4:
+            assert 0
+
+    # Checks whether a pending message from server is available.
+    # If not, returns immediately with None. Otherwise, does
+    # the same processing as wait_msg.
+    def check_msg(self):
+        self.sock.setblocking(False)
+        return self.wait_msg()