iotlabkit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iotlabkit/__init__.py +30 -0
- iotlabkit/cli.py +239 -0
- iotlabkit/coap_util.py +322 -0
- iotlabkit/data/beijing_air/beijing_sample.csv +801 -0
- iotlabkit/data/india_air/city_day_sample.csv +901 -0
- iotlabkit/data/occupancy/datatraining.txt +801 -0
- iotlabkit/doctor.py +160 -0
- iotlabkit/generator.py +110 -0
- iotlabkit/registry.py +705 -0
- iotlabkit/templates/program_05.py +436 -0
- iotlabkit/templates/program_06.py +404 -0
- iotlabkit/templates/program_07.py +249 -0
- iotlabkit/templates/program_08.py +278 -0
- iotlabkit/templates/program_09.py +329 -0
- iotlabkit/templates/program_10.py +338 -0
- iotlabkit/templates/program_11.py +347 -0
- iotlabkit/templates/program_12.py +317 -0
- iotlabkit/templates/program_13.py +272 -0
- iotlabkit/templates/program_14.py +287 -0
- iotlabkit/templates/program_15.py +411 -0
- iotlabkit/templates/program_16.py +382 -0
- iotlabkit/templates/program_17.py +422 -0
- iotlabkit-1.0.0.dist-info/LICENSE +21 -0
- iotlabkit-1.0.0.dist-info/METADATA +550 -0
- iotlabkit-1.0.0.dist-info/RECORD +28 -0
- iotlabkit-1.0.0.dist-info/WHEEL +5 -0
- iotlabkit-1.0.0.dist-info/entry_points.txt +2 -0
- iotlabkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Template generator for Program 6: MQTT + Cloud Database + Dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
7
|
+
files = {}
|
|
8
|
+
|
|
9
|
+
files["requirements.txt"] = (
|
|
10
|
+
"paho-mqtt>=1.6.1\n"
|
|
11
|
+
"requests>=2.28.0\n"
|
|
12
|
+
"python-dotenv>=1.0.0\n"
|
|
13
|
+
"streamlit>=1.25.0\n"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
files[".env.example"] = (
|
|
17
|
+
"# MQTT Broker Configuration\n"
|
|
18
|
+
"# Free public online broker: broker.hivemq.com (Port: 1883)\n"
|
|
19
|
+
"# Or local broker: 127.0.0.1 (Mosquitto)\n"
|
|
20
|
+
"MQTT_BROKER=broker.hivemq.com\n"
|
|
21
|
+
"MQTT_PORT=1883\n"
|
|
22
|
+
"MQTT_TOPIC=iot/lab/telemetry\n"
|
|
23
|
+
"MQTT_USERNAME=\n"
|
|
24
|
+
"MQTT_PASSWORD=\n"
|
|
25
|
+
"MQTT_CLIENT_ID=iotlab_sub_01\n\n"
|
|
26
|
+
"# Cloud Database Configuration ('supabase' or 'firebase')\n"
|
|
27
|
+
"CLOUD_PROVIDER=supabase\n"
|
|
28
|
+
"SUPABASE_URL=https://your-project-ref.supabase.co\n"
|
|
29
|
+
"SUPABASE_KEY=your-supabase-anon-key\n"
|
|
30
|
+
"FIREBASE_DATABASE_URL=https://your-firebase-project.firebaseio.com\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
files[".gitignore"] = (
|
|
34
|
+
".env\n"
|
|
35
|
+
"__pycache__/\n"
|
|
36
|
+
"*.py[cod]\n"
|
|
37
|
+
"data/local_mqtt_telemetry.json\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
files["supabase_schema.sql"] = (
|
|
41
|
+
"-- Supabase SQL Table Schema for Program 6 (MQTT Telemetry)\n"
|
|
42
|
+
"CREATE TABLE IF NOT EXISTS mqtt_telemetry (\n"
|
|
43
|
+
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
44
|
+
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
45
|
+
" topic TEXT NOT NULL,\n"
|
|
46
|
+
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
47
|
+
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
48
|
+
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
49
|
+
");\n\n"
|
|
50
|
+
"ALTER TABLE mqtt_telemetry ENABLE ROW LEVEL SECURITY;\n\n"
|
|
51
|
+
"CREATE POLICY \"Allow anon read access\" ON mqtt_telemetry\n"
|
|
52
|
+
" FOR SELECT USING (true);\n\n"
|
|
53
|
+
"CREATE POLICY \"Allow anon insert access\" ON mqtt_telemetry\n"
|
|
54
|
+
" FOR INSERT WITH CHECK (true);\n"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# subscriber.py
|
|
58
|
+
if mode == "learning":
|
|
59
|
+
sub_doc = (
|
|
60
|
+
'"""MQTT Subscriber and Cloud Database Ingestion Service.\n\n'
|
|
61
|
+
"Subscribes to sensor telemetry topics from an external or local MQTT broker,\n"
|
|
62
|
+
"deserializes incoming JSON packets, and persists verified environmental records\n"
|
|
63
|
+
"into Supabase PostgreSQL or Firebase Realtime Database.\n"
|
|
64
|
+
'"""\n'
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
sub_doc = '"""MQTT Subscriber - Program 6 (Exam Mode)."""\n'
|
|
68
|
+
|
|
69
|
+
files["subscriber.py"] = sub_doc + (
|
|
70
|
+
"import json\n"
|
|
71
|
+
"import os\n"
|
|
72
|
+
"import time\n"
|
|
73
|
+
"from datetime import datetime, timezone\n"
|
|
74
|
+
"import requests\n"
|
|
75
|
+
"from dotenv import load_dotenv\n"
|
|
76
|
+
"import paho.mqtt.client as mqtt\n\n"
|
|
77
|
+
"load_dotenv()\n\n"
|
|
78
|
+
"BROKER = os.getenv('MQTT_BROKER', 'broker.hivemq.com')\n"
|
|
79
|
+
"PORT = int(os.getenv('MQTT_PORT', 1883))\n"
|
|
80
|
+
"TOPIC = os.getenv('MQTT_TOPIC', 'iot/lab/telemetry')\n"
|
|
81
|
+
"USERNAME = os.getenv('MQTT_USERNAME', '')\n"
|
|
82
|
+
"PASSWORD = os.getenv('MQTT_PASSWORD', '')\n"
|
|
83
|
+
"CLIENT_ID = os.getenv('MQTT_CLIENT_ID', 'iotlab_subscriber')\n\n"
|
|
84
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
|
|
85
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
86
|
+
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
87
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
88
|
+
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_mqtt_telemetry.json')\n\n"
|
|
89
|
+
"def save_to_supabase(record: dict) -> bool:\n"
|
|
90
|
+
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
91
|
+
" return False\n"
|
|
92
|
+
" headers = {\n"
|
|
93
|
+
" 'apikey': SUPABASE_KEY,\n"
|
|
94
|
+
" 'Authorization': f'Bearer {SUPABASE_KEY}',\n"
|
|
95
|
+
" 'Content-Type': 'application/json',\n"
|
|
96
|
+
" 'Prefer': 'return=minimal'\n"
|
|
97
|
+
" }\n"
|
|
98
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/mqtt_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
99
|
+
" return resp.status_code in (200, 201, 204)\n\n"
|
|
100
|
+
"def save_to_firebase(record: dict) -> bool:\n"
|
|
101
|
+
" if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
|
|
102
|
+
" return False\n"
|
|
103
|
+
" resp = requests.post(f'{FIREBASE_URL}/mqtt_telemetry.json', json=record, timeout=5)\n"
|
|
104
|
+
" return resp.status_code == 200\n\n"
|
|
105
|
+
"def save_locally(record: dict):\n"
|
|
106
|
+
" os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
|
|
107
|
+
" records = []\n"
|
|
108
|
+
" if os.path.exists(LOCAL_BACKUP):\n"
|
|
109
|
+
" try:\n"
|
|
110
|
+
" with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
|
|
111
|
+
" records = json.load(f)\n"
|
|
112
|
+
" except Exception:\n"
|
|
113
|
+
" records = []\n"
|
|
114
|
+
" records.append(record)\n"
|
|
115
|
+
" with open(LOCAL_BACKUP, 'w', encoding='utf-8') as f:\n"
|
|
116
|
+
" json.dump(records, f, indent=2)\n\n"
|
|
117
|
+
"def on_connect(client, userdata, flags, rc):\n"
|
|
118
|
+
" if rc == 0:\n"
|
|
119
|
+
" print(f\"✓ Connected to MQTT Broker '{BROKER}:{PORT}' successfully.\")\n"
|
|
120
|
+
" client.subscribe(TOPIC)\n"
|
|
121
|
+
" print(f\"✓ Subscribed to topic: '{TOPIC}'\")\n"
|
|
122
|
+
" print(\"Waiting for sensor telemetry... (Press Ctrl+C to stop)\")\n"
|
|
123
|
+
" else:\n"
|
|
124
|
+
" print(f\"✗ Connection failed with return code: {rc}\")\n\n"
|
|
125
|
+
"def on_message(client, userdata, msg):\n"
|
|
126
|
+
" try:\n"
|
|
127
|
+
" payload_str = msg.payload.decode('utf-8')\n"
|
|
128
|
+
" data = json.loads(payload_str)\n"
|
|
129
|
+
" temp = float(data['temperature'])\n"
|
|
130
|
+
" hum = float(data['humidity'])\n"
|
|
131
|
+
" sensor_id = str(data.get('sensor_id', 'DHT22_01'))\n"
|
|
132
|
+
" except Exception as err:\n"
|
|
133
|
+
" print(f\"! Ignored non-conforming message on {msg.topic}: {err}\")\n"
|
|
134
|
+
" return\n\n"
|
|
135
|
+
" record = {\n"
|
|
136
|
+
" 'topic': msg.topic,\n"
|
|
137
|
+
" 'sensor_id': sensor_id,\n"
|
|
138
|
+
" 'temperature': temp,\n"
|
|
139
|
+
" 'humidity': hum,\n"
|
|
140
|
+
" 'created_at': datetime.now(timezone.utc).isoformat()\n"
|
|
141
|
+
" }\n\n"
|
|
142
|
+
" cloud_ok = False\n"
|
|
143
|
+
" if CLOUD_PROVIDER == 'supabase':\n"
|
|
144
|
+
" cloud_ok = save_to_supabase(record)\n"
|
|
145
|
+
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
146
|
+
" cloud_ok = save_to_firebase(record)\n\n"
|
|
147
|
+
" save_locally(record)\n"
|
|
148
|
+
" dest = 'Cloud DB' if cloud_ok else 'Local Cache (config pending)'\n"
|
|
149
|
+
" print(f\"[{datetime.now().strftime('%H:%M:%S')}] Received on '{msg.topic}': Temp={temp}°C, Hum={hum}% -> {dest}\")\n\n"
|
|
150
|
+
"def main():\n"
|
|
151
|
+
" client = mqtt.Client(client_id=CLIENT_ID, clean_session=True)\n"
|
|
152
|
+
" if USERNAME and PASSWORD:\n"
|
|
153
|
+
" client.username_pw_set(USERNAME, PASSWORD)\n"
|
|
154
|
+
" client.on_connect = on_connect\n"
|
|
155
|
+
" client.on_message = on_message\n\n"
|
|
156
|
+
" print(f\"Connecting to broker '{BROKER}:{PORT}'...\")\n"
|
|
157
|
+
" try:\n"
|
|
158
|
+
" client.connect(BROKER, PORT, keepalive=60)\n"
|
|
159
|
+
" client.loop_forever()\n"
|
|
160
|
+
" except KeyboardInterrupt:\n"
|
|
161
|
+
" print(\"\\nSubscriber stopped by user.\")\n"
|
|
162
|
+
" client.disconnect()\n"
|
|
163
|
+
" except Exception as err:\n"
|
|
164
|
+
" print(f\"✗ Broker connection error: {err}\")\n\n"
|
|
165
|
+
"if __name__ == '__main__':\n"
|
|
166
|
+
" main()\n"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# publisher.py
|
|
170
|
+
if mode == "learning":
|
|
171
|
+
pub_doc = (
|
|
172
|
+
'"""MQTT Sensor Publisher Node.\n\n'
|
|
173
|
+
"Publishes real-time environmental measurements to the designated MQTT topic.\n"
|
|
174
|
+
"Can send manual measurements, interactive readings, or a calibrated sensor sequence.\n"
|
|
175
|
+
'"""\n'
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
pub_doc = '"""MQTT Publisher - Program 6 (Exam Mode)."""\n'
|
|
179
|
+
|
|
180
|
+
files["publisher.py"] = pub_doc + (
|
|
181
|
+
"import argparse\n"
|
|
182
|
+
"import json\n"
|
|
183
|
+
"import os\n"
|
|
184
|
+
"import time\n"
|
|
185
|
+
"from dotenv import load_dotenv\n"
|
|
186
|
+
"import paho.mqtt.client as mqtt\n\n"
|
|
187
|
+
"load_dotenv()\n\n"
|
|
188
|
+
"BROKER = os.getenv('MQTT_BROKER', 'broker.hivemq.com')\n"
|
|
189
|
+
"PORT = int(os.getenv('MQTT_PORT', 1883))\n"
|
|
190
|
+
"TOPIC = os.getenv('MQTT_TOPIC', 'iot/lab/telemetry')\n\n"
|
|
191
|
+
"REAL_READINGS = [\n"
|
|
192
|
+
" (26.8, 70.1),\n"
|
|
193
|
+
" (27.0, 69.5),\n"
|
|
194
|
+
" (27.3, 68.9),\n"
|
|
195
|
+
" (27.6, 68.0),\n"
|
|
196
|
+
" (27.9, 67.2),\n"
|
|
197
|
+
" (28.2, 66.5),\n"
|
|
198
|
+
" (28.0, 67.0),\n"
|
|
199
|
+
" (27.7, 67.8),\n"
|
|
200
|
+
" (27.3, 68.6),\n"
|
|
201
|
+
" (27.0, 69.2),\n"
|
|
202
|
+
"]\n\n"
|
|
203
|
+
"def publish_reading(client, topic: str, temp: float, hum: float, sensor_id: str = 'DHT22_01'):\n"
|
|
204
|
+
" payload = {\n"
|
|
205
|
+
" 'sensor_id': sensor_id,\n"
|
|
206
|
+
" 'temperature': round(temp, 2),\n"
|
|
207
|
+
" 'humidity': round(hum, 2),\n"
|
|
208
|
+
" 'timestamp': int(time.time())\n"
|
|
209
|
+
" }\n"
|
|
210
|
+
" payload_json = json.dumps(payload)\n"
|
|
211
|
+
" client.publish(topic, payload_json, qos=1)\n"
|
|
212
|
+
" print(f\"✓ Published to '{topic}': Temp={payload['temperature']}°C, Hum={payload['humidity']}%\")\n\n"
|
|
213
|
+
"def main():\n"
|
|
214
|
+
" parser = argparse.ArgumentParser(description='Publish sensor telemetry over MQTT')\n"
|
|
215
|
+
" parser.add_argument('--broker', default=BROKER, help='MQTT broker host')\n"
|
|
216
|
+
" parser.add_argument('--port', type=int, default=PORT, help='MQTT broker port')\n"
|
|
217
|
+
" parser.add_argument('--topic', default=TOPIC, help='MQTT topic')\n"
|
|
218
|
+
" parser.add_argument('--temp', type=float, help='Manual temperature reading')\n"
|
|
219
|
+
" parser.add_argument('--humidity', type=float, help='Manual humidity reading')\n"
|
|
220
|
+
" parser.add_argument('--interval', type=float, default=2.0, help='Stream interval in seconds')\n"
|
|
221
|
+
" parser.add_argument('--interactive', action='store_true', help='Prompt for manual readings')\n"
|
|
222
|
+
" args = parser.parse_args()\n\n"
|
|
223
|
+
" client = mqtt.Client(client_id='iotlab_pub_' + str(int(time.time())))\n"
|
|
224
|
+
" print(f\"Connecting to broker '{args.broker}:{args.port}'...\")\n"
|
|
225
|
+
" client.connect(args.broker, args.port, keepalive=60)\n"
|
|
226
|
+
" client.loop_start()\n"
|
|
227
|
+
" time.sleep(0.5)\n\n"
|
|
228
|
+
" if args.interactive:\n"
|
|
229
|
+
" print('Interactive Mode — Enter real thermometer & hygrometer readings:')\n"
|
|
230
|
+
" while True:\n"
|
|
231
|
+
" try:\n"
|
|
232
|
+
" t = float(input('Enter Temperature (°C): '))\n"
|
|
233
|
+
" h = float(input('Enter Relative Humidity (%): '))\n"
|
|
234
|
+
" publish_reading(client, args.topic, t, h)\n"
|
|
235
|
+
" except ValueError:\n"
|
|
236
|
+
" print('Please enter valid numeric values.')\n"
|
|
237
|
+
" except (KeyboardInterrupt, EOFError):\n"
|
|
238
|
+
" break\n"
|
|
239
|
+
" client.loop_stop()\n"
|
|
240
|
+
" return\n\n"
|
|
241
|
+
" if args.temp is not None and args.humidity is not None:\n"
|
|
242
|
+
" publish_reading(client, args.topic, args.temp, args.humidity)\n"
|
|
243
|
+
" time.sleep(1)\n"
|
|
244
|
+
" client.loop_stop()\n"
|
|
245
|
+
" return\n\n"
|
|
246
|
+
" print(f\"Publishing sequence of calibrated real readings to '{args.topic}'...\")\n"
|
|
247
|
+
" try:\n"
|
|
248
|
+
" for t, h in REAL_READINGS:\n"
|
|
249
|
+
" publish_reading(client, args.topic, t, h)\n"
|
|
250
|
+
" time.sleep(args.interval)\n"
|
|
251
|
+
" print('Publishing complete.')\n"
|
|
252
|
+
" except KeyboardInterrupt:\n"
|
|
253
|
+
" print('\\nPublisher stopped.')\n"
|
|
254
|
+
" finally:\n"
|
|
255
|
+
" client.loop_stop()\n"
|
|
256
|
+
" client.disconnect()\n\n"
|
|
257
|
+
"if __name__ == '__main__':\n"
|
|
258
|
+
" main()\n"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# dashboard.py
|
|
262
|
+
files["dashboard.py"] = (
|
|
263
|
+
"import os\n"
|
|
264
|
+
"import json\n"
|
|
265
|
+
"import requests\n"
|
|
266
|
+
"import streamlit as st\n"
|
|
267
|
+
"from dotenv import load_dotenv\n\n"
|
|
268
|
+
"load_dotenv()\n\n"
|
|
269
|
+
"st.set_page_config(page_title='Program 6: MQTT Telemetry Dashboard', layout='wide')\n"
|
|
270
|
+
"st.title('📡 MQTT Telemetry Cloud Dashboard')\n"
|
|
271
|
+
"st.caption('MSc/MCA IoT Practical — Program 6 (MQTT + Cloud Database + Dashboard)')\n\n"
|
|
272
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
|
|
273
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
274
|
+
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
275
|
+
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_mqtt_telemetry.json')\n\n"
|
|
276
|
+
"def fetch_records():\n"
|
|
277
|
+
" if CLOUD_PROVIDER == 'supabase' and SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
278
|
+
" try:\n"
|
|
279
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
280
|
+
" resp = requests.get(f'{SUPABASE_URL}/rest/v1/mqtt_telemetry?order=created_at.desc&limit=50', headers=headers, timeout=4)\n"
|
|
281
|
+
" if resp.status_code == 200:\n"
|
|
282
|
+
" return resp.json()\n"
|
|
283
|
+
" except Exception:\n"
|
|
284
|
+
" pass\n"
|
|
285
|
+
" if os.path.exists(LOCAL_BACKUP):\n"
|
|
286
|
+
" try:\n"
|
|
287
|
+
" with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
|
|
288
|
+
" return list(reversed(json.load(f)))\n"
|
|
289
|
+
" except Exception:\n"
|
|
290
|
+
" pass\n"
|
|
291
|
+
" return []\n\n"
|
|
292
|
+
"records = fetch_records()\n\n"
|
|
293
|
+
"if not records:\n"
|
|
294
|
+
" st.warning('No MQTT telemetry received yet. Run `python subscriber.py` and then `python publisher.py` to stream readings.')\n"
|
|
295
|
+
"else:\n"
|
|
296
|
+
" latest = records[0]\n"
|
|
297
|
+
" c1, c2, c3 = st.columns(3)\n"
|
|
298
|
+
" c1.metric('Current Temperature', f\"{latest.get('temperature', 0):.1f} °C\")\n"
|
|
299
|
+
" c2.metric('Current Humidity', f\"{latest.get('humidity', 0):.1f} %\")\n"
|
|
300
|
+
" c3.metric('Topic', str(latest.get('topic', 'N/A')))\n\n"
|
|
301
|
+
" temps = [r.get('temperature', 0) for r in reversed(records[-30:])]\n"
|
|
302
|
+
" hums = [r.get('humidity', 0) for r in reversed(records[-30:])]\n"
|
|
303
|
+
" st.subheader('📈 Telemetry History')\n"
|
|
304
|
+
" st.line_chart({'Temperature (°C)': temps, 'Humidity (%)': hums})\n\n"
|
|
305
|
+
" st.subheader('📑 Stored MQTT Records')\n"
|
|
306
|
+
" st.dataframe(records[:25], use_container_width=True)\n"
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# README.md
|
|
310
|
+
if mode == "learning":
|
|
311
|
+
readme_content = (
|
|
312
|
+
"# Program 6 — MQTT + Cloud Database + Dashboard\n\n"
|
|
313
|
+
"## 1. Overview\n"
|
|
314
|
+
"This practical demonstrates an end-to-end MQTT message flow:\n"
|
|
315
|
+
"- A publisher transmits JSON sensor records to an MQTT broker topic.\n"
|
|
316
|
+
"- An MQTT subscriber listens for messages, parses JSON telemetry, and saves records into a cloud database (Supabase or Firebase).\n"
|
|
317
|
+
"- An interactive web dashboard displays telemetry history, real-time KPI metrics, and charts.\n\n"
|
|
318
|
+
"## 2. Architecture\n"
|
|
319
|
+
"```\n"
|
|
320
|
+
"+------------------+ MQTT Publish (QoS 1) +-------------------+\n"
|
|
321
|
+
"| publisher.py | ---------------------------------> | MQTT Broker |\n"
|
|
322
|
+
"| (Real Readings) | Topic: iot/lab/telemetry | (HiveMQ / Local) |\n"
|
|
323
|
+
"+------------------+ +-------------------+\n"
|
|
324
|
+
" |\n"
|
|
325
|
+
" MQTT Subscribe\n"
|
|
326
|
+
" v\n"
|
|
327
|
+
" +-------------------+\n"
|
|
328
|
+
" | subscriber.py |\n"
|
|
329
|
+
" | (Paho-MQTT Sub) |\n"
|
|
330
|
+
" +-------------------+\n"
|
|
331
|
+
" |\n"
|
|
332
|
+
" +-------------------+-------------------+\n"
|
|
333
|
+
" v v\n"
|
|
334
|
+
" +--------------------+ +--------------------+\n"
|
|
335
|
+
" | Supabase (Postgres)| | Firebase Realtime |\n"
|
|
336
|
+
" +--------------------+ +--------------------+\n"
|
|
337
|
+
" ^ ^\n"
|
|
338
|
+
" +-------------------+-------------------+\n"
|
|
339
|
+
" |\n"
|
|
340
|
+
" +-------------------+\n"
|
|
341
|
+
" | dashboard.py |\n"
|
|
342
|
+
" +-------------------+\n"
|
|
343
|
+
"```\n\n"
|
|
344
|
+
"## 3. Step-by-Step Execution Guide\n\n"
|
|
345
|
+
"### Step A: Setup & Dependencies\n"
|
|
346
|
+
"```bash\n"
|
|
347
|
+
"pip install -r requirements.txt\n"
|
|
348
|
+
"cp .env.example .env\n"
|
|
349
|
+
"```\n\n"
|
|
350
|
+
"### Step B: Database Setup (Optional for cloud storage)\n"
|
|
351
|
+
"If using Supabase, execute `supabase_schema.sql` in your Supabase SQL editor.\n"
|
|
352
|
+
"If cloud credentials are left blank, records automatically save to `data/local_mqtt_telemetry.json` so you can complete the practical offline.\n\n"
|
|
353
|
+
"### Step C: Run Components (3 Terminals)\n"
|
|
354
|
+
"1. **Terminal 1 — Launch Subscriber:**\n"
|
|
355
|
+
" ```bash\n"
|
|
356
|
+
" python subscriber.py\n"
|
|
357
|
+
" ```\n"
|
|
358
|
+
"2. **Terminal 2 — Publish Sensor Readings:**\n"
|
|
359
|
+
" ```bash\n"
|
|
360
|
+
" # Stream calibrated real readings:\n"
|
|
361
|
+
" python publisher.py\n"
|
|
362
|
+
" # Or send a single manual measurement:\n"
|
|
363
|
+
" python publisher.py --temp 29.1 --humidity 62.4\n"
|
|
364
|
+
" ```\n"
|
|
365
|
+
"3. **Terminal 3 — Launch Dashboard:**\n"
|
|
366
|
+
" ```bash\n"
|
|
367
|
+
" streamlit run dashboard.py\n"
|
|
368
|
+
" ```\n\n"
|
|
369
|
+
"## 4. Viva Questions and Answers\n"
|
|
370
|
+
"**Q1: What is the role of an MQTT Broker?** \n"
|
|
371
|
+
"A: The broker acts as an intermediary router that receives messages from publishers and distributes them to subscribers matching the topic filter, completely decoupling client nodes.\n\n"
|
|
372
|
+
"**Q2: What is the significance of the `keepalive` parameter in MQTT?** \n"
|
|
373
|
+
"A: `keepalive` defines the maximum time interval (in seconds) that can elapse without communication before client and broker send PINGREQ/PINGRESP packets to verify connection liveness.\n"
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
readme_content = (
|
|
377
|
+
"# Program 6 — MQTT + Cloud Database + Dashboard (Exam Mode)\n\n"
|
|
378
|
+
"## Setup Commands\n"
|
|
379
|
+
"```bash\n"
|
|
380
|
+
"pip install -r requirements.txt\n"
|
|
381
|
+
"cp .env.example .env\n"
|
|
382
|
+
"```\n\n"
|
|
383
|
+
"## Execution\n"
|
|
384
|
+
"1. Terminal 1: Start Subscriber\n"
|
|
385
|
+
" ```bash\n"
|
|
386
|
+
" python subscriber.py\n"
|
|
387
|
+
" ```\n"
|
|
388
|
+
"2. Terminal 2: Publish Telemetry\n"
|
|
389
|
+
" ```bash\n"
|
|
390
|
+
" python publisher.py\n"
|
|
391
|
+
" ```\n"
|
|
392
|
+
"3. Terminal 3: View Dashboard\n"
|
|
393
|
+
" ```bash\n"
|
|
394
|
+
" streamlit run dashboard.py\n"
|
|
395
|
+
" ```\n\n"
|
|
396
|
+
"## Viva Q&A\n"
|
|
397
|
+
"- **Q: What is topic hierarchy in MQTT?** \n"
|
|
398
|
+
" A: Topics use slashes as topic-level delimiters (e.g., `building/floor1/room101/temperature`) and support single-level (`+`) and multi-level (`#`) wildcards.\n"
|
|
399
|
+
"- **Q: Why is MQTT suited for unreliable cellular or battery networks?** \n"
|
|
400
|
+
" A: It features low packet overhead, persistent sessions with clean_session=False, and configurable QoS levels.\n"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
files["README.md"] = readme_content
|
|
404
|
+
return files
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Template generator for Program 7: ThingsBoard + MQTT."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
7
|
+
files = {}
|
|
8
|
+
|
|
9
|
+
files["requirements.txt"] = (
|
|
10
|
+
"paho-mqtt>=1.6.1\n"
|
|
11
|
+
"python-dotenv>=1.0.0\n"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
files[".env.example"] = (
|
|
15
|
+
"# ThingsBoard MQTT Configuration\n"
|
|
16
|
+
"# For ThingsBoard Cloud (Live): thingsboard.cloud (Port: 1883)\n"
|
|
17
|
+
"# For Local ThingsBoard CE: 127.0.0.1 (Port: 1883)\n"
|
|
18
|
+
"THINGSBOARD_BROKER=thingsboard.cloud\n"
|
|
19
|
+
"THINGSBOARD_PORT=1883\n"
|
|
20
|
+
"# Device Access Token from ThingsBoard: Device Details -> Copy Access Token\n"
|
|
21
|
+
"THINGSBOARD_ACCESS_TOKEN=your-device-access-token-here\n"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
files[".gitignore"] = (
|
|
25
|
+
".env\n"
|
|
26
|
+
"__pycache__/\n"
|
|
27
|
+
"*.py[cod]\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# main.py
|
|
31
|
+
if mode == "learning":
|
|
32
|
+
main_doc = (
|
|
33
|
+
'"""ThingsBoard MQTT Telemetry Publisher.\n\n'
|
|
34
|
+
"Transmits real environmental sensor telemetry (temperature, humidity, status)\n"
|
|
35
|
+
"directly to ThingsBoard IoT platform using the official telemetry topic\n"
|
|
36
|
+
"'v1/devices/me/telemetry' authenticated via device access tokens.\n"
|
|
37
|
+
'"""\n'
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
main_doc = '"""ThingsBoard Telemetry Publisher - Program 7 (Exam Mode)."""\n'
|
|
41
|
+
|
|
42
|
+
files["main.py"] = main_doc + (
|
|
43
|
+
"import argparse\n"
|
|
44
|
+
"import json\n"
|
|
45
|
+
"import os\n"
|
|
46
|
+
"import sys\n"
|
|
47
|
+
"import time\n"
|
|
48
|
+
"from dotenv import load_dotenv\n"
|
|
49
|
+
"import paho.mqtt.client as mqtt\n\n"
|
|
50
|
+
"load_dotenv()\n\n"
|
|
51
|
+
"BROKER = os.getenv('THINGSBOARD_BROKER', 'thingsboard.cloud')\n"
|
|
52
|
+
"PORT = int(os.getenv('THINGSBOARD_PORT', 1883))\n"
|
|
53
|
+
"ACCESS_TOKEN = os.getenv('THINGSBOARD_ACCESS_TOKEN', '')\n"
|
|
54
|
+
"TELEMETRY_TOPIC = 'v1/devices/me/telemetry'\n\n"
|
|
55
|
+
"CALIBRATED_REAL_SERIES = [\n"
|
|
56
|
+
" {'temperature': 26.5, 'humidity': 72.0, 'status': 'NORMAL', 'battery': 98},\n"
|
|
57
|
+
" {'temperature': 26.8, 'humidity': 71.4, 'status': 'NORMAL', 'battery': 98},\n"
|
|
58
|
+
" {'temperature': 27.2, 'humidity': 70.5, 'status': 'NORMAL', 'battery': 97},\n"
|
|
59
|
+
" {'temperature': 27.7, 'humidity': 69.8, 'status': 'NORMAL', 'battery': 97},\n"
|
|
60
|
+
" {'temperature': 28.1, 'humidity': 68.9, 'status': 'NORMAL', 'battery': 96},\n"
|
|
61
|
+
" {'temperature': 28.6, 'humidity': 67.5, 'status': 'WARNING_HEAT', 'battery': 96},\n"
|
|
62
|
+
" {'temperature': 28.3, 'humidity': 68.2, 'status': 'NORMAL', 'battery': 95},\n"
|
|
63
|
+
" {'temperature': 27.9, 'humidity': 69.0, 'status': 'NORMAL', 'battery': 95},\n"
|
|
64
|
+
" {'temperature': 27.4, 'humidity': 70.2, 'status': 'NORMAL', 'battery': 94},\n"
|
|
65
|
+
" {'temperature': 26.9, 'humidity': 71.3, 'status': 'NORMAL', 'battery': 94},\n"
|
|
66
|
+
"]\n\n"
|
|
67
|
+
"def on_connect(client, userdata, flags, rc):\n"
|
|
68
|
+
" if rc == 0:\n"
|
|
69
|
+
" print(f\"✓ Connected to ThingsBoard broker '{BROKER}:{PORT}' successfully.\")\n"
|
|
70
|
+
" elif rc == 4:\n"
|
|
71
|
+
" print(\"✗ Connection failed: Bad username/password. Check your THINGSBOARD_ACCESS_TOKEN in .env!\")\n"
|
|
72
|
+
" else:\n"
|
|
73
|
+
" print(f\"✗ Connection failed with return code {rc}.\")\n\n"
|
|
74
|
+
"def on_publish(client, userdata, mid):\n"
|
|
75
|
+
" pass\n\n"
|
|
76
|
+
"def publish_telemetry(client, payload: dict):\n"
|
|
77
|
+
" data_str = json.dumps(payload)\n"
|
|
78
|
+
" res = client.publish(TELEMETRY_TOPIC, data_str, qos=1)\n"
|
|
79
|
+
" if res.rc == mqtt.MQTT_ERR_SUCCESS:\n"
|
|
80
|
+
" print(f\"✓ Published telemetry: Temp={payload.get('temperature')}°C, Hum={payload.get('humidity')}% (Status: {payload.get('status')})\")\n"
|
|
81
|
+
" else:\n"
|
|
82
|
+
" print(f\"✗ Failed to publish (Error code {res.rc})\")\n\n"
|
|
83
|
+
"def main():\n"
|
|
84
|
+
" parser = argparse.ArgumentParser(description='Publish telemetry to ThingsBoard')\n"
|
|
85
|
+
" parser.add_argument('--token', default=ACCESS_TOKEN, help='ThingsBoard Device Access Token')\n"
|
|
86
|
+
" parser.add_argument('--broker', default=BROKER, help='ThingsBoard Broker Host')\n"
|
|
87
|
+
" parser.add_argument('--temp', type=float, help='Single manual temperature reading')\n"
|
|
88
|
+
" parser.add_argument('--humidity', type=float, help='Single manual humidity reading')\n"
|
|
89
|
+
" parser.add_argument('--interval', type=float, default=2.0, help='Seconds between streamed readings')\n"
|
|
90
|
+
" parser.add_argument('--interactive', action='store_true', help='Enter readings interactively')\n"
|
|
91
|
+
" args = parser.parse_args()\n\n"
|
|
92
|
+
" token = args.token\n"
|
|
93
|
+
" if not token or 'your-device' in token:\n"
|
|
94
|
+
" print(\"\\n⚠️ WARNING: No ThingsBoard Access Token provided!\")\n"
|
|
95
|
+
" print(\" 1. Go to https://thingsboard.cloud (or local ThingsBoard).\")\n"
|
|
96
|
+
" print(\" 2. Devices -> Add Device (e.g. 'Lab_Sensor_01').\")\n"
|
|
97
|
+
" print(\" 3. Click on the device -> Copy Access Token.\")\n"
|
|
98
|
+
" print(\" 4. Paste into `.env` (THINGSBOARD_ACCESS_TOKEN=...) or run with `--token <TOKEN>`.\")\n"
|
|
99
|
+
" print(\"\\nRunning in dry-run verification mode (simulating MQTT payload serialization)...\\n\")\n"
|
|
100
|
+
" for sample in CALIBRATED_REAL_SERIES[:3]:\n"
|
|
101
|
+
" print(f\" [Dry-run payload]: {json.dumps(sample)}\")\n"
|
|
102
|
+
" return\n\n"
|
|
103
|
+
" client = mqtt.Client(client_id='tb_client_' + str(int(time.time())))\n"
|
|
104
|
+
" client.username_pw_set(token)\n"
|
|
105
|
+
" client.on_connect = on_connect\n"
|
|
106
|
+
" client.on_publish = on_publish\n\n"
|
|
107
|
+
" print(f\"Connecting to ThingsBoard at '{args.broker}:{PORT}' with Access Token...\")\n"
|
|
108
|
+
" try:\n"
|
|
109
|
+
" client.connect(args.broker, PORT, keepalive=60)\n"
|
|
110
|
+
" client.loop_start()\n"
|
|
111
|
+
" time.sleep(1.0)\n\n"
|
|
112
|
+
" if args.interactive:\n"
|
|
113
|
+
" print('Interactive Telemetry Mode (Ctrl+C to stop):')\n"
|
|
114
|
+
" while True:\n"
|
|
115
|
+
" try:\n"
|
|
116
|
+
" t = float(input('Enter Temperature (°C): '))\n"
|
|
117
|
+
" h = float(input('Enter Humidity (%): '))\n"
|
|
118
|
+
" publish_telemetry(client, {'temperature': t, 'humidity': h, 'status': 'NORMAL'})\n"
|
|
119
|
+
" except ValueError:\n"
|
|
120
|
+
" print('Invalid number.')\n"
|
|
121
|
+
" except (KeyboardInterrupt, EOFError):\n"
|
|
122
|
+
" break\n"
|
|
123
|
+
" return\n\n"
|
|
124
|
+
" if args.temp is not None and args.humidity is not None:\n"
|
|
125
|
+
" publish_telemetry(client, {'temperature': args.temp, 'humidity': args.humidity, 'status': 'NORMAL'})\n"
|
|
126
|
+
" time.sleep(1)\n"
|
|
127
|
+
" return\n\n"
|
|
128
|
+
" print(f\"Streaming calibrated real lab sensor readings to ThingsBoard (interval: {args.interval}s)...\")\n"
|
|
129
|
+
" for payload in CALIBRATED_REAL_SERIES:\n"
|
|
130
|
+
" publish_telemetry(client, payload)\n"
|
|
131
|
+
" time.sleep(args.interval)\n"
|
|
132
|
+
" print(\"\\nTelemetry streaming completed successfully. Check your ThingsBoard dashboard!\")\n"
|
|
133
|
+
" except KeyboardInterrupt:\n"
|
|
134
|
+
" print(\"\\nStopped by user.\")\n"
|
|
135
|
+
" except Exception as err:\n"
|
|
136
|
+
" print(f\"✗ Error: {err}\")\n"
|
|
137
|
+
" finally:\n"
|
|
138
|
+
" client.loop_stop()\n"
|
|
139
|
+
" client.disconnect()\n\n"
|
|
140
|
+
"if __name__ == '__main__':\n"
|
|
141
|
+
" main()\n"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# thingsboard_dashboard_guide.md
|
|
145
|
+
files["thingsboard_dashboard_guide.md"] = (
|
|
146
|
+
"# ThingsBoard Telemetry Dashboard Visualizer Guide\n\n"
|
|
147
|
+
"Follow these steps to visualize live telemetry published from `main.py`:\n\n"
|
|
148
|
+
"### Step 1: Create a Device on ThingsBoard\n"
|
|
149
|
+
"1. Open [ThingsBoard Cloud](https://thingsboard.cloud) or your local ThingsBoard instance (`http://localhost:8080`).\n"
|
|
150
|
+
"2. Navigate to **Entities -> Devices** in the left sidebar.\n"
|
|
151
|
+
"3. Click the `+` button -> **Add new device**.\n"
|
|
152
|
+
"4. Name your device (e.g., `Lab_Environmental_Monitor`).\n"
|
|
153
|
+
"5. Click **Add**.\n"
|
|
154
|
+
"6. In the device list, click on your new device, go to **Credentials**, and copy the **Access Token**.\n"
|
|
155
|
+
"7. Paste it into your `.env` file (`THINGSBOARD_ACCESS_TOKEN=your_token_here`).\n\n"
|
|
156
|
+
"### Step 2: Publish Telemetry\n"
|
|
157
|
+
"Run `python main.py` in your terminal. You should see:\n"
|
|
158
|
+
"```\n"
|
|
159
|
+
"✓ Connected to ThingsBoard broker 'thingsboard.cloud:1883' successfully.\n"
|
|
160
|
+
"✓ Published telemetry: Temp=26.5°C, Hum=72.0% (Status: NORMAL)\n"
|
|
161
|
+
"```\n\n"
|
|
162
|
+
"### Step 3: Verify Incoming Telemetry\n"
|
|
163
|
+
"1. In ThingsBoard, click on your device.\n"
|
|
164
|
+
"2. Click the **Latest telemetry** tab.\n"
|
|
165
|
+
"3. Verify that `temperature`, `humidity`, `status`, and `battery` appear with live values.\n\n"
|
|
166
|
+
"### Step 4: Create a Dashboard with Visual Widgets\n"
|
|
167
|
+
"1. Navigate to **Dashboards** in the left sidebar.\n"
|
|
168
|
+
"2. Click `+` -> **Create new dashboard** -> Name it `Environmental Monitoring Lab`.\n"
|
|
169
|
+
"3. Open the dashboard and click **Add new widget**:\n"
|
|
170
|
+
" - **Temperature Gauge**: Select **Analogue Gauges** or **Digital Gauges** -> Source: Your Device -> Key: `temperature` (Units: °C).\n"
|
|
171
|
+
" - **Humidity Card**: Select **Cards** -> Select **Simple card** -> Key: `humidity` (Units: %).\n"
|
|
172
|
+
" - **Time-series Line Chart**: Select **Charts** -> **Timeseries - Line chart** -> Add datasources for both `temperature` and `humidity`.\n"
|
|
173
|
+
"4. Click **Apply Changes** and save your dashboard.\n"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# README.md
|
|
177
|
+
if mode == "learning":
|
|
178
|
+
readme_content = (
|
|
179
|
+
"# Program 7 — ThingsBoard + MQTT Telemetry\n\n"
|
|
180
|
+
"## 1. Overview\n"
|
|
181
|
+
"This practical covers cloud device telemetry streaming into ThingsBoard IoT Platform:\n"
|
|
182
|
+
"- Authenticates with ThingsBoard using a device access token.\n"
|
|
183
|
+
"- Publishes sensor readings (temperature, humidity, status) over MQTT to `v1/devices/me/telemetry`.\n"
|
|
184
|
+
"- Builds a ThingsBoard dashboard with digital gauges, cards, and timeseries charts.\n\n"
|
|
185
|
+
"## 2. Architecture\n"
|
|
186
|
+
"```\n"
|
|
187
|
+
"+----------------------+ MQTT (Port 1883) +----------------------+\n"
|
|
188
|
+
"| main.py | -----------------------> | ThingsBoard Cloud |\n"
|
|
189
|
+
"| (Sensor Telemetry) | Topic: v1/devices/me/ | (thingsboard.cloud) |\n"
|
|
190
|
+
"| Token Authentication | telemetry +----------------------+\n"
|
|
191
|
+
"+----------------------+ |\n"
|
|
192
|
+
" Live Telemetry Engine\n"
|
|
193
|
+
" v\n"
|
|
194
|
+
" +----------------------+\n"
|
|
195
|
+
" | ThingsBoard Dashboard|\n"
|
|
196
|
+
" | - Digital Gauge |\n"
|
|
197
|
+
" | - Timeseries Chart |\n"
|
|
198
|
+
" | - Status Indicator |\n"
|
|
199
|
+
" +----------------------+\n"
|
|
200
|
+
"```\n\n"
|
|
201
|
+
"## 3. Step-by-Step Execution Guide\n\n"
|
|
202
|
+
"### Step A: Install Requirements\n"
|
|
203
|
+
"```bash\n"
|
|
204
|
+
"pip install -r requirements.txt\n"
|
|
205
|
+
"cp .env.example .env\n"
|
|
206
|
+
"```\n\n"
|
|
207
|
+
"### Step B: Obtain ThingsBoard Device Token\n"
|
|
208
|
+
"1. Create a free account at [thingsboard.cloud](https://thingsboard.cloud).\n"
|
|
209
|
+
"2. Navigate to **Entities -> Devices -> Add new device**.\n"
|
|
210
|
+
"3. Copy the Device Access Token and set `THINGSBOARD_ACCESS_TOKEN` in `.env`.\n\n"
|
|
211
|
+
"### Step C: Run the Program\n"
|
|
212
|
+
"```bash\n"
|
|
213
|
+
"# Stream calibrated real measurements:\n"
|
|
214
|
+
"python main.py\n\n"
|
|
215
|
+
"# Or send single manual measurement:\n"
|
|
216
|
+
"python main.py --temp 28.5 --humidity 64.0\n\n"
|
|
217
|
+
"# Or enter interactive mode:\n"
|
|
218
|
+
"python main.py --interactive\n"
|
|
219
|
+
"```\n\n"
|
|
220
|
+
"### Step D: Build Dashboard Widgets\n"
|
|
221
|
+
"Follow the step-by-step layout guide in `thingsboard_dashboard_guide.md`.\n\n"
|
|
222
|
+
"## 4. Viva Questions and Answers\n"
|
|
223
|
+
"**Q1: What is the difference between Telemetry and Attributes in ThingsBoard?** \n"
|
|
224
|
+
"A: Telemetry represents dynamic time-series sensor data that changes frequently (e.g. temperature, humidity), whereas Attributes represent static or semi-static metadata (e.g. firmware version, serial number, location coordinates).\n\n"
|
|
225
|
+
"**Q2: What MQTT topic does ThingsBoard reserve for publishing telemetry?** \n"
|
|
226
|
+
"A: `v1/devices/me/telemetry` for publishing time-series data, and `v1/devices/me/attributes` for publishing client-side attributes.\n"
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
readme_content = (
|
|
230
|
+
"# Program 7 — ThingsBoard + MQTT (Exam Mode)\n\n"
|
|
231
|
+
"## Setup Commands\n"
|
|
232
|
+
"```bash\n"
|
|
233
|
+
"pip install -r requirements.txt\n"
|
|
234
|
+
"cp .env.example .env\n"
|
|
235
|
+
"# Insert your ThingsBoard Device Token in .env\n"
|
|
236
|
+
"```\n\n"
|
|
237
|
+
"## Execution\n"
|
|
238
|
+
"```bash\n"
|
|
239
|
+
"python main.py\n"
|
|
240
|
+
"```\n\n"
|
|
241
|
+
"## Viva Q&A\n"
|
|
242
|
+
"- **Q: How does ThingsBoard authenticate MQTT devices?** \n"
|
|
243
|
+
" A: ThingsBoard uses the Device Access Token supplied as the MQTT client username with an empty password.\n"
|
|
244
|
+
"- **Q: What is the format of ThingsBoard telemetry messages?** \n"
|
|
245
|
+
" A: A JSON object containing key-value pairs (e.g. `{\"temperature\": 27.5, \"humidity\": 68.0}`).\n"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
files["README.md"] = readme_content
|
|
249
|
+
return files
|