mirror of
https://bitbucket.org/myhomie/mycorerepository.git
synced 2025-12-06 01:31:19 +00:00
Added all missing file (.git issue)
This commit is contained in:
parent
e55358c438
commit
521b27505e
@ -1 +0,0 @@
|
||||
Subproject commit cb649680218eada1c7b1fcca4b1bfd6f492ae5c6
|
||||
23
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/MCP3008.py
Normal file
23
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/MCP3008.py
Normal file
@ -0,0 +1,23 @@
|
||||
from spidev import SpiDev
|
||||
|
||||
class MCP3008:
|
||||
def __init__(self, bus = 0, device = 0):
|
||||
self.bus, self.device = bus, device
|
||||
self.spi = SpiDev()
|
||||
self.open()
|
||||
self.spi.max_speed_hz = 1000000 # 1MHz
|
||||
|
||||
def open(self):
|
||||
self.spi.open(self.bus, self.device)
|
||||
self.spi.max_speed_hz = 1000000 # 1MHz
|
||||
|
||||
def read(self, channel = 0):
|
||||
cmd1 = 4 | 2 | (( channel & 4) >> 2)
|
||||
cmd2 = (channel & 3) << 6
|
||||
|
||||
adc = self.spi.xfer2([cmd1, cmd2, 0])
|
||||
data = ((adc[1] & 15) << 8) + adc[2]
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self.spi.close()
|
||||
BIN
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/MCP3008.pyc
Normal file
BIN
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/MCP3008.pyc
Normal file
Binary file not shown.
6
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/README.md
Normal file
6
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
Raspberry Pi Gas Sensor MQ Python Example
|
||||
================
|
||||
|
||||
Tutorial (german): https://tutorials-raspberrypi.de/raspberry-pi-gas-sensor-mq2-konfigurieren-und-auslesen
|
||||
|
||||
Tutorial (english): https://tutorials-raspberrypi.com/configure-and-read-out-the-raspberry-pi-gas-sensor-mq-x
|
||||
17
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/example.py
Normal file
17
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/example.py
Normal file
@ -0,0 +1,17 @@
|
||||
from mq import *
|
||||
import sys, time
|
||||
|
||||
try:
|
||||
print("Press CTRL+C to abort.")
|
||||
|
||||
mq = MQ();
|
||||
while True:
|
||||
perc = mq.MQPercentage()
|
||||
sys.stdout.write("\r")
|
||||
sys.stdout.write("\033[K")
|
||||
sys.stdout.write("LPG: %g ppm, CO: %g ppm, Smoke: %g ppm" % (perc["GAS_LPG"], perc["CO"], perc["SMOKE"]))
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.1)
|
||||
|
||||
except:
|
||||
print("\nAbort by user")
|
||||
140
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/mq.py
Normal file
140
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/mq.py
Normal file
@ -0,0 +1,140 @@
|
||||
|
||||
# adapted from sandboxelectronics.com/?p=165
|
||||
|
||||
import time
|
||||
import math
|
||||
from MCP3008 import MCP3008
|
||||
|
||||
class MQ():
|
||||
|
||||
######################### Hardware Related Macros #########################
|
||||
MQ_PIN = 0 # define which analog input channel you are going to use (MCP3008)
|
||||
RL_VALUE = 5 # define the load resistance on the board, in kilo ohms
|
||||
RO_CLEAN_AIR_FACTOR = 9.83 # RO_CLEAR_AIR_FACTOR=(Sensor resistance in clean air)/RO,
|
||||
# which is derived from the chart in datasheet
|
||||
|
||||
######################### Software Related Macros #########################
|
||||
CALIBARAION_SAMPLE_TIMES = 50 # define how many samples you are going to take in the calibration phase
|
||||
CALIBRATION_SAMPLE_INTERVAL = 500 # define the time interal(in milisecond) between each samples in the
|
||||
# cablibration phase
|
||||
READ_SAMPLE_INTERVAL = 50 # define how many samples you are going to take in normal operation
|
||||
READ_SAMPLE_TIMES = 5 # define the time interal(in milisecond) between each samples in
|
||||
# normal operation
|
||||
|
||||
######################### Application Related Macros ######################
|
||||
GAS_LPG = 0
|
||||
GAS_CO = 1
|
||||
GAS_SMOKE = 2
|
||||
|
||||
def __init__(self, Ro=10, analogPin=0):
|
||||
self.Ro = Ro
|
||||
self.MQ_PIN = analogPin
|
||||
self.adc = MCP3008()
|
||||
|
||||
self.LPGCurve = [2.3,0.21,-0.47] # two points are taken from the curve.
|
||||
# with these two points, a line is formed which is "approximately equivalent"
|
||||
# to the original curve.
|
||||
# data format:{ x, y, slope}; point1: (lg200, 0.21), point2: (lg10000, -0.59)
|
||||
self.COCurve = [2.3,0.72,-0.34] # two points are taken from the curve.
|
||||
# with these two points, a line is formed which is "approximately equivalent"
|
||||
# to the original curve.
|
||||
# data format:[ x, y, slope]; point1: (lg200, 0.72), point2: (lg10000, 0.15)
|
||||
self.SmokeCurve =[2.3,0.53,-0.44] # two points are taken from the curve.
|
||||
# with these two points, a line is formed which is "approximately equivalent"
|
||||
# to the original curve.
|
||||
# data format:[ x, y, slope]; point1: (lg200, 0.53), point2: (lg10000, -0.22)
|
||||
|
||||
print("Calibrating...")
|
||||
self.Ro = self.MQCalibration(self.MQ_PIN)
|
||||
print("Calibration is done...\n")
|
||||
print("Ro=%f kohm" % self.Ro)
|
||||
|
||||
|
||||
def MQPercentage(self):
|
||||
val = {}
|
||||
read = self.MQRead(self.MQ_PIN)
|
||||
val["GAS_LPG"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_LPG)
|
||||
val["CO"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_CO)
|
||||
val["SMOKE"] = self.MQGetGasPercentage(read/self.Ro, self.GAS_SMOKE)
|
||||
return val
|
||||
|
||||
######################### MQResistanceCalculation #########################
|
||||
# Input: raw_adc - raw value read from adc, which represents the voltage
|
||||
# Output: the calculated sensor resistance
|
||||
# Remarks: The sensor and the load resistor forms a voltage divider. Given the voltage
|
||||
# across the load resistor and its resistance, the resistance of the sensor
|
||||
# could be derived.
|
||||
############################################################################
|
||||
def MQResistanceCalculation(self, raw_adc):
|
||||
return float(self.RL_VALUE*(1023.0-raw_adc)/float(raw_adc));
|
||||
|
||||
|
||||
######################### MQCalibration ####################################
|
||||
# Input: mq_pin - analog channel
|
||||
# Output: Ro of the sensor
|
||||
# Remarks: This function assumes that the sensor is in clean air. It use
|
||||
# MQResistanceCalculation to calculates the sensor resistance in clean air
|
||||
# and then divides it with RO_CLEAN_AIR_FACTOR. RO_CLEAN_AIR_FACTOR is about
|
||||
# 10, which differs slightly between different sensors.
|
||||
############################################################################
|
||||
def MQCalibration(self, mq_pin):
|
||||
val = 0.0
|
||||
for i in range(self.CALIBARAION_SAMPLE_TIMES): # take multiple samples
|
||||
val += self.MQResistanceCalculation(self.adc.read(mq_pin))
|
||||
time.sleep(self.CALIBRATION_SAMPLE_INTERVAL/1000.0)
|
||||
|
||||
val = val/self.CALIBARAION_SAMPLE_TIMES # calculate the average value
|
||||
|
||||
val = val/self.RO_CLEAN_AIR_FACTOR # divided by RO_CLEAN_AIR_FACTOR yields the Ro
|
||||
# according to the chart in the datasheet
|
||||
|
||||
return val;
|
||||
|
||||
|
||||
######################### MQRead ##########################################
|
||||
# Input: mq_pin - analog channel
|
||||
# Output: Rs of the sensor
|
||||
# Remarks: This function use MQResistanceCalculation to caculate the sensor resistenc (Rs).
|
||||
# The Rs changes as the sensor is in the different consentration of the target
|
||||
# gas. The sample times and the time interval between samples could be configured
|
||||
# by changing the definition of the macros.
|
||||
############################################################################
|
||||
def MQRead(self, mq_pin):
|
||||
rs = 0.0
|
||||
|
||||
for i in range(self.READ_SAMPLE_TIMES):
|
||||
rs += self.MQResistanceCalculation(self.adc.read(mq_pin))
|
||||
time.sleep(self.READ_SAMPLE_INTERVAL/1000.0)
|
||||
|
||||
rs = rs/self.READ_SAMPLE_TIMES
|
||||
|
||||
return rs
|
||||
|
||||
######################### MQGetGasPercentage ##############################
|
||||
# Input: rs_ro_ratio - Rs divided by Ro
|
||||
# gas_id - target gas type
|
||||
# Output: ppm of the target gas
|
||||
# Remarks: This function passes different curves to the MQGetPercentage function which
|
||||
# calculates the ppm (parts per million) of the target gas.
|
||||
############################################################################
|
||||
def MQGetGasPercentage(self, rs_ro_ratio, gas_id):
|
||||
if ( gas_id == self.GAS_LPG ):
|
||||
return self.MQGetPercentage(rs_ro_ratio, self.LPGCurve)
|
||||
elif ( gas_id == self.GAS_CO ):
|
||||
return self.MQGetPercentage(rs_ro_ratio, self.COCurve)
|
||||
elif ( gas_id == self.GAS_SMOKE ):
|
||||
return self.MQGetPercentage(rs_ro_ratio, self.SmokeCurve)
|
||||
return 0
|
||||
|
||||
######################### MQGetPercentage #################################
|
||||
# Input: rs_ro_ratio - Rs divided by Ro
|
||||
# pcurve - pointer to the curve of the target gas
|
||||
# Output: ppm of the target gas
|
||||
# Remarks: By using the slope and a point of the line. The x(logarithmic value of ppm)
|
||||
# of the line could be derived if y(rs_ro_ratio) is provided. As it is a
|
||||
# logarithmic coordinate, power of 10 is used to convert the result to non-logarithmic
|
||||
# value.
|
||||
############################################################################
|
||||
def MQGetPercentage(self, rs_ro_ratio, pcurve):
|
||||
return (math.pow(10,( ((math.log(rs_ro_ratio)-pcurve[1])/ pcurve[2]) + pcurve[0])))
|
||||
|
||||
BIN
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/mq.pyc
Normal file
BIN
RPI Code/MQ-2_/Raspberry-Pi-Gas-Sensor-MQ/mq.pyc
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
||||
Subproject commit 34f9422f89605c0994ae718d8ec42322ddaec996
|
||||
110
RPI Code/Meross_/MerossIot/.gitignore
vendored
Normal file
110
RPI Code/Meross_/MerossIot/.gitignore
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
# 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/
|
||||
*.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/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# 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/
|
||||
|
||||
# Local test
|
||||
local_test/
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
21
RPI Code/Meross_/MerossIot/LICENSE
Normal file
21
RPI Code/Meross_/MerossIot/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alberto Geniola
|
||||
|
||||
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.
|
||||
102
RPI Code/Meross_/MerossIot/README.md
Normal file
102
RPI Code/Meross_/MerossIot/README.md
Normal file
@ -0,0 +1,102 @@
|
||||
[](https://albertogeniola.visualstudio.com/Meross/_build/latest?definitionId=1)
|
||||

|
||||
|
||||
# Meross IoT library
|
||||
A pure-python based library providing API for controlling Meross IoT devices over the internet.
|
||||
|
||||
To see what devices are currently supported, checkout the *Currently supported devices* section.
|
||||
Hopefully, more Meross hardware will be supported in the future.
|
||||
|
||||
This library is still work in progress, therefore use it with caution.
|
||||
|
||||
## Installation
|
||||
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
|
||||
So, the installation is as simple as typing the following command:
|
||||
|
||||
```
|
||||
pip install meross_iot --upgrade
|
||||
```
|
||||
|
||||
## Usage
|
||||
The following script demonstrates how to use this library.
|
||||
|
||||
```python
|
||||
import time
|
||||
import sys
|
||||
from meross_iot.api import MerossHttpClient
|
||||
|
||||
if __name__=='__main__':
|
||||
httpHandler = MerossHttpClient(email="YOUR_MEROSS_CLOUD_EMAIL", password="YOUR_PASSWORD")
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
print("Listing Devices...")
|
||||
devices = httpHandler.list_supported_devices()
|
||||
|
||||
for counter, device in enumerate(devices):
|
||||
print("Playing with device: %d" % counter)
|
||||
# Returns most of the info about the power plug
|
||||
print("\nGetting system data...")
|
||||
data = device.get_sys_data()
|
||||
|
||||
# Turns the power-plug on
|
||||
print("\nTurning the device on...")
|
||||
device.turn_off()
|
||||
|
||||
# Turns the power-plug off
|
||||
print("\nTurning the device off...")
|
||||
device.turn_on()
|
||||
|
||||
# Reads the historical device consumption
|
||||
print("\nReading consumption data...")
|
||||
consumption = device.get_power_consumptionX()
|
||||
|
||||
# Returns the list of WIFI Network available for the plug
|
||||
# (Note. this takes some time to complete)
|
||||
print("\nScanning Wifi...")
|
||||
wifi_list = device.get_wifi_list()
|
||||
|
||||
# Info about the device
|
||||
print("\nGetting device trace...")
|
||||
trace = device.get_trace()
|
||||
print("\nGetting device debug...")
|
||||
debug = device.get_debug()
|
||||
|
||||
# Returns the capabilities of this device
|
||||
print("\nRetrieving device abilities...")
|
||||
abilities = device.get_abilities()
|
||||
|
||||
# I still have to figure this out :S
|
||||
# The following command is not yet implemented on all devices
|
||||
# and might not work as expected.
|
||||
# report = device.get_report()
|
||||
|
||||
# Returns the current power consumption and voltage from the plug
|
||||
# (Note: this is not really realtime, but close enough)
|
||||
print("\nReading electricity...")
|
||||
electricity = device.get_electricity()
|
||||
|
||||
```
|
||||
|
||||
## Currently supported devices
|
||||
Even though this library was firstly meant to drive only the Meross MSS310,
|
||||
other nice developers contributed to its realization. The following is the
|
||||
currently supported list of devices:
|
||||
|
||||
- MSS310 both hw v1 and v2 (Thanks to [DanoneKiD](https://github.com/DanoneKiD))
|
||||
- MSS210 (Thanks to [ictes](https://github.com/ictes))
|
||||
- MSS110 (Thanks to [soberstadt](https://github.com/soberstadt))
|
||||
- MSS425E (Thanks to [ping-localhost](https://github.com/ping-localhost))
|
||||
|
||||
## Protocol details
|
||||
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
|
||||
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
|
||||
|
||||
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
|
||||
|
||||
## Donate!
|
||||
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy. However, if you liked or appreciated by work, why don't you buy me a beer? It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6HPAB89UYSZF2)
|
||||
|
||||
|
||||
|
||||
102
RPI Code/Meross_/MerossIot/README.md.save
Normal file
102
RPI Code/Meross_/MerossIot/README.md.save
Normal file
@ -0,0 +1,102 @@
|
||||
[](https://albertogeniola.visualstudio.com/Meross/_build/latest?definitionId=1)
|
||||

|
||||
|
||||
# Meross IoT library
|
||||
A pure-python based library providing API for controlling Meross IoT devices over the internet.
|
||||
|
||||
To see what devices are currently supported, checkout the *Currently supported devices* section.
|
||||
Hopefully, more Meross hardware will be supported in the future.
|
||||
|
||||
This library is still work in progress, therefore use it with caution.
|
||||
|
||||
## Installation
|
||||
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
|
||||
So, the installation is as simple as typing the following command:
|
||||
|
||||
```
|
||||
pip install meross_iot --upgrade
|
||||
```
|
||||
|
||||
## Usage
|
||||
The following script demonstrates how to use this library.
|
||||
|
||||
```python
|
||||
import time
|
||||
import sys
|
||||
from meross_iot.api import MerossHttpClient
|
||||
|
||||
if __name__=='__main__':
|
||||
httpHandler = MerossHttpClient(email="YOUR_MEROSS_CLOUD_EMAIL", password="YOUR_PASSWORD")
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
print("Listing Devices...")
|
||||
devices = httpHandler.list_supported_devices()
|
||||
|
||||
for counter, device in enumerate(devices):
|
||||
print("Playing with device: %d" % counter)
|
||||
# Returns most of the info about the power plug
|
||||
print("\nGetting system data...")
|
||||
data = device.get_sys_data()
|
||||
|
||||
# Turns the power-plug on
|
||||
print("\nTurning the device on...")
|
||||
device.turn_off()
|
||||
|
||||
# Turns the power-plug off
|
||||
print("\nTurning the device off...")
|
||||
device.turn_on()
|
||||
|
||||
# Reads the historical device consumption
|
||||
print("\nReading consumption data...")
|
||||
consumption = device.get_power_consumptionX()
|
||||
|
||||
# Returns the list of WIFI Network available for the plug
|
||||
# (Note. this takes some time to complete)
|
||||
print("\nScanning Wifi...")
|
||||
wifi_list = device.get_wifi_list()
|
||||
|
||||
# Info about the device
|
||||
print("\nGetting device trace...")
|
||||
trace = device.get_trace()
|
||||
print("\nGetting device debug...")
|
||||
debug = device.get_debug()
|
||||
|
||||
# Returns the capabilities of this device
|
||||
print("\nRetrieving device abilities...")
|
||||
abilities = device.get_abilities()
|
||||
|
||||
# I still have to figure this out :S
|
||||
# The following command is not yet implemented on all devices
|
||||
# and might not work as expected.
|
||||
# report = device.get_report()
|
||||
|
||||
# Returns the current power consumption and voltage from the plug
|
||||
# (Note: this is not really realtime, but close enough)
|
||||
print("\nReading electricity...")
|
||||
electricity = device.get_electricity()
|
||||
|
||||
```
|
||||
|
||||
## Currently supported devices
|
||||
Even though this library was firstly meant to drive only the Meross MSS310,
|
||||
other nice developers contributed to its realization. The following is the
|
||||
currently supported list of devices:
|
||||
|
||||
- MSS310 both hw v1 and v2 (Thanks to [DanoneKiD](https://github.com/DanoneKiD))
|
||||
- MSS210 (Thanks to [ictes](https://github.com/ictes))
|
||||
- MSS110 (Thanks to [soberstadt](https://github.com/soberstadt))
|
||||
- MSS425E (Thanks to [ping-localhost](https://github.com/ping-localhost))
|
||||
|
||||
## Protocol details
|
||||
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
|
||||
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
|
||||
|
||||
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
|
||||
|
||||
## Donate!
|
||||
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy. However, if you liked or appreciated by work, why don't you buy me a beer? It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6HPAB89UYSZF2)
|
||||
|
||||
|
||||
|
||||
BIN
RPI Code/Meross_/MerossIot/ext-res/meross_cloud_arch.png
Normal file
BIN
RPI Code/Meross_/MerossIot/ext-res/meross_cloud_arch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" version="9.4.6" editor="www.draw.io" type="github"><diagram id="838e86de-a6c6-3703-056b-65dabb823eb9" name="Page-1">7Vjfj5s4EP5rIt09BBEMIXlMsptrpVbKaVe63tPKCw74ahjOOJukf/2Nwfxme9s23UirJg+xPw9mZr75hpgJ2SSnPyTN4o8QMjFx7PA0ITcTx5m5zhx/NHIukYXtlkAkeWiMGuCOf2EGtA164CHLO4YKQCiedcEA0pQFqoNRKeHYNduD6N41oxEbAHcBFUP0Lx6q2ETh+A3+jvEoru48my/LlYRWxiaSPKYhHFsQuZ2QjQRQ5Sg5bZjQyavyUl63fWa1dkyyVL3oAsPEExUHE9zHP+/vEVlL+MzkxJkL3Gj9qEeRHv3GQVkJk5DnVgDJhKwc2579PjQ0AapzlTU4KMFTtqlJsdFqD6nagABZ2BD8brWn60jSkLNmLYUUt1nnSrs1dsGeC9HC98UH8ZDmMQvN3Z6YVBx5/EAfmdhBzhWHFNceQSkdS22wEjzSCwoyRKmZBegQ5oSsY5UInM9MAKZCZ041N0HrW9I8K6Pd85P2Y42UZ3oxOUVaHRY95q4lWQ4HGbD3gfZnjdNy1LVK/lXqIZOgIABRp1j7zE7PFsCsLivUI4OEKXlGE3OB45tKNFJ0HK+cH5vCnhuTuFXTFUaNlKJ656bacGAK7pniWw6K7939/S5HaLV7P6ifWit2n9YWHd0CMVUjenTXPF6qHvqcJnlAmRUIOIQWzfhDQlPsCUnBSb9Sbdtf3swvw6Y777NpD9j0R9j0L8AmmQ0YYyG2TTMFqWKIIKXitkGxyRzScIxHduLqk4Ytz8z+Nkb/MKXORnH0oAChZu8PoAkqdii90S58PZnocSE9k7HqeUJlxCqz+XjSJRNU8afu/j+SwmqPdje+uyPVY6+rhaLgeFI8qtqZGy/Z/630QiNrGnyOCk7GWmlxs1XVz0abm/HnJlZKP4tXOnRnG4QpsTg2tD1HtmXx3HC2IVUUfzSOkt8esy8AyTRkT0xAxuRUL0xzJMHZejamYEvs6cxZWFkaXaLx2V5HKu5sKBVvRCreJaTivAmpONeUivNLKq8mFf+KUnHfhFTINaVCfknltaRC3CtKxf85UvH7WsFcyPOnyk5PihVrXk13THJ0X/81/iZx9c9wi4AFwdip73HhuZ793XIk3ogcl68kR7L4OTQ539vSrpr1H22CxaUrKem5ZZABT1Xe2nmngdYxyfc7KiWk95akZ09s+2v2OCg9aPivQ3lZSXjDDl28YSlOwrsXvFt5g507AogEmyYU+winAlt4jgFMq2V3Udg+5AmVKovxoP/gLrLTBXu56y2sbjcnyxppH6jJsJ9X2Dc0Cpw27/3KImrenpLb/wA=</diagram></mxfile>
|
||||
1
RPI Code/Meross_/MerossIot/meross_iot/__init__.py
Normal file
1
RPI Code/Meross_/MerossIot/meross_iot/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
name = "meross_iot"
|
||||
137
RPI Code/Meross_/MerossIot/meross_iot/api.py
Normal file
137
RPI Code/Meross_/MerossIot/meross_iot/api.py
Normal file
@ -0,0 +1,137 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from meross_iot.device_factory import build_wrapper
|
||||
|
||||
# Appears to be used as a part of the signature algorithm as constant "salt" (kinda useless)
|
||||
_SECRET = "23x17ahWarFH6w29"
|
||||
_MEROSS_URL = "https://iot.meross.com"
|
||||
_LOGIN_URL = "%s%s" % (_MEROSS_URL, "/v1/Auth/Login")
|
||||
_LOG_URL = "%s%s" % (_MEROSS_URL, "/v1/log/user")
|
||||
_DEV_LIST = "%s%s" % (_MEROSS_URL, "/v1/Device/devList")
|
||||
|
||||
|
||||
class MerossHttpClient:
|
||||
_token = None
|
||||
_key = None
|
||||
_userid = None
|
||||
_userEmail = None
|
||||
|
||||
_email = None
|
||||
_password = None
|
||||
_authenticated = False
|
||||
|
||||
def __init__(self, email, password):
|
||||
self._email = email
|
||||
self._password = password
|
||||
|
||||
def _authenticated_post(self,
|
||||
url, # type: str
|
||||
params_data # type: dict
|
||||
):
|
||||
|
||||
nonce = self._generate_nonce(16)
|
||||
timestamp_millis = int(round(time.time() * 1000))
|
||||
login_params = self._encode_params(params_data)
|
||||
|
||||
# Generate the md5-hash (called signature)
|
||||
m = hashlib.md5()
|
||||
datatosign = '%s%s%s%s' % (_SECRET, timestamp_millis, nonce, login_params)
|
||||
m.update(datatosign.encode("utf8"))
|
||||
md5hash = m.hexdigest()
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic" if self._token is None else "Basic %s" % self._token,
|
||||
"vender": "Meross",
|
||||
"AppVersion": "1.3.0",
|
||||
"AppLanguage": "EN",
|
||||
"User-Agent": "okhttp/3.6.0"
|
||||
}
|
||||
|
||||
payload = {
|
||||
'params': login_params,
|
||||
'sign': md5hash,
|
||||
'timestamp': timestamp_millis,
|
||||
'nonce': nonce
|
||||
}
|
||||
|
||||
# Perform the request.
|
||||
r = requests.post(url, data=payload, headers=headers)
|
||||
|
||||
# Check if that is ok.
|
||||
if r.status_code != 200:
|
||||
raise AuthenticatedPostException()
|
||||
|
||||
# Save returned value
|
||||
jsondata = r.json()
|
||||
# print(jsondata)
|
||||
|
||||
if jsondata["info"].lower() != "success":
|
||||
raise AuthenticatedPostException()
|
||||
|
||||
return jsondata["data"]
|
||||
|
||||
def _encode_params(self,
|
||||
parameters # type: dict
|
||||
):
|
||||
jsonstring = json.dumps(parameters)
|
||||
return str(base64.b64encode(jsonstring.encode("utf8")), "utf8")
|
||||
|
||||
def _generate_nonce(self, length):
|
||||
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
||||
|
||||
def _login(self):
|
||||
try:
|
||||
data = {"email": self._email, "password": self._password}
|
||||
response_data = self._authenticated_post(_LOGIN_URL, params_data=data)
|
||||
self._token = response_data["token"]
|
||||
self._key = response_data["key"]
|
||||
self._userid = response_data["userid"]
|
||||
self._userEmail = response_data["email"]
|
||||
self._authenticated = True
|
||||
except:
|
||||
return False
|
||||
|
||||
try:
|
||||
# The protocol does not really need the following call. However we want to be nice do it anyways
|
||||
self._log()
|
||||
except:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def _log(self, ):
|
||||
data = {'extra': {}, 'model': 'Android,Android SDK built for x86_64', 'system': 'Android',
|
||||
'uuid': '493dd9174941ed58waitForOpenWifi', 'vendor': 'Meross', 'version': '6.0'}
|
||||
response_data = self._authenticated_post(_LOG_URL, params_data=data)
|
||||
|
||||
def list_devices(self):
|
||||
if not self._authenticated and not self._login():
|
||||
raise UnauthorizedException()
|
||||
|
||||
return self._authenticated_post(_DEV_LIST, {})
|
||||
|
||||
def list_supported_devices(self):
|
||||
supported_devices = []
|
||||
for dev in self.list_devices():
|
||||
deviceType = dev['deviceType']
|
||||
device = build_wrapper(self._token, self._key, self._userid, deviceType, dev)
|
||||
if device is not None:
|
||||
supported_devices.append(device)
|
||||
# else log...
|
||||
|
||||
return supported_devices
|
||||
|
||||
|
||||
class AuthenticatedPostException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnauthorizedException(Exception):
|
||||
pass
|
||||
20
RPI Code/Meross_/MerossIot/meross_iot/device_factory.py
Normal file
20
RPI Code/Meross_/MerossIot/meross_iot/device_factory.py
Normal file
@ -0,0 +1,20 @@
|
||||
from meross_iot.supported_devices.power_plugs import Mss310, Mss210, Mss110, Mss425e
|
||||
|
||||
|
||||
def build_wrapper(
|
||||
token,
|
||||
key,
|
||||
user_id,
|
||||
device_type, # type: str
|
||||
device_specs # type: dict
|
||||
):
|
||||
if device_type.lower() == "mss310":
|
||||
return Mss310(token, key, user_id, **device_specs)
|
||||
elif device_type.lower() == "mss210":
|
||||
return Mss210(token, key, user_id, **device_specs)
|
||||
elif device_type.lower() == "mss110":
|
||||
return Mss110(token, key, user_id, **device_specs)
|
||||
elif device_type.lower() == "mss425e":
|
||||
return Mss425e(token, key, user_id, **device_specs)
|
||||
else:
|
||||
return None
|
||||
@ -0,0 +1,412 @@
|
||||
debug = False
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import ssl
|
||||
import string
|
||||
import sys
|
||||
import time
|
||||
from enum import Enum
|
||||
from hashlib import md5
|
||||
from logging import StreamHandler
|
||||
from threading import RLock, Condition
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
from paho.mqtt import MQTTException
|
||||
|
||||
from meross_iot.utilities.synchronization import AtomicCounter
|
||||
|
||||
l = logging.getLogger("meross_powerplug")
|
||||
l.addHandler(StreamHandler(stream=sys.stdout))
|
||||
l.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class ClientStatus(Enum):
|
||||
INITIALIZED = 1
|
||||
CONNECTING = 2
|
||||
CONNECTED = 3
|
||||
SUBSCRIBED = 4
|
||||
CONNECTION_DROPPED = 5
|
||||
|
||||
|
||||
class Device:
|
||||
_status_lock = None
|
||||
_client_status = None
|
||||
|
||||
_token = None
|
||||
_key = None
|
||||
_user_id = None
|
||||
_domain = None
|
||||
_port = 2001
|
||||
_channels = []
|
||||
|
||||
_uuid = None
|
||||
_client_id = None
|
||||
_app_id = None
|
||||
|
||||
# Device informations
|
||||
_name = None
|
||||
_type = None
|
||||
_hwversion = None
|
||||
_fwversion = None
|
||||
|
||||
# Topic name where the client should publish to its commands. Every client should have a dedicated one.
|
||||
_client_request_topic = None
|
||||
|
||||
# Topic name in which the client retrieves its own responses from server.
|
||||
_client_response_topic = None
|
||||
|
||||
# Topic where important notifications are pushed (needed if any other client is dealing with the same device)
|
||||
_user_topic = None
|
||||
|
||||
# Paho mqtt client object
|
||||
_mqtt_client = None
|
||||
|
||||
# Waiting condition used to wait for command ACKs
|
||||
_waiting_message_ack_queue = None
|
||||
_waiting_subscribers_queue = None
|
||||
_waiting_message_id = None
|
||||
|
||||
_ack_response = None
|
||||
|
||||
# Block for at most 10 seconds.
|
||||
_command_timeout = 10
|
||||
|
||||
_error = None
|
||||
|
||||
_status = None
|
||||
|
||||
def __init__(self,
|
||||
token,
|
||||
key,
|
||||
user_id,
|
||||
**kwords):
|
||||
|
||||
self._status_lock = RLock()
|
||||
|
||||
self._waiting_message_ack_queue = Condition()
|
||||
self._waiting_subscribers_queue = Condition()
|
||||
self._subscription_count = AtomicCounter(0)
|
||||
|
||||
self._set_status(ClientStatus.INITIALIZED)
|
||||
|
||||
self._token = token,
|
||||
self._key = key
|
||||
self._user_id = user_id
|
||||
self._uuid = kwords['uuid']
|
||||
if "domain" in kwords:
|
||||
self._domain = kwords['domain']
|
||||
else:
|
||||
self._domain = "eu-iot.meross.com"
|
||||
if "channels" in kwords:
|
||||
self._channels = kwords['channels']
|
||||
|
||||
# Informations about device
|
||||
if "devName" in kwords:
|
||||
self._name = kwords['devName']
|
||||
if "deviceType" in kwords:
|
||||
self._type = kwords['deviceType']
|
||||
if "fmwareVersion" in kwords:
|
||||
self._fwversion = kwords['fmwareVersion']
|
||||
if "hdwareVersion" in kwords:
|
||||
self._hwversion = kwords['hdwareVersion']
|
||||
|
||||
self._generate_client_and_app_id()
|
||||
|
||||
# Password is calculated as the MD5 of USERID concatenated with KEY
|
||||
md5_hash = md5()
|
||||
clearpwd = "%s%s" % (self._user_id, self._key)
|
||||
md5_hash.update(clearpwd.encode("utf8"))
|
||||
hashed_password = md5_hash.hexdigest()
|
||||
|
||||
# Start the mqtt client
|
||||
self._mqtt_client = mqtt.Client(client_id=self._client_id,
|
||||
protocol=mqtt.MQTTv311) # ex. app-id -> app:08d4c9f99da40203ebc798a76512ec14
|
||||
self._mqtt_client.on_connect = self._on_connect
|
||||
self._mqtt_client.on_message = self._on_message
|
||||
self._mqtt_client.on_disconnect = self._on_disconnect
|
||||
self._mqtt_client.on_subscribe = self._on_subscribe
|
||||
self._mqtt_client.on_log = self._on_log
|
||||
self._mqtt_client.username_pw_set(username=self._user_id, password=hashed_password)
|
||||
self._mqtt_client.tls_set(ca_certs=None, certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
ciphers=None)
|
||||
|
||||
self._mqtt_client.connect(self._domain, self._port, keepalive=30)
|
||||
self._set_status(ClientStatus.CONNECTING)
|
||||
|
||||
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
|
||||
self._mqtt_client.loop_start()
|
||||
|
||||
with self._waiting_subscribers_queue:
|
||||
self._waiting_subscribers_queue.wait()
|
||||
if self._client_status != ClientStatus.SUBSCRIBED:
|
||||
# An error has occurred
|
||||
raise Exception(self._error)
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc):
|
||||
l.info("Disconnection detected. Reason: %s" % str(rc))
|
||||
|
||||
# We should clean all the data structures.
|
||||
with self._status_lock:
|
||||
self._subscription_count = AtomicCounter(0)
|
||||
self._error = "Connection dropped by the server"
|
||||
self._set_status(ClientStatus.CONNECTION_DROPPED)
|
||||
|
||||
with self._waiting_subscribers_queue:
|
||||
self._waiting_subscribers_queue.notify_all()
|
||||
|
||||
with self._waiting_message_ack_queue:
|
||||
self._waiting_message_ack_queue.notify_all()
|
||||
|
||||
if rc == mqtt.MQTT_ERR_SUCCESS:
|
||||
pass
|
||||
else:
|
||||
# TODO: Should we reconnect by calling again the client.loop_start() ?
|
||||
client.loop_stop()
|
||||
|
||||
def _on_unsubscribe(self):
|
||||
l.info("Unsubscribed from topic")
|
||||
self._subscription_count.dec()
|
||||
|
||||
def _on_subscribe(self, client, userdata, mid, granted_qos):
|
||||
l.info("Succesfully subscribed!")
|
||||
if self._subscription_count.inc() == 2:
|
||||
with self._waiting_subscribers_queue:
|
||||
self._set_status(ClientStatus.SUBSCRIBED)
|
||||
self._waiting_subscribers_queue.notify_all()
|
||||
|
||||
def _on_connect(self, client, userdata, rc, other):
|
||||
l.info("Connected with result code %s" % str(rc))
|
||||
self._set_status(ClientStatus.SUBSCRIBED)
|
||||
|
||||
self._set_status(ClientStatus.CONNECTED)
|
||||
|
||||
self._client_request_topic = "/appliance/%s/subscribe" % self._uuid
|
||||
self._client_response_topic = "/app/%s-%s/subscribe" % (self._user_id, self._app_id)
|
||||
self._user_topic = "/app/%s/subscribe" % self._user_id
|
||||
|
||||
# Subscribe to the relevant topics
|
||||
l.info("Subscribing to topics...")
|
||||
client.subscribe(self._user_topic)
|
||||
client.subscribe(self._client_response_topic)
|
||||
|
||||
# The callback for when a PUBLISH message is received from the server.
|
||||
def _on_message(self, client, userdata, msg):
|
||||
l.debug(msg.topic + " --> " + str(msg.payload))
|
||||
|
||||
try:
|
||||
message = json.loads(str(msg.payload, "utf8"))
|
||||
header = message['header']
|
||||
|
||||
message_hash = md5()
|
||||
strtohash = "%s%s%s" % (header['messageId'], self._key, header['timestamp'])
|
||||
message_hash.update(strtohash.encode("utf8"))
|
||||
expected_signature = message_hash.hexdigest().lower()
|
||||
|
||||
if (header['sign'] != expected_signature):
|
||||
raise MQTTException('The signature did not match!')
|
||||
|
||||
# If the message is the RESP for some previous action, process return the control to the "stopped" method.
|
||||
if header['messageId'] == self._waiting_message_id:
|
||||
with self._waiting_message_ack_queue:
|
||||
self._ack_response = message
|
||||
self._waiting_message_ack_queue.notify()
|
||||
|
||||
# Otherwise process it accordingly
|
||||
elif self._message_from_self(message):
|
||||
if header['method'] == "PUSH" and 'payload' in message and 'toggle' in message['payload']:
|
||||
self._handle_toggle(message)
|
||||
else:
|
||||
l.debug("UNKNOWN msg received by %s" % self._uuid)
|
||||
# if header['method'] == "PUSH":
|
||||
# TODO
|
||||
else:
|
||||
# do nothing because the message was from a different device
|
||||
pass
|
||||
except Exception as e:
|
||||
l.debug("%s failed to process message because: %s" % (self._uuid, e))
|
||||
|
||||
def _on_log(self, client, userdata, level, buf):
|
||||
# print("Data: %s - Buff: %s" % (userdata, buf))
|
||||
pass
|
||||
|
||||
def _generate_client_and_app_id(self):
|
||||
md5_hash = md5()
|
||||
md5_hash.update(("%s%s" % ("API", self._uuid)).encode("utf8"))
|
||||
self._app_id = md5_hash.hexdigest()
|
||||
self._client_id = 'app:%s' % md5_hash.hexdigest()
|
||||
|
||||
def _mqtt_message(self, method, namespace, payload):
|
||||
# Generate a random 16 byte string
|
||||
randomstring = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(16))
|
||||
|
||||
# Hash it as md5
|
||||
md5_hash = md5()
|
||||
md5_hash.update(randomstring.encode('utf8'))
|
||||
messageId = md5_hash.hexdigest().lower()
|
||||
timestamp = int(round(time.time()))
|
||||
|
||||
# Hash the messageId, the key and the timestamp
|
||||
md5_hash = md5()
|
||||
strtohash = "%s%s%s" % (messageId, self._key, timestamp)
|
||||
md5_hash.update(strtohash.encode("utf8"))
|
||||
signature = md5_hash.hexdigest().lower()
|
||||
|
||||
data = {
|
||||
"header":
|
||||
{
|
||||
"from": self._client_response_topic,
|
||||
"messageId": messageId, # Example: "122e3e47835fefcd8aaf22d13ce21859"
|
||||
"method": method, # Example: "GET",
|
||||
"namespace": namespace, # Example: "Appliance.System.All",
|
||||
"payloadVersion": 1,
|
||||
"sign": signature, # Example: "b4236ac6fb399e70c3d61e98fcb68b74",
|
||||
"timestamp": timestamp
|
||||
},
|
||||
"payload": payload
|
||||
}
|
||||
strdata = json.dumps(data)
|
||||
l.debug("--> %s" % strdata)
|
||||
self._mqtt_client.publish(topic=self._client_request_topic, payload=strdata.encode("utf-8"))
|
||||
return messageId
|
||||
|
||||
def _wait_for_status(self, status):
|
||||
ok = False
|
||||
while not ok:
|
||||
if not self._status_lock.acquire(True, self._command_timeout):
|
||||
raise TimeoutError()
|
||||
ok = status == self._client_status
|
||||
self._status_lock.release()
|
||||
|
||||
def _set_status(self, status):
|
||||
with self._status_lock:
|
||||
self._client_status = status
|
||||
|
||||
def _execute_cmd(self, method, namespace, payload):
|
||||
with self._waiting_subscribers_queue:
|
||||
while self._client_status != ClientStatus.SUBSCRIBED:
|
||||
self._waiting_subscribers_queue.wait()
|
||||
|
||||
# Execute the command and retrieve the message-id
|
||||
self._waiting_message_id = self._mqtt_message(method, namespace, payload)
|
||||
|
||||
# Wait synchronously until we get the ACK.
|
||||
with self._waiting_message_ack_queue:
|
||||
self._waiting_message_ack_queue.wait()
|
||||
|
||||
return self._ack_response['payload']
|
||||
|
||||
def _message_from_self(self, message):
|
||||
try:
|
||||
return 'from' in message['header'] and message['header']['from'].split('/')[2] == self._uuid
|
||||
except:
|
||||
return False
|
||||
|
||||
def _handle_toggle(self, message):
|
||||
if 'onoff' in message['payload']['toggle']:
|
||||
self._status = (message['payload']['toggle']['onoff'] == 1)
|
||||
|
||||
def get_sys_data(self):
|
||||
return self._execute_cmd("GET", "Appliance.System.All", {})
|
||||
|
||||
def get_wifi_list(self):
|
||||
return self._execute_cmd("GET", "Appliance.Config.WifiList", {})
|
||||
|
||||
def get_trace(self):
|
||||
return self._execute_cmd("GET", "Appliance.Config.Trace", {})
|
||||
|
||||
def get_debug(self):
|
||||
return self._execute_cmd("GET", "Appliance.System.Debug", {})
|
||||
|
||||
def get_abilities(self):
|
||||
return self._execute_cmd("GET", "Appliance.System.Ability", {})
|
||||
|
||||
def get_report(self):
|
||||
return self._execute_cmd("GET", "Appliance.System.Report", {})
|
||||
|
||||
def get_status(self):
|
||||
if self._status is None:
|
||||
self._status = self.get_sys_data()['all']['control']['toggle']['onoff'] == 1
|
||||
return self._status
|
||||
|
||||
def device_id(self):
|
||||
return self._uuid
|
||||
|
||||
def get_channels(self):
|
||||
return self._channels
|
||||
|
||||
def turn_on(self):
|
||||
self._status = True
|
||||
payload = {"channel": 0, "toggle": {"onoff": 1}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
|
||||
|
||||
def turn_off(self):
|
||||
self._status = False
|
||||
payload = {"channel": 0, "toggle": {"onoff": 0}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
|
||||
|
||||
|
||||
class Mss310(Device):
|
||||
def get_power_consumptionX(self):
|
||||
return self._execute_cmd("GET", "Appliance.Control.ConsumptionX", {})
|
||||
|
||||
def get_electricity(self):
|
||||
return self._execute_cmd("GET", "Appliance.Control.Electricity", {})
|
||||
|
||||
def turn_on(self):
|
||||
if self._hwversion.split(".")[0] == "2":
|
||||
payload = {'togglex': {"onoff": 1}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
else:
|
||||
payload = {"channel": 0, "toggle": {"onoff": 1}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
|
||||
|
||||
def turn_off(self):
|
||||
if self._hwversion.split(".")[0] == "2":
|
||||
payload = {'togglex': {"onoff": 0}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
else:
|
||||
payload = {"channel": 0, "toggle": {"onoff": 0}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.Toggle", payload)
|
||||
|
||||
|
||||
class Mss425e(Device):
|
||||
# TODO Implement for all channels
|
||||
def _handle_toggle(self, message):
|
||||
return None
|
||||
|
||||
# TODO Implement for all channels
|
||||
def get_status(self):
|
||||
return None
|
||||
|
||||
def turn_on(self):
|
||||
payload = {'togglex': {"onoff": 1}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
|
||||
def turn_off(self):
|
||||
payload = {'togglex': {"onoff": 0}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
|
||||
def turn_on_channel(self, channel):
|
||||
payload = {'togglex': {'channel': channel, 'onoff': 1}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
|
||||
def turn_off_channel(self, channel):
|
||||
payload = {'togglex': {'channel': channel, 'onoff': 0}}
|
||||
return self._execute_cmd("SET", "Appliance.Control.ToggleX", payload)
|
||||
|
||||
def enable_usb(self):
|
||||
return self.turn_on_channel(4)
|
||||
|
||||
def disable_usb(self):
|
||||
return self.turn_off_channel(4)
|
||||
|
||||
|
||||
class Mss210(Device):
|
||||
pass
|
||||
|
||||
|
||||
class Mss110(Device):
|
||||
pass
|
||||
@ -0,0 +1,19 @@
|
||||
from threading import RLock
|
||||
|
||||
|
||||
class AtomicCounter(object):
|
||||
_lock = None
|
||||
|
||||
def __init__(self, initialValue):
|
||||
self._lock = RLock()
|
||||
self._val = initialValue
|
||||
|
||||
def dec(self):
|
||||
with self._lock:
|
||||
self._val -= 1
|
||||
return self._val
|
||||
|
||||
def inc(self):
|
||||
with self._lock:
|
||||
self._val += 1
|
||||
return self._val
|
||||
2
RPI Code/Meross_/MerossIot/requirements.txt
Normal file
2
RPI Code/Meross_/MerossIot/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
paho-mqtt>=1.3.1
|
||||
requests>=2.19.1
|
||||
40
RPI Code/Meross_/MerossIot/setup.py
Normal file
40
RPI Code/Meross_/MerossIot/setup.py
Normal file
@ -0,0 +1,40 @@
|
||||
from os import path
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='meross_iot',
|
||||
version='0.1.4.2',
|
||||
packages=find_packages(exclude=('tests',)),
|
||||
url='https://github.com/albertogeniola/MerossIot',
|
||||
license='MIT',
|
||||
author='Alberto Geniola',
|
||||
author_email='albertogeniola@gmail.com',
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Operating System :: OS Independent'
|
||||
],
|
||||
description='A simple library to deal with Meross devices. At the moment MSS110, MSS210, MSS310 smart plugs and '
|
||||
'the MSS425E power strip',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
keywords='meross smartplug iot mqtt domotic switch mss310 mss210 mss110 mss425e',
|
||||
project_urls={
|
||||
'Documentation': 'https://github.com/albertogeniola/MerossIot',
|
||||
'Funding': 'https://donate.pypi.org',
|
||||
'Source': 'https://github.com/albertogeniola/MerossIot',
|
||||
'Tracker': 'https://github.com/albertogeniola/MerossIot/issues',
|
||||
},
|
||||
install_requires=[
|
||||
'paho-mqtt>=1.3.1',
|
||||
'requests>=2.19.1'
|
||||
],
|
||||
python_requires='>=3',
|
||||
test_suite='tests'
|
||||
)
|
||||
53
RPI Code/Meross_/MerossIot/tests/RealTest.py
Normal file
53
RPI Code/Meross_/MerossIot/tests/RealTest.py
Normal file
@ -0,0 +1,53 @@
|
||||
import time
|
||||
import sys
|
||||
from meross_iot.api import MerossHttpClient
|
||||
|
||||
if __name__=='__main__':
|
||||
httpHandler = MerossHttpClient(email="thomas.fransolet@hotmail.be", password="Awesome09")
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
print("Listing Devices...")
|
||||
devices = httpHandler.list_supported_devices()
|
||||
|
||||
for counter, device in enumerate(devices):
|
||||
print("Playing with device: %d" % counter)
|
||||
# Returns most of the info about the power plug
|
||||
print("\nGetting system data...")
|
||||
data = device.get_sys_data()
|
||||
|
||||
# Turns the power-plug on
|
||||
print("\nTurning the device on...")
|
||||
device.turn_off()
|
||||
|
||||
# Turns the power-plug off
|
||||
print("\nTurning the device off...")
|
||||
device.turn_on()
|
||||
|
||||
# Reads the historical device consumption
|
||||
print("\nReading consumption data...")
|
||||
consumption = device.get_power_consumptionX()
|
||||
|
||||
# Returns the list of WIFI Network available for the plug
|
||||
# (Note. this takes some time to complete)
|
||||
print("\nScanning Wifi...")
|
||||
wifi_list = device.get_wifi_list()
|
||||
|
||||
# Info about the device
|
||||
print("\nGetting device trace...")
|
||||
trace = device.get_trace()
|
||||
print("\nGetting device debug...")
|
||||
debug = device.get_debug()
|
||||
|
||||
# Returns the capabilities of this device
|
||||
print("\nRetrieving device abilities...")
|
||||
abilities = device.get_abilities()
|
||||
|
||||
# I still have to figure this out :S
|
||||
# The following command is not yet implemented on all devices
|
||||
# and might not work as expected.
|
||||
# report = device.get_report()
|
||||
|
||||
# Returns the current power consumption and voltage from the plug
|
||||
# (Note: this is not really realtime, but close enough)
|
||||
print("\nReading electricity...")
|
||||
electricity = device.get_electricity()
|
||||
0
RPI Code/Meross_/MerossIot/tests/__init__.py
Normal file
0
RPI Code/Meross_/MerossIot/tests/__init__.py
Normal file
69
RPI Code/Meross_/MerossIot/tests/test_power_plugs.py
Normal file
69
RPI Code/Meross_/MerossIot/tests/test_power_plugs.py
Normal file
@ -0,0 +1,69 @@
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
|
||||
from meross_iot.api import MerossHttpClient
|
||||
from meross_iot.supported_devices.power_plugs import Mss310
|
||||
|
||||
EMAIL = os.environ.get('thomas.fransolet@hotmail.be')
|
||||
PASSWORD = os.environ.get('Awesome09')
|
||||
|
||||
|
||||
class TestHttpMethods(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = MerossHttpClient(email=EMAIL, password=PASSWORD)
|
||||
|
||||
def test_device_listing(self):
|
||||
devices = self.client.list_devices()
|
||||
assert devices is not None
|
||||
assert len(devices) > 0
|
||||
|
||||
def test_supported_device_listing(self):
|
||||
devices = self.client.list_supported_devices()
|
||||
assert devices is not None
|
||||
assert len(devices) > 0
|
||||
|
||||
|
||||
class TestMSS310Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
httpHandler = MerossHttpClient(email=EMAIL, password=PASSWORD)
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = httpHandler.list_supported_devices()
|
||||
for counter, device in enumerate(devices):
|
||||
if isinstance(device, Mss310):
|
||||
self.device = device
|
||||
break
|
||||
|
||||
def test_power_cycle(self):
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
def test_get_info(self):
|
||||
consumption = self.device.get_power_consumptionX()
|
||||
assert consumption is not None
|
||||
|
||||
wifi_list = self.device.get_wifi_list()
|
||||
assert wifi_list is not None
|
||||
|
||||
trace = self.device.get_trace()
|
||||
assert trace is not None
|
||||
|
||||
debug = self.device.get_debug()
|
||||
assert debug is not None
|
||||
|
||||
abilities = self.device.get_abilities()
|
||||
assert abilities is not None
|
||||
|
||||
electricity = self.device.get_electricity()
|
||||
assert electricity is not None
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 0e49e69854f060518309892ebb8684e9dea457bd
|
||||
BIN
RPI Code/Meross_2/MerossIot_/.git - Raccourci.lnk
Normal file
BIN
RPI Code/Meross_2/MerossIot_/.git - Raccourci.lnk
Normal file
Binary file not shown.
115
RPI Code/Meross_2/MerossIot_/.gitignore
vendored
Normal file
115
RPI Code/Meross_2/MerossIot_/.gitignore
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
# 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/
|
||||
*.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/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# 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/
|
||||
|
||||
# Local test
|
||||
local_test/*
|
||||
utilities/InfoGather.py
|
||||
|
||||
# Dev utils
|
||||
ext-res/tests/node_modules
|
||||
ext-res/tests/package-lock.json
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
21
RPI Code/Meross_2/MerossIot_/LICENSE
Normal file
21
RPI Code/Meross_2/MerossIot_/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alberto Geniola
|
||||
|
||||
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.
|
||||
269
RPI Code/Meross_2/MerossIot_/README.md
Normal file
269
RPI Code/Meross_2/MerossIot_/README.md
Normal file
@ -0,0 +1,269 @@
|
||||

|
||||

|
||||

|
||||
[](https://badge.fury.io/py/meross-iot)
|
||||
[](https://pepy.tech/project/meross-iot)
|
||||

|
||||
[](https://beerpay.io/albertogeniola/MerossIot)
|
||||
|
||||
# Meross IoT library
|
||||
A pure-python based library providing API for controlling Meross IoT devices over the internet.
|
||||
|
||||
To see what devices are currently supported, checkout the *Currently supported devices* section.
|
||||
However, some devices _might work as expected even if they are not listed_ among the supported devices.
|
||||
In such cases, you're invited to open an issue and report tbe working/non-working status of your device.
|
||||
This will help us to keep track of new devices and current support status of the library.
|
||||
|
||||
|
||||
This library is still work in progress, therefore use it with caution.
|
||||
|
||||
## Installation
|
||||
Due to the popularity of the library, I've decided to list it publicly on the Pipy index.
|
||||
So, the installation is as simple as typing the following command:
|
||||
|
||||
```
|
||||
pip install meross_iot==0.3.1.3 --upgrade
|
||||
```
|
||||
|
||||
## Usage
|
||||
The following script demonstrates how to use this library.
|
||||
|
||||
```python
|
||||
from meross_iot.manager import MerossManager
|
||||
from meross_iot.meross_event import MerossEventType
|
||||
from meross_iot.cloud.devices.light_bulbs import GenericBulb
|
||||
from meross_iot.cloud.devices.power_plugs import GenericPlug
|
||||
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
|
||||
|
||||
|
||||
def event_handler(eventobj):
|
||||
if eventobj.event_type == MerossEventType.DEVICE_ONLINE_STATUS:
|
||||
print("Device online status changed: %s went %s" % (eventobj.device.name, eventobj.status))
|
||||
pass
|
||||
elif eventobj.event_type == MerossEventType.DEVICE_SWITCH_STATUS:
|
||||
print("Switch state changed: Device %s (channel %d) went %s" % (eventobj.device.name, eventobj.channel_id,
|
||||
eventobj.switch_state))
|
||||
else:
|
||||
print("Unknown event!")
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
# Initiates the Meross Cloud Manager. This is in charge of handling the communication with the remote endpoint
|
||||
manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
|
||||
# Register event handlers for the manager...
|
||||
manager.register_event_handler(event_handler)
|
||||
|
||||
# Starts the manager
|
||||
manager.start()
|
||||
|
||||
# You can retrieve the device you are looking for in various ways:
|
||||
# By kind
|
||||
bulbs = manager.get_devices_by_kind(GenericBulb)
|
||||
plugs = manager.get_devices_by_kind(GenericPlug)
|
||||
door_openers = manager.get_devices_by_kind(GenericGarageDoorOpener)
|
||||
all_devices = manager.get_supported_devices()
|
||||
|
||||
# Print some basic specs about the discovered devices
|
||||
print("All the bulbs I found:")
|
||||
for b in bulbs:
|
||||
print(b)
|
||||
|
||||
print("All the plugs I found:")
|
||||
for p in plugs:
|
||||
print(p)
|
||||
|
||||
print("All the garage openers I found:")
|
||||
for g in door_openers:
|
||||
print(g)
|
||||
|
||||
print("All the supported devices I found:")
|
||||
for d in all_devices:
|
||||
print(d)
|
||||
|
||||
# You can also retrieve devices by the UUID/name
|
||||
# a_device = manager.get_device_by_name("My Plug")
|
||||
# a_device = manager.get_device_by_uuid("My Plug")
|
||||
|
||||
# Or you can retrieve all the device by the HW type
|
||||
# all_mss310 = manager.get_devices_by_type("mss310")
|
||||
|
||||
# ------------------------------
|
||||
# Let's play the garage openers.
|
||||
# ------------------------------
|
||||
for g in door_openers:
|
||||
if not g.online:
|
||||
print("The garage controller %s seems to be offline. Cannot play with that..." % g.name)
|
||||
continue
|
||||
|
||||
print("Opening door %s..." % g.name)
|
||||
g.open_door()
|
||||
print("Closing door %s..." % g.name)
|
||||
g.close_door()
|
||||
|
||||
# ---------------------
|
||||
# Let's play with bulbs
|
||||
# ---------------------
|
||||
for b in bulbs: # type: GenericBulb
|
||||
if not b.online:
|
||||
print("The bulb %s seems to be offline. Cannot play with that..." % b.name)
|
||||
continue
|
||||
|
||||
print("Let's play with bulb %s" % b.name)
|
||||
if not b.supports_light_control():
|
||||
print("Too bad bulb %s does not support light control %s" % b.name)
|
||||
else:
|
||||
# Let's make it red!
|
||||
b.set_light_color(rgb=(255, 0, 0))
|
||||
|
||||
b.turn_on()
|
||||
time.sleep(1)
|
||||
b.turn_off()
|
||||
|
||||
# ---------------------------
|
||||
# Let's play with smart plugs
|
||||
# ---------------------------
|
||||
for p in plugs: # type: GenericPlug
|
||||
if not p.online:
|
||||
print("The plug %s seems to be offline. Cannot play with that..." % p.name)
|
||||
continue
|
||||
|
||||
print("Let's play with smart plug %s" % p.name)
|
||||
|
||||
channels = len(p.get_channels())
|
||||
print("The plug %s supports %d channels." % (p.name, channels))
|
||||
for i in range(0, channels):
|
||||
print("Turning on channel %d of %s" % (i, p.name))
|
||||
p.turn_on_channel(i)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
print("Turning off channel %d of %s" % (i, p.name))
|
||||
p.turn_off_channel(i)
|
||||
|
||||
usb_channel = p.get_usb_channel_index()
|
||||
if usb_channel is not None:
|
||||
print("Awesome! This device also supports USB power.")
|
||||
p.enable_usb()
|
||||
time.sleep(1)
|
||||
p.disable_usb()
|
||||
|
||||
if p.supports_electricity_reading():
|
||||
print("Awesome! This device also supports power consumption reading.")
|
||||
print("Current consumption is: %s" % str(p.get_electricity()))
|
||||
|
||||
# At this point, we are all done playing with the library, so we gracefully disconnect and clean resources.
|
||||
print("We are done playing. Cleaning resources...")
|
||||
manager.stop()
|
||||
|
||||
print("Bye bye!")
|
||||
```
|
||||
|
||||
## Currently supported devices
|
||||
Starting from v0.2.0.0, this library should support the majority of Meross devices on the market.
|
||||
The list of tested devices is the following:
|
||||
- MSL120
|
||||
- MSS110
|
||||
- MSS210
|
||||
- MSS310
|
||||
- MSS310h
|
||||
- MSS425e
|
||||
- MSS530H
|
||||
- MSG100
|
||||
|
||||
I'd like to thank all the people who contributed to the early stage of library development,
|
||||
who stimulated me to continue the development and making this library support more devices:
|
||||
|
||||
Thanks to [DanoneKiD](https://github.com/DanoneKiD), [virtualdj](https://github.com/virtualdj), [ictes](https://github.com/ictes), [soberstadt](https://github.com/soberstadt), [ping-localhost](https://github.com/ping-localhost).
|
||||
|
||||
## Protocol details
|
||||
This library was implemented by reverse-engineering the network communications between the plug and the meross network.
|
||||
Anyone can do the same by simply installing a Man-In-The-Middle proxy and routing the ssl traffic of an Android emulator through the sniffer.
|
||||
|
||||
If you want to understand how the Meross protocol works, [have a look at the Wiki](https://github.com/albertogeniola/MerossIot/wiki). Be aware: this is still work in progress, so some pages of the wiki might still be blank/under construction.
|
||||
|
||||
## Homeassistant integration
|
||||
Yeah, it happened. As soon as I started developing this library, I've discovered the HomeAssistant world.
|
||||
Thus, I've decided to spend some time to develop a full featured Homeassistant custom component, that you find [here](https://github.com/albertogeniola/meross-homeassistant).
|
||||
Thanks to @troykelly who made a wish and supported my efforts in developing such component!
|
||||
|
||||
## Donate!
|
||||
I like reverse engineering and protocol inspection, I think it keeps your mind trained and healthy.
|
||||
However, if you liked or appreciated by work, why don't you buy me a beer?
|
||||
It would really motivate me to continue working on this repository to improve documentation, code and extend the supported meross devices.
|
||||
|
||||
Moreover, donations will make me raise money to spend on other Meross devices.
|
||||
So far, I've bought the following devices:
|
||||
- MSL120
|
||||
- MSS210
|
||||
- MSS310
|
||||
- MSS425E
|
||||
- MSS530H
|
||||
- MSG100
|
||||
|
||||
By issuing a donation, you will:
|
||||
1. Give me the opportunity to buy new devices and support them in this library
|
||||
1. Pay part of electricity bill used to keep running the plugs 24/7
|
||||
(Note that they are used for Unit-Testing on the continuous integration engine when someone pushes a PR... I love DEVOPing!)
|
||||
1. You'll increase the quality of my coding sessions with free-beer!
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CQQCK3RN32BHL&source=url)
|
||||
|
||||
[](https://beerpay.io/albertogeniola/MerossIot) [](https://beerpay.io/albertogeniola/MerossIot?focus=wish)
|
||||
|
||||
### Look at these babies!
|
||||
|
||||
<p>
|
||||
Look at the test environment that ensures high quality code of the library!
|
||||
</p>
|
||||
<img src="ext-res/plugs/test-env.jpg" alt="Current test environemnt" width="400" />
|
||||
<p>When a pull-request is performed against this repository, a CI pipeline takes care of building the code,
|
||||
testing it on Python 3.5/3.6/3.7, relying on some junit tests and, if all the tests pass as expected, the library
|
||||
is released on Pypi. However, to ensure that the code <i>really works</i>,
|
||||
the pipeline will issue on/off commands against real devices, that are dedicated 24/7 to the tests.
|
||||
Such devices have been bought by myself (with contributions received by donators).
|
||||
However, keeping such devices connected 24/7 has a cost, which I sustain happily due to the success of the library.
|
||||
Anyways, feel free to contribute via donations!
|
||||
</p>
|
||||
|
||||
## Changelog
|
||||
### 0.3.1.3
|
||||
- Added event fire capability to GenericBulb class.
|
||||
- Fixed bulb state kwargs bug
|
||||
- Improved set_light_color function for bulbs
|
||||
### 0.3.0.2
|
||||
- Fixed door closing checks when using the async + callback close() and open() methods.
|
||||
### 0.3.0.1
|
||||
- Added get_power_consumption() and get_electricity() methods as abstract methods of AbstractMerossDevice
|
||||
- Fixed regression passing manager parameter when firing Meross events.
|
||||
### 0.3.0.0rc4
|
||||
- Added added switch_state to the generated event
|
||||
### 0.3.0.0rc3
|
||||
- Added quick fix for MSS560 color control
|
||||
### 0.3.0.0rc2
|
||||
- Fixed Major bugs with MSG100
|
||||
- Updated README examples
|
||||
### 0.3.0.0rc1
|
||||
- Added MSG100 support
|
||||
- Fixed errors being logged when power consumptionX command was issued on powerplugs
|
||||
### 0.3.0.0b1
|
||||
- General refactor of the library
|
||||
- Added event-based support
|
||||
- Fixed default mqtt broker address for non-european devices
|
||||
### 0.2.2.1
|
||||
- Added basic bulb support: turning on/off and light control
|
||||
- Implemented MSL120 support
|
||||
- Implemented MSL120 automatic test
|
||||
- Extended example script usage to show how to control the light bulbs
|
||||
- Added maximum retry limit for execute_command and connect()
|
||||
### 0.2.1.1
|
||||
- Code refactoring to support heterogeneous devices (bulbs, plugs, garage openers)
|
||||
### 0.2.1.0
|
||||
- Implemented auto-reconnect on lost connection
|
||||
- Improving locking system in order to prevent library hangs when no ack is received
|
||||
BIN
RPI Code/Meross_2/MerossIot_/ext-res/meross_cloud_arch.png
Normal file
BIN
RPI Code/Meross_2/MerossIot_/ext-res/meross_cloud_arch.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36" version="9.4.6" editor="www.draw.io" type="github"><diagram id="838e86de-a6c6-3703-056b-65dabb823eb9" name="Page-1">7Vjfj5s4EP5rIt09BBEMIXlMsptrpVbKaVe63tPKCw74ahjOOJukf/2Nwfxme9s23UirJg+xPw9mZr75hpgJ2SSnPyTN4o8QMjFx7PA0ITcTx5m5zhx/NHIukYXtlkAkeWiMGuCOf2EGtA164CHLO4YKQCiedcEA0pQFqoNRKeHYNduD6N41oxEbAHcBFUP0Lx6q2ETh+A3+jvEoru48my/LlYRWxiaSPKYhHFsQuZ2QjQRQ5Sg5bZjQyavyUl63fWa1dkyyVL3oAsPEExUHE9zHP+/vEVlL+MzkxJkL3Gj9qEeRHv3GQVkJk5DnVgDJhKwc2579PjQ0AapzlTU4KMFTtqlJsdFqD6nagABZ2BD8brWn60jSkLNmLYUUt1nnSrs1dsGeC9HC98UH8ZDmMQvN3Z6YVBx5/EAfmdhBzhWHFNceQSkdS22wEjzSCwoyRKmZBegQ5oSsY5UInM9MAKZCZ041N0HrW9I8K6Pd85P2Y42UZ3oxOUVaHRY95q4lWQ4HGbD3gfZnjdNy1LVK/lXqIZOgIABRp1j7zE7PFsCsLivUI4OEKXlGE3OB45tKNFJ0HK+cH5vCnhuTuFXTFUaNlKJ656bacGAK7pniWw6K7939/S5HaLV7P6ifWit2n9YWHd0CMVUjenTXPF6qHvqcJnlAmRUIOIQWzfhDQlPsCUnBSb9Sbdtf3swvw6Y777NpD9j0R9j0L8AmmQ0YYyG2TTMFqWKIIKXitkGxyRzScIxHduLqk4Ytz8z+Nkb/MKXORnH0oAChZu8PoAkqdii90S58PZnocSE9k7HqeUJlxCqz+XjSJRNU8afu/j+SwmqPdje+uyPVY6+rhaLgeFI8qtqZGy/Z/630QiNrGnyOCk7GWmlxs1XVz0abm/HnJlZKP4tXOnRnG4QpsTg2tD1HtmXx3HC2IVUUfzSOkt8esy8AyTRkT0xAxuRUL0xzJMHZejamYEvs6cxZWFkaXaLx2V5HKu5sKBVvRCreJaTivAmpONeUivNLKq8mFf+KUnHfhFTINaVCfknltaRC3CtKxf85UvH7WsFcyPOnyk5PihVrXk13THJ0X/81/iZx9c9wi4AFwdip73HhuZ793XIk3ogcl68kR7L4OTQ539vSrpr1H22CxaUrKem5ZZABT1Xe2nmngdYxyfc7KiWk95akZ09s+2v2OCg9aPivQ3lZSXjDDl28YSlOwrsXvFt5g507AogEmyYU+winAlt4jgFMq2V3Udg+5AmVKovxoP/gLrLTBXu56y2sbjcnyxppH6jJsJ9X2Dc0Cpw27/3KImrenpLb/wA=</diagram></mxfile>
|
||||
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/devices.jpg
Normal file
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/devices.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/test-env.jpg
Normal file
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/test-env.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/testdevices.jpg
Normal file
BIN
RPI Code/Meross_2/MerossIot_/ext-res/plugs/testdevices.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
12
RPI Code/Meross_2/MerossIot_/ext-res/tests/package.json
Normal file
12
RPI Code/Meross_2/MerossIot_/ext-res/tests/package.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "lab-master-switch",
|
||||
"version": "0.0.1",
|
||||
"email": "albertogeniola@gmail.com",
|
||||
"main": "switch.js",
|
||||
"bin": "./switch.js",
|
||||
"dependencies": {
|
||||
"minimist": ">=1.2.0",
|
||||
"tplink-cloud-api": ">=0.3.8",
|
||||
"uuid": ">=3.3.2"
|
||||
}
|
||||
}
|
||||
55
RPI Code/Meross_2/MerossIot_/ext-res/tests/switch.js
Normal file
55
RPI Code/Meross_2/MerossIot_/ext-res/tests/switch.js
Normal file
@ -0,0 +1,55 @@
|
||||
const minimist = require('minimist');
|
||||
const { login } = require("tplink-cloud-api");
|
||||
const uuidV4 = require("uuid/v4");
|
||||
|
||||
const TPLINK_USER = process.env.TPLINK_USER;
|
||||
const TPLINK_PASS = process.env.TPLINK_PASS;
|
||||
const TPLINK_TERM = process.env.TPLINK_TERM || uuidV4();
|
||||
|
||||
async function main() {
|
||||
var args = minimist(process.argv.slice(2));
|
||||
|
||||
// Check args
|
||||
var username;
|
||||
if ('username' in args) {
|
||||
username = args.username;
|
||||
} else if (TPLINK_USER) {
|
||||
username = TPLINK_USER;
|
||||
} else {
|
||||
console.error("Missing username. You can either set the user name to the environment variable TPLINK_USER or pass it via --username parameter");
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
var password;
|
||||
if ('password' in args) {
|
||||
password = args.password;
|
||||
} else if (TPLINK_PASS) {
|
||||
password = TPLINK_PASS;
|
||||
} else {
|
||||
console.error("Missing password. You can either set the user name to the environment variable TPLINK_PASS or pass it via --password parameter");
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!'toggle' in args) {
|
||||
console.error("Missing required parameter: --toggle");
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!'dev' in args) {
|
||||
console.error("Missing required parameter: --dev");
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// log in to cloud, return a connected tplink object
|
||||
const tplink = await login(username, password, TPLINK_TERM);
|
||||
|
||||
let deviceList = await tplink.getDeviceList();
|
||||
|
||||
console.log("Controlling", args.dev);
|
||||
if (args.toggle==='on' || args.toggle===1)
|
||||
await tplink.getHS110(args.dev).powerOn();
|
||||
else if (args.toggle === 'off' || args.toggle===0)
|
||||
await tplink.getHS110(args.dev).powerOff();
|
||||
}
|
||||
|
||||
main();
|
||||
1
RPI Code/Meross_2/MerossIot_/meross_iot/__init__.py
Normal file
1
RPI Code/Meross_2/MerossIot_/meross_iot/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
name = "meross_iot"
|
||||
133
RPI Code/Meross_2/MerossIot_/meross_iot/api.py
Normal file
133
RPI Code/Meross_2/MerossIot_/meross_iot/api.py
Normal file
@ -0,0 +1,133 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
import requests
|
||||
from meross_iot.credentials import MerossCloudCreds
|
||||
|
||||
|
||||
# Appears to be used as a part of the signature algorithm as constant "salt" (kinda useless)
|
||||
_SECRET = "23x17ahWarFH6w29"
|
||||
_MEROSS_URL = "https://iot.meross.com"
|
||||
_LOGIN_URL = "%s%s" % (_MEROSS_URL, "/v1/Auth/Login")
|
||||
_LOG_URL = "%s%s" % (_MEROSS_URL, "/v1/log/user")
|
||||
_DEV_LIST = "%s%s" % (_MEROSS_URL, "/v1/Device/devList")
|
||||
|
||||
|
||||
class MerossHttpClient:
|
||||
_cloud_creds = None
|
||||
|
||||
_email = None
|
||||
_password = None
|
||||
_authenticated = False
|
||||
|
||||
def __init__(self, email, password):
|
||||
self._email = email
|
||||
self._password = password
|
||||
|
||||
def _authenticated_post(self,
|
||||
url, # type: str
|
||||
params_data # type: dict
|
||||
):
|
||||
|
||||
nonce = self._generate_nonce(16)
|
||||
timestamp_millis = int(round(time.time() * 1000))
|
||||
login_params = self._encode_params(params_data)
|
||||
|
||||
# Generate the md5-hash (called signature)
|
||||
m = hashlib.md5()
|
||||
datatosign = '%s%s%s%s' % (_SECRET, timestamp_millis, nonce, login_params)
|
||||
m.update(datatosign.encode("utf8"))
|
||||
md5hash = m.hexdigest()
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic" if self._cloud_creds is None else "Basic %s" % self._cloud_creds.token,
|
||||
"vender": "Meross",
|
||||
"AppVersion": "1.3.0",
|
||||
"AppLanguage": "EN",
|
||||
"User-Agent": "okhttp/3.6.0"
|
||||
}
|
||||
|
||||
payload = {
|
||||
'params': login_params,
|
||||
'sign': md5hash,
|
||||
'timestamp': timestamp_millis,
|
||||
'nonce': nonce
|
||||
}
|
||||
|
||||
# Perform the request.
|
||||
r = requests.post(url, data=payload, headers=headers)
|
||||
|
||||
# Check if that is ok.
|
||||
if r.status_code != 200:
|
||||
raise AuthenticatedPostException()
|
||||
|
||||
# Save returned value
|
||||
jsondata = r.json()
|
||||
# print(jsondata)
|
||||
|
||||
if jsondata["info"].lower() != "success":
|
||||
raise AuthenticatedPostException()
|
||||
|
||||
return jsondata["data"]
|
||||
|
||||
@staticmethod
|
||||
def _encode_params(parameters # type: dict
|
||||
):
|
||||
jsonstring = json.dumps(parameters)
|
||||
return str(base64.b64encode(jsonstring.encode("utf8")), "utf8")
|
||||
|
||||
@staticmethod
|
||||
def _generate_nonce(length):
|
||||
return ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
||||
|
||||
def _login(self):
|
||||
try:
|
||||
data = {"email": self._email, "password": self._password}
|
||||
response_data = self._authenticated_post(_LOGIN_URL, params_data=data)
|
||||
creds = MerossCloudCreds()
|
||||
creds.token = response_data["token"]
|
||||
creds.key = response_data["key"]
|
||||
creds.user_id = response_data["userid"]
|
||||
creds.user_email = response_data["email"]
|
||||
self._cloud_creds = creds
|
||||
self._authenticated = True
|
||||
except Exception as e:
|
||||
# TODO: LOG
|
||||
return False
|
||||
|
||||
try:
|
||||
# The protocol does not really need the following call. However we want to be nice do it anyways
|
||||
self._log()
|
||||
except:
|
||||
# TODO: LOG
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
def _log(self):
|
||||
data = {'extra': {}, 'model': 'Android,Android SDK built for x86_64', 'system': 'Android',
|
||||
'uuid': '493dd9174941ed58waitForOpenWifi', 'vendor': 'Meross', 'version': '6.0'}
|
||||
self._authenticated_post(_LOG_URL, params_data=data)
|
||||
|
||||
def list_devices(self):
|
||||
if not self._authenticated and not self._login():
|
||||
raise UnauthorizedException()
|
||||
|
||||
return self._authenticated_post(_DEV_LIST, {})
|
||||
|
||||
def get_cloud_credentials(self):
|
||||
if not self._authenticated and not self._login():
|
||||
raise UnauthorizedException()
|
||||
|
||||
return self._cloud_creds
|
||||
|
||||
|
||||
class AuthenticatedPostException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnauthorizedException(Exception):
|
||||
pass
|
||||
22
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/abilities.py
Normal file
22
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/abilities.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Common abilities
|
||||
ALL = 'Appliance.System.All'
|
||||
ABILITY = 'Appliance.System.Ability'
|
||||
REPORT = 'Appliance.System.Report'
|
||||
ONLINE = 'Appliance.System.Online'
|
||||
WIFI_LIST = 'Appliance.Config.WifiList'
|
||||
DEBUG = 'Appliance.System.Debug'
|
||||
TRACE = 'Appliance.Config.Trace'
|
||||
|
||||
# Power plug/bulbs abilities
|
||||
TOGGLE = 'Appliance.Control.Toggle'
|
||||
TOGGLEX = 'Appliance.Control.ToggleX'
|
||||
TRIGGER = 'Appliance.Control.Trigger'
|
||||
TRIGGERX = 'Appliance.Control.TriggerX'
|
||||
ELECTRICITY = 'Appliance.Control.Electricity'
|
||||
CONSUMPTIONX = 'Appliance.Control.ConsumptionX'
|
||||
|
||||
# Garage opener abilities
|
||||
GARAGE_DOOR_STATE = 'Appliance.GarageDoor.State'
|
||||
|
||||
# Bulbs-only abilities
|
||||
LIGHT = 'Appliance.Control.Light'
|
||||
356
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/client.py
Normal file
356
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/client.py
Normal file
@ -0,0 +1,356 @@
|
||||
from hashlib import md5
|
||||
import string
|
||||
from threading import Event, RLock
|
||||
import json
|
||||
import uuid as UUID
|
||||
import ssl
|
||||
import random
|
||||
import copy
|
||||
import time
|
||||
from meross_iot.cloud.timeouts import SHORT_TIMEOUT
|
||||
from meross_iot.cloud.exceptions.CommandTimeoutException import CommandTimeoutException
|
||||
from meross_iot.logger import CONNECTION_MANAGER_LOGGER as l
|
||||
import paho.mqtt.client as mqtt
|
||||
from meross_iot.credentials import MerossCloudCreds
|
||||
from meross_iot.cloud.client_status import ClientStatus
|
||||
from meross_iot.cloud.connection import ConnectionStatusManager
|
||||
from meross_iot.utilities.synchronization import AtomicCounter
|
||||
from meross_iot.logger import NETWORK_DATA as networkl
|
||||
|
||||
|
||||
def build_client_request_topic(client_uuid):
|
||||
return "/appliance/%s/subscribe" % client_uuid
|
||||
|
||||
|
||||
class PendingMessageResponse(object):
|
||||
"""
|
||||
This class is used as an Handle for mqtt messages that expect an ACK back.
|
||||
When a callback is passed to the constructor, this object is configured as an "async" waiter.
|
||||
Instead, passing a None callback, makes this object to act as a synchronously waiter.
|
||||
It is meant to be used internally by the library, in order to handle ACK waiting and callback calling.
|
||||
Note that this object is not thread safe.
|
||||
"""
|
||||
_message_id = None
|
||||
_callback = None
|
||||
_event = None
|
||||
_response = None
|
||||
_error = None
|
||||
|
||||
def __init__(self, message_id, callback=None):
|
||||
self._message_id = message_id
|
||||
|
||||
# Only instantiate an event if no callback has been specified
|
||||
if callback is None:
|
||||
self._event = Event()
|
||||
else:
|
||||
self._callback = callback
|
||||
|
||||
def wait_for_response(self, timeout=SHORT_TIMEOUT):
|
||||
"""
|
||||
This method blocks until an ACK/RESPONSE message is received for the corresponding message_id that it refers
|
||||
to. Note that this method only works when the user is synchronously waiting for the response message.
|
||||
This method raises an exception if invoked when a callback was specified in the constructor.
|
||||
:param timeout:
|
||||
:return:
|
||||
"""
|
||||
if self._event is None:
|
||||
raise Exception("Error: you can invoke this method only if you don't use a callback (i.e. sync invocation)")
|
||||
|
||||
# Wait until we receive the message.
|
||||
# If timeout occurs, return failure and None as received message.
|
||||
success = self._event.wait(timeout=timeout)
|
||||
return success, self._response
|
||||
|
||||
def notify_message_received(self, error=None, response=None):
|
||||
self._response = copy.deepcopy(response)
|
||||
self._error = error
|
||||
|
||||
if self._event is not None:
|
||||
self._event.set()
|
||||
elif self._callback is not None:
|
||||
try:
|
||||
self._callback(self._error, self._response)
|
||||
except:
|
||||
l.exception("Unhandled error occurred while executing the callback")
|
||||
|
||||
|
||||
class MerossCloudClient(object):
|
||||
# Meross Cloud credentials, which are provided by the HTTP Api.
|
||||
_cloud_creds = None
|
||||
|
||||
# Connection info
|
||||
connection_status = None
|
||||
_domain = None
|
||||
_port = 2001
|
||||
_ca_cert = None
|
||||
|
||||
# App and client ID
|
||||
_app_id = None
|
||||
_client_id = None
|
||||
|
||||
# Paho mqtt client object
|
||||
_mqtt_client = None
|
||||
|
||||
# Callback to be invoked every time a push notification is received from the MQTT broker
|
||||
_push_message_callback = None
|
||||
|
||||
# This dictionary is used to keep track of messages issued to the broker that are waiting for an ACK
|
||||
# The key is the message_id, the value is the PendingMessageResponse object.
|
||||
# Access to this resource is protected with exclusive locking
|
||||
_pending_response_messages = None
|
||||
_pending_responses_lock = None
|
||||
|
||||
def __init__(self,
|
||||
cloud_credentials, # type: MerossCloudCreds
|
||||
push_message_callback=None, # type: callable
|
||||
**kwords):
|
||||
|
||||
self.connection_status = ConnectionStatusManager()
|
||||
self._cloud_creds = cloud_credentials
|
||||
self._pending_response_messages = dict()
|
||||
self._pending_responses_lock = RLock()
|
||||
self._push_message_callback = push_message_callback
|
||||
self._subscription_count = AtomicCounter(0)
|
||||
|
||||
if "domain" in kwords:
|
||||
self._domain = kwords['domain']
|
||||
else:
|
||||
self._domain = "iot.meross.com"
|
||||
|
||||
# Lookup port and certificate for MQTT server
|
||||
self._port = kwords.get('port', MerossCloudClient._port)
|
||||
self._ca_cert = kwords.get('ca_cert', None)
|
||||
|
||||
self._generate_client_and_app_id()
|
||||
|
||||
# Password is calculated as the MD5 of USERID concatenated with KEY
|
||||
md5_hash = md5()
|
||||
clearpwd = "%s%s" % (self._cloud_creds.user_id, self._cloud_creds.key)
|
||||
md5_hash.update(clearpwd.encode("utf8"))
|
||||
hashed_password = md5_hash.hexdigest()
|
||||
|
||||
# Start the mqtt client
|
||||
self._mqtt_client = mqtt.Client(client_id=self._client_id,
|
||||
protocol=mqtt.MQTTv311) # ex. app-id -> app:08d4c9f99da40203ebc798a76512ec14
|
||||
self._mqtt_client.on_connect = self._on_connect
|
||||
self._mqtt_client.on_message = self._on_message
|
||||
self._mqtt_client.on_disconnect = self._on_disconnect
|
||||
self._mqtt_client.on_subscribe = self._on_subscribe
|
||||
|
||||
# Avoid login if user_id is None
|
||||
if self._cloud_creds.user_id is not None:
|
||||
self._mqtt_client.username_pw_set(username=self._cloud_creds.user_id,
|
||||
password=hashed_password)
|
||||
self._mqtt_client.tls_set(ca_certs=self._ca_cert, certfile=None,
|
||||
keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
|
||||
tls_version=ssl.PROTOCOL_TLS,
|
||||
ciphers=None)
|
||||
|
||||
def close(self):
|
||||
l.info("Closing the MQTT connection...")
|
||||
self._mqtt_client.disconnect()
|
||||
l.debug("Waiting for the client to disconnect...")
|
||||
self.connection_status.wait_for_status(ClientStatus.CONNECTION_DROPPED)
|
||||
|
||||
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
|
||||
l.debug("Stopping the MQTT looper.")
|
||||
self._mqtt_client.loop_stop(True)
|
||||
|
||||
l.info("Client has been fully disconnected.")
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Starts the connection to the MQTT broker
|
||||
:return:
|
||||
"""
|
||||
l.info("Initializing the MQTT connection...")
|
||||
self._mqtt_client.connect(self._domain, self._port, keepalive=30)
|
||||
self.connection_status.update_status(ClientStatus.CONNECTING)
|
||||
|
||||
# Starts a new thread that handles mqtt protocol and calls us back via callbacks
|
||||
l.debug("(Re)Starting the MQTT looper.")
|
||||
self._mqtt_client.loop_stop(True)
|
||||
self._mqtt_client.loop_start()
|
||||
|
||||
l.debug("Waiting for the client to connect...")
|
||||
self.connection_status.wait_for_status(ClientStatus.SUBSCRIBED)
|
||||
l.info("Client connected to MQTT broker and subscribed to relevant topics.")
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# MQTT Handlers
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
def _on_disconnect(self, client, userdata, rc):
|
||||
l.info("Disconnection detected. Reason: %s" % str(rc))
|
||||
|
||||
# When the mqtt connection is dropped, we need to reset the subscription counter.
|
||||
self._subscription_count = AtomicCounter(0)
|
||||
self.connection_status.update_status(ClientStatus.CONNECTION_DROPPED)
|
||||
|
||||
# TODO: should we handle disconnection in some way at this level?
|
||||
|
||||
if rc == mqtt.MQTT_ERR_SUCCESS:
|
||||
pass
|
||||
else:
|
||||
client.loop_stop(True)
|
||||
|
||||
def _on_unsubscribe(self):
|
||||
l.debug("Unsubscribed from topic")
|
||||
self._subscription_count.dec()
|
||||
|
||||
def _on_subscribe(self, client, userdata, mid, granted_qos):
|
||||
l.debug("Succesfully subscribed to topic. Subscription count: %d" % self._subscription_count.get())
|
||||
if self._subscription_count.inc() == 2:
|
||||
self.connection_status.update_status(ClientStatus.SUBSCRIBED)
|
||||
|
||||
def _on_connect(self, client, userdata, rc, other):
|
||||
l.debug("Connected with result code %s" % str(rc))
|
||||
self.connection_status.update_status(ClientStatus.CONNECTED)
|
||||
|
||||
self._client_response_topic = "/app/%s-%s/subscribe" % (self._cloud_creds.user_id, self._app_id)
|
||||
self._user_topic = "/app/%s/subscribe" % self._cloud_creds.user_id
|
||||
|
||||
# Subscribe to the relevant topics
|
||||
l.debug("Subscribing to topics...")
|
||||
client.subscribe(self._user_topic)
|
||||
client.subscribe(self._client_response_topic)
|
||||
|
||||
def _on_message(self, client, userdata, msg):
|
||||
"""
|
||||
This handler is called when a message is received from the MQTT broker, on the subscribed topics.
|
||||
The current implementation checks the validity of the message itself, by verifying its signature.
|
||||
|
||||
:param client: is the MQTT client reference, useful to respond back
|
||||
:param userdata: metadata about the received data
|
||||
:param msg: message that was received
|
||||
:return: nothing, it simply handles the message accordingly.
|
||||
"""
|
||||
networkl.debug(msg.topic + " --> " + str(msg.payload))
|
||||
|
||||
try:
|
||||
message = json.loads(str(msg.payload, "utf8"))
|
||||
header = message['header']
|
||||
|
||||
message_hash = md5()
|
||||
strtohash = "%s%s%s" % (header['messageId'], self._cloud_creds.key, header['timestamp'])
|
||||
message_hash.update(strtohash.encode("utf8"))
|
||||
expected_signature = message_hash.hexdigest().lower()
|
||||
|
||||
if header['sign'] != expected_signature:
|
||||
# TODO: custom exception for invalid signature
|
||||
raise Exception('The signature did not match!')
|
||||
|
||||
# Check if there is any thread waiting for this message or if there is a callback that we need to invoke.
|
||||
# If so, do it here.
|
||||
handle = None
|
||||
with self._pending_responses_lock:
|
||||
msg_id = header['messageId']
|
||||
handle = self._pending_response_messages.get(msg_id)
|
||||
|
||||
from_myself = False
|
||||
if handle is not None:
|
||||
# There was a handle for this message-id. It means it is a response message to some
|
||||
# request performed by the library itself.
|
||||
from_myself = True
|
||||
try:
|
||||
l.debug("Calling handle event handler for message %s" % msg_id)
|
||||
# Call the handler
|
||||
handle.notify_message_received(error=None, response=message)
|
||||
l.debug("Done handler for message %s" % msg_id)
|
||||
|
||||
# Remove the message from the pending queue
|
||||
with self._pending_responses_lock:
|
||||
del self._pending_response_messages[msg_id]
|
||||
except:
|
||||
l.exception("Error occurred while invoking message handler")
|
||||
|
||||
# Let's also catch all the "PUSH" notifications and dispatch them to the push_notification_callback.
|
||||
if self._push_message_callback is not None and header['method'] == "PUSH" and 'namespace' in header:
|
||||
self._push_message_callback(message, from_myself=from_myself)
|
||||
|
||||
except Exception:
|
||||
l.exception("Failed to process message.")
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# Protocol Handlers
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
def execute_cmd(self, dst_dev_uuid, method, namespace, payload, callback=None, timeout=SHORT_TIMEOUT):
|
||||
start = time.time()
|
||||
# Build the mqtt message we will send to the broker
|
||||
message, message_id = self._build_mqtt_message(method, namespace, payload)
|
||||
|
||||
# Register the waiting handler for that message
|
||||
handle = PendingMessageResponse(message_id=message_id, callback=callback)
|
||||
with self._pending_responses_lock:
|
||||
self._pending_response_messages[message_id] = handle
|
||||
|
||||
# Send the message to the broker
|
||||
l.debug("Executing message-id %s, %s on %s command for device %s" % (message_id, method,
|
||||
namespace, dst_dev_uuid))
|
||||
self._mqtt_client.publish(topic=build_client_request_topic(dst_dev_uuid), payload=message)
|
||||
|
||||
# If the caller has specified a callback, we don't need to actrively wait for the message ACK. So we can
|
||||
# immediately return.
|
||||
if callback is not None:
|
||||
return None
|
||||
|
||||
# Otherwise, we need to wait until the message is received.
|
||||
l.debug("Waiting for response to message-id %s" % message_id)
|
||||
success, resp = handle.wait_for_response(timeout=timeout)
|
||||
if not success:
|
||||
raise CommandTimeoutException("A timeout occurred while waiting for the ACK: %d" % timeout)
|
||||
|
||||
elapsed = time.time() - start
|
||||
|
||||
l.debug("Message-id: %s, command %s-%s command for device %s took %s" % (message_id, method,
|
||||
namespace, dst_dev_uuid, str(elapsed)))
|
||||
return resp['payload']
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# Protocol utilities
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
def _build_mqtt_message(self, method, namespace, payload):
|
||||
"""
|
||||
Sends a message to the Meross MQTT broker, respecting the protocol payload.
|
||||
:param method:
|
||||
:param namespace:
|
||||
:param payload:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# Generate a random 16 byte string
|
||||
randomstring = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(16))
|
||||
|
||||
# Hash it as md5
|
||||
md5_hash = md5()
|
||||
md5_hash.update(randomstring.encode('utf8'))
|
||||
messageId = md5_hash.hexdigest().lower()
|
||||
timestamp = int(round(time.time()))
|
||||
|
||||
# Hash the messageId, the key and the timestamp
|
||||
md5_hash = md5()
|
||||
strtohash = "%s%s%s" % (messageId, self._cloud_creds.key, timestamp)
|
||||
md5_hash.update(strtohash.encode("utf8"))
|
||||
signature = md5_hash.hexdigest().lower()
|
||||
|
||||
data = {
|
||||
"header":
|
||||
{
|
||||
"from": self._client_response_topic,
|
||||
"messageId": messageId, # Example: "122e3e47835fefcd8aaf22d13ce21859"
|
||||
"method": method, # Example: "GET",
|
||||
"namespace": namespace, # Example: "Appliance.System.All",
|
||||
"payloadVersion": 1,
|
||||
"sign": signature, # Example: "b4236ac6fb399e70c3d61e98fcb68b74",
|
||||
"timestamp": timestamp
|
||||
},
|
||||
"payload": payload
|
||||
}
|
||||
strdata = json.dumps(data)
|
||||
return strdata.encode("utf-8"), messageId
|
||||
|
||||
def _generate_client_and_app_id(self):
|
||||
md5_hash = md5()
|
||||
rnd_uuid = UUID.uuid4()
|
||||
md5_hash.update(("%s%s" % ("API", rnd_uuid)).encode("utf8"))
|
||||
self._app_id = md5_hash.hexdigest()
|
||||
self._client_id = 'app:%s' % md5_hash.hexdigest()
|
||||
@ -0,0 +1,9 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ClientStatus(Enum):
|
||||
INITIALIZED = 1
|
||||
CONNECTING = 2
|
||||
CONNECTED = 3
|
||||
SUBSCRIBED = 4
|
||||
CONNECTION_DROPPED = 5
|
||||
95
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/connection.py
Normal file
95
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/connection.py
Normal file
@ -0,0 +1,95 @@
|
||||
import datetime
|
||||
from threading import RLock, Condition
|
||||
|
||||
from meross_iot.cloud.client_status import ClientStatus
|
||||
from meross_iot.cloud.exceptions.StatusTimeoutException import StatusTimeoutException
|
||||
from meross_iot.cloud.timeouts import SHORT_TIMEOUT
|
||||
from meross_iot.meross_event import ClientConnectionEvent
|
||||
from meross_iot.logger import CONNECTION_MANAGER_LOGGER as l
|
||||
|
||||
|
||||
class ConnectionStatusManager(object):
|
||||
# The connection status of the device is represented by the following variable.
|
||||
# It is protected by the following variable, called _client_connection_status_lock.
|
||||
# The child classes should never change/access these variables directly, though.
|
||||
_status = None
|
||||
_lock = None
|
||||
|
||||
# List of callbacks that should be called when an event occurs
|
||||
_connection_event_callbacks = None
|
||||
_connection_event_callbacks_lock = None
|
||||
|
||||
# This condition object is used to synchronize multiple threads waiting for the connection to
|
||||
# get into a specific state.
|
||||
_status_condition = None
|
||||
|
||||
def __init__(self):
|
||||
self._connection_event_callbacks_lock = RLock()
|
||||
self._connection_event_callbacks = []
|
||||
|
||||
self._lock = RLock()
|
||||
self._status_condition = Condition(self._lock)
|
||||
self._status = ClientStatus.INITIALIZED
|
||||
|
||||
def get_status(self):
|
||||
with self._status_condition:
|
||||
return self._status
|
||||
|
||||
def update_status(self, status):
|
||||
old_status = None
|
||||
new_status = None
|
||||
with self._status_condition:
|
||||
old_status = self._status
|
||||
new_status = status
|
||||
self._status = status
|
||||
self._status_condition.notify_all()
|
||||
|
||||
# If the connection status has changed, fire the event.
|
||||
if old_status != new_status:
|
||||
self._fire_connection_event(new_status)
|
||||
|
||||
def check_status(self, expected_status):
|
||||
with self._lock:
|
||||
return expected_status == self._status
|
||||
|
||||
def check_status_in(self, expected_statuses):
|
||||
with self._lock:
|
||||
return self._status in expected_statuses
|
||||
|
||||
def wait_for_status(self, expected_status, timeout=SHORT_TIMEOUT):
|
||||
start_time = datetime.datetime.now()
|
||||
with self._status_condition:
|
||||
while self._status != expected_status:
|
||||
elapsed = datetime.datetime.now().timestamp() - start_time.timestamp()
|
||||
to = timeout - elapsed
|
||||
timeout_hit = to < 0 or not self._status_condition.wait(to)
|
||||
|
||||
if timeout_hit:
|
||||
# An error has occurred
|
||||
raise StatusTimeoutException("Error while waiting for status %s. Last status is: %s" %
|
||||
(expected_status, self._status))
|
||||
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
# Event Handling
|
||||
# ------------------------------------------------------------------------------------------------
|
||||
def register_connection_event_callback(self, callback):
|
||||
with self._connection_event_callbacks_lock:
|
||||
if callback not in self._connection_event_callbacks:
|
||||
self._connection_event_callbacks.append(callback)
|
||||
else:
|
||||
l.debug("Callback was already registered.")
|
||||
|
||||
def unregister_connection_event_callback(self, callback):
|
||||
with self._connection_event_callbacks_lock:
|
||||
if callback in self._connection_event_callbacks:
|
||||
self._connection_event_callbacks.remove(callback)
|
||||
else:
|
||||
l.debug("Callback was present: nothing to unregister.")
|
||||
|
||||
def _fire_connection_event(self, connection_status):
|
||||
evt = ClientConnectionEvent(current_status=connection_status)
|
||||
for c in self._connection_event_callbacks:
|
||||
try:
|
||||
c(evt)
|
||||
except:
|
||||
l.exception("Unhandled error occurred while executing event handler")
|
||||
187
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/device.py
Normal file
187
RPI Code/Meross_2/MerossIot_/meross_iot/cloud/device.py
Normal file
@ -0,0 +1,187 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from threading import RLock
|
||||
|
||||
from meross_iot.cloud.exceptions.OfflineDeviceException import OfflineDeviceException
|
||||
from meross_iot.cloud.timeouts import LONG_TIMEOUT, SHORT_TIMEOUT
|
||||
from meross_iot.cloud.abilities import *
|
||||
from meross_iot.logger import DEVICE_LOGGER as l
|
||||
from meross_iot.meross_event import DeviceOnlineStatusEvent
|
||||
|
||||
|
||||
class AbstractMerossDevice(ABC):
|
||||
# Device status + lock to protect concurrent access
|
||||
_state_lock = None
|
||||
online = False
|
||||
|
||||
# Device info and connection parameters
|
||||
uuid = None
|
||||
app_id = None
|
||||
name = None
|
||||
type = None
|
||||
hwversion = None
|
||||
fwversion = None
|
||||
|
||||
# Cached list of abilities
|
||||
_abilities = None
|
||||
|
||||
# Cloud client: the object that handles mqtt communication with the Meross Cloud
|
||||
__cloud_client = None
|
||||
|
||||
# Data structure for firing events.
|
||||
__event_handlers_lock = None
|
||||
__event_handlers = None
|
||||
|
||||
def __init__(self, cloud_client, device_uuid, **kwargs):
|
||||
self.__cloud_client = cloud_client
|
||||
self._state_lock = RLock()
|
||||
self.__event_handlers_lock = RLock()
|
||||
self.__event_handlers = []
|
||||
|
||||
self.uuid = device_uuid
|
||||
|
||||
if "channels" in kwargs:
|
||||
self._channels = kwargs['channels']
|
||||
|
||||
# Information about device
|
||||
if "devName" in kwargs:
|
||||
self.name = kwargs['devName']
|
||||
if "deviceType" in kwargs:
|
||||
self.type = kwargs['deviceType']
|
||||
if "fmwareVersion" in kwargs:
|
||||
self.fwversion = kwargs['fmwareVersion']
|
||||
if "hdwareVersion" in kwargs:
|
||||
self.hwversion = kwargs['hdwareVersion']
|
||||
if "onlineStatus" in kwargs:
|
||||
self.online = kwargs['onlineStatus'] == 1
|
||||
|
||||
def handle_push_notification(self, namespace, payload, from_myself=False):
|
||||
# Handle the ONLINE push notification
|
||||
# Leave the rest to the specific implementation
|
||||
if namespace == ONLINE:
|
||||
old_online_status = self.online
|
||||
status = payload['online']['status']
|
||||
if status == 2:
|
||||
with self._state_lock:
|
||||
self.online = False
|
||||
elif status == 1:
|
||||
with self._state_lock:
|
||||
self.online = True
|
||||
else:
|
||||
l.error("Unknown online status has been reported from the device: %d" % status)
|
||||
|
||||
# If the online status changed, fire the corresponding event
|
||||
if old_online_status != self.online:
|
||||
evt = DeviceOnlineStatusEvent(self, self.online)
|
||||
self.fire_event(evt)
|
||||
else:
|
||||
self._handle_push_notification(namespace, payload, from_myself=from_myself)
|
||||
|
||||
def register_event_callback(self, callback):
|
||||
with self.__event_handlers_lock:
|
||||
if callback not in self.__event_handlers:
|
||||
self.__event_handlers.append(callback)
|
||||
else:
|
||||
l.debug("The callback you tried to register is already present.")
|
||||
pass
|
||||
|
||||
def unregister_event_callback(self, callback):
|
||||
with self.__event_handlers_lock:
|
||||
if callback in self.__event_handlers:
|
||||
self.__event_handlers.remove(callback)
|
||||
else:
|
||||
l.debug("The callback you tried to unregister is not present.")
|
||||
pass
|
||||
|
||||
def fire_event(self, eventobj):
|
||||
for c in self.__event_handlers:
|
||||
try:
|
||||
c(eventobj)
|
||||
except:
|
||||
l.exception("Unhandled error occurred while executing the registered event-callback")
|
||||
|
||||
@abstractmethod
|
||||
def _handle_push_notification(self, namespace, payload, from_myself=False):
|
||||
"""
|
||||
Handles push messages for this device. This method should be implemented by the base class in order
|
||||
to catch status changes issued by other clients (i.e. the Meross app on the user's device).
|
||||
:param namespace:
|
||||
:param message:
|
||||
:param from_myself: boolean flag. When true, it means that the notification is generated in response to a
|
||||
command that was issued by this client. When false, it means that another client generated the event.
|
||||
:return:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_status(self):
|
||||
pass
|
||||
|
||||
def execute_command(self, command, namespace, payload, callback=None, timeout=SHORT_TIMEOUT):
|
||||
with self._state_lock:
|
||||
# If the device is not online, what's the point of issuing the command?
|
||||
if not self.online:
|
||||
raise OfflineDeviceException("The device %s (%s) is offline. The command cannot be executed" %
|
||||
(self.name, self.uuid))
|
||||
|
||||
return self.__cloud_client.execute_cmd(self.uuid, command, namespace, payload, callback=callback, timeout=timeout)
|
||||
|
||||
def get_sys_data(self):
|
||||
return self.execute_command("GET", ALL, {})
|
||||
|
||||
def get_abilities(self):
|
||||
# TODO: Make this cached value expire after a bit...
|
||||
if self._abilities is None:
|
||||
self._abilities = self.execute_command("GET", ABILITY, {})['ability']
|
||||
return self._abilities
|
||||
|
||||
def get_report(self):
|
||||
return self.execute_command("GET", REPORT, {})
|
||||
|
||||
def get_wifi_list(self):
|
||||
if WIFI_LIST in self.get_abilities():
|
||||
return self.execute_command("GET", WIFI_LIST, {}, timeout=LONG_TIMEOUT)
|
||||
else:
|
||||
l.error("This device does not support the WIFI_LIST ability")
|
||||
return None
|
||||
|
||||
def get_trace(self):
|
||||
if TRACE in self.get_abilities():
|
||||
return self.execute_command("GET", TRACE, {})
|
||||
else:
|
||||
l.error("This device does not support the TRACE ability")
|
||||
return None
|
||||
|
||||
def get_debug(self):
|
||||
if DEBUG in self.get_abilities():
|
||||
return self.execute_command("GET", DEBUG, {})
|
||||
else:
|
||||
l.error("This device does not support the DEBUG ability")
|
||||
return None
|
||||
|
||||
def supports_consumption_reading(self):
|
||||
return CONSUMPTIONX in self.get_abilities()
|
||||
|
||||
def supports_electricity_reading(self):
|
||||
return ELECTRICITY in self.get_abilities()
|
||||
|
||||
def supports_light_control(self):
|
||||
return LIGHT in self.get_abilities()
|
||||
|
||||
@abstractmethod
|
||||
def get_power_consumption(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_electricity(self):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
basic_info = "%s: %s (%s, HW %s, FW %s): " % (
|
||||
self.__class__.name,
|
||||
self.name,
|
||||
self.type,
|
||||
self.hwversion,
|
||||
self.fwversion
|
||||
)
|
||||
|
||||
return basic_info
|
||||
@ -0,0 +1,19 @@
|
||||
from meross_iot.cloud.devices.power_plugs import GenericPlug
|
||||
from meross_iot.cloud.devices.light_bulbs import GenericBulb
|
||||
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
|
||||
|
||||
|
||||
def build_wrapper(
|
||||
cloud_client,
|
||||
device_type, # type: str
|
||||
device_uuid, # type: str
|
||||
device_specs # type: dict
|
||||
):
|
||||
if device_type.startswith('msl') or device_type.startswith('mss560m'):
|
||||
return GenericBulb(cloud_client, device_uuid=device_uuid, **device_specs)
|
||||
elif device_type.startswith('mss'):
|
||||
return GenericPlug(cloud_client, device_uuid=device_uuid, **device_specs)
|
||||
elif device_type.startswith('msg'):
|
||||
return GenericGarageDoorOpener(cloud_client, device_uuid=device_uuid, **device_specs)
|
||||
else:
|
||||
return GenericPlug(cloud_client, device_uuid=device_uuid, **device_specs)
|
||||
@ -0,0 +1,173 @@
|
||||
from meross_iot.cloud.abilities import *
|
||||
from meross_iot.cloud.device import AbstractMerossDevice
|
||||
from meross_iot.logger import POWER_PLUGS_LOGGER as l
|
||||
from meross_iot.meross_event import DeviceDoorStatusEvent
|
||||
from threading import Event
|
||||
|
||||
|
||||
def parse_state(state):
|
||||
if isinstance(state, str):
|
||||
strstate = state.strip().lower()
|
||||
if strstate == 'open':
|
||||
return True
|
||||
elif strstate == 'closed':
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Invalid state provided.")
|
||||
|
||||
elif isinstance(state, bool):
|
||||
return state
|
||||
|
||||
elif isinstance(state, int):
|
||||
if state == 0:
|
||||
return False
|
||||
elif state == 1:
|
||||
return True
|
||||
else:
|
||||
return ValueError("Invalid state provided.")
|
||||
|
||||
|
||||
def compare_same_states(state1, state2):
|
||||
s1 = parse_state(state1)
|
||||
s2 = parse_state(state2)
|
||||
return s1 == s2
|
||||
|
||||
|
||||
class GenericGarageDoorOpener(AbstractMerossDevice):
|
||||
# Channels
|
||||
_channels = []
|
||||
|
||||
# Dictionary {channel_id (door) -> status}
|
||||
_door_state = None
|
||||
|
||||
def __init__(self, cloud_client, device_uuid, **kwords):
|
||||
super(GenericGarageDoorOpener, self).__init__(cloud_client, device_uuid, **kwords)
|
||||
|
||||
def get_status(self):
|
||||
with self._state_lock:
|
||||
if self._door_state is None:
|
||||
self._get_status_impl()
|
||||
|
||||
return self._door_state
|
||||
|
||||
def _handle_push_notification(self, namespace, payload, from_myself=False):
|
||||
def fire_garage_door_state_change(dev, channel_id, o_state, n_state, f_myself):
|
||||
if o_state != n_state:
|
||||
evt = DeviceDoorStatusEvent(dev=dev, channel_id=channel_id, door_state=n_state,
|
||||
generated_by_myself=f_myself)
|
||||
self.fire_event(evt)
|
||||
|
||||
with self._state_lock:
|
||||
if namespace == GARAGE_DOOR_STATE:
|
||||
for door in payload['state']:
|
||||
channel_index = door['channel']
|
||||
state = door['open'] == 1
|
||||
old_state = self._door_state[channel_index]
|
||||
self._door_state[channel_index] = state
|
||||
fire_garage_door_state_change(self, channel_index, old_state, state, from_myself)
|
||||
|
||||
elif namespace == REPORT:
|
||||
# For now, we simply ignore push notification of these kind.
|
||||
# In the future, we might think of handling such notification by caching them
|
||||
# and avoid the network round-trip when asking for power consumption (if the latest report is
|
||||
# recent enough)
|
||||
pass
|
||||
|
||||
else:
|
||||
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
|
||||
|
||||
def _get_status_impl(self):
|
||||
if self._door_state is None:
|
||||
self._door_state = {}
|
||||
data = self.get_sys_data()['all']
|
||||
if 'digest' in data:
|
||||
for c in data['digest']['garageDoor']:
|
||||
self._door_state[c['channel']] = c['open'] == 1
|
||||
return self._door_state
|
||||
|
||||
def _get_channel_id(self, channel):
|
||||
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
|
||||
if channel in self._channels:
|
||||
return self._channels.index(channel)
|
||||
|
||||
# if a channel name is given, lookup the channel id from the name
|
||||
if isinstance(channel, str):
|
||||
for i, c in enumerate(self.get_channels()):
|
||||
if c['devName'] == channel:
|
||||
return c['channel']
|
||||
|
||||
# If an integer is given assume that is the channel ID
|
||||
elif isinstance(channel, int):
|
||||
return channel
|
||||
|
||||
# In other cases return an error
|
||||
raise Exception("Invalid channel specified.")
|
||||
|
||||
def _operate_door(self, channel, state, callback, wait_for_sensor_confirmation):
|
||||
# If the door is already in the target status, do not execute the command.
|
||||
already_in_state = False
|
||||
with self._state_lock:
|
||||
already_in_state = self.get_status()[channel] == state
|
||||
|
||||
if already_in_state and callback is None:
|
||||
l.info("Command was not executed: the door state is already %s" % ("open" if state else "closed"))
|
||||
return
|
||||
elif already_in_state and callback is not None:
|
||||
callback(None, self._door_state[channel])
|
||||
return
|
||||
|
||||
payload = {"state": {"channel": channel, "open": state, "uuid": self.uuid}}
|
||||
if wait_for_sensor_confirmation:
|
||||
door_event = None
|
||||
if callback is None:
|
||||
door_event = Event()
|
||||
|
||||
def waiter(data):
|
||||
self.unregister_event_callback(waiter)
|
||||
if data.channel != channel:
|
||||
return
|
||||
if callback is None:
|
||||
door_event.set()
|
||||
else:
|
||||
if not compare_same_states(data.door_state, state):
|
||||
callback("Operation failed", data.door_state)
|
||||
else:
|
||||
callback(None, data.door_state)
|
||||
|
||||
self.register_event_callback(waiter)
|
||||
self.execute_command(command="SET", namespace=GARAGE_DOOR_STATE, payload=payload, callback=None)
|
||||
|
||||
if callback is None:
|
||||
door_event.wait()
|
||||
current_state = self._door_state[channel]
|
||||
if current_state != state:
|
||||
raise Exception("Operation failed.")
|
||||
|
||||
else:
|
||||
self.execute_command(command="SET", namespace=GARAGE_DOOR_STATE, payload=payload, callback=callback)
|
||||
|
||||
def open_door(self, channel=0, callback=None, ensure_opened=True):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._operate_door(c, 1, callback=callback, wait_for_sensor_confirmation=ensure_opened)
|
||||
|
||||
def close_door(self, channel=0, callback=None, ensure_closed=True):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._operate_door(c, 0, callback=callback, wait_for_sensor_confirmation=ensure_closed)
|
||||
|
||||
def get_channels(self):
|
||||
return self._channels
|
||||
|
||||
def get_power_consumption(self):
|
||||
return None
|
||||
|
||||
def get_electricity(self):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
base_str = super().__str__()
|
||||
with self._state_lock:
|
||||
if not self.online:
|
||||
return base_str
|
||||
doors = "Doors -> "
|
||||
doors += ",".join(["%d = %s" % (k, "OPEN" if v else "CLOSED") for k, v in enumerate(self.get_status())])
|
||||
return base_str + doors
|
||||
@ -0,0 +1,247 @@
|
||||
from meross_iot.cloud.abilities import *
|
||||
from meross_iot.cloud.device import AbstractMerossDevice
|
||||
from meross_iot.logger import BULBS_LOGGER as l
|
||||
from meross_iot.meross_event import BulbSwitchStateChangeEvent, BulbLightStateChangeEvent
|
||||
|
||||
|
||||
def to_rgb(rgb):
|
||||
if rgb is None:
|
||||
return None
|
||||
elif isinstance(rgb, int):
|
||||
return rgb
|
||||
elif isinstance(rgb, tuple):
|
||||
red, green, blue = rgb
|
||||
elif isinstance(rgb, dict):
|
||||
red = rgb['red']
|
||||
green = rgb['green']
|
||||
blue = rgb['blue']
|
||||
else:
|
||||
raise Exception("Invalid value for RGB!")
|
||||
|
||||
r = red << 16
|
||||
g = green << 8
|
||||
b = blue
|
||||
|
||||
return r+g+b
|
||||
|
||||
|
||||
class GenericBulb(AbstractMerossDevice):
|
||||
# Bulb state: dictionary of channel-id/bulb-state
|
||||
_state = None
|
||||
|
||||
def __init__(self, cloud_client, device_uuid, **kwords):
|
||||
self._state = {}
|
||||
super(GenericBulb, self).__init__(cloud_client, device_uuid, **kwords)
|
||||
|
||||
def _channel_control_impl(self, channel, status):
|
||||
if TOGGLE in self.get_abilities():
|
||||
return self._toggle(status)
|
||||
elif TOGGLEX in self.get_abilities():
|
||||
return self._togglex(channel, status)
|
||||
else:
|
||||
raise Exception("The current device does not support neither TOGGLE nor TOGGLEX.")
|
||||
|
||||
def _update_state(self, channel, **kwargs):
|
||||
with self._state_lock:
|
||||
if channel not in self._state:
|
||||
self._state[channel] = {}
|
||||
for k in kwargs:
|
||||
if k == 'onoff':
|
||||
self._state[channel]['onoff'] = kwargs[k]
|
||||
elif kwargs[k] is not None:
|
||||
self._state[channel][k] = kwargs[k]
|
||||
|
||||
def _toggle(self, status):
|
||||
payload = {"channel": 0, "toggle": {"onoff": status}}
|
||||
return self.execute_command("SET", TOGGLE, payload)
|
||||
|
||||
def _togglex(self, channel, status):
|
||||
payload = {'togglex': {"onoff": status, "channel": channel}}
|
||||
return self.execute_command("SET", TOGGLEX, payload)
|
||||
|
||||
def _handle_push_notification(self, namespace, payload, from_myself=False):
|
||||
def fire_bulb_switch_state_change(dev, channel_id, o_state, n_state, f_myself):
|
||||
if o_state != n_state:
|
||||
evt = BulbSwitchStateChangeEvent(dev=dev, channel_id=channel_id, is_on=n_state,
|
||||
generated_by_myself=f_myself)
|
||||
self.fire_event(evt)
|
||||
|
||||
def fire_bulb_light_state_change(dev, channel_id, o_state, n_state, f_myself):
|
||||
if o_state != n_state:
|
||||
evt = BulbLightStateChangeEvent(dev=dev, channel_id=channel_id, light_state=n_state,
|
||||
generated_by_myself=f_myself)
|
||||
self.fire_event(evt)
|
||||
|
||||
with self._state_lock:
|
||||
if namespace == TOGGLE:
|
||||
on_status=payload['toggle']['onoff'] == 1
|
||||
channel_index = 0
|
||||
old_state = self._state.get(channel_index)
|
||||
new_state = on_status
|
||||
self._update_state(channel=0, onoff=on_status)
|
||||
fire_bulb_switch_state_change(self, channel_id=0, o_state=old_state, n_state=new_state,
|
||||
f_myself=from_myself)
|
||||
|
||||
elif namespace == TOGGLEX:
|
||||
if isinstance(payload['togglex'], list):
|
||||
for c in payload['togglex']:
|
||||
channel_index = c['channel']
|
||||
on_status = c['onoff'] == 1
|
||||
old_state = self._state.get(channel_index)
|
||||
if old_state is not None:
|
||||
old_state = old_state.get('onoff')
|
||||
self._update_state(channel=channel_index, onoff=on_status)
|
||||
fire_bulb_switch_state_change(self, channel_id=channel_index, o_state=old_state,
|
||||
n_state=on_status, f_myself=from_myself)
|
||||
elif isinstance(payload['togglex'], dict):
|
||||
channel_index = payload['togglex']['channel']
|
||||
on_status = payload['togglex']['onoff'] == 1
|
||||
old_state = self._state.get(channel_index).get('onoff')
|
||||
if old_state is not None:
|
||||
old_state = old_state.get('onoff')
|
||||
self._update_state(channel=channel_index, onoff=on_status)
|
||||
fire_bulb_switch_state_change(self, channel_id=channel_index, o_state=old_state, n_state=on_status,
|
||||
f_myself=from_myself)
|
||||
|
||||
elif namespace == LIGHT:
|
||||
channel_index = payload['light']['channel']
|
||||
old_state = self._state.get(channel_index)
|
||||
new_state = payload['light']
|
||||
del new_state['channel']
|
||||
self._update_state(channel=channel_index, **new_state)
|
||||
fire_bulb_light_state_change(self, channel_id=channel_index, o_state=old_state, n_state=new_state,
|
||||
f_myself=from_myself)
|
||||
|
||||
elif namespace == REPORT:
|
||||
# For now, we simply ignore push notification of these kind.
|
||||
# In the future, we might think of handling such notification by caching them
|
||||
# and avoid the network round-trip when asking for power consumption (if the latest report is
|
||||
# recent enough)
|
||||
pass
|
||||
|
||||
else:
|
||||
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
|
||||
|
||||
def _get_status_impl(self):
|
||||
res = {}
|
||||
data = self.get_sys_data()['all']
|
||||
if 'digest' in data:
|
||||
light_channel = data['digest']['light']['channel']
|
||||
res[light_channel] = data['digest']['light']
|
||||
|
||||
for c in data['digest']['togglex']:
|
||||
res[c['channel']]['onoff'] = c['onoff'] == 1
|
||||
elif 'control' in data:
|
||||
res[0]['onoff'] = data['control']['toggle']['onoff'] == 1
|
||||
return res
|
||||
|
||||
def _get_channel_id(self, channel):
|
||||
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
|
||||
if channel in self._channels:
|
||||
return self._channels.index(channel)
|
||||
|
||||
# if a channel name is given, lookup the channel id from the name
|
||||
if isinstance(channel, str):
|
||||
for i, c in enumerate(self.get_channels()):
|
||||
if c['devName'] == channel:
|
||||
return c['channel']
|
||||
|
||||
# If an integer is given assume that is the channel ID
|
||||
elif isinstance(channel, int):
|
||||
return channel
|
||||
|
||||
# In other cases return an error
|
||||
raise Exception("Invalid channel specified.")
|
||||
|
||||
def get_status(self, channel=0):
|
||||
# In order to optimize the network traffic, we don't call the get_status() api at every request.
|
||||
# On the contrary, we call it the first time. Then, the rest of the API will silently listen
|
||||
# for state changes and will automatically update the self._state structure listening for
|
||||
# messages of the device.
|
||||
# Such approach, however, has a side effect. If we call TOGGLE/TOGGLEX and immediately after we call
|
||||
# get_status(), the reported status will be still the old one. This is a race condition because the
|
||||
# "status" RESPONSE will be delivered some time after the TOGGLE REQUEST. It's not a big issue for now,
|
||||
# and synchronizing the two things would be inefficient and probably not very useful.
|
||||
# Just remember to wait some time before testing the status of the item after a toggle.
|
||||
c = self._get_channel_id(channel)
|
||||
if self._state == {}:
|
||||
current_state = self._get_status_impl()
|
||||
with self._state_lock:
|
||||
self._state = current_state
|
||||
return self._state[c]
|
||||
|
||||
def get_channels(self):
|
||||
return self._channels
|
||||
|
||||
def get_channel_status(self, channel):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
c = self._get_channel_id(ch_id)
|
||||
return self.get_status(c)
|
||||
|
||||
def turn_on_channel(self, channel):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
c = self._get_channel_id(ch_id)
|
||||
return self._channel_control_impl(c, 1)
|
||||
|
||||
def turn_off_channel(self, channel):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
c = self._get_channel_id(ch_id)
|
||||
return self._channel_control_impl(c, 0)
|
||||
|
||||
def turn_on(self, channel=0):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
c = self._get_channel_id(ch_id)
|
||||
return self._channel_control_impl(c, 1)
|
||||
|
||||
def turn_off(self, channel=0):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
c = self._get_channel_id(ch_id)
|
||||
return self._channel_control_impl(c, 0)
|
||||
|
||||
def set_light_color(self, channel=0, rgb=None, luminance=100, temperature=100, capacity=5):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
|
||||
# Convert the RGB to integer
|
||||
color = to_rgb(rgb)
|
||||
|
||||
payload = {
|
||||
'light': {
|
||||
'capacity': capacity,
|
||||
'channel': ch_id,
|
||||
'gradual': 0,
|
||||
'luminance': luminance,
|
||||
'rgb': color,
|
||||
'temperature': temperature
|
||||
}
|
||||
}
|
||||
|
||||
# TODO: fix this as soon as we get hands on a real MSS560 and see what the payload looks like...
|
||||
# handle mss560m differently
|
||||
if self.type.lower() == 'mss560m':
|
||||
pl = {
|
||||
'light': self.get_light_color()
|
||||
}
|
||||
pl['light']['channel'] = channel
|
||||
pl['light']['luminance'] = luminance
|
||||
payload = pl
|
||||
|
||||
self.execute_command(command='SET', namespace=LIGHT, payload=payload)
|
||||
|
||||
def get_light_color(self, channel=0):
|
||||
ch_id = self._get_channel_id(channel)
|
||||
return self.get_status(channel=ch_id)
|
||||
|
||||
def get_power_consumption(self):
|
||||
return None
|
||||
|
||||
def get_electricity(self):
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
base_str = super().__str__()
|
||||
with self._state_lock:
|
||||
if not self.online:
|
||||
return base_str
|
||||
channels = "Channels: "
|
||||
channels += ",".join(["%d = %s" % (k, "ON" if v else "OFF") for k, v in enumerate(self._state)])
|
||||
return base_str + "\n" + "\n" + channels
|
||||
@ -0,0 +1,199 @@
|
||||
from meross_iot.cloud.abilities import *
|
||||
from meross_iot.cloud.device import AbstractMerossDevice
|
||||
from meross_iot.logger import POWER_PLUGS_LOGGER as l
|
||||
from meross_iot.meross_event import DeviceSwitchStatusEvent
|
||||
|
||||
|
||||
class GenericPlug(AbstractMerossDevice):
|
||||
# Channels
|
||||
_channels = []
|
||||
|
||||
# Dictionary {channel->status}
|
||||
_state = {}
|
||||
|
||||
def __init__(self, cloud_client, device_uuid, **kwords):
|
||||
super(GenericPlug, self).__init__(cloud_client, device_uuid, **kwords)
|
||||
|
||||
def _get_consumptionx(self):
|
||||
return self.execute_command("GET", CONSUMPTIONX, {})
|
||||
|
||||
def _get_electricity(self):
|
||||
return self.execute_command("GET", ELECTRICITY, {})
|
||||
|
||||
def _toggle(self, status, callback=None):
|
||||
payload = {"channel": 0, "toggle": {"onoff": status}}
|
||||
return self.execute_command("SET", TOGGLE, payload, callback=callback)
|
||||
|
||||
def _togglex(self, channel, status, callback=None):
|
||||
payload = {'togglex': {"onoff": status, "channel": channel}}
|
||||
return self.execute_command("SET", TOGGLEX, payload, callback=callback)
|
||||
|
||||
def _channel_control_impl(self, channel, status, callback=None):
|
||||
if TOGGLE in self.get_abilities():
|
||||
return self._toggle(status, callback=callback)
|
||||
elif TOGGLEX in self.get_abilities():
|
||||
return self._togglex(channel, status, callback=callback)
|
||||
else:
|
||||
raise Exception("The current device does not support neither TOGGLE nor TOGGLEX.")
|
||||
|
||||
def _handle_push_notification(self, namespace, payload, from_myself=False):
|
||||
def fire_switch_state_change(dev, channel_id, o_state, n_state, f_myself):
|
||||
if o_state != n_state:
|
||||
evt = DeviceSwitchStatusEvent(dev=dev, channel_id=channel_id, switch_state=n_state,
|
||||
generated_by_myself=f_myself)
|
||||
self.fire_event(evt)
|
||||
|
||||
with self._state_lock:
|
||||
if namespace == TOGGLE:
|
||||
# Update the local state and fire the event only if the state actually changed
|
||||
channel_index = 0
|
||||
old_switch_state = self._state.get(channel_index)
|
||||
switch_state = payload['toggle']['onoff'] == 1
|
||||
self._state[channel_index] = switch_state
|
||||
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
|
||||
|
||||
elif namespace == TOGGLEX:
|
||||
if isinstance(payload['togglex'], list):
|
||||
for c in payload['togglex']:
|
||||
# Update the local state and fire the event only if the state actually changed
|
||||
channel_index = c['channel']
|
||||
old_switch_state = self._state.get(channel_index)
|
||||
switch_state = c['onoff'] == 1
|
||||
self._state[channel_index] = switch_state
|
||||
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
|
||||
|
||||
elif isinstance(payload['togglex'], dict):
|
||||
# Update the local state and fire the event only if the state actually changed
|
||||
channel_index = payload['togglex']['channel']
|
||||
old_switch_state = self._state.get(channel_index)
|
||||
switch_state = payload['togglex']['onoff'] == 1
|
||||
self._state[channel_index] = switch_state
|
||||
fire_switch_state_change(self, channel_index, old_switch_state, switch_state, from_myself)
|
||||
|
||||
elif namespace == REPORT or namespace == CONSUMPTIONX:
|
||||
# For now, we simply ignore push notification of these kind.
|
||||
# In the future, we might think of handling such notification by caching them
|
||||
# and avoid the network round-trip when asking for power consumption (if the latest report is
|
||||
# recent enough)
|
||||
pass
|
||||
|
||||
else:
|
||||
l.error("Unknown/Unsupported namespace/command: %s" % namespace)
|
||||
|
||||
def _get_status_impl(self):
|
||||
res = {}
|
||||
data = self.get_sys_data()['all']
|
||||
if 'digest' in data:
|
||||
for c in data['digest']['togglex']:
|
||||
res[c['channel']] = c['onoff'] == 1
|
||||
elif 'control' in data:
|
||||
res[0] = data['control']['toggle']['onoff'] == 1
|
||||
return res
|
||||
|
||||
def _get_channel_id(self, channel):
|
||||
# Otherwise, if the passed channel looks like the channel spec, lookup its array indexindex
|
||||
if channel in self._channels:
|
||||
return self._channels.index(channel)
|
||||
|
||||
# if a channel name is given, lookup the channel id from the name
|
||||
if isinstance(channel, str):
|
||||
for i, c in enumerate(self.get_channels()):
|
||||
if c['devName'] == channel:
|
||||
return c['channel']
|
||||
|
||||
# If an integer is given assume that is the channel ID
|
||||
elif isinstance(channel, int):
|
||||
return channel
|
||||
|
||||
# In other cases return an error
|
||||
raise Exception("Invalid channel specified.")
|
||||
|
||||
def get_status(self, channel=0):
|
||||
# In order to optimize the network traffic, we don't call the get_status() api at every request.
|
||||
# On the contrary, we only call it the first time. Then, the rest of the API will silently listen
|
||||
# for state changes and will automatically update the self._state structure listening for
|
||||
# messages of the device.
|
||||
# Such approach, however, has a side effect. If we call TOGGLE/TOGGLEX and immediately after we call
|
||||
# get_status(), the reported status will be still the old one. This is a race condition because the
|
||||
# "status" RESPONSE will be delivered some time after the TOGGLE REQUEST. It's not a big issue for now,
|
||||
# and synchronizing the two things would be inefficient and probably not very useful.
|
||||
# Just remember to wait some time before testing the status of the item after a toggle.
|
||||
with self._state_lock:
|
||||
c = self._get_channel_id(channel)
|
||||
if self._state == {}:
|
||||
self._state = self._get_status_impl()
|
||||
return self._state[c]
|
||||
|
||||
def get_power_consumption(self):
|
||||
if CONSUMPTIONX in self.get_abilities():
|
||||
return self._get_consumptionx()['consumptionx']
|
||||
else:
|
||||
# Not supported!
|
||||
return None
|
||||
|
||||
def get_electricity(self):
|
||||
if ELECTRICITY in self.get_abilities():
|
||||
return self._get_electricity()['electricity']
|
||||
else:
|
||||
# Not supported!
|
||||
return None
|
||||
|
||||
def get_channels(self):
|
||||
return self._channels
|
||||
|
||||
def get_channel_status(self, channel):
|
||||
c = self._get_channel_id(channel)
|
||||
return self.get_status(c)
|
||||
|
||||
def turn_on_channel(self, channel, callback=None):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._channel_control_impl(c, 1, callback=callback)
|
||||
|
||||
def turn_off_channel(self, channel, callback=None):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._channel_control_impl(c, 0, callback=callback)
|
||||
|
||||
def turn_on(self, channel=0, callback=None):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._channel_control_impl(c, 1, callback=callback)
|
||||
|
||||
def turn_off(self, channel=0, callback=None):
|
||||
c = self._get_channel_id(channel)
|
||||
return self._channel_control_impl(c, 0, callback=callback)
|
||||
|
||||
def get_usb_channel_index(self):
|
||||
# Look for the usb channel
|
||||
for i, c in enumerate(self.get_channels()):
|
||||
if 'type' in c and c['type'] == 'USB':
|
||||
return i
|
||||
return None
|
||||
|
||||
def enable_usb(self, callback=None):
|
||||
c = self.get_usb_channel_index()
|
||||
if c is None:
|
||||
return
|
||||
else:
|
||||
return self.turn_on_channel(c, callback=callback)
|
||||
|
||||
def disable_usb(self, callback=None):
|
||||
c = self.get_usb_channel_index()
|
||||
if c is None:
|
||||
return
|
||||
else:
|
||||
return self.turn_off_channel(c, callback=callback)
|
||||
|
||||
def get_usb_status(self):
|
||||
c = self.get_usb_channel_index()
|
||||
if c is None:
|
||||
return
|
||||
else:
|
||||
return self.get_channel_status(c)
|
||||
|
||||
def __str__(self):
|
||||
base_str = super().__str__()
|
||||
with self._state_lock:
|
||||
if not self.online:
|
||||
return base_str
|
||||
channels = "Channels: "
|
||||
channels += ",".join(["%d = %s" % (k, "ON" if v else "OFF") for k, v in enumerate(self._state)])
|
||||
return base_str + "\n" + "\n" + channels
|
||||
@ -0,0 +1,2 @@
|
||||
class CommandTimeoutException(Exception):
|
||||
pass
|
||||
@ -0,0 +1,2 @@
|
||||
class ConnectionDroppedException(Exception):
|
||||
pass
|
||||
@ -0,0 +1,2 @@
|
||||
class OfflineDeviceException(Exception):
|
||||
pass
|
||||
@ -0,0 +1,2 @@
|
||||
class StatusTimeoutException(Exception):
|
||||
pass
|
||||
@ -0,0 +1,2 @@
|
||||
LONG_TIMEOUT = 30.0 # For wifi scan
|
||||
SHORT_TIMEOUT = 10.0 # For any other command
|
||||
5
RPI Code/Meross_2/MerossIot_/meross_iot/credentials.py
Normal file
5
RPI Code/Meross_2/MerossIot_/meross_iot/credentials.py
Normal file
@ -0,0 +1,5 @@
|
||||
class MerossCloudCreds(object):
|
||||
token = None
|
||||
key = None
|
||||
user_id = None
|
||||
user_email = None
|
||||
24
RPI Code/Meross_2/MerossIot_/meross_iot/logger.py
Normal file
24
RPI Code/Meross_2/MerossIot_/meross_iot/logger.py
Normal file
@ -0,0 +1,24 @@
|
||||
import logging
|
||||
from logging import StreamHandler
|
||||
from sys import stdout
|
||||
|
||||
ROOT_MEROSS_LOGGER = logging.getLogger("meross")
|
||||
MANAGER_LOGGER = ROOT_MEROSS_LOGGER.getChild("manager")
|
||||
CONNECTION_MANAGER_LOGGER = ROOT_MEROSS_LOGGER.getChild("connection")
|
||||
NETWORK_DATA = ROOT_MEROSS_LOGGER.getChild("network_data")
|
||||
POWER_PLUGS_LOGGER = ROOT_MEROSS_LOGGER.getChild("power_plugs")
|
||||
BULBS_LOGGER = ROOT_MEROSS_LOGGER.getChild("light_bulbs")
|
||||
DEVICE_LOGGER = ROOT_MEROSS_LOGGER.getChild("generic_device")
|
||||
|
||||
|
||||
h = StreamHandler(stream=stdout)
|
||||
ROOT_MEROSS_LOGGER.addHandler(h)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
h.setFormatter(formatter)
|
||||
|
||||
|
||||
# Call this module to adjust the verbosity of the stream output. By default, only INFO is written to STDOUT log.
|
||||
def set_log_level(root=logging.DEBUG, connection=logging.INFO, network=logging.INFO):
|
||||
ROOT_MEROSS_LOGGER.setLevel(root)
|
||||
CONNECTION_MANAGER_LOGGER.setLevel(connection)
|
||||
NETWORK_DATA.setLevel(network)
|
||||
177
RPI Code/Meross_2/MerossIot_/meross_iot/manager.py
Normal file
177
RPI Code/Meross_2/MerossIot_/meross_iot/manager.py
Normal file
@ -0,0 +1,177 @@
|
||||
from meross_iot.api import MerossHttpClient
|
||||
from meross_iot.cloud.client import MerossCloudClient
|
||||
from meross_iot.cloud.device_factory import build_wrapper
|
||||
from threading import RLock
|
||||
from meross_iot.logger import MANAGER_LOGGER as l
|
||||
from meross_iot.meross_event import DeviceOnlineStatusEvent
|
||||
|
||||
|
||||
class MerossManager(object):
|
||||
# HTTPClient object used to discover devices
|
||||
_http_client = None
|
||||
|
||||
# Dictionary of devices that are currently handled by this manager
|
||||
# as UUID -> Device
|
||||
_devices = None
|
||||
|
||||
# Lock item used to protect access to the device collection
|
||||
_devices_lock = None
|
||||
|
||||
# Cloud credentials to be used against the Meross MQTT cloud
|
||||
_cloud_creds = None
|
||||
|
||||
_cloud_client = None
|
||||
|
||||
# List of callbacks that should be called when an event occurs
|
||||
_event_callbacks = None
|
||||
_event_callbacks_lock = None
|
||||
|
||||
def __init__(self, meross_email, meross_password):
|
||||
self._devices_lock = RLock()
|
||||
self._devices = dict()
|
||||
self._event_callbacks_lock = RLock()
|
||||
self._event_callbacks = []
|
||||
|
||||
self._http_client = MerossHttpClient(email=meross_email, password=meross_password)
|
||||
self._cloud_creds = self._http_client.get_cloud_credentials()
|
||||
|
||||
# Instantiate the mqtt cloud client
|
||||
self._cloud_client = MerossCloudClient(cloud_credentials=self._cloud_creds,
|
||||
push_message_callback=self._dispatch_push_notification)
|
||||
self._cloud_client.connection_status.register_connection_event_callback(callback=self._fire_event)
|
||||
|
||||
def start(self):
|
||||
# Connect to the mqtt broker
|
||||
self._cloud_client.connect()
|
||||
self._discover_devices()
|
||||
|
||||
def stop(self):
|
||||
self._cloud_client.close()
|
||||
|
||||
def register_event_handler(self, callback):
|
||||
with self._event_callbacks_lock:
|
||||
if callback in self._event_callbacks:
|
||||
pass
|
||||
else:
|
||||
self._event_callbacks.append(callback)
|
||||
|
||||
def unregister_event_handler(self, callback):
|
||||
with self._event_callbacks_lock:
|
||||
if callback not in self._event_callbacks:
|
||||
pass
|
||||
else:
|
||||
self._event_callbacks.remove(callback)
|
||||
|
||||
def get_device_by_uuid(self, uuid):
|
||||
dev = None
|
||||
with self._devices_lock:
|
||||
dev = self._devices.get(uuid)
|
||||
|
||||
return dev
|
||||
|
||||
def get_device_by_name(self, name):
|
||||
with self._devices_lock:
|
||||
for k, v in self._devices.items():
|
||||
if v.name.lower() == name.lower():
|
||||
return v
|
||||
return None
|
||||
|
||||
def get_supported_devices(self):
|
||||
return [x for k, x in self._devices.items()]
|
||||
|
||||
def get_devices_by_kind(self, clazz):
|
||||
res = []
|
||||
with self._devices_lock:
|
||||
for k, v in self._devices.items():
|
||||
if isinstance(v, clazz):
|
||||
res.append(v)
|
||||
return res
|
||||
|
||||
def get_devices_by_type(self, type_name):
|
||||
res = []
|
||||
with self._devices_lock:
|
||||
for k, v in self._devices.items():
|
||||
if v.type.lower() == type_name.lower():
|
||||
res.append(v)
|
||||
return res
|
||||
|
||||
def _dispatch_push_notification(self, message, from_myself=False):
|
||||
"""
|
||||
When a push notification is received from the MQTT client, it needs to be delivered to the
|
||||
corresponding device. This method serves that scope.
|
||||
:param message:
|
||||
:param from_myself: boolean flag. When True, it means that the message received is related to a
|
||||
previous request issued by this client. When is false, it means the message is related to some other
|
||||
client.
|
||||
:return:
|
||||
"""
|
||||
header = message['header'] # type: dict
|
||||
payload = message['payload'] # type: dict
|
||||
|
||||
# Identify the UUID of the target device by looking at the FROM field of the message header
|
||||
dev_uuid = header['from'].split('/')[2]
|
||||
device = None
|
||||
with self._devices_lock:
|
||||
device = self._devices.get(dev_uuid)
|
||||
|
||||
if device is not None:
|
||||
namespace = header['namespace']
|
||||
device.handle_push_notification(namespace, payload, from_myself=from_myself)
|
||||
else:
|
||||
# If we receive a push notification from a device that is not yet contained into our registry,
|
||||
# it probably means a new one has just been registered with the meross cloud.
|
||||
# Therefor, let's retrieve info from the HTTP api.
|
||||
self._discover_devices()
|
||||
|
||||
def _discover_devices(self, online_only=False):
|
||||
"""
|
||||
Discovers the devices that are visible via HTTP API and update the internal list of
|
||||
managed devices accordingly.
|
||||
:return:
|
||||
"""
|
||||
for dev in self._http_client.list_devices():
|
||||
online = dev['onlineStatus']
|
||||
|
||||
if online_only and online != 1:
|
||||
# The device is not online, so we skip it.
|
||||
continue
|
||||
|
||||
# If the device we have discovered is not in the list we already handle, we need to add it.
|
||||
self._handle_device_discovered(dev)
|
||||
|
||||
return self._devices
|
||||
|
||||
def _handle_device_discovered(self, dev):
|
||||
d_type = dev['deviceType']
|
||||
d_uuid = dev['uuid']
|
||||
device = build_wrapper(device_type=d_type, device_uuid=d_uuid, cloud_client=self._cloud_client,
|
||||
device_specs=dev)
|
||||
|
||||
if device is not None:
|
||||
# Check if the discovered device is already in the list of handled devices.
|
||||
# If not, add it right away. Otherwise, ignore it.
|
||||
is_new = False
|
||||
new_dev = None
|
||||
with self._devices_lock:
|
||||
if d_uuid not in self._devices:
|
||||
is_new = True
|
||||
new_dev = build_wrapper(cloud_client=self._cloud_client,
|
||||
device_type=d_type, device_uuid=d_uuid, device_specs=dev)
|
||||
self._devices[d_uuid] = new_dev
|
||||
|
||||
# If this is new device, register the event handler for it and fire the ONLINE event.
|
||||
if is_new:
|
||||
with self._event_callbacks_lock:
|
||||
for c in self._event_callbacks:
|
||||
new_dev.register_event_callback(c)
|
||||
|
||||
evt = DeviceOnlineStatusEvent(new_dev, new_dev.online)
|
||||
self._fire_event(evt)
|
||||
|
||||
def _fire_event(self, eventobj):
|
||||
for c in self._event_callbacks:
|
||||
try:
|
||||
c(eventobj)
|
||||
except:
|
||||
l.exception("An unhandled error occurred while invoking callback")
|
||||
|
||||
103
RPI Code/Meross_2/MerossIot_/meross_iot/meross_event.py
Normal file
103
RPI Code/Meross_2/MerossIot_/meross_iot/meross_event.py
Normal file
@ -0,0 +1,103 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MerossEventType(Enum):
|
||||
# Fired when the MQTT client connects/disconnects to the MQTT broker
|
||||
CLIENT_CONNECTION = 10
|
||||
DEVICE_ONLINE_STATUS = 100
|
||||
DEVICE_SWITCH_STATUS = 1000
|
||||
DEVICE_BULB_SWITCH_STATE = 2000
|
||||
DEVICE_BULB_STATE = 2001
|
||||
GARAGE_DOOR_STATUS = 2000
|
||||
|
||||
|
||||
class MerossEvent(object):
|
||||
event_type = None # type: MerossEventType
|
||||
|
||||
def __init__(self, event_type):
|
||||
self.event_type = event_type
|
||||
|
||||
|
||||
class ClientConnectionEvent(MerossEvent):
|
||||
status = None
|
||||
|
||||
def __init__(self, current_status):
|
||||
super(ClientConnectionEvent, self).__init__(MerossEventType.CLIENT_CONNECTION)
|
||||
self.status = current_status
|
||||
|
||||
|
||||
class DeviceOnlineStatusEvent(MerossEvent):
|
||||
# Pointer to the device object
|
||||
device = None
|
||||
|
||||
# Current status of the device
|
||||
status = None
|
||||
|
||||
def __init__(self, dev, current_status):
|
||||
super(DeviceOnlineStatusEvent, self).__init__(MerossEventType.DEVICE_ONLINE_STATUS)
|
||||
self.device = dev
|
||||
self.status = "online" if current_status else "offline"
|
||||
|
||||
|
||||
class DeviceSwitchStatusEvent(MerossEvent):
|
||||
# Pointer to the device object
|
||||
device = None
|
||||
|
||||
# Channel ID where the event occurred
|
||||
channel_id = None
|
||||
|
||||
# Current state of the switch where the event occurred
|
||||
switch_state = None
|
||||
|
||||
# Indicates id the event was generated by a command issued by the library itself.
|
||||
# This is particularly useful in the case the user handler wants only to react
|
||||
# to events generated by third parties.
|
||||
generated_by_myself = None
|
||||
|
||||
def __init__(self, dev, channel_id, switch_state, generated_by_myself):
|
||||
super(DeviceSwitchStatusEvent, self).__init__(MerossEventType.DEVICE_SWITCH_STATUS)
|
||||
self.device = dev
|
||||
self.channel_id = channel_id
|
||||
self.switch_state = switch_state
|
||||
self.generated_by_myself = generated_by_myself
|
||||
|
||||
|
||||
class DeviceDoorStatusEvent(MerossEvent):
|
||||
# Pointer to the device object
|
||||
device = None
|
||||
|
||||
# Current state of the door
|
||||
door_state = None
|
||||
|
||||
# Channel related to the door controller
|
||||
channel = None
|
||||
|
||||
# Indicates id the event was generated by a command issued by the library itself.
|
||||
# This is particularly useful in the case the user handler wants only to react
|
||||
# to events generated by third parties.
|
||||
generated_by_myself = None
|
||||
|
||||
def __init__(self, dev, channel_id, door_state, generated_by_myself):
|
||||
super(DeviceDoorStatusEvent, self).__init__(MerossEventType.GARAGE_DOOR_STATUS)
|
||||
self.device = dev
|
||||
self.channel = channel_id
|
||||
self.door_state = "open" if door_state else "closed"
|
||||
self.generated_by_myself = generated_by_myself
|
||||
|
||||
|
||||
class BulbSwitchStateChangeEvent(MerossEvent):
|
||||
def __init__(self, dev, channel_id, is_on, generated_by_myself):
|
||||
super(BulbSwitchStateChangeEvent, self).__init__(MerossEventType.DEVICE_BULB_SWITCH_STATE)
|
||||
self.device = dev
|
||||
self.channel = channel_id
|
||||
self.is_on = is_on
|
||||
self.generated_by_myself = generated_by_myself
|
||||
|
||||
|
||||
class BulbLightStateChangeEvent(MerossEvent):
|
||||
def __init__(self, dev, channel_id, light_state, generated_by_myself):
|
||||
super(BulbLightStateChangeEvent, self).__init__(MerossEventType.DEVICE_BULB_STATE)
|
||||
self.device = dev
|
||||
self.channel = channel_id
|
||||
self.light_state = light_state
|
||||
self.generated_by_myself = generated_by_myself
|
||||
@ -0,0 +1,23 @@
|
||||
from threading import RLock
|
||||
|
||||
|
||||
class AtomicCounter(object):
|
||||
_lock = None
|
||||
|
||||
def __init__(self, initialValue):
|
||||
self._lock = RLock()
|
||||
self._val = initialValue
|
||||
|
||||
def dec(self):
|
||||
with self._lock:
|
||||
self._val -= 1
|
||||
return self._val
|
||||
|
||||
def inc(self):
|
||||
with self._lock:
|
||||
self._val += 1
|
||||
return self._val
|
||||
|
||||
def get(self):
|
||||
with self._lock:
|
||||
return self._val
|
||||
3
RPI Code/Meross_2/MerossIot_/requirements.txt
Normal file
3
RPI Code/Meross_2/MerossIot_/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
paho-mqtt>=1.3.1
|
||||
requests>=2.19.1
|
||||
retrying>=1.3.3
|
||||
43
RPI Code/Meross_2/MerossIot_/setup.py
Normal file
43
RPI Code/Meross_2/MerossIot_/setup.py
Normal file
@ -0,0 +1,43 @@
|
||||
from os import path
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
with open(path.join(here, 'README.md'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='meross_iot',
|
||||
version='0.3.1.3',
|
||||
packages=find_packages(exclude=('tests',)),
|
||||
url='https://github.com/albertogeniola/MerossIot',
|
||||
license='MIT',
|
||||
author='Alberto Geniola',
|
||||
author_email='albertogeniola@gmail.com',
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Operating System :: OS Independent'
|
||||
],
|
||||
description='A simple library to deal with Meross devices. At the moment MSS110, MSS210, MSS310, MSS310H '
|
||||
'smart plugs and the MSS425E power strip. Other meross device might work out of the box with limited '
|
||||
'functionality. Give it a try and, in case of problems, let the developer know by opening an issue '
|
||||
'on Github.',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
keywords='meross smartplug smartbulb iot mqtt domotic switch mss310 mss210 mss110 mss425e msl20 msg100',
|
||||
project_urls={
|
||||
'Documentation': 'https://github.com/albertogeniola/MerossIot',
|
||||
'Funding': 'https://donate.pypi.org',
|
||||
'Source': 'https://github.com/albertogeniola/MerossIot',
|
||||
'Tracker': 'https://github.com/albertogeniola/MerossIot/issues',
|
||||
},
|
||||
install_requires=[
|
||||
'paho-mqtt>=1.3.1',
|
||||
'requests>=2.19.1',
|
||||
'retrying>=1.3.3',
|
||||
],
|
||||
python_requires='>=3.5',
|
||||
test_suite='tests'
|
||||
)
|
||||
0
RPI Code/Meross_2/MerossIot_/tests/__init__.py
Normal file
0
RPI Code/Meross_2/MerossIot_/tests/__init__.py
Normal file
143
RPI Code/Meross_2/MerossIot_/tests/readme.py
Normal file
143
RPI Code/Meross_2/MerossIot_/tests/readme.py
Normal file
@ -0,0 +1,143 @@
|
||||
from meross_iot.manager import MerossManager
|
||||
from meross_iot.meross_event import MerossEventType
|
||||
from meross_iot.cloud.devices.light_bulbs import GenericBulb
|
||||
from meross_iot.cloud.devices.power_plugs import GenericPlug
|
||||
from meross_iot.cloud.devices.door_openers import GenericGarageDoorOpener
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL') or "YOUR_MEROSS_CLOUD_EMAIL"
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD') or "YOUR_MEROSS_CLOUD_PASSWORD"
|
||||
|
||||
|
||||
def event_handler(eventobj):
|
||||
if eventobj.event_type == MerossEventType.DEVICE_ONLINE_STATUS:
|
||||
print("Device online status changed: %s went %s" % (eventobj.device.name, eventobj.status))
|
||||
pass
|
||||
|
||||
elif eventobj.event_type == MerossEventType.DEVICE_SWITCH_STATUS:
|
||||
print("Switch state changed: Device %s (channel %d) went %s" % (eventobj.device.name, eventobj.channel_id,
|
||||
eventobj.switch_state))
|
||||
elif eventobj.event_type == MerossEventType.CLIENT_CONNECTION:
|
||||
print("MQTT connection state changed: client went %s" % eventobj.status)
|
||||
|
||||
# TODO: Give example of reconnection?
|
||||
|
||||
elif eventobj.event_type == MerossEventType.GARAGE_DOOR_STATUS:
|
||||
print("Garage door is now %s" % eventobj.door_state)
|
||||
|
||||
else:
|
||||
print("Unknown event!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Initiates the Meross Cloud Manager. This is in charge of handling the communication with the remote endpoint
|
||||
manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
|
||||
# Register event handlers for the manager...
|
||||
manager.register_event_handler(event_handler)
|
||||
|
||||
# Starts the manager
|
||||
manager.start()
|
||||
|
||||
# You can retrieve the device you are looking for in various ways:
|
||||
# By kind
|
||||
bulbs = manager.get_devices_by_kind(GenericBulb)
|
||||
plugs = manager.get_devices_by_kind(GenericPlug)
|
||||
door_openers = manager.get_devices_by_kind(GenericGarageDoorOpener)
|
||||
all_devices = manager.get_supported_devices()
|
||||
|
||||
# Print some basic specs about the discovered devices
|
||||
print("All the bulbs I found:")
|
||||
for b in bulbs:
|
||||
print(b)
|
||||
|
||||
print("All the plugs I found:")
|
||||
for p in plugs:
|
||||
print(p)
|
||||
|
||||
print("All the garage openers I found:")
|
||||
for g in door_openers:
|
||||
print(g)
|
||||
|
||||
print("All the supported devices I found:")
|
||||
for d in all_devices:
|
||||
print(d)
|
||||
|
||||
# You can also retrieve devices by the UUID/name
|
||||
# a_device = manager.get_device_by_name("My Plug")
|
||||
# a_device = manager.get_device_by_uuid("My Plug")
|
||||
|
||||
# Or you can retrieve all the device by the HW type
|
||||
# all_mss310 = manager.get_devices_by_type("mss310")
|
||||
|
||||
# ------------------------------
|
||||
# Let's play the garage openers.
|
||||
# ------------------------------
|
||||
for g in door_openers:
|
||||
if not g.online:
|
||||
print("The garage controller %s seems to be offline. Cannot play with that..." % g.name)
|
||||
continue
|
||||
|
||||
print("Opening door %s..." % g.name)
|
||||
g.open_door()
|
||||
print("Closing door %s..." % g.name)
|
||||
g.close_door()
|
||||
|
||||
# ---------------------
|
||||
# Let's play with bulbs
|
||||
# ---------------------
|
||||
for b in bulbs: # type: GenericBulb
|
||||
if not b.online:
|
||||
print("The bulb %s seems to be offline. Cannot play with that..." % b.name)
|
||||
continue
|
||||
|
||||
print("Let's play with bulb %s" % b.name)
|
||||
if not b.supports_light_control():
|
||||
print("Too bad bulb %s does not support light control %s" % b.name)
|
||||
else:
|
||||
# Let's make it red!
|
||||
b.set_light_color(rgb=(255, 0, 0))
|
||||
|
||||
b.turn_on()
|
||||
time.sleep(1)
|
||||
b.turn_off()
|
||||
|
||||
# ---------------------------
|
||||
# Let's play with smart plugs
|
||||
# ---------------------------
|
||||
for p in plugs: # type: GenericPlug
|
||||
if not p.online:
|
||||
print("The plug %s seems to be offline. Cannot play with that..." % p.name)
|
||||
continue
|
||||
|
||||
print("Let's play with smart plug %s" % p.name)
|
||||
|
||||
channels = len(p.get_channels())
|
||||
print("The plug %s supports %d channels." % (p.name, channels))
|
||||
for i in range(0, channels):
|
||||
print("Turning on channel %d of %s" % (i, p.name))
|
||||
p.turn_on_channel(i)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
print("Turning off channel %d of %s" % (i, p.name))
|
||||
p.turn_off_channel(i)
|
||||
|
||||
usb_channel = p.get_usb_channel_index()
|
||||
if usb_channel is not None:
|
||||
print("Awesome! This device also supports USB power.")
|
||||
p.enable_usb()
|
||||
time.sleep(1)
|
||||
p.disable_usb()
|
||||
|
||||
if p.supports_electricity_reading():
|
||||
print("Awesome! This device also supports power consumption reading.")
|
||||
print("Current consumption is: %s" % str(p.get_electricity()))
|
||||
|
||||
# At this point, we are all done playing with the library, so we gracefully disconnect and clean resources.
|
||||
print("We are done playing. Cleaning resources...")
|
||||
manager.stop()
|
||||
|
||||
print("Bye bye!")
|
||||
61
RPI Code/Meross_2/MerossIot_/tests/test_async.py
Normal file
61
RPI Code/Meross_2/MerossIot_/tests/test_async.py
Normal file
@ -0,0 +1,61 @@
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
from meross_iot.manager import MerossManager
|
||||
from threading import Thread, current_thread
|
||||
import random
|
||||
from meross_iot.logger import set_log_level
|
||||
from logging import DEBUG, INFO
|
||||
|
||||
from meross_iot.utilities.synchronization import AtomicCounter
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL')
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD')
|
||||
|
||||
|
||||
class TestMSS425ETest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.counter = AtomicCounter(0)
|
||||
set_log_level(INFO, INFO)
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('mss425e')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device mss425e")
|
||||
|
||||
def print_result(self, error, res):
|
||||
# TODO: assertions
|
||||
print("Error: %s, Result: %s" % (error, res))
|
||||
print("Counter=%d" % self.counter.inc())
|
||||
|
||||
# TODO: This fails. We need to investigate why.
|
||||
"""
|
||||
def test_async(self):
|
||||
for i in range(0, 40):
|
||||
op = bool(random.getrandbits(1))
|
||||
channel = random.randrange(0, len(self.device.get_channels()))
|
||||
if not op:
|
||||
self.device.turn_off_channel(channel, callback=self.print_result)
|
||||
else:
|
||||
self.device.turn_on_channel(channel, callback=self.print_result)
|
||||
while self.counter.get() < 40:
|
||||
time.sleep(1)
|
||||
|
||||
def test_sync(self):
|
||||
for i in range(0, 30):
|
||||
print("Executing command %d" % i)
|
||||
time.sleep(0.01)
|
||||
channel = random.randrange(0, len(self.device.get_channels()))
|
||||
self.device.turn_off_channel(channel)
|
||||
time.sleep(0.01)
|
||||
self.device.turn_on_channel(channel)
|
||||
time.sleep(0.01)
|
||||
print("Done command %d" % i)
|
||||
"""
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.stop()
|
||||
54
RPI Code/Meross_2/MerossIot_/tests/test_bulbs.py
Normal file
54
RPI Code/Meross_2/MerossIot_/tests/test_bulbs.py
Normal file
@ -0,0 +1,54 @@
|
||||
from meross_iot.manager import MerossManager
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
import random
|
||||
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL')
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD')
|
||||
|
||||
|
||||
class TestMSL120Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('msl120')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device msl120")
|
||||
|
||||
def test_power_cycle(self):
|
||||
time.sleep(2)
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status()['onoff'])
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status()['onoff'])
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(self.device.get_status()['onoff'])
|
||||
|
||||
def test_get_info(self):
|
||||
state = self.device.get_status()
|
||||
assert state is not None
|
||||
|
||||
def test_set_light_color(self):
|
||||
r = int(random.random() * 255)
|
||||
g = int(random.random() * 255)
|
||||
b = int(random.random() * 255)
|
||||
self.device.set_light_color(channel=0, rgb=(r, g, b))
|
||||
time.sleep(5)
|
||||
bulb_state = self.device.get_light_color(channel=0)
|
||||
# TODO: RGB state is somehow normalized on the server side. We need to investigate the logic behind that...
|
||||
|
||||
def tearDown(self):
|
||||
self.device.turn_off()
|
||||
self.manager.stop()
|
||||
22
RPI Code/Meross_2/MerossIot_/tests/test_http.py
Normal file
22
RPI Code/Meross_2/MerossIot_/tests/test_http.py
Normal file
@ -0,0 +1,22 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from meross_iot.api import MerossHttpClient
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL')
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD')
|
||||
|
||||
|
||||
class TestHttpMethods(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = MerossHttpClient(email=EMAIL, password=PASSWORD)
|
||||
|
||||
def test_device_listing(self):
|
||||
devices = self.client.list_devices()
|
||||
assert devices is not None
|
||||
assert len(devices) > 0
|
||||
|
||||
def test_supported_device_listing(self):
|
||||
devices = self.client.list_devices()
|
||||
assert devices is not None
|
||||
assert len(devices) > 0
|
||||
212
RPI Code/Meross_2/MerossIot_/tests/test_power_plugs.py
Normal file
212
RPI Code/Meross_2/MerossIot_/tests/test_power_plugs.py
Normal file
@ -0,0 +1,212 @@
|
||||
import os
|
||||
import time
|
||||
import unittest
|
||||
from meross_iot.manager import MerossManager
|
||||
|
||||
EMAIL = os.environ.get('MEROSS_EMAIL')
|
||||
PASSWORD = os.environ.get('MEROSS_PASSWORD')
|
||||
|
||||
|
||||
class TestMSS210Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('mss210')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device ms210")
|
||||
|
||||
def test_power_cycle(self):
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
def test_get_info(self):
|
||||
state = self.device.get_status()
|
||||
assert state is not None
|
||||
|
||||
wifi_list = self.device.get_wifi_list()
|
||||
assert wifi_list is not None
|
||||
|
||||
trace = self.device.get_trace()
|
||||
assert trace is not None
|
||||
|
||||
debug = self.device.get_debug()
|
||||
assert debug is not None
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.stop()
|
||||
|
||||
|
||||
class TestMSS310Test(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('mss310')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device mss310")
|
||||
|
||||
def test_power_cycle(self):
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
def test_get_info(self):
|
||||
consumption = self.device.get_power_consumption()
|
||||
assert consumption is not None
|
||||
|
||||
wifi_list = self.device.get_wifi_list()
|
||||
assert wifi_list is not None
|
||||
|
||||
trace = self.device.get_trace()
|
||||
assert trace is not None
|
||||
|
||||
debug = self.device.get_debug()
|
||||
assert debug is not None
|
||||
|
||||
abilities = self.device.get_abilities()
|
||||
assert abilities is not None
|
||||
|
||||
electricity = self.device.get_electricity()
|
||||
assert electricity is not None
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.stop()
|
||||
|
||||
|
||||
class TestMSS425ETest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('mss425e')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device mss425e")
|
||||
|
||||
def test_power_cycle(self):
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
def test_usb(self):
|
||||
self.device.enable_usb()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_usb_status())
|
||||
|
||||
self.device.enable_usb()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_usb_status())
|
||||
|
||||
def test_channels(self):
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
# Test each channel one by one
|
||||
for c in self.device.get_channels():
|
||||
self.device.turn_on_channel(c)
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_channel_status(c))
|
||||
|
||||
time.sleep(2)
|
||||
self.device.turn_off_channel(c)
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_channel_status(c))
|
||||
|
||||
def test_get_info(self):
|
||||
state = self.device.get_status()
|
||||
assert state is not None
|
||||
|
||||
wifi_list = self.device.get_wifi_list()
|
||||
assert wifi_list is not None
|
||||
|
||||
trace = self.device.get_trace()
|
||||
assert trace is not None
|
||||
|
||||
debug = self.device.get_debug()
|
||||
assert debug is not None
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.stop()
|
||||
|
||||
|
||||
class TestMSS530HTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manager = MerossManager(meross_email=EMAIL, meross_password=PASSWORD)
|
||||
self.manager.start()
|
||||
|
||||
# Retrieves the list of supported devices
|
||||
devices = self.manager.get_devices_by_type('mss530h')
|
||||
if len(devices) > 0:
|
||||
self.device = devices[0]
|
||||
else:
|
||||
raise Exception("Could not find device mss530h")
|
||||
|
||||
def test_power_cycle(self):
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
self.device.turn_on()
|
||||
time.sleep(2)
|
||||
self.assertTrue(self.device.get_status())
|
||||
|
||||
self.device.turn_off()
|
||||
time.sleep(2)
|
||||
self.assertFalse(self.device.get_status())
|
||||
|
||||
def test_get_info(self):
|
||||
state = self.device.get_status()
|
||||
assert state is not None
|
||||
|
||||
wifi_list = self.device.get_wifi_list()
|
||||
assert wifi_list is not None
|
||||
|
||||
trace = self.device.get_trace()
|
||||
assert trace is not None
|
||||
|
||||
debug = self.device.get_debug()
|
||||
assert debug is not None
|
||||
|
||||
def tearDown(self):
|
||||
self.manager.stop()
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 85c67593dba9bb2df42681ba042e30fb0f3c719c
|
||||
105
RPI Code/pyparrot_/pyparrot/.gitignore
vendored
Normal file
105
RPI Code/pyparrot_/pyparrot/.gitignore
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
# 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/
|
||||
*.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/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# 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/
|
||||
|
||||
21
RPI Code/pyparrot_/pyparrot/LICENSE
Normal file
21
RPI Code/pyparrot_/pyparrot/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 amymcgovern
|
||||
|
||||
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.
|
||||
2
RPI Code/pyparrot_/pyparrot/MANIFEST.in
Normal file
2
RPI Code/pyparrot_/pyparrot/MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include pyparrot/commandsandsensors/*.xml
|
||||
include pyparrot/utils/*.sdp
|
||||
69
RPI Code/pyparrot_/pyparrot/README.md
Normal file
69
RPI Code/pyparrot_/pyparrot/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# pyparrot
|
||||
Python interface for Parrot Drones
|
||||
|
||||
pyparrot was designed and implemented by Dr. Amy McGovern to program Parrot Mambo and Parrot Bebop 2
|
||||
drones using python. This interface was developed to teach K-20 STEM concepts
|
||||
(programming, math, and more) by programming a drone to fly autonomously.
|
||||
Anyone can use it who is interested in autonomous drone programming!
|
||||
|
||||
# Installation, Quick-start, Documentation, FAQs
|
||||
|
||||
Extensive documentation is available at [https://pyparrot.readthedocs.io](https://pyparrot.readthedocs.io)
|
||||
|
||||
# Major updates and releases:
|
||||
* 03/02/2019: Version 1.5.14: Fixed ffmpeg vision bug where it wasn't properly killing the ffmpeg subprocess
|
||||
* 03/02/2019: Version 1.5.13: Added removal of old files in images directory by default to ffmpeg vision (can turn it off with a parameter)
|
||||
* 02/19/2019: Version 1.5.12: Added pull request of wificonnection parameters and added ability to specify IP address (default uses mDNS still)
|
||||
* 01/25/2019: Version 1.5.11: Added an example of using a cv2.namedWindow to show two vision windows (but it has issues on mac os 10.14 because it isn't a main thread)
|
||||
* 10/29/2018: Version 1.5.10: Updated the groundcam to not break on disconnect with BLE. Also have updated documentation with slides from workshop and windows FAQs.
|
||||
* 10/21/2018: Version 1.5.9: Fixed the wifiConnection without mDNS to work for Bebop (mDNS still works on bebop!). Verified that mambo and bebop work now with latest firmware.
|
||||
* 10/19/2018: Version 1.5.8: Parrot broke mDNS in firmware 3.0.26 (and ftp is still broken). Disabled the groundcam and hard-coded the IP address and ports for the mambo. Long term we want mDNS back. tested backwards compatiblity on older firmware and it works.
|
||||
* 10/13/2018: Version 1.5.7: Parrot released a security update/firmware upgrade to 3.0.25 that breaks ftp login for Mambo. pyparrot now allows the mambo to still connect without errors but the groundcam will not work until we hear from parrot. Also added example for joystick for the parrot swing from victor804
|
||||
* 10/05/2018: Version 1.5.6: Removed a bug in the library on pypi where an old file was hanging around
|
||||
* 09/19/2018: Version 1.5.5: Added joystink demo for swing (thanks Victor804)
|
||||
* 09/06/2018: Version 1.5.4: Removed wait in indoor mode for bebop 1
|
||||
* 09/06/2018: Version 1.5.3: Added indoor mode for bebop 1
|
||||
* 8/30/2018: Version 1.5.2: Updated camera pan_tilt for Bebop 1 (thanks Victor804)
|
||||
* 8/21/2018: Version 1.5.1: fixed small fix for typo in minidrones (for swing)
|
||||
* 8/18/2018: Version 1.5.0: major update to suppport parrot swing drones (thank you Victor804). This does break a small backwards compatibility in that you need to import Mambo from Minidrone instead of Mambo. Everything else remains the same.
|
||||
* 8/9/2018: Version 1.4.31: hard-coded name for vision stream on windows
|
||||
* 8/9/2018: Version 1.4.30: fixed vision bug in windows using VLC (tempfile issues) and also made fps a parameter for VLC vision
|
||||
* 7/16/2018: Version 1.4.29: added bebop user sensor callback function to match mambo
|
||||
* 7/15/2018: Version 1.4.28: added bebop battery state to default state variables (was in the dictionary only before)
|
||||
* 7/13/2018: Version 1.4.27: updated Mambo() initialization to not require address for wifi mode and also updated groundcam demo for Mambo
|
||||
* 7/12/2018: Version 1.4.26: added new Bebop commands (mostly setting max limits for the bebop)
|
||||
* 7/11/2018: Version 1.4.25: fixed groundcam pictures for Mambo
|
||||
* 7/8/2018: Version 1.4.24: switched tempfile to back to NamedTemporaryFile in DroneVisionGUI due to OS incompatibilities
|
||||
* 7/8/2018: Version 1.4.23: switched tempfile to SpooledTemporaryFile in DroneVisionGUI to make it faster (uses memory instead of disk)
|
||||
* 7/6/2018: Version 1.4.22: Added a wait in flat_trim for Bebop until it is received (optional)
|
||||
* 7/5/2018: Version 1.4.21: Added max_tilt and max_altitude to the Bebop commands.
|
||||
* 7/4/2018: Version 1.4.20: While move_relative is implemented, it seems to have a firmware bug so DO NOT USE.
|
||||
* 7/4/2018: Version 1.4.19: Added move_relative command to the Bebop API. For now, only dx, dy, and dradians should be used as there seems to be a bug internal to the firmware on dz.
|
||||
* 6/17/2018: Version 1.4.18 Added landed button status to the Drone Vision GUI for safety in user code
|
||||
* 6/16/2018: Version 1.4.17 Added flat trim to mambo also
|
||||
* 6/16/2018: Version 1.4.16 Added flat trim to bebop
|
||||
* 6/15/2018: Version 1.4.15 Removed a stray print, updated documentation, cast turn_degrees arguments to an int in Mambo.
|
||||
* 6/11/2018: Version 1.4.14 Added bebop sdp file to the release on pip
|
||||
* 6/7/2018: Version 1.4.13 Fixed duration in PCMD to use milliseconds instead of integer seconds
|
||||
* 6/7/2018: Version 1.4.12 Added an option to fly_direct to allow the command to be sent once
|
||||
* 6/6/2018: Version 1.4.11 Fixed a stray import statment not fixed from the move to pip
|
||||
* 5/31/2018: Version 1.4.10 Documentation updated significantly and moved to readthedocs
|
||||
* 5/30/2018: Version 1.4.7 and 1.4.8 and 1.4.9 fixed scripts location to release find_mambo script and added readthedocs documents
|
||||
* 5/29/2018: Version 1.4.6 Accepted fixes for Bebop 1 compatibility
|
||||
* 5/28/2018: Version 1.4.5 Fixed imports for new pypi structure and added xml files to pypi.
|
||||
* 5/25/2018: Version 1.4.3. Uploaded to pypi so pyparrot can now be installed directory from pip. Updated documentation for new vision.
|
||||
* 5/23/2018: Updated function (contributed) to download pictures from Mambo's downward facing camera.
|
||||
* 3/25/2018: Added DroneVisionGUI which is a version of the vision that shows the video stream (for Bebop or Mambo) in real time.
|
||||
* 2/22/2018: Version 1.3.2. Updated DroneVision to make the vision processing faster. Interface changed to only have the user call open_vision and close_vision (and not start_video_buffering)
|
||||
* 2/10/2018: Version 1.3.1. Updated DroneVision to work on Windows.
|
||||
* 2/8/2018: Version 1.3. Vision is working for both the Mambo and Bebop in a general interface called DroneVision. Major documenation updates as well.
|
||||
* 2/6/2018: Updated Mambo to add speed settings for tilt & vertical. Needed for class.
|
||||
* 2/4/2018: Unofficial updates to add ffmpeg support to the vision (will make an official release with examples soon)
|
||||
* 12/09/2017: Version 1.2. Mambo now gives estimated orientation using quaternions. Bebop now streams vision, which is accessible via VLC or other video clients. Coming soon: opencv hooks into the vision.
|
||||
* 12/02/2017: Version 1.1. Fixed sensors with multiple values for Mambo and Bebop.
|
||||
* 11/26/2017: Initial release, version 1.0. Working wifi and BLE for Mambo, initial flight for Bebop.
|
||||
|
||||
# Programming and using your drones responsibly
|
||||
|
||||
It is your job to program and use your drones responsibly! We are not responsible for any losses or damages of your drones or injuries. Please fly safely and obey all laws.
|
||||
|
||||
322
RPI Code/pyparrot_/pyparrot/coursework/droneMapGUI.py
Normal file
322
RPI Code/pyparrot_/pyparrot/coursework/droneMapGUI.py
Normal file
@ -0,0 +1,322 @@
|
||||
"""
|
||||
GUI for AI class using drones. Allows you to quickly create a map of
|
||||
a room with obstacles for navigation and search.
|
||||
|
||||
Amy McGovern dramymcgovern@gmail.com
|
||||
"""
|
||||
|
||||
from tkinter import *
|
||||
import numpy as np
|
||||
from tkinter import filedialog
|
||||
import os
|
||||
import pickle
|
||||
|
||||
class DroneGUI:
|
||||
def __init__(self):
|
||||
self.root = Tk()
|
||||
self.room_map = None
|
||||
self.obstacle_ids = None
|
||||
self.factor = None
|
||||
self.obstacle_color = "#7575a3"
|
||||
self.goal_color = "green"
|
||||
self.start_color = "red"
|
||||
self.start_id = 2
|
||||
self.goal_id = 3
|
||||
self.obstacle_id = 1
|
||||
|
||||
def translate_click(self, event):
|
||||
"""
|
||||
Translate the click event into room coordinates and map coordinates
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
print("clicked at", event.x, event.y)
|
||||
center_x = event.x
|
||||
center_y = event.y
|
||||
|
||||
# calculate the lower left corner of the box
|
||||
factor = 10 * self.scale_val
|
||||
lower_x = int(center_x / factor) * factor
|
||||
lower_y = int(center_y / factor) * factor
|
||||
#print("lower x and y are ", lower_x, lower_y)
|
||||
|
||||
# translate the click into the map
|
||||
map_x = int(center_x / self.factor)
|
||||
map_y = self.room_map.shape[1] - int(center_y / self.factor) - 1
|
||||
#print("map x and y are ", map_x, map_y)
|
||||
|
||||
return (center_x, center_y, lower_x, lower_y, map_x, map_y)
|
||||
|
||||
def toggle_obstacle_click(self, event):
|
||||
"""
|
||||
Toggle an obstacle with left button clicks
|
||||
|
||||
:param event: the tkinter event
|
||||
:return: nothing
|
||||
"""
|
||||
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
|
||||
|
||||
|
||||
if (self.room_map[map_x, map_y] == 0):
|
||||
self.draw_obstacle(lower_x,lower_y, size=self.factor, color=self.obstacle_color, map_x=map_x, map_y=map_y)
|
||||
self.room_map[map_x, map_y] = self.obstacle_id
|
||||
else:
|
||||
#print("Deleting obstacle ", self.obstacle_ids[map_x, map_y])
|
||||
self.clear_obstacle(map_x, map_y)
|
||||
self.room_map[map_x, map_y] = 0
|
||||
|
||||
def change_obstacle_type_click(self, event):
|
||||
"""
|
||||
Right click brings up a menu to let you choose what kind of obstacle this is
|
||||
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
print("In popup menu")
|
||||
popup_menu = Menu(self.root, tearoff=0)
|
||||
popup_menu.add_command(label="Set to start", command=lambda: self.draw_start_click(event))
|
||||
popup_menu.add_command(label="Set to goal", command=lambda: self.draw_goal_click(event))
|
||||
popup_menu.add_command(label="Remove", command=lambda: self.toggle_obstacle_click(event))
|
||||
|
||||
popup_menu.post(event.x, event.y)
|
||||
|
||||
|
||||
def draw_goal_click(self, event):
|
||||
"""
|
||||
Draw a green box for a goal at button 1 clicks
|
||||
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
|
||||
|
||||
# clear whatever was there
|
||||
self.clear_obstacle(map_x, map_y)
|
||||
self.room_map[map_x, map_y] = 0
|
||||
|
||||
# and save the click into the map
|
||||
self.room_map[map_x, map_y] = self.goal_id
|
||||
self.draw_obstacle(lower_x,lower_y, self.factor, color=self.goal_color, map_x=map_x, map_y=map_y)
|
||||
|
||||
|
||||
def draw_start_click(self, event):
|
||||
"""
|
||||
Draw a red box for the start
|
||||
|
||||
:param event:
|
||||
:return:
|
||||
"""
|
||||
(center_x, center_y, lower_x, lower_y, map_x, map_y) = self.translate_click(event)
|
||||
|
||||
# clear whatever was there
|
||||
self.clear_obstacle(map_x, map_y)
|
||||
self.room_map[map_x, map_y] = 0
|
||||
|
||||
# and save the click into the map
|
||||
self.room_map[map_x, map_y] = self.start_id
|
||||
self.draw_obstacle(lower_x,lower_y, self.factor, color=self.start_color, map_x=map_x, map_y=map_y)
|
||||
|
||||
|
||||
def draw_obstacle(self, x, y, size, color, map_x, map_y):
|
||||
# draw the rectangle
|
||||
obs_id = self.room_canvas.create_rectangle(x, y, x + size, y + size, fill=color)
|
||||
#print("Obstacle id is ", obs_id)
|
||||
self.obstacle_ids[map_x, map_y] = obs_id
|
||||
|
||||
def clear_obstacle(self, map_x, map_y):
|
||||
# draw the rectangle
|
||||
self.room_canvas.delete(self.obstacle_ids[map_x, map_y])
|
||||
self.obstacle_ids[map_x, map_y] = 0
|
||||
|
||||
def set_scale(self):
|
||||
try:
|
||||
self.scale_val = int(self.scale.get())
|
||||
except:
|
||||
self.scale_val = 1
|
||||
|
||||
# set the factor used for drawing translations
|
||||
self.factor = 10 * self.scale_val
|
||||
|
||||
def create_room(self):
|
||||
"""
|
||||
Create the window with the room grid
|
||||
|
||||
Uses the scale parameter set from the first gui window to decide how big the boxes are (scale must be an int)
|
||||
|
||||
Draws a grid with black lines every 10 * scale pixels (e.g. every decimeter) and then draws a thicker
|
||||
line every meter (e.g. every 10 lines)
|
||||
|
||||
:return:
|
||||
"""
|
||||
length = float(self.length.get())
|
||||
height = float(self.height.get())
|
||||
|
||||
# initialize the internal map
|
||||
self.room_map = np.zeros((int(length * 10), int(height * 10)))
|
||||
self.obstacle_ids = np.zeros((int(length * 10), int(height * 10)), dtype='int')
|
||||
print(self.room_map.shape)
|
||||
|
||||
print("Length is %0.1f and height is %0.1f" % (length, height))
|
||||
|
||||
self.set_scale()
|
||||
self.draw_room(length, height)
|
||||
|
||||
def draw_room(self, length, height):
|
||||
# each pixel is scale * 1 cm so multiply by 100 to get the width/height from the meters
|
||||
canvas_width = int(length * 100 * self.scale_val)
|
||||
canvas_height = int(height * 100 * self.scale_val)
|
||||
|
||||
# create the blank canvas
|
||||
room = Toplevel(self.root)
|
||||
|
||||
# put the menu into the room
|
||||
# menu code mostly from
|
||||
# https://www.python-course.eu/tkinter_menus.php
|
||||
menu = Menu(room)
|
||||
room.config(menu=menu)
|
||||
filemenu = Menu(menu)
|
||||
menu.add_cascade(label="File", menu=filemenu)
|
||||
filemenu.add_command(label="Save Map", command=self.save_file_menu)
|
||||
filemenu.add_separator()
|
||||
filemenu.add_command(label="Exit", command=self.root.quit)
|
||||
|
||||
helpmenu = Menu(menu)
|
||||
menu.add_cascade(label="Help", menu=helpmenu)
|
||||
helpmenu.add_command(label="About...", command=self.about_menu)
|
||||
|
||||
# draw the room
|
||||
self.room_canvas = Canvas(room, width=canvas_width, height=canvas_height, bg="#ffffe6")
|
||||
self.room_canvas.pack()
|
||||
|
||||
# how to draw a checkered canvas from
|
||||
# https://www.python-course.eu/tkinter_canvas.php
|
||||
# vertical lines at an interval of "line_distance" pixel
|
||||
line_distance = 10 * self.scale_val
|
||||
for x in range(line_distance, canvas_width, line_distance):
|
||||
if (x % (line_distance * 10) == 0):
|
||||
self.room_canvas.create_line(x, 0, x, canvas_height, fill="red", width=2)
|
||||
else:
|
||||
self.room_canvas.create_line(x, 0, x, canvas_height, fill="black")
|
||||
|
||||
# horizontal lines at an interval of "line_distance" pixel
|
||||
for y in range(line_distance, canvas_height, line_distance):
|
||||
if (y % (line_distance * 10) == 0):
|
||||
self.room_canvas.create_line(0, y, canvas_width, y, fill="red", width=2)
|
||||
else:
|
||||
self.room_canvas.create_line(0, y, canvas_width, y, fill="black")
|
||||
|
||||
# bind the button clicks to draw out the map
|
||||
self.room_canvas.bind("<Button-1>", self.toggle_obstacle_click)
|
||||
self.room_canvas.bind("<Button-2>", self.change_obstacle_type_click)
|
||||
|
||||
# add in the obstacles (if any exist already)
|
||||
(xs, ys) = np.nonzero(self.room_map)
|
||||
factor = 10 * self.scale_val
|
||||
for i, x in enumerate(xs):
|
||||
y = ys[i]
|
||||
lower_x = x * factor
|
||||
lower_y = (self.room_map.shape[1] - y - 1) * factor
|
||||
if (self.room_map[x, y] == 1):
|
||||
self.draw_obstacle(lower_x, lower_y, factor, color="#7575a3", map_x=x, map_y=y)
|
||||
elif (self.room_map[x, y] == 2):
|
||||
self.draw_obstacle(lower_x, lower_y, factor, color="red", map_x=x, map_y=y)
|
||||
elif (self.room_map[x, y] == 3):
|
||||
self.draw_obstacle(lower_x, lower_y, factor, color="green", map_x=x, map_y=y)
|
||||
|
||||
def draw_map_from_file(self):
|
||||
width = self.room_map.shape[1] / 10.0
|
||||
length = self.room_map.shape[0] /10.0
|
||||
#print("length and width of loaded room are ", length,width)
|
||||
#print("Scale is ", self.scale_val)
|
||||
self.draw_room(length, width)
|
||||
|
||||
def open_file_menu(self):
|
||||
"""
|
||||
Load a map from a file
|
||||
:return:
|
||||
"""
|
||||
filename = filedialog.askopenfilename(initialdir=os.getcwd(),
|
||||
title="Select map file",
|
||||
filetypes=(("map files", "*.map"), ("all files", "*.*")))
|
||||
|
||||
fp = open(filename, "rb")
|
||||
self.scale_val = pickle.load(fp)
|
||||
self.room_map = pickle.load(fp)
|
||||
#print("scale val is ", self.scale_val)
|
||||
#print("room map is ", self.room_map)
|
||||
fp.close()
|
||||
self.draw_map_from_file()
|
||||
|
||||
def save_file_menu(self):
|
||||
"""
|
||||
Bring up a save file dialog and then save
|
||||
:return:
|
||||
"""
|
||||
filename = filedialog.asksaveasfile(initialdir=os.getcwd(),
|
||||
title="Save map file",
|
||||
defaultextension=".map")
|
||||
print("saving to ", filename.name)
|
||||
fp = open(filename.name, "wb")
|
||||
pickle.dump(self.scale_val, fp)
|
||||
pickle.dump(self.room_map, fp)
|
||||
fp.close()
|
||||
|
||||
|
||||
def about_menu(self):
|
||||
pass
|
||||
|
||||
def draw_initial_gui(self):
|
||||
"""
|
||||
Draws the intial GUI that lets you make a new room
|
||||
or load one from a file
|
||||
:return:
|
||||
"""
|
||||
|
||||
# menu code mostly from
|
||||
# https://www.python-course.eu/tkinter_menus.php
|
||||
menu = Menu(self.root)
|
||||
self.root.config(menu=menu)
|
||||
filemenu = Menu(menu)
|
||||
menu.add_cascade(label="File", menu=filemenu)
|
||||
filemenu.add_command(label="Open Map", command=self.open_file_menu)
|
||||
filemenu.add_separator()
|
||||
filemenu.add_command(label="Exit", command=self.root.quit)
|
||||
|
||||
helpmenu = Menu(menu)
|
||||
menu.add_cascade(label="Help", menu=helpmenu)
|
||||
helpmenu.add_command(label="About...", command=self.about_menu)
|
||||
|
||||
# draw the request to create a new room
|
||||
Label(self.root, text="Enter the size of the room you are flying in (decimals to tenths)").grid(row=0, columnspan=2)
|
||||
Label(self.root, text="Length (x) (meters)").grid(row=1)
|
||||
Label(self.root, text="Height (y) (meters)").grid(row=2)
|
||||
Label(self.root, text="1 cm = _ pixels").grid(row=3)
|
||||
|
||||
# the entry boxes
|
||||
self.length = Entry(self.root)
|
||||
self.length.grid(row=1, column=1)
|
||||
|
||||
self.height = Entry(self.root)
|
||||
self.height.grid(row=2, column=1)
|
||||
|
||||
self.scale = Entry(self.root)
|
||||
self.scale.grid(row=3, column=1)
|
||||
|
||||
# action buttons
|
||||
Button(self.root, text='Quit', command=self.root.quit).grid(row=4, column=0, pady=4)
|
||||
Button(self.root, text='Create room', command=self.create_room).grid(row=4, column=1, pady=4)
|
||||
|
||||
def go(self):
|
||||
"""
|
||||
Start the main GUI loop
|
||||
:return:
|
||||
"""
|
||||
mainloop()
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui = DroneGUI()
|
||||
gui.draw_initial_gui()
|
||||
gui.go()
|
||||
59
RPI Code/pyparrot_/pyparrot/demoMambo.py
Normal file
59
RPI Code/pyparrot_/pyparrot/demoMambo.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""
|
||||
Demo the direct flying for the python interface
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
|
||||
# you will need to change this to the address of YOUR mambo
|
||||
mamboAddr = "d0:3a:69:b9:e6:5a"
|
||||
|
||||
# make my mambo object
|
||||
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
|
||||
mambo = Mambo(mamboAddr, use_wifi=False)
|
||||
|
||||
print("trying to connect")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
if (success):
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
mambo.smart_sleep(2)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("taking off!")
|
||||
mambo.safe_takeoff(5)
|
||||
"""
|
||||
print("Flying direct: going forward (positive pitch)")
|
||||
mambo.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Showing turning (in place) using turn_degrees")
|
||||
mambo.turn_degrees(90)
|
||||
mambo.smart_sleep(2)
|
||||
mambo.turn_degrees(-90)
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("Flying direct: yaw")
|
||||
mambo.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going backwards (negative pitch)")
|
||||
mambo.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
|
||||
|
||||
print("Flying direct: roll")
|
||||
mambo.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going up")
|
||||
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
|
||||
|
||||
print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
|
||||
mambo.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=3)
|
||||
"""
|
||||
print("landing")
|
||||
mambo.safe_land(5)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("disconnect")
|
||||
mambo.disconnect()
|
||||
20
RPI Code/pyparrot_/pyparrot/docs/Makefile
Normal file
20
RPI Code/pyparrot_/pyparrot/docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = python -msphinx
|
||||
SPHINXPROJ = pyparrot
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
BIN
RPI Code/pyparrot_/pyparrot/docs/OK2018Afternoon.pdf
Normal file
BIN
RPI Code/pyparrot_/pyparrot/docs/OK2018Afternoon.pdf
Normal file
Binary file not shown.
BIN
RPI Code/pyparrot_/pyparrot/docs/OK2018Morning.pdf
Normal file
BIN
RPI Code/pyparrot_/pyparrot/docs/OK2018Morning.pdf
Normal file
Binary file not shown.
28
RPI Code/pyparrot_/pyparrot/docs/about.rst
Normal file
28
RPI Code/pyparrot_/pyparrot/docs/about.rst
Normal file
@ -0,0 +1,28 @@
|
||||
.. title:: About pyparrot
|
||||
|
||||
.. about:
|
||||
|
||||
About the pyparrot project
|
||||
==========================
|
||||
|
||||
About pyparrot
|
||||
-----------------
|
||||
|
||||
Pyparrot was developed by `Dr. Amy McGovern <http://www.mcgovern-fagg.org/amy/>`_ with the support of
|
||||
the `University of Oklahoma <https://www.kipr.org>`_ and
|
||||
the `Kiss Institute for Practical Robotics <https://www.kipr.org>`_. The original goal was to teach children to program
|
||||
using educational programs like `botball <http://www.botball.org>`_. The pyparrot project has been adopted by groups
|
||||
around the world and has been used in both K-12 settings and at the university level.
|
||||
|
||||
Educational Programs Using pyparrot
|
||||
-----------------------------------
|
||||
|
||||
If you would like to be added to this list, please email dramymcgovern @ gmail.com (without the spaces).
|
||||
|
||||
* `Botball <http://www.botball.org>`_
|
||||
* `University of Oklahoma School of Computer Science <http://www.ou.edu/coe/cs>`_
|
||||
* `Talenteahaus <http://www.talentehaus.at>`_
|
||||
* `Tech Garage <https://tech-garage.org>`_
|
||||
* `St Eugene College <http://www.steugene.qld.edu.au>`_
|
||||
* `KIPR/botball <http://www.kipr.org/>`_
|
||||
|
||||
164
RPI Code/pyparrot_/pyparrot/docs/bebopcommands.rst
Normal file
164
RPI Code/pyparrot_/pyparrot/docs/bebopcommands.rst
Normal file
@ -0,0 +1,164 @@
|
||||
.. title:: Bebop Commands and Sensors
|
||||
|
||||
.. bebopcommands:
|
||||
|
||||
Bebop Commands and Sensors
|
||||
==============================
|
||||
|
||||
Bebop commands
|
||||
--------------
|
||||
|
||||
Each of the public commands available to control the bebop is listed below with its documentation.
|
||||
The code is also well documented and you can also look at the API through readthedocs.
|
||||
All of the functions preceeded with an underscore are intended to be internal functions and are not listed below.
|
||||
|
||||
Creating a Bebop object
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``Bebop(drone_type="Bebop2")`` create a Bebop object with an optional drone_type argument that can be used to create
|
||||
a bebop one or bebop 2 object. Default is Bebop 2. Note, there is limited support for the original bebop since
|
||||
I do not own one for testing.
|
||||
|
||||
Connecting and disconnecting
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``connect(num_retries)`` connect to the Bebop's wifi services. This performs a handshake.
|
||||
This can take several seconds to ensure the connection is working.
|
||||
You can specify a maximum number of re-tries. Returns true if the connection suceeded or False otherwise.
|
||||
|
||||
``disconnect()`` disconnect from the wifi connection
|
||||
|
||||
Takeoff and landing
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``takeoff()`` Sends a single takeoff command to the bebop. This is not the recommended method.
|
||||
|
||||
``safe_takeoff(timeout)`` This is the recommended method for takeoff. It sends a command and then checks the
|
||||
sensors (via flying state) to ensure the bebop is actually taking off. Then it waits until the bebop is
|
||||
flying or hovering to return. It will timeout and return if the time exceeds timeout seconds.
|
||||
|
||||
``land()`` Sends a single land command to the bebop. This is not the recommended method.
|
||||
|
||||
``safe_land(timeout)`` This is the recommended method to land the bebop. Sends commands
|
||||
until the bebop has actually reached the landed state. It will timeout and return if the time exceeds timeout seconds.
|
||||
|
||||
Flying
|
||||
^^^^^^
|
||||
|
||||
``flip(direction)`` Sends the flip command to the bebop. Valid directions to flip are: front, back, right, left.
|
||||
|
||||
``fly_direct(roll, pitch, yaw, vertical_movement, duration)`` Fly the bebop directly using the
|
||||
specified roll, pitch, yaw, and vertical movements. The commands are repeated for duration seconds.
|
||||
Note there are currently no sensors reported back to the user to ensure that these are working but hopefully
|
||||
that is addressed in a future firmware upgrade. Each value ranges from -100 to 100 and is essentially a percentage
|
||||
and direction of the max_tilt (for roll/pitch) or max_vertical_speed (for vertical movement).
|
||||
|
||||
``move_relative(dx, dy, dz, dradians)`` Moves the bebop a relative number of meters in x (forward/backward,
|
||||
forward is positive), y (right/left, right is positive), dz (up/down, positive is down), and dradians.
|
||||
If you use this command INDOORS, make sure you either have FULL GPS coverage or NO GPS coverage (e.g. cover the front of the bebop
|
||||
with tin foil to keep it from getting a lock). If it has mixed coverage, it randomly flies at high speed in random
|
||||
directions after the command executes. This is a known issue in Parrot's firmware and they state that a fix is coming.
|
||||
|
||||
``set_max_altitude(altitude)`` Set the maximum allowable altitude in meters.
|
||||
The altitude must be between 0.5 and 150 meters.
|
||||
|
||||
``set_max_distance(distance)`` Set max distance between the takeoff and the drone in meters.
|
||||
The distance must be between 10 and 2000 meters.
|
||||
|
||||
``enable_geofence(value)`` If geofence is enabled, the drone won't fly over the given max distance.
|
||||
Valid value: 1 if the drone can't fly further than max distance, 0 if no limitation on the drone should be done.
|
||||
|
||||
``set_max_tilt(tilt)`` Set the maximum allowable tilt in degrees for the drone (this limits speed).
|
||||
The tilt must be between 5 (very slow) and 30 (very fast) degrees.
|
||||
|
||||
``set_max_tilt_rotation_speed(speed)`` Set the maximum allowable tilt rotation speed in degree/s.
|
||||
The tilt rotation speed must be between 80 and 300 degree/s.
|
||||
|
||||
``set_max_vertical_speed(speed)`` Set the maximum allowable vertical speed in m/s.
|
||||
The vertical speed must be between 0.5 and 2.5 m/s.
|
||||
|
||||
``set_max_rotation_speed(speed)`` Set the maximum allowable rotation speed in degree/s.
|
||||
The rotation speed must be between 10 and 200 degree/s.
|
||||
|
||||
``set_flat_trim(duration=0)`` Tell the Bebop to run with a flat trim. If duration > 0, waits for the comand to be acknowledged
|
||||
|
||||
``set_hull_protection(present)`` Set the presence of hull protection (only for bebop 1).
|
||||
The value must be 1 if hull protection is present or 0 if not present. This is only useful for the bebop 1.
|
||||
|
||||
``set_indoor(is_outdoor)`` Set the bebop 1 (ignored on bebop 2) to indoor or outdoor mode.
|
||||
The value must be 1 if bebop 1 is outdoors or 0 if it is indoors. This is only useful for the bebop 1.
|
||||
|
||||
Pausing or sleeping in a thread safe manner
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``smart_sleep(seconds)`` This sleeps the number of seconds (which can be a floating point) but wakes for all
|
||||
wifi notifications. You should use this instead of time.sleep to be consistent with the mambo but it is not
|
||||
required (whereas time.sleep() will break a mambo using BLE).
|
||||
|
||||
Video camera
|
||||
^^^^^^^^^^^^
|
||||
|
||||
``start_video_stream()``: tells the bebop to start streaming the video. These are really intended to be
|
||||
called within the DroneVision or DroneVisionGUI functions and not directly by the user (but you can call
|
||||
them directly if you are writing your own vision routines).
|
||||
|
||||
``stop_video_stream()``: tells the bebop to stop streaming the video. Same as above: intended to be called
|
||||
by the DroneVision or DroneVisionGUI routines.
|
||||
|
||||
``set_video_stream_mode(mode)``: set the video mode to one of three choices: "low_latency",
|
||||
"high_reliability", "high_reliability_low_framerate". low_latency is the default.
|
||||
|
||||
``pan_tilt_camera(tilt_degrees, pan_degrees)``: Send the command to pan/tilt the camera by the specified number of degrees in pan/tilt.
|
||||
Note, this only seems to work in small increments. Use pan_tilt_velocity to get the camera to look straight downward.
|
||||
|
||||
``pan_tilt_camera_velocity(self, tilt_velocity, pan_velocity, duration=0)``: Send the command to tilt the camera by
|
||||
the specified number of degrees per second in pan/tilt. This function has two modes. First, if duration is 0,
|
||||
the initial velocity is sent and then the function returns (meaning the camera will keep moving).
|
||||
If duration is greater than 0, the command executes for that amount of time and then sends a stop command to
|
||||
the camera and then returns.
|
||||
|
||||
``set_picture_format(format)``: Change the picture format to raw, jpeg, snapshot or jpeg_fisheye.
|
||||
|
||||
``set_white_balance(type)``: Change the type of white balance between: auto, tungsten, daylight, cloudy or cool_white.
|
||||
|
||||
``set_exposition(value)``: Change the image exposition between -1.5 and 1.5.
|
||||
|
||||
``set_saturation(value)``: Change the image saturation between -100 and 100.
|
||||
|
||||
``set_timelapse(enable, interval)``: To start a timelapse set enable at 1 and an interval between 8 and 300 sec.
|
||||
To stop the timelapse just set enable to 0.
|
||||
|
||||
``set_video_stabilization(mode)``: Change the video stabilization between 4 modes: roll_pitch, pitch, roll, none.
|
||||
|
||||
``set_video_recording(mode)``: Change the video recording mode between quality and time.
|
||||
|
||||
``set_video_framerate(framerate)``: Change the video framerate between: 24_FPS, 25_FPS or 30_FPS.
|
||||
|
||||
``set_video_resolutions(type)``: Change the video resolutions for stream and rec between rec1080_stream480, rec720_stream720.
|
||||
|
||||
Sensor commands
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
``ask_for_state_update()`` This sends a request to the bebop to send back ALL states. The data returns
|
||||
fairly quickly although not instantly. The bebop already has a sensor refresh rate of 10Hz but not all sensors are sent
|
||||
automatically. If you are looking for a specific sensor that is not automatically sent, you can call this but I don't
|
||||
recommend sending it over and over. Most of the sensors you need should be sent at either the 10Hz rate or as an event
|
||||
is called that triggers that sensor.
|
||||
|
||||
Bebop sensors
|
||||
-------------
|
||||
|
||||
All of the sensor data that is passed back to the Bebop is saved in a python dictionary. As needed, other variables
|
||||
are stored outside the dictionary but you can get everything you need from the dictionary itself. All of the data
|
||||
is stored in the BebopSensors class.
|
||||
|
||||
The easiest way to interact with the sensors is to call:
|
||||
|
||||
``bebop.set_user_sensor_callback(function, args)``. This sets a user callback function with optional
|
||||
arguments that is called each time a sensor is updated. The refresh rate on wifi is 10Hz.
|
||||
|
||||
The sensors are:
|
||||
|
||||
* battery (defaults to 100 and stays at that level until a real reading is received from the drone)
|
||||
* flying_state: This is updated as frequently as the drone sends it out and can be one of "landed", "takingoff", "hovering", "flying", "landing", "emergency", "usertakeoff", "motor_ramping", "emergency_landing". These are the values as specified in `ardrone3.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/ardrone3.xml>`_.
|
||||
* sensors_dict: all other sensors are saved by name in a dictionary. The names come from the `ardrone3.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/ardrone3.xml>`_ and `common.xml <https://github.com/amymcgovern/pyparrot/blob/master/pyparrot/commandsandsensors/common.xml>`_.
|
||||
179
RPI Code/pyparrot_/pyparrot/docs/conf.py
Normal file
179
RPI Code/pyparrot_/pyparrot/docs/conf.py
Normal file
@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# pyparrot documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue May 29 13:55:14 2018.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
# the mock stuff was borrowed from hagelslag to help make things work on readthedocs
|
||||
from mock import Mock as MagicMock
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return Mock()
|
||||
|
||||
MOCK_MODULES = ['numpy', 'scipy', 'zeroconf', 'cv2', 'untangle', 'bluepy', 'bluepy.btle',
|
||||
'ipaddress', 'queue', 'http.server', 'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui',
|
||||
'PyQt5.QtWidgets']
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.mathjax',
|
||||
'sphinx.ext.viewcode']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
source_suffix = ['.rst', '.md']
|
||||
#source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'pyparrot'
|
||||
copyright = '2018, Amy McGovern'
|
||||
author = 'Amy McGovern'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.5'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.5.3'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'nature'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# This is required for the alabaster theme
|
||||
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
|
||||
html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pyparrotdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'pyparrot.tex', 'pyparrot Documentation',
|
||||
'Amy McGovern', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'pyparrot', 'pyparrot Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'pyparrot', 'pyparrot Documentation',
|
||||
author, 'pyparrot', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
20
RPI Code/pyparrot_/pyparrot/docs/contact.rst
Normal file
20
RPI Code/pyparrot_/pyparrot/docs/contact.rst
Normal file
@ -0,0 +1,20 @@
|
||||
.. title:: Contact the pyparrot developers
|
||||
|
||||
.. contact:
|
||||
|
||||
Contact the pyparrot developers
|
||||
===============================
|
||||
|
||||
Contribute
|
||||
----------
|
||||
|
||||
We welcome your contributions via bug report or pull request.
|
||||
|
||||
* Issue Tracker: `<https://github.com/amymcgovern/pyparrot/issues>`_
|
||||
* Pull requests: `<https://github.com/amymcgovern/pyparrot/pulls>`_
|
||||
* Source Code: `<https://github.com/amymcgovern/pyparrot>`_
|
||||
|
||||
Support
|
||||
-------
|
||||
If you are having issues, please let us know by reporting issues on GitHub using the issue
|
||||
tracker `<https://github.com/amymcgovern/pyparrot/issues>`_.
|
||||
62
RPI Code/pyparrot_/pyparrot/docs/faq.rst
Normal file
62
RPI Code/pyparrot_/pyparrot/docs/faq.rst
Normal file
@ -0,0 +1,62 @@
|
||||
.. title:: Frequently Asked Questions
|
||||
|
||||
.. faq:
|
||||
|
||||
Frequently Asked Questions
|
||||
====================================
|
||||
|
||||
Below is a list of common errors and how to fix them.
|
||||
|
||||
Vision isn't showing anything on the minidrone
|
||||
----------------------------------------------
|
||||
|
||||
The Minidrone camera puts itself into a "resting" state after not flying for several minutes. To solve this, you
|
||||
either need to fly again (a simple takeoff and landing will suffice) or reboot the minidrone and reconnect.
|
||||
|
||||
I'm using windows and my drone gives me lots of timeout errors
|
||||
---------------------------------------------------------------
|
||||
This is a windows security setting and it can be fixed. Go into your windows firewall settings (control panel,
|
||||
system and security, allow a program through Windows Firewall) and change the settings
|
||||
for python.exe to be allowed through the firewall for both home/private networks and public networks. Your sensors will
|
||||
suddenly be able to send data to your machine and safe_land will start working again as well as any sensors!
|
||||
|
||||
My drone does takeoff and landing but nothing else
|
||||
--------------------------------------------------
|
||||
|
||||
Likely you have the remote controller on and attached! For some reason, if the remote is on,
|
||||
it will allow the python code to takeoff & land but no other commands will work.
|
||||
Turn off the remote and your code should work fine!
|
||||
|
||||
Errors connecting to the drone
|
||||
------------------------------
|
||||
|
||||
There are two common errors that I see when flying. One requires the drone to reboot and one requires the
|
||||
computer controlling the drone to reboot.
|
||||
|
||||
Connection failed
|
||||
^^^^^^^^^^^^^^^^^
|
||||
If you fail to connect to the drone, you will see an error message like this:
|
||||
|
||||
::
|
||||
|
||||
connection failed: did you remember to connect your machine to the Drone's wifi network?
|
||||
|
||||
The most likely cause is that you forgot to connect to the drone's wifi. If you tried to connect,
|
||||
sometimes that connection fails. Try again or let the connection sit for a minute and try your program again.
|
||||
|
||||
If you are on the wifi but you get connection refused errors, reboot the drone.
|
||||
|
||||
Address in use
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
The second common error is about the address being in use, as shown below.
|
||||
|
||||
::
|
||||
|
||||
OSError: [Errno 48] Address already in use
|
||||
|
||||
|
||||
There are two ways to fix this, depending on the issue. It is possible you tried to run a second program while
|
||||
you still had a first program running. If this is the case, make sure you stop all of your minidrone programs and then
|
||||
restart only one. If you are not running a second minidrone program, then the solution is to reboot. This sometimes
|
||||
happens due to the program crashing before it releases the socket.
|
||||
13
RPI Code/pyparrot_/pyparrot/docs/gettingstartedslides.rst
Normal file
13
RPI Code/pyparrot_/pyparrot/docs/gettingstartedslides.rst
Normal file
@ -0,0 +1,13 @@
|
||||
.. title:: Slides from Workshops Teaching PyParrot
|
||||
|
||||
.. gettingstartedslides:
|
||||
|
||||
Workshop Materials and Slides
|
||||
==============================
|
||||
We have taught several workshops on using pyparrot. As we continue to teach and develop materials for
|
||||
pyparrot, we will continue to share the curriculum materials here. Since the 2018 OK workshop slides supercede the
|
||||
GCER 2018 slides, we only share the 2018 OK Workshop slides for now.
|
||||
|
||||
* Morning slides from the OK workshop (focus on initial setup, initial flying) :download:`OK2018Morning.pdf <OK2018Morning.pdf>`
|
||||
|
||||
* Afternoon slides from the OK workshop (learning to use sensors, extra slides on vision though we did not discuss at workshop) :download:`OK2018Afternoon.pdf <OK2018Afternoon.pdf>`.
|
||||
38
RPI Code/pyparrot_/pyparrot/docs/index.rst
Normal file
38
RPI Code/pyparrot_/pyparrot/docs/index.rst
Normal file
@ -0,0 +1,38 @@
|
||||
.. pyparrot documentation master file, created by
|
||||
sphinx-quickstart on Tue May 29 13:16:36 2018.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to pyparrot's documentation!
|
||||
====================================
|
||||
pyparrot was designed by Dr. Amy McGovern to program Parrot Minidrone (primarily Mambo FPV but Swing is also
|
||||
supported) and Parrot Bebop (1 or 2) drones using Python. This interface was developed to teach kids of all ages (K-20) STEM
|
||||
concepts (programming, math, and more) by having them program a drone
|
||||
to fly autonomously. Anyone can use it who is interested in
|
||||
autonomous drone programming!
|
||||
|
||||
Main documentation
|
||||
==========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
installation.rst
|
||||
quickstartminidrone.rst
|
||||
quickstartbebop.rst
|
||||
gettingstartedslides.rst
|
||||
minidronecommands.rst
|
||||
bebopcommands.rst
|
||||
vision.rst
|
||||
faq.rst
|
||||
contact.rst
|
||||
about.rst
|
||||
license.rst
|
||||
modules.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`modindex` is a good place to start if you want to read API docs
|
||||
* :ref:`genindex` of ALL functions (warning, this is huge and overwhelming)
|
||||
* :ref:`search`
|
||||
189
RPI Code/pyparrot_/pyparrot/docs/installation.rst
Normal file
189
RPI Code/pyparrot_/pyparrot/docs/installation.rst
Normal file
@ -0,0 +1,189 @@
|
||||
.. title:: Installation
|
||||
|
||||
.. installation:
|
||||
|
||||
Installation
|
||||
===============
|
||||
|
||||
You have two choices for installing pyparrot: using the ``source`` code directly or downloading with ``pip``.
|
||||
**Note** Pyparrot will only work with python 3. This choice was made because the support for multi-threaded
|
||||
programs is improved in python 3.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
The choice of related packages is dependent on your choice of drone (Mambo, Mambo FPV, Bebop 1 or 2, Swing, Anafi) and
|
||||
to the operating system that you will be using to develop.
|
||||
|
||||
Hardware/Drone requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* **Parrot Mambo FPV**: If you have a Mambo FPV (e.g. you have the camera), you can use the wifi interface. The wifi interface will work on Mac, Linux, or Windows.
|
||||
|
||||
* **Parrot Mambo Fly or Code**: If you have a Mambo without the camera, you will use the BLE interface. pyparrot currently only supports Linux for BLE. The BLE interface was developed on a Raspberry Pi 3 Model B but it has been tested on other Linux machines.
|
||||
|
||||
* **Parrot Swing**: To use the Swing you will use the BLE interface.
|
||||
|
||||
* **Parrot Bebop 2**: The Bebop interface was tested on a Bebop 2 using a laptop with wifi (any wifi enabled device should work).
|
||||
|
||||
* **Parrot Bebop 1**: A Bebop 1 will also work with any wifi enabled device.
|
||||
|
||||
* **Parrot Anafi**: Per the development board, the Anafi should work with very minor changes. I will work to officially suppport it once the SDK from parrot is released for it.
|
||||
|
||||
Software requirements
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Software requirements are listed below by type of connection to the drone.
|
||||
|
||||
* All drones: Python 3
|
||||
|
||||
I use the `<https://www.anaconda.com/download/>`_:: installer and package manager for python. Note, when you
|
||||
install anaconda, install the Visual Studio option, especially if you have windows. Otherwise you will need to install
|
||||
Visual Studio separately. The zeroconf package (listed below) requires developer tools because it needs to be compiled.
|
||||
|
||||
* All drones: untangle package (this is used to parse the xml files in the parrot SDK)
|
||||
|
||||
|
||||
::
|
||||
|
||||
pip install untangle
|
||||
|
||||
|
||||
|
||||
* Vision: If you intend to process the camera files, you will need to install opencv and then either ffmpeg
|
||||
or VLC. I installed ffmpeg using brew for the mac but apt-get on linux should also work. For VLC, you MUST install
|
||||
the actual `VLC <https://www.videolan.org/vlc/index.html`_ program (and not just the library in python)
|
||||
and it needs to be version 3.0.1 or greater.
|
||||
|
||||
* Wifi connection: `zeroconf <https://pypi.python.org/pypi/zeroconf>`_ To install zeroconf software do the following:
|
||||
|
||||
::
|
||||
|
||||
pip install zeroconf
|
||||
|
||||
|
||||
* BLE connection: pybluez (note this is ONLY for support without the camera!) This is ONLY supported on linux.
|
||||
To install the BLE software do the following:
|
||||
|
||||
::
|
||||
|
||||
sudo apt-get install bluetooth
|
||||
sudo apt-get install bluez
|
||||
sudo apt-get install python-bluez
|
||||
|
||||
|
||||
Note it is also possible that you will need to install bluepy (if it isn't already there). These commands should do it:
|
||||
|
||||
::
|
||||
|
||||
sudo apt-get install python-pip libglib2.0-dev
|
||||
sudo pip install bluepy
|
||||
sudo apt-get update
|
||||
|
||||
|
||||
|
||||
Installing From Source
|
||||
----------------------
|
||||
|
||||
First download pyparrot by cloning the repository from `<https://github.com/amymcgovern/pyparrot>`_ The instructions for this are below.
|
||||
|
||||
|
||||
::
|
||||
|
||||
git clone https://github.com/amymcgovern/pyparrot
|
||||
cd pyparrot
|
||||
|
||||
|
||||
Make sure you install the necessary other packages (wifi or BLE, vision, etc) as specified above.
|
||||
|
||||
Installing From Pip
|
||||
-------------------
|
||||
|
||||
To install from pip, type
|
||||
|
||||
|
||||
::
|
||||
|
||||
pip install pyparrot
|
||||
|
||||
|
||||
Make sure you install the necessary other packages (wifi or BLE, vision, etc) as specified above.
|
||||
|
||||
Installation guide for windows users who might need more help
|
||||
-------------------------------------------------------------
|
||||
|
||||
Thank you to @JackdQuinn for contributing this.
|
||||
|
||||
Make sure you install **Visual Studio** either using Anaconda or by downloading it from Microsoft. Note that Visual
|
||||
Studio is free but it is required for compilation of the wifi module zeroconf, and specifically of the netifaces
|
||||
module that zeroconf requires. It is a very large download if you chose to do it outside of anaconda so you will
|
||||
want to start that download first.
|
||||
|
||||
If you install python without anaconda, when you install choose Special install Python and
|
||||
click add python to path (this will clear up some command line call issues).
|
||||
|
||||
Again, if you chose regular python and not anaconda, you can check installation by typing py in the windows command line.
|
||||
|
||||
::
|
||||
|
||||
py
|
||||
|
||||
Once you are sure that python started, you will want to quit python. type: ``quit()`` to exit python
|
||||
|
||||
::
|
||||
|
||||
quit()
|
||||
|
||||
If you chose to use anaconda, bring up the anaconda menu and open an anaconda prompt to verify that it installed.
|
||||
The rest of the instructions depend on whether you chose python or anaconda for your installation. If you chose python,
|
||||
use the windows command prompt for pip. If you chose anaconda, use your anaconda prompt.
|
||||
|
||||
If you type the pip command (with no options), it will produce a long list of options. This tells you that you
|
||||
are at the right command prompt to do the rest of the installation.
|
||||
**Note, the pip command will not work inside of python.** This is a command prompt command, not a python command.
|
||||
|
||||
::
|
||||
|
||||
pip
|
||||
|
||||
|
||||
Sometimes pip tells you that it wants to upgrade. For windows, the command is:
|
||||
|
||||
::
|
||||
|
||||
python -m pip install -U pip
|
||||
|
||||
To actually install, use the commands described above (and repeated here).
|
||||
|
||||
::
|
||||
|
||||
pip install untangle
|
||||
pip install pyparrot
|
||||
pip install zeroconf
|
||||
|
||||
**Note that visual studio is a requirement for zeroconf**
|
||||
|
||||
Testing your install
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The first step is to connect your connect your controlling device (laptop, computer, etc) to the wifi for the drone.
|
||||
Look for a wifi network named Mambo_number where number changes for each drone.
|
||||
|
||||
After connection to your drone its time to run code! You can download all the example code from
|
||||
these docs. Below is a short set of commands of how to run that code.
|
||||
|
||||
Run code by cd'ing down to the directory (the folder your python code is in) and running the desired python file from the cmd line
|
||||
|
||||
Example:
|
||||
* open command line either through windows or anaconda (depending on your installation method)
|
||||
* type: ``cd desktop``
|
||||
* this will Change your Directory to the desktop
|
||||
* type: ``dir``
|
||||
* this will display a list of all the folders (directories) on the desktop
|
||||
* type: ``cd yourFolderNameHere``
|
||||
* type: ``dir``
|
||||
* this will display all the files and folders in the directory
|
||||
* type: ``py TheNameOfTheFileYouWantToRun.py`` or ``python TheNameOfTheFileYouWantToRun.py``
|
||||
* When you click enter the file will begin to run, if you are using the demo scripts you should see lots of nice feedback as it changes states. You can use the arrow keys to go through your history of commands which can save you lots of time if your file names are long.
|
||||
* If you have several connects and disconnects try restarting your computer or resetting your ip (for the more technically inclined)
|
||||
* If you have crashes where the drone is flipping to one side when it shouldn't check the blades and bumpers. The bumpers can shift after a crash and prevent the blades from spinning, or slow down their spin, which causes unintended flips
|
||||
32
RPI Code/pyparrot_/pyparrot/docs/license.rst
Normal file
32
RPI Code/pyparrot_/pyparrot/docs/license.rst
Normal file
@ -0,0 +1,32 @@
|
||||
.. title:: License
|
||||
|
||||
.. license:
|
||||
|
||||
License
|
||||
===============
|
||||
|
||||
MIT License
|
||||
-----------
|
||||
|
||||
The project is licensed under the MIT License.
|
||||
|
||||
Copyright (c) 2017 amymcgovern
|
||||
|
||||
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.
|
||||
|
||||
152
RPI Code/pyparrot_/pyparrot/docs/minidronecommands.rst
Normal file
152
RPI Code/pyparrot_/pyparrot/docs/minidronecommands.rst
Normal file
@ -0,0 +1,152 @@
|
||||
.. title:: Minidrone Commands and Sensors
|
||||
|
||||
.. minidronecommands:
|
||||
|
||||
Minidrone Commands and Sensors
|
||||
==============================
|
||||
|
||||
Minidrone commands
|
||||
--------------
|
||||
|
||||
Each of the public commands available to control the minidrone is listed below with its documentation.
|
||||
The code is also well documented and you can also look at the API through readthedocs.
|
||||
All of the functions preceeded with an underscore are intended to be internal functions and are not listed below.
|
||||
|
||||
Creating a Mambo object
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``Mambo(address="", use_wifi=True/False)``
|
||||
create a mambo object with the specific harware address (found using findMinidrone). The use_wifi argument defaults to
|
||||
False (which means BLE is the default). Set to True to use wifi. You can only use wifi if you have a FPV camera
|
||||
installed on your Mambo! If you are using wifi, the hardware address argument can be ignored (it defaults to an empty
|
||||
string).
|
||||
|
||||
Creating a Swing object
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``Swing(address="")``
|
||||
create a Swing object with the specific harware address (found using findMinidrone).
|
||||
|
||||
Connecting and disconnecting
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``connect(num_retries)`` connect to the Minidrone either using BLE services and characteristics or wifi
|
||||
(specified when you created the Mambo object). This can take several seconds to ensure the connection is working.
|
||||
You can specify a maximum number of re-tries. Returns true if the connection suceeded or False otherwise.
|
||||
|
||||
``disconnect()`` disconnect from the BLE or wifi connection
|
||||
|
||||
Takeoff and landing
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``safe_takeoff(timeout)`` This is the recommended method for takeoff. It sends a command and then checks the
|
||||
sensors (via flying state) to ensure the minidrone is actually taking off. Then it waits until the minidrone is
|
||||
flying or hovering to return. It will timeout and return if the time exceeds timeout seconds.
|
||||
|
||||
``safe_land(timeout)`` This is the recommended method to land the minidrone. Sends commands
|
||||
until the minidrone has actually reached the landed state. It will timeout and return if the time exceeds timeout seconds.
|
||||
|
||||
``takeoff()`` Sends a single takeoff command to the minidrone. This is not the recommended method.
|
||||
|
||||
``land()`` Sends a single land command to the minidrone. This is not the recommended method.
|
||||
|
||||
``turn_on_auto_takeoff()`` This puts the minidrone in throw mode. When it is in throw mode, the eyes will blink.
|
||||
|
||||
Flying
|
||||
^^^^^^
|
||||
|
||||
``hover()`` and ``set_flat_trim()`` both tell the drone to assume the current configuration is a flat trim and it will
|
||||
use this as the default when not receiving commands. This enables good hovering when not sending commands.
|
||||
|
||||
``flip(direction)`` Sends the flip command to the minidrone. Valid directions to flip are: front, back, right, left.
|
||||
|
||||
``turn_degrees(degrees)`` Turns the minidrone in place the specified number of degrees.
|
||||
The range is -180 to 180. This can be accomplished in direct_fly() as well but this one uses the
|
||||
internal minidrone sensors (which are not sent out right now) so it is more accurate.
|
||||
|
||||
``fly_direct(roll, pitch, yaw, vertical_movement, duration)`` Fly the minidrone directly using the
|
||||
specified roll, pitch, yaw, and vertical movements. The commands are repeated for duration seconds.
|
||||
Note there are currently no sensors reported back to the user to ensure that these are working but hopefully
|
||||
that is addressed in a future firmware upgrade. Each value ranges from -100 to 100 and is essentially a percentage
|
||||
and direction of the max_tilt (for roll/pitch) or max_vertical_speed (for vertical movement).
|
||||
|
||||
``set_max_tilt(degrees)`` Set the maximum tilt in degrees. Be careful as this makes your drone go slower or faster!
|
||||
It is important to note that the fly_direct command uses this value in conjunction with the -100 to 100 percentages.
|
||||
|
||||
``set_max_vertical_speed(speed)`` Set the maximum vertical speed in m/s. Be careful as this makes your drone go up/down faster!
|
||||
|
||||
Pausing or sleeping in a thread safe manner
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``smart_sleep(seconds)`` This sleeps the number of seconds (which can be a floating point) but wakes for all
|
||||
BLE or wifi notifications. **Note, if you are using BLE: This comamnd is VERY important**. **NEVER** use regular
|
||||
time.sleep() as your BLE will disconnect regularly! Use smart_sleep instead! time.sleep() is ok if you are using
|
||||
wifi but smart_sleep() handles that for you.
|
||||
|
||||
USB accessories: Claw and Gun
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
``open_claw()`` Open the claw. Note that the claw should be attached for this to work.
|
||||
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the claw with the FPV camera attached.
|
||||
|
||||
``close_claw()`` Close the claw. Note that the claw should be attached for this to work.
|
||||
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the claw with the FPV camera attached.
|
||||
|
||||
``fire_gun()`` Fires the gun. Note that the gun should be attached for this to work.
|
||||
The id is obtained from a prior ``ask_for_state_update()`` call. Note that you cannot use the gun with the FPV camera attached.
|
||||
|
||||
Swing specific commands
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
``set_plane_gear_box(state)`` Choose the swing angle in plane mode. There are 3 tilt modes: gear_1, gear_2, gear_3.
|
||||
Warning gear_3 is very fast.
|
||||
|
||||
``set_flying_mode(mode)`` Choose flight mode between: quadricopter, plane_forward, plane_backward.
|
||||
|
||||
Ground facing camera
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
``take_picture()``` The minidrone will take a picture with the downward facing camera. It only stores up to 40 pictures
|
||||
internally so this function deletes them after 35 have been taken. Make sure you are downloading them either
|
||||
using the mobile interface or through the python code.
|
||||
|
||||
**Note**: Parrot broke the ability to access the groundcam in their latest (3.0.25) firmware upgrade. We will reenable these
|
||||
functions as soon as parrot fixes the firmware but for now, they will only work in versions 3.0.24 and below.
|
||||
|
||||
``get_groundcam_pictures_names()`` Returns the names of the pictures stored internally from the groundcam. Only for the mambo.
|
||||
|
||||
``get_groundcam_picture(name)`` Returns the picture with the specified name. Only for the mambo.
|
||||
|
||||
Sensor related commands
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
``ask_for_state_update()`` This sends a request to the minidrone to send back ALL states
|
||||
(this includes the claw and gun states). This really only needs to be called once at the start of the program
|
||||
to initialize some of the state variables. If you are on wifi, many of the other variables are sent at 2Hz. If you are
|
||||
on BLE, you will want to use this command to get more state information but keep in mind it will be slow.
|
||||
This command will return immediately but you should wait a few seconds before using the new state information
|
||||
as it has to be updated.
|
||||
|
||||
|
||||
Mambo sensors
|
||||
-------------
|
||||
|
||||
All of the sensor data that is passed back to the program is saved. Note that Parrot sends back more
|
||||
information via wifi than via BLE, due to the limited BLE bandwidth. The sensors are saved in Minidrone.sensors.
|
||||
This is an instance of a MamboSensors class, which can be seen at the top of the Minidrone.py file.
|
||||
|
||||
The easiest way to interact with the sensors is to call:
|
||||
|
||||
``minidrone.set_user_sensor_callback(function, args)``. This sets a user callback function with optional
|
||||
arguments that is called each time a sensor is updated. The refresh rate on wifi is 2Hz.
|
||||
|
||||
The sensors are:
|
||||
|
||||
* battery (defaults to 100 and stays at that level until a real reading is received from the drone)
|
||||
* flying_state: This is updated as frequently as the drone sends it out and can be one of "landed", "takingoff", "hovering", "flying", "landing", "emergency", "rolling", "init". These are the values as specified in `minidrone.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/minidrone.xml>`_.
|
||||
* gun_id: defaults to 0 (as far as I can tell, it is only ever 0 when it comes from the drone anyway)
|
||||
* gun_state: "READY" or "BUSY" as sent by the drone, if a gun is attached. Defaults to None.
|
||||
* claw_id: defaults to 0
|
||||
* claw_state: "OPENING", "OPENED", "CLOSING", "CLOSED" as sent by the drone, if a claw is attached. Defaults to None.
|
||||
* speed_x, speed_y, speed_z, speed_ts: the speed in x (forward > 0), y (right > 0), and z (down > 0). The ts is the timestamp that the speed was valid.
|
||||
* altitude, altitude_ts: wifi only, altitude in meters. Zero is where you took off. The ts is the timestamp where the altitude was valid.
|
||||
* quaternion_w, quaternion_x, quaternion_y, quaternion_z, quaternion_ts: wifi only. Quaternion as estimated from takeoff (which is set to 0). Ranges from -1 to 1. ts is the timestamp where this was valid.
|
||||
* ``get_estimated_z_orientation()``: returns the estimated orientation using the unit quaternions. Note that 0 is the direction the drone is facing when you boot it up
|
||||
* sensors_dict: all other sensors are saved by name in a dictionary. The names come from the `minidrone.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/minidrone.xml>`_ and `common.xml <https://github.com/amymcgovern/pyparrot/blob/master/commandsandsensors/common.xml>`_.
|
||||
7
RPI Code/pyparrot_/pyparrot/docs/modules.rst
Normal file
7
RPI Code/pyparrot_/pyparrot/docs/modules.rst
Normal file
@ -0,0 +1,7 @@
|
||||
pyparrot
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
pyparrot
|
||||
@ -0,0 +1,30 @@
|
||||
pyparrot.commandsandsensors package
|
||||
===================================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
pyparrot.commandsandsensors.DroneCommandParser module
|
||||
-----------------------------------------------------
|
||||
|
||||
.. automodule:: pyparrot.commandsandsensors.DroneCommandParser
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.commandsandsensors.DroneSensorParser module
|
||||
----------------------------------------------------
|
||||
|
||||
.. automodule:: pyparrot.commandsandsensors.DroneSensorParser
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: pyparrot.commandsandsensors
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
30
RPI Code/pyparrot_/pyparrot/docs/pyparrot.networking.rst
Normal file
30
RPI Code/pyparrot_/pyparrot/docs/pyparrot.networking.rst
Normal file
@ -0,0 +1,30 @@
|
||||
pyparrot.networking package
|
||||
===========================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
pyparrot.networking.bleConnection module
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: pyparrot.networking.bleConnection
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.networking.wifiConnection module
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: pyparrot.networking.wifiConnection
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: pyparrot.networking
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
64
RPI Code/pyparrot_/pyparrot/docs/pyparrot.rst
Normal file
64
RPI Code/pyparrot_/pyparrot/docs/pyparrot.rst
Normal file
@ -0,0 +1,64 @@
|
||||
pyparrot package
|
||||
================
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
pyparrot.commandsandsensors
|
||||
pyparrot.networking
|
||||
pyparrot.scripts
|
||||
pyparrot.utils
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
pyparrot.Bebop module
|
||||
---------------------
|
||||
|
||||
.. automodule:: pyparrot.Bebop
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.DroneVision module
|
||||
---------------------------
|
||||
|
||||
.. automodule:: pyparrot.DroneVision
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.DroneVisionGUI module
|
||||
------------------------------
|
||||
|
||||
.. automodule:: pyparrot.DroneVisionGUI
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.Minidrone module
|
||||
---------------------
|
||||
|
||||
.. automodule:: pyparrot.Minidrone
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.VisionServer module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: pyparrot.VisionServer
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: pyparrot
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
22
RPI Code/pyparrot_/pyparrot/docs/pyparrot.scripts.rst
Normal file
22
RPI Code/pyparrot_/pyparrot/docs/pyparrot.scripts.rst
Normal file
@ -0,0 +1,22 @@
|
||||
pyparrot.scripts package
|
||||
========================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
pyparrot.scripts.findMinidrone module
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: pyparrot.scripts.findMinidrone
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: pyparrot.scripts
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
38
RPI Code/pyparrot_/pyparrot/docs/pyparrot.utils.rst
Normal file
38
RPI Code/pyparrot_/pyparrot/docs/pyparrot.utils.rst
Normal file
@ -0,0 +1,38 @@
|
||||
pyparrot.utils package
|
||||
======================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
pyparrot.utils.NonBlockingStreamReader module
|
||||
---------------------------------------------
|
||||
|
||||
.. automodule:: pyparrot.utils.NonBlockingStreamReader
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.utils.colorPrint module
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: pyparrot.utils.colorPrint
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
pyparrot.utils.vlc module
|
||||
-------------------------
|
||||
|
||||
.. automodule:: pyparrot.utils.vlc
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: pyparrot.utils
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
199
RPI Code/pyparrot_/pyparrot/docs/quickstartbebop.rst
Normal file
199
RPI Code/pyparrot_/pyparrot/docs/quickstartbebop.rst
Normal file
@ -0,0 +1,199 @@
|
||||
.. title:: Quick Start with a Bebop
|
||||
|
||||
.. quickstartmambo:
|
||||
|
||||
Quick Start Guide with a Bebop
|
||||
==============================
|
||||
|
||||
Using the pyparrot library on the Bebop
|
||||
---------------------------------------
|
||||
|
||||
Before running any of the sample code, you will need to connect to your drone. To control the Bebop, you need to
|
||||
connect your controlling device (laptop, computer, etc) to the wifi for the drone. Look for the wifi network
|
||||
named Bebop_number where number varies for each drone.
|
||||
|
||||
Quick start: Demo Code
|
||||
-----------------------
|
||||
I have provided a set of `example <https://github.com/amymcgovern/pyparrot/tree/master/examples>`_ scripts for both the
|
||||
Mambo and the Bebop.
|
||||
|
||||
Demo of the trick commands on the bebop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The code shown below is the
|
||||
`demoBebopTricks.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopTricks.py>`_.
|
||||
demoBebopTricks.py will take off, demonstrate all 4 types of flips, and then land. It is a good program to
|
||||
verify that your connection to your bebop is working well. The bebop can flip just like the Mambo! This does
|
||||
the exact same thing as the Mambo tricks demo: take off, flip in all 4 directions, land.
|
||||
**Be sure to run it in a room large enough to perform the flips!**
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demos the tricks on the bebop. Make sure you have enough room to perform them!
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Bebop import Bebop
|
||||
|
||||
bebop = Bebop()
|
||||
|
||||
print("connecting")
|
||||
success = bebop.connect(10)
|
||||
print(success)
|
||||
|
||||
print("sleeping")
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
bebop.ask_for_state_update()
|
||||
|
||||
bebop.safe_takeoff(10)
|
||||
|
||||
print("flip left")
|
||||
print("flying state is %s" % bebop.sensors.flying_state)
|
||||
success = bebop.flip(direction="left")
|
||||
print("mambo flip result %s" % success)
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
print("flip right")
|
||||
print("flying state is %s" % bebop.sensors.flying_state)
|
||||
success = bebop.flip(direction="right")
|
||||
print("mambo flip result %s" % success)
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
print("flip front")
|
||||
print("flying state is %s" % bebop.sensors.flying_state)
|
||||
success = bebop.flip(direction="front")
|
||||
print("mambo flip result %s" % success)
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
print("flip back")
|
||||
print("flying state is %s" % bebop.sensors.flying_state)
|
||||
success = bebop.flip(direction="back")
|
||||
print("mambo flip result %s" % success)
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
bebop.smart_sleep(5)
|
||||
bebop.safe_land(10)
|
||||
|
||||
print("DONE - disconnecting")
|
||||
bebop.disconnect()
|
||||
|
||||
Outdoor or large area demo of the direct flight commands on the bebop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The second example program shows how to directly fly the bebop by controlling the yaw, pitch, roll, and
|
||||
vertical movement parameters. **Make sure you try this one in a large enough room!**
|
||||
This code is provided in
|
||||
`demoBebopDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopDirectFlight.py>`_
|
||||
and is also shown below.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Flies the bebop in a fairly wide arc. You want to be sure you have room for this. (it is commented
|
||||
out but even what is here is still going to require a large space)
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
from pyparrot.Bebop import Bebop
|
||||
|
||||
bebop = Bebop()
|
||||
|
||||
print("connecting")
|
||||
success = bebop.connect(10)
|
||||
print(success)
|
||||
|
||||
print("sleeping")
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
bebop.ask_for_state_update()
|
||||
|
||||
bebop.safe_takeoff(10)
|
||||
|
||||
print("Flying direct: going forward (positive pitch)")
|
||||
bebop.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: yaw")
|
||||
bebop.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going backwards (negative pitch)")
|
||||
bebop.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
|
||||
|
||||
print("Flying direct: roll")
|
||||
bebop.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going up")
|
||||
bebop.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
|
||||
|
||||
print("Turning relative")
|
||||
bebop.move_relative(0, 0, 0, math.radians(90))
|
||||
|
||||
# this works but requires a larger test space than I currently have. Uncomment with care and test only in large spaces!
|
||||
#print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
|
||||
#bebop.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=5)
|
||||
|
||||
bebop.smart_sleep(5)
|
||||
bebop.safe_land(10)
|
||||
|
||||
print("DONE - disconnecting")
|
||||
bebop.disconnect()
|
||||
|
||||
|
||||
Indoor demo of the direct flight commands on the bebop
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you couldn't run the outdoor or large space demo to test your bebop, this one is designed for a smaller space.
|
||||
It simply takes off, turns, and lands. **Make sure you are still flying in a safe place!** This code is provided in
|
||||
`demoBebopIndoors.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoBebopIndoors.py>`_
|
||||
and is also shown below.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the Bebop indoors (sets small speeds and then flies just a small amount)
|
||||
Note, the bebop will hurt your furniture if it hits it. Even though this is a very small
|
||||
amount of flying, be sure you are doing this in an open area and are prepared to catch!
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Bebop import Bebop
|
||||
|
||||
bebop = Bebop()
|
||||
|
||||
print("connecting")
|
||||
success = bebop.connect(10)
|
||||
print(success)
|
||||
|
||||
if (success):
|
||||
print("turning on the video")
|
||||
bebop.start_video_stream()
|
||||
|
||||
print("sleeping")
|
||||
bebop.smart_sleep(2)
|
||||
|
||||
bebop.ask_for_state_update()
|
||||
|
||||
bebop.safe_takeoff(10)
|
||||
|
||||
# set safe indoor parameters
|
||||
bebop.set_max_tilt(5)
|
||||
bebop.set_max_vertical_speed(1)
|
||||
|
||||
# trying out the new hull protector parameters - set to 1 for a hull protection and 0 without protection
|
||||
bebop.set_hull_protection(1)
|
||||
|
||||
print("Flying direct: Slow move for indoors")
|
||||
bebop.fly_direct(roll=0, pitch=20, yaw=0, vertical_movement=0, duration=2)
|
||||
|
||||
bebop.smart_sleep(5)
|
||||
|
||||
bebop.safe_land(10)
|
||||
|
||||
print("DONE - disconnecting")
|
||||
bebop.stop_video_stream()
|
||||
bebop.smart_sleep(5)
|
||||
bebop.disconnect()
|
||||
604
RPI Code/pyparrot_/pyparrot/docs/quickstartminidrone.rst
Normal file
604
RPI Code/pyparrot_/pyparrot/docs/quickstartminidrone.rst
Normal file
@ -0,0 +1,604 @@
|
||||
.. title:: Quick Start with a Minidrone
|
||||
|
||||
.. quickstartmambo:
|
||||
|
||||
Quick Start Guide with a Minidrone
|
||||
==============================
|
||||
|
||||
Using the pyparrot library on the Minidrone
|
||||
---------------------------------------
|
||||
|
||||
Before running any of the sample code, you will need to connect to your drone. If you have a Mambo FPV, I highly
|
||||
recommend using the wifi connection since it sends much more information using wifi than BLE. If you have a Mambo Code
|
||||
or a Mambo Fly or Swing(neither of which has a camera), then you need to use the BLE connection.
|
||||
|
||||
wifi connection
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
If you are using the wifi (e.g. Mambo FPV), you need to connect your controlling device (laptop, computer, etc)
|
||||
to the wifi for the drone. Look for a wifi network named Mambo_number where number changes for each drone.
|
||||
|
||||
BLE connection
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
If you do not have a camera or want to use BLE for other reasons(e.g. swarm), you will first need to find the
|
||||
BLE address of your Minidrone(s). BLE permissions on linux require that this command run in sudo mode.
|
||||
To run this, from the bin directory for your python installation, type:
|
||||
|
||||
::
|
||||
|
||||
sudo findMinidrone
|
||||
|
||||
|
||||
This will identify all BLE devices within hearing of the Pi. The Minidrone's specific address will be printed at the end.
|
||||
Save the address and use it in your connection code (discussed below). If findMinidrone does not
|
||||
report "FOUND A MAMBO!" or "FOUND A SWING!", then be sure your minidrone is turned on when you run the findMambo code and that your Pi
|
||||
(or other linux box) has its BLE interface turned on.
|
||||
|
||||
The output should look something like this. I removed my own BLE addresses from my network for security but I am
|
||||
showing the address of the mambo that I use for all the demo scripts.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
~/miniconda3/bin $ sudo ./find_mambo
|
||||
Discovered device <address removed>
|
||||
Discovered device <address removed>
|
||||
Discovered device <address removed>
|
||||
Discovered device e0:14:d0:63:3d:d0
|
||||
Received new data from <address removed>
|
||||
Discovered device <address removed>
|
||||
Discovered device <address removed>
|
||||
Received new data from <address removed>
|
||||
Discovered device <address removed>
|
||||
FOUND A MAMBO!
|
||||
Device e0:14:d0:63:3d:d0 (random), RSSI=-60 dB
|
||||
Complete Local Name = Mambo_<numbers>
|
||||
|
||||
|
||||
|
||||
Quick start: Demo Code
|
||||
-----------------------
|
||||
|
||||
I have provided a set of `example <https://github.com/amymcgovern/pyparrot/tree/master/examples>`_ scripts for both the
|
||||
Mambo and the Bebop. Note that you will need to edit the minidrone scripts to either use your own BLE address or to
|
||||
ensure that use_wifi=True is set, so that it connects using wifi.
|
||||
**Note that you do not need to run any of the other code in sudo mode!** That was only for discovery.
|
||||
|
||||
Demo of the trick commands on the mambo
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The code shown below is the
|
||||
`demoMamboTricks.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboTricks.py>`_.
|
||||
demoMamboTricks.py will take off, demonstrate all 4 types of flips, and then land. It is a good program to
|
||||
verify that your connection to your mambo is working well. Be sure to run it in a room large enough
|
||||
to perform the flips! The highlighted lines need to change for YOUR mambo and connection choices.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the trick flying for the python interface
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
|
||||
# you will need to change this to the address of YOUR mambo
|
||||
mamboAddr = "e0:14:d0:63:3d:d0"
|
||||
|
||||
# make my mambo object
|
||||
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
|
||||
mambo = Mambo(mamboAddr, use_wifi=True)
|
||||
|
||||
print("trying to connect")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
if (success):
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
mambo.smart_sleep(2)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("taking off!")
|
||||
mambo.safe_takeoff(5)
|
||||
|
||||
if (mambo.sensors.flying_state != "emergency"):
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
print("Flying direct: going up")
|
||||
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=20, duration=1)
|
||||
|
||||
print("flip left")
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
success = mambo.flip(direction="left")
|
||||
print("mambo flip result %s" % success)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("flip right")
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
success = mambo.flip(direction="right")
|
||||
print("mambo flip result %s" % success)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("flip front")
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
success = mambo.flip(direction="front")
|
||||
print("mambo flip result %s" % success)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("flip back")
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
success = mambo.flip(direction="back")
|
||||
print("mambo flip result %s" % success)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("landing")
|
||||
print("flying state is %s" % mambo.sensors.flying_state)
|
||||
mambo.safe_land(5)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("disconnect")
|
||||
mambo.disconnect()
|
||||
|
||||
|
||||
|
||||
|
||||
Demo of the direct flight commands on the mambo
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The second example program shows how to directly fly the mambo by controlling the yaw, pitch, roll, and
|
||||
vertical movement parameters. **Make sure you try this one in a large enough room!**
|
||||
This code is provided in
|
||||
`demoMamboDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboDirectFlight.py>`_
|
||||
and is also shown below. Again, the highlighted lines must be changed to the parameters for your mambo and connection.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the direct flying for the python interface
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
|
||||
# you will need to change this to the address of YOUR mambo
|
||||
mamboAddr = "e0:14:d0:63:3d:d0"
|
||||
|
||||
# make my mambo object
|
||||
# remember to set True/False for the wifi depending on if you are using the wifi or the BLE to connect
|
||||
mambo = Mambo(mamboAddr, use_wifi=True)
|
||||
|
||||
print("trying to connect")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
if (success):
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
mambo.smart_sleep(2)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("taking off!")
|
||||
mambo.safe_takeoff(5)
|
||||
|
||||
print("Flying direct: going forward (positive pitch)")
|
||||
mambo.fly_direct(roll=0, pitch=50, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Showing turning (in place) using turn_degrees")
|
||||
mambo.turn_degrees(90)
|
||||
mambo.smart_sleep(2)
|
||||
mambo.turn_degrees(-90)
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("Flying direct: yaw")
|
||||
mambo.fly_direct(roll=0, pitch=0, yaw=50, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going backwards (negative pitch)")
|
||||
mambo.fly_direct(roll=0, pitch=-50, yaw=0, vertical_movement=0, duration=0.5)
|
||||
|
||||
print("Flying direct: roll")
|
||||
mambo.fly_direct(roll=50, pitch=0, yaw=0, vertical_movement=0, duration=1)
|
||||
|
||||
print("Flying direct: going up")
|
||||
mambo.fly_direct(roll=0, pitch=0, yaw=0, vertical_movement=50, duration=1)
|
||||
|
||||
print("Flying direct: going around in a circle (yes you can mix roll, pitch, yaw in one command!)")
|
||||
mambo.fly_direct(roll=25, pitch=0, yaw=50, vertical_movement=0, duration=3)
|
||||
|
||||
print("landing")
|
||||
mambo.safe_land(5)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("disconnect")
|
||||
mambo.disconnect()
|
||||
|
||||
|
||||
|
||||
Demo of the USB claw accessory
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If your mambo has the USB accessories (claw and gun), you can control them but you *MUST* be in BLE mode.
|
||||
The mambo can only handle one USB accessory at a time and the camera counts as a USB accessory so you must use
|
||||
the BLE connection only. `demoMamboClaw.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboClaw.py>`_
|
||||
show how to use the claw accessory. The highlighted line must be changed to the BLE address for your mambo and the use_wifi
|
||||
parameter must stay at False. In this demo program, the mambo takes off, opens and closes the claw, and lands again.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the claw for the python interface
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
|
||||
# you will need to change this to the address of YOUR mambo
|
||||
mamboAddr = "e0:14:d0:63:3d:d0"
|
||||
|
||||
# make my mambo object
|
||||
# remember you can't use the claw with the camera installed so this must be BLE connected to work
|
||||
mambo = Mambo(mamboAddr, use_wifi=False)
|
||||
|
||||
print("trying to connect")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
mambo.smart_sleep(2)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("taking off!")
|
||||
mambo.safe_takeoff(5)
|
||||
|
||||
print("open and close the claw")
|
||||
mambo.open_claw()
|
||||
# you have to sleep to let the claw open (it needs time to do it)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
mambo.close_claw()
|
||||
# you have to sleep to let the claw close (it needs time to do it)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("landing")
|
||||
mambo.safe_land(5)
|
||||
mambo.smart_sleep(5)
|
||||
|
||||
print("disconnect")
|
||||
mambo.disconnect()
|
||||
|
||||
Demo of the USB gun accessory
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`demoMamboGun.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboGun.py>`_
|
||||
show how to use the gun accessory. The highlighted line must be changed to the BLE address for your mambo and the use_wifi
|
||||
parameter must stay at False. In this demo program, the mambo takes off, fires the gun, and lands again.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the gun for the python interface
|
||||
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
|
||||
# you will need to change this to the address of YOUR mambo
|
||||
mamboAddr = "e0:14:d0:63:3d:d0"
|
||||
|
||||
# make my mambo object
|
||||
# remember you can't use the gun with the camera installed so this must be BLE connected to work
|
||||
mambo = Mambo(mamboAddr, use_wifi=False)
|
||||
|
||||
print("trying to connect")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
# get the state information
|
||||
print ("sleeping")
|
||||
mambo.smart_sleep(2)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(2)
|
||||
|
||||
print("shoot the gun")
|
||||
mambo.fire_gun()
|
||||
|
||||
# sleep to ensure it does the firing
|
||||
mambo.smart_sleep(15)
|
||||
|
||||
print("disconnect")
|
||||
mambo.disconnect()
|
||||
|
||||
|
||||
|
||||
Demo of the ground-facing camera
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`demoMamboGroundcam.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoMamboGroundcam.py>`_
|
||||
show how to use the mambo's ground-facing camera. This feature **ONLY** works in wifi mode. It can be slow
|
||||
to download the frames so do not count on this running at several frames per second. The example code shown
|
||||
below takes off, takes a picture, and then grabs a random picture from the ground facing camera set.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo of the groundcam
|
||||
Mambo takes off, takes a picture and shows a RANDOM frame, not the last one
|
||||
Author: Valentin Benke, https://github.com/Vabe7
|
||||
Author: Amy McGovern
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Mambo
|
||||
import cv2
|
||||
|
||||
mambo = Mambo(None, use_wifi=True) #address is None since it only works with WiFi anyway
|
||||
print("trying to connect to mambo now")
|
||||
success = mambo.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
if (success):
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
mambo.smart_sleep(1)
|
||||
mambo.ask_for_state_update()
|
||||
mambo.smart_sleep(1)
|
||||
mambo.safe_takeoff(5)
|
||||
|
||||
# take the photo
|
||||
pic_success = mambo.take_picture()
|
||||
|
||||
# need to wait a bit for the photo to show up
|
||||
mambo.smart_sleep(0.5)
|
||||
|
||||
picture_names = mambo.groundcam.get_groundcam_pictures_names() #get list of availible files
|
||||
print(picture_names)
|
||||
|
||||
frame = mambo.groundcam.get_groundcam_picture(picture_names[0],True) #get frame which is the first in the array
|
||||
|
||||
if frame is not None:
|
||||
if frame is not False:
|
||||
cv2.imshow("Groundcam", frame)
|
||||
cv2.waitKey(100)
|
||||
|
||||
mambo.safe_land(5)
|
||||
mambo.disconnect()
|
||||
|
||||
Demo of the flying mode on the swing
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
`demoSwingDirectFlight.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoSwingDirectFlight.py>`_
|
||||
You can see how to use the set_flying_mode command. I advise you to have enough space to use this script.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
"""
|
||||
Demo the direct flying for the python interface
|
||||
|
||||
Author: Victor804
|
||||
"""
|
||||
|
||||
from pyparrot.Minidrone import Swing
|
||||
|
||||
# you will need to change this to the address of YOUR swing
|
||||
swingAddr = "e0:14:04:a7:3d:cb"
|
||||
|
||||
# make my swing object
|
||||
swing = Swing(swingAddr)
|
||||
|
||||
print("trying to connect")
|
||||
success = swing.connect(num_retries=3)
|
||||
print("connected: %s" % success)
|
||||
|
||||
if (success):
|
||||
# get the state information
|
||||
print("sleeping")
|
||||
swing.smart_sleep(2)
|
||||
swing.ask_for_state_update()
|
||||
swing.smart_sleep(2)
|
||||
|
||||
print("taking off!")
|
||||
swing.safe_takeoff(5)
|
||||
|
||||
print("plane forward")
|
||||
swing.set_flying_mode("plane_forward")
|
||||
|
||||
swing.smart_sleep(1)
|
||||
|
||||
print("quadricopter")
|
||||
swing.set_flying_mode("quadricopter")
|
||||
|
||||
print("landing")
|
||||
swing.safe_land(5)
|
||||
swing.smart_sleep(5)
|
||||
|
||||
print("disconnect")
|
||||
swing.disconnect()
|
||||
|
||||
|
||||
Demo joystick for Swing
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
`demoSwingJoystick.py <https://github.com/amymcgovern/pyparrot/blob/master/examples/demoSwingJoystick.py>`_
|
||||
Example code to control the swig with a joystick. Easy to modify for your needs.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
import pygame
|
||||
import sys
|
||||
from pyparrot.Minidrone import Swing
|
||||
|
||||
def joystick_init():
|
||||
"""
|
||||
Initializes the controller, allows the choice of the controller.
|
||||
If no controller is detected returns an error.
|
||||
|
||||
:param:
|
||||
:return joystick:
|
||||
"""
|
||||
pygame.init()
|
||||
pygame.joystick.init()
|
||||
|
||||
joystick_count = pygame.joystick.get_count()
|
||||
|
||||
if joystick_count > 0:
|
||||
for i in range(joystick_count):
|
||||
joystick = pygame.joystick.Joystick(i)
|
||||
joystick.init()
|
||||
|
||||
name = joystick.get_name()
|
||||
print([i], name)
|
||||
|
||||
joystick.quit()
|
||||
|
||||
else:
|
||||
sys.exit("Error: No joystick detected")
|
||||
|
||||
selected_joystick = eval(input("Enter your joystick number:"))
|
||||
|
||||
if selected_joystick not in range(joystick_count):
|
||||
sys.exit("Error: Your choice is not valid")
|
||||
|
||||
joystick = pygame.joystick.Joystick(selected_joystick)
|
||||
joystick.init()
|
||||
|
||||
return joystick
|
||||
|
||||
|
||||
def mapping_button(joystick, dict_commands):
|
||||
"""
|
||||
Associating a controller key with a command in dict_commands.
|
||||
|
||||
:param joystick, dict_commands:
|
||||
:return mapping:
|
||||
"""
|
||||
mapping = {}
|
||||
|
||||
for command in dict_commands:
|
||||
print("Press the key", command)
|
||||
done = False
|
||||
while not done:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.JOYBUTTONDOWN:
|
||||
if event.button not in (value for value in mapping.values()):
|
||||
mapping[command] = event.button
|
||||
done = True
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def mapping_axis(joystick, axes=["pitch", "roll", "yaw", "vertical"]):
|
||||
"""
|
||||
Associating the analog thumbsticks of the controller with a command in dict commands
|
||||
|
||||
:param joystick, dict_commands:
|
||||
:return mapping:
|
||||
"""
|
||||
mapping = {}
|
||||
|
||||
for i in axes:
|
||||
print("Push the", i, "axis")
|
||||
done = False
|
||||
while not done:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.JOYAXISMOTION:
|
||||
if event.axis not in (value for value in mapping.values()):
|
||||
mapping[i] = event.axis
|
||||
done = True
|
||||
|
||||
return mapping
|
||||
|
||||
|
||||
def _parse_button(dict_commands, button):
|
||||
"""
|
||||
Send the commands to the drone.
|
||||
If multiple commands are assigned to a key each command will be sent one by one to each press.
|
||||
|
||||
:param dict_commands, button:
|
||||
:return:
|
||||
"""
|
||||
commands = dict_commands[button][0]
|
||||
args = dict_commands[button][-1]
|
||||
|
||||
command = commands[0]
|
||||
arg = args[0]
|
||||
|
||||
if len(commands) == 1:
|
||||
if len(args) == 1:
|
||||
command(arg)
|
||||
|
||||
else:
|
||||
command(arg)
|
||||
dict_commands[button][-1] = args[1:]+[arg]
|
||||
|
||||
else:
|
||||
if len(commands) == 1:
|
||||
command(arg)
|
||||
dict_commands[button][0] = commands[1:]+[command]
|
||||
|
||||
else:
|
||||
command(arg)
|
||||
dict_commands[button][0] = commands[1:]+[command]
|
||||
dict_commands[button][-1] = args[1:]+[arg]
|
||||
|
||||
|
||||
def main_loop(joystick, dict_commands, mapping_button, mapping_axis):
|
||||
"""
|
||||
First connects to the drone and makes a flat trim.
|
||||
Then in a loop read the events of the controller to send commands to the drone.
|
||||
|
||||
:param joystick, dict_commands, mapping_button, mapping_axis:
|
||||
:return:
|
||||
"""
|
||||
swing.connect(10)
|
||||
swing.flat_trim()
|
||||
|
||||
while True:
|
||||
pygame.event.get()
|
||||
|
||||
pitch = joystick.get_axis(mapping_axis["pitch"])*-100
|
||||
roll = joystick.get_axis(mapping_axis["roll"])*100
|
||||
yaw = joystick.get_axis(mapping_axis["yaw"])*100
|
||||
vertical = joystick.get_axis(mapping_axis["vertical"])*-100
|
||||
|
||||
swing.fly_direct(roll, pitch, yaw, vertical, 0.1)
|
||||
|
||||
for button, value in mapping_button.items():
|
||||
if joystick.get_button(value):
|
||||
_parse_button(dict_commands, button)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
swing = Swing("e0:14:04:a7:3d:cb")
|
||||
|
||||
#Example of dict_commands
|
||||
dict_commands = {
|
||||
"takeoff_landing":[ #Name of the button
|
||||
[swing.safe_takeoff, swing.safe_land],#Commands execute one by one
|
||||
[5]#Argument for executing the function
|
||||
],
|
||||
"fly_mode":[
|
||||
[swing.set_flying_mode],
|
||||
["quadricopter", "plane_forward"]
|
||||
],
|
||||
"plane_gear_box_up":[
|
||||
[swing.set_plane_gear_box],
|
||||
[((swing.sensors.plane_gear_box[:-1]+str(int(swing.sensors.plane_gear_box[-1])+1)) if swing.sensors.plane_gear_box[-1] != "3" else "gear_3")]#"gear_1" => "gear_2" => "gear_3"
|
||||
],
|
||||
"plane_gear_box_down":[
|
||||
[swing.set_plane_gear_box],
|
||||
[((swing.sensors.plane_gear_box[:-1]+str(int(swing.sensors.plane_gear_box[-1])-1)) if swing.sensors.plane_gear_box[-1] != "1" else "gear_1")]#"gear_3" => "gear_2" => "gear_1"
|
||||
]
|
||||
}
|
||||
|
||||
joystick = joystick_init()
|
||||
|
||||
mapping_button = mapping_button(joystick, dict_commands)
|
||||
mapping_axis = mapping_axis(joystick)
|
||||
|
||||
main_loop(joystick, dict_commands, mapping_button, mapping_axis)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user