-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9d32c78
commit f7e2fa5
Showing
58 changed files
with
3,208 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# qt_atsign_plant_demo | ||
|
||
Qt-Atsign Plant Demo involves two components: | ||
1. [Qt App](#qt-app) | ||
2. [Plant](#plant) | ||
|
||
This demo is a good demonstration of Atsign and Qt's strong integration with other systems. In this demo, each device (the app and the plant) are able to communicate with one another using Atsign's technology whilst maintaining security, privacy, and strong user experience with Qt's libraries. | ||
|
||
## Complete BOM | ||
|
||
| Item | Quantity | Description | Link | | ||
| ---- | -------- | ----------- | ---- | | ||
| SunFounder 7 Inch HDMI 1024×600 USB IPS LCD Touchscreen Display Monitor for Raspberry Pi 400 4 3 Model B, 2 Model B, and 1 Model B+, Windows Capacitive Touch Screen | 1 | Screen for the Qt App | [Amazon](https://www.amazon.ca/gp/product/B07Y889J3X/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1) | | ||
| Raspberry Pi 4 Model B (2GB) | 1 | For the Headless Plant | | | ||
| Raspberry Pi 4 Model B (8GB) | 1 | For the Qt App | | | ||
| Dupont Jumper Wires (F-F, M-M, and M-F) | | For general wiring of sensors and other electronic components | | | ||
| Breadboard | 2 | 2.2" x 7" (5.5 cm x 17 cm) | | | ||
| Relay Modules | 1 | Used to control the 5v DC water pump | | | ||
| 5V DC Microsubmersible Water Pump | 1 | Used to pump water from the reservoir to the plant | [Amazon](https://www.amazon.ca/Micro-Submersible-Water-Tubing-Garden/dp/B095VSB54J/ref=sr_1_6?keywords=5v+dc+water+pump&qid=1705336213&sr=8-6) | | ||
| DHT11 sensor | 1 | Used to measure temperature & humidity | | | ||
| Soil Moisture and Water Level | 1 | Used to measure soil moisture and water level | [Whadda](https://whadda.com/product/soil-moisture-sensor-water-level-sensor-module-wpse303/) | | ||
|
||
## Physical CAD | ||
|
||
Iteration 1 - initially, we had the water reservoir separate from the plant | ||
|
||
![Alt text](image-1.png) | ||
|
||
Iteration 2 - in the next iteration, the water reservoir would sit directly under the plant because the | ||
|
||
![iteration 2](image.png) | ||
|
||
## Qt App | ||
|
||
The Qt App is written in python using Qt's QML. The app is designed to run on a Raspberry Pi 4 Model B (8GB) with a 7" touchscreen. | ||
|
||
The app is running on a raspberry pi connected to a 7" touch screen which is used to interface with the plant. This is a good demonstration of remotely controlling another device securely and privately using Atsign's technology. | ||
|
||
## Plant | ||
|
||
The Plant is headless (no screen) and is designed to run on a Raspberry Pi 4 Model B (2GB). The plant is connected to the Qt App via Atsign technology. | ||
|
||
The plant is running 4 sensors (digital temperature and humidity, soil moisture, and water level) and 1 relay module connected to a 5v DC microsubmersible water pump (to pump water from the reservoir to the plant). | ||
|
||
## Data Model | ||
|
||
The data models we used for communication between the Plant and Qt App. (REVISED NOV 19 2023) | ||
|
||
### Plant → Qt App | ||
|
||
To update the Qt App on its sensor values, two things need to occur. | ||
|
||
1. Creates 1 AtKey (@qtapp:31291.qtdemodata@plant) | ||
|
||
Example: | ||
```json | ||
@qtapp:31291.datapoints.qtplant@plant | ||
{ | ||
“timestamp”: 31291 | ||
“type”: “sensorData” | ||
“data”: { | ||
“temperature”: 21, | ||
“humidity”: 51, | ||
“waterLevel”: 20, | ||
“soilMoisture”: 34, | ||
“waterFlow”: 1, | ||
“light”: 50 | ||
} | ||
} | ||
``` | ||
|
||
2. Updates 1 AtKey (@qtapp:YYYY-MM-DD.qtdemo@plant) | ||
|
||
Example: | ||
```json | ||
@qtapp:2023-11-20.days.qtplant@plant | ||
“[31291, 12345, 5678]” | ||
``` | ||
|
||
|
||
|
||
### Qt App → Plant | ||
|
||
To tell the plant to run the water pump, one notification is sent: | ||
|
||
1. (pump for a certain amount of seconds) | ||
|
||
Example: | ||
|
||
```json | ||
|
||
@plant:pump.qtplant@qtapp | ||
|
||
{ | ||
“timestamp”: 3129319391 | ||
“type”: “pumpWithSeconds” | ||
“data”: { | ||
“seconds”: 3 | ||
} | ||
} | ||
``` | ||
|
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
from at_client.connections.notification.atevents import AtEvent, AtEventType | ||
from at_client.util.encryptionutil import EncryptionUtil | ||
from at_client.common.keys import AtKey, Metadata, SharedKey | ||
from at_client.common import AtSign | ||
from at_client import AtClient | ||
import threading | ||
import json | ||
import queue | ||
import RPi.GPIO as GPIO | ||
from time import sleep, time | ||
import Adafruit_ADS1x15 | ||
import dht11 | ||
import datetime | ||
|
||
QT_APP_ATSIGN_STR = '@qt_app' | ||
PLANT_ATSIGN_STR = '@qt_plant' | ||
qt_app_atsign = AtSign(QT_APP_ATSIGN_STR) | ||
plant_atsign = AtSign(PLANT_ATSIGN_STR) | ||
|
||
def main(): | ||
atclient = AtClient(plant_atsign, queue=queue.Queue(maxsize=100), verbose=False) | ||
atkeys: list[AtKey] = atclient.get_at_keys(regex='qtplant', fetch_metadata=False) | ||
count = 0 | ||
for atkey in atkeys: | ||
res = atclient.delete(atkey) | ||
count += 1 | ||
print('Deleted %s keys' % count) | ||
|
||
|
||
|
||
if __name__ == '__main__': | ||
main() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
#!/usr/bin/env python3 | ||
from at_client.connections.notification.atevents import AtEvent, AtEventType | ||
from at_client.util.encryptionutil import EncryptionUtil | ||
from at_client.common.keys import AtKey, Metadata, SharedKey | ||
from at_client.common import AtSign | ||
from at_client import AtClient | ||
import threading | ||
import json | ||
import queue | ||
import RPi.GPIO as GPIO | ||
from time import sleep, time | ||
import Adafruit_ADS1x15 | ||
import dht11 | ||
import datetime | ||
|
||
DHT11_PIN = 17 | ||
DC_WATER_PUMP = 4 # GPIO PIN = 4 | ||
|
||
PLANT_ATSIGN_STR = '@qt_plant' | ||
QT_APP_ATSIGN_STR = '@qt_app' | ||
# QT_APP_ATSIGN_STR = '@39gorilla' | ||
|
||
plant_atsign = AtSign(PLANT_ATSIGN_STR) | ||
qt_app_atsign = AtSign(QT_APP_ATSIGN_STR) | ||
|
||
GPIO.setmode(GPIO.BCM) | ||
GPIO.setup(DC_WATER_PUMP, GPIO.OUT) | ||
adc = Adafruit_ADS1x15.ADS1115() | ||
GAIN = 1 | ||
instance = dht11.DHT11(pin=DHT11_PIN) | ||
|
||
def run_pump_for_seconds(seconds: int, verbose: bool = True): | ||
# run pump for `q_seconds` | ||
if(verbose): | ||
print('Running pump for %s seconds' % seconds) | ||
print('Pump ON HIGH') | ||
GPIO.output(DC_WATER_PUMP, GPIO.HIGH) | ||
sleep(seconds) | ||
if(verbose): | ||
print('Pump OFF LOW') | ||
GPIO.output(DC_WATER_PUMP, GPIO.LOW) | ||
|
||
# flushes `notify:list` ` | ||
def clear_all_notifications(atclient: AtClient, regex: str = 'qtplant'): | ||
notifications = atclient.secondary_connection.execute_command('notify:list %s' %(regex), retry_on_exception=3) | ||
# strip `data:` from string | ||
notifications = str(notifications)[5:] | ||
print('notifications: %s' % notifications) | ||
# split by , | ||
if notifications != 'null': | ||
notifications = json.loads(notifications) | ||
for notification in notifications: | ||
notification_id = notification['id'] | ||
print('Removing notification with id `%s`' % notification_id) | ||
response = atclient.secondary_connection.execute_command('notify:remove:' + str(notification_id), retry_on_exception=3) | ||
print(response) | ||
else: | ||
return 0 | ||
|
||
def notify_data(client: AtClient, payload): | ||
timestamp = time() | ||
#print type of data and value | ||
# print("Type (%s): %s" %(type(payload), payload)) | ||
data = { | ||
'type': 'sensorData', | ||
'timestamp': timestamp, | ||
'data': payload | ||
} | ||
|
||
data = json.dumps(data) | ||
sharedkey = SharedKey.from_string(str(qt_app_atsign) + ':sensors.qtplant' + str(plant_atsign)) | ||
iv_nonce = EncryptionUtil.generate_iv_nonce() | ||
metadata = Metadata( | ||
#ttl = 3 second in miliseconds | ||
ttl=3000, | ||
ttr=-1, | ||
iv_nonce=iv_nonce, | ||
) | ||
sharedkey.metadata = metadata | ||
sleep(0.5) | ||
res = client.notify(sharedkey, data) | ||
print('Notification ID: %s' % res) | ||
print('SharedKey: %s' % sharedkey) | ||
print('Data: %s' % data) | ||
|
||
def log_data(client: AtClient): | ||
values = [0]*4 | ||
for i in range(4): | ||
values[i] = adc.read_adc(i, gain=GAIN) | ||
MIN = 0 | ||
MAX = 1023 | ||
water_level = (values[0] - MIN) / (MAX - MIN) | ||
soil_moisture = (values[1] - MIN) / (MAX - MIN) | ||
result = instance.read() | ||
while not result.is_valid(): | ||
result = instance.read() | ||
temperature = result.temperature | ||
humidity = result.humidity | ||
timestamp = time() | ||
# get mm, dd, yyyy | ||
mm = int(datetime.datetime.fromtimestamp(timestamp).strftime('%m')) | ||
dd = int(datetime.datetime.fromtimestamp(timestamp).strftime('%d')) | ||
yyyy = int(datetime.datetime.fromtimestamp(timestamp).strftime('%Y')) | ||
|
||
timestamp_sharedkey = SharedKey.from_string(str(qt_app_atsign) + ':' + str(timestamp) + '.datapoints.qtplant' + str(plant_atsign)) | ||
timestamp_sharedkey_data = { | ||
'water_level': water_level, | ||
'soil_moisture': soil_moisture, | ||
'temperature': temperature, | ||
'humidity': humidity, | ||
'timestamp': str(timestamp) | ||
} | ||
notify_data(client, timestamp_sharedkey_data) | ||
timestamp_sharedkey_data = json.dumps(timestamp_sharedkey_data) | ||
timestamp_sharedkey.metadata = Metadata( | ||
ttr = -1, # do not refersh, this data will never change | ||
ccd = True, # if plant deletes it, the app will not be able to read it | ||
) | ||
|
||
day_timestamps_sharedkey = SharedKey.from_string(str(qt_app_atsign) + ':' + str(mm) + '-' + str(dd) + '-' + str(yyyy) + '.days.qtplant' + str(plant_atsign)) | ||
day_timestamps_sharedkey.metadata = Metadata( | ||
ttr = -1, # do not refersh, this data will never change | ||
ccd = True, # if plant deletes it, the app will not be able to read it | ||
) | ||
|
||
|
||
try: | ||
day_timestamps_sharedkey_data = client.get(day_timestamps_sharedkey) | ||
day_timestamps_sharedkey_data = day_timestamps_sharedkey_data[1:-1] # remove brackets | ||
day_timestamps_sharedkey_data = day_timestamps_sharedkey_data.split(', ') # split by comma | ||
day_timestamps_sharedkey_data = [float(timestamp) for timestamp in day_timestamps_sharedkey_data] | ||
day_timestamps_sharedkey_data.append(timestamp) # append timestamp | ||
day_timestamps_sharedkey_data.sort() | ||
except: | ||
day_timestamps_sharedkey_data = [timestamp] | ||
day_timestamps_sharedkey_data = '[' + ', '.join([str(timestamp) for timestamp in day_timestamps_sharedkey_data]) + ']' | ||
|
||
print('Logging data to key `' + str(timestamp_sharedkey) + '` with data `' + str(timestamp_sharedkey_data) + '`') | ||
client.put(timestamp_sharedkey, str(timestamp_sharedkey_data)) | ||
print('Logged.') | ||
|
||
print('Logging data to key `' + str(day_timestamps_sharedkey) + '` with data `' + str(day_timestamps_sharedkey_data) + '`') | ||
client.put(day_timestamps_sharedkey, day_timestamps_sharedkey_data) | ||
print('Logged.') | ||
|
||
|
||
|
||
|
||
def main(): | ||
# start monitor on a thread | ||
atclient = AtClient(plant_atsign, queue=queue.Queue(maxsize=100), verbose=False) | ||
threading.Thread(target=atclient.start_monitor, args=("qtplant",)).start() | ||
|
||
while True: | ||
try: | ||
at_event = atclient.queue.get(block=False) | ||
event_type = at_event.event_type | ||
event_data = at_event.event_data | ||
|
||
if event_type == AtEventType.UPDATE_NOTIFICATION: | ||
atclient.handle_event(atclient.queue, at_event) | ||
cmd = "notify:remove:" + str(event_data["id"]) | ||
print('executing \'%s\'' %(cmd)) | ||
atclient.secondary_connection.execute_command(cmd, retry_on_exception=3) | ||
print('executed \'%s\'' %(cmd)) | ||
pass | ||
if event_type != AtEventType.DECRYPTED_UPDATE_NOTIFICATION: | ||
continue | ||
try: | ||
decrypted_value_dict = json.loads(event_data['decryptedValue']) | ||
except: | ||
print('(1) Failed to parse `%s`' % event_data['decryptedValue']) | ||
continue | ||
|
||
try: | ||
q_type: str = decrypted_value_dict['type'] | ||
q_timestamp: float = decrypted_value_dict['timestamp'] | ||
q_data: dict = decrypted_value_dict['data'] | ||
print("\nType (%s): %s" %(type(q_type), q_type)) | ||
print("Timestamp (%s): %s" %(type(q_timestamp), q_timestamp)) | ||
print("Data (%s): %s" %(type(q_data), q_data)) | ||
if q_type == 'pumpWithSeconds': | ||
q_seconds: int = q_data['seconds'] | ||
print("Seconds (%s): %s\n" %(type(q_seconds), str(q_seconds))) | ||
run_pump_for_seconds(q_seconds) | ||
clear_all_notifications(atclient) | ||
except Exception as e: | ||
# print(e) | ||
pass | ||
except Exception as e: | ||
# print(e)p | ||
print('No events in queue.') | ||
pass | ||
ss = datetime.datetime.now().second | ||
# log_data every 5 seconds | ||
if ss % 5 == 0: | ||
log_data(atclient) | ||
pass | ||
sleep(1) | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"files": [ | ||
"main.py", | ||
"plant_monitor.py", | ||
"PlantMonitor/AppSettings.qml", | ||
"PlantMonitor/BottomBar.qml", | ||
"PlantMonitor/Constants.qml", | ||
"PlantMonitor/DataScrollView.qml", | ||
"PlantMonitor/DataSwipeView.qml", | ||
"PlantMonitor/DataTile.qml", | ||
"PlantMonitor/GraphsLive.qml", | ||
"PlantMonitor/GraphsHistorical.qml", | ||
"PlantMonitor/Home.qml", | ||
"PlantMonitor/Main.qml", | ||
"PlantMonitor/qmldir", | ||
"PlantMonitor/SideBar.qml", | ||
"PlantMonitor/Stats.qml", | ||
"PlantMonitor/Images/arrow.svg", | ||
"PlantMonitor/Images/atsign-logo-dark.svg", | ||
"PlantMonitor/Images/atsign-logo-light.svg", | ||
"PlantMonitor/Images/drop.svg", | ||
"PlantMonitor/Images/historical.svg", | ||
"PlantMonitor/Images/home.svg", | ||
"PlantMonitor/Images/humidity.svg", | ||
"PlantMonitor/Images/live.svg", | ||
"PlantMonitor/Images/moisture.svg", | ||
"PlantMonitor/Images/moon.svg", | ||
"PlantMonitor/Images/more.svg", | ||
"PlantMonitor/Images/question.svg", | ||
"PlantMonitor/Images/Qt-Group-logo-black.png", | ||
"PlantMonitor/Images/Qt-Group-logo-white.png", | ||
"PlantMonitor/Images/Qt-logo-black.svg", | ||
"PlantMonitor/Images/Qt-logo-white.svg", | ||
"PlantMonitor/Images/settings.svg", | ||
"PlantMonitor/Images/stats.svg", | ||
"PlantMonitor/Images/sun.svg", | ||
"PlantMonitor/Images/theme.svg", | ||
"PlantMonitor/Images/thermometer.svg", | ||
"PlantMonitor/Images/water-tank.svg", | ||
"PlantMonitor/Images/watering-can.svg", | ||
"PlantMonitor/Images/CES-slides.pdf", | ||
"TODO.md", | ||
"Past7.json", | ||
"Past30.json", | ||
"PlantMonitor/About.qml", | ||
"PlantMonitor/Water.qml" | ||
] | ||
} |
Oops, something went wrong.