Skip to content

Commit

Permalink
feat: qt atsign plant demo
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremyTubongbanua committed Jan 15, 2024
1 parent 9d32c78 commit f7e2fa5
Show file tree
Hide file tree
Showing 58 changed files with 3,208 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ order:
shows how to build a simple end-to-end encrypted messaging app with the
atPlatform.

6. `qt_atsign_plant_demo` a demo of using Atsign's technology with Qt's QML in Python to remotely and securely control/monitor a plant from a Qt app involving 2 raspberry pis, 4 sensors, and 1 actuator. Check it out here: [qt_atsign_plant_demo](./qt_atsign_plant_demo/)

We are super glad that you are beginning your journey as an atDev. We highly
recommend that you join our discord dev community for troubleshooting, dev
updates, and much more!
102 changes: 102 additions & 0 deletions qt_atsign_plant_demo/README.md
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
}
}
```

Binary file added qt_atsign_plant_demo/image-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added qt_atsign_plant_demo/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
33 changes: 33 additions & 0 deletions qt_atsign_plant_demo/plant/delete_all_data.py
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()

202 changes: 202 additions & 0 deletions qt_atsign_plant_demo/plant/main.py
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()
48 changes: 48 additions & 0 deletions qt_atsign_plant_demo/qt_app/CESAtsignDemo.pyproject
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"
]
}
Loading

0 comments on commit f7e2fa5

Please sign in to comment.