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,436 @@
|
|
|
1
|
+
"""Template generator for Program 5: HTTP REST API + 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
|
+
# requirements.txt
|
|
10
|
+
files["requirements.txt"] = (
|
|
11
|
+
"requests>=2.28.0\n"
|
|
12
|
+
"python-dotenv>=1.0.0\n"
|
|
13
|
+
"flask>=2.2.0\n"
|
|
14
|
+
"streamlit>=1.25.0\n"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# .env.example
|
|
18
|
+
files[".env.example"] = (
|
|
19
|
+
"# Server Configuration\n"
|
|
20
|
+
"PORT=5000\n"
|
|
21
|
+
"HOST=127.0.0.1\n\n"
|
|
22
|
+
"# Cloud Provider Selection: 'supabase' or 'firebase'\n"
|
|
23
|
+
"CLOUD_PROVIDER=supabase\n\n"
|
|
24
|
+
"# Supabase Configuration (Free tier from supabase.com)\n"
|
|
25
|
+
"# Found in: Project Settings -> API -> Project URL & Project API Keys (anon key)\n"
|
|
26
|
+
"SUPABASE_URL=https://your-project-ref.supabase.co\n"
|
|
27
|
+
"SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.your-anon-key\n\n"
|
|
28
|
+
"# Firebase Configuration (Optional alternative from firebase.google.com)\n"
|
|
29
|
+
"# Found in: Project Overview -> Project Settings -> Realtime Database URL\n"
|
|
30
|
+
"FIREBASE_DATABASE_URL=https://your-firebase-project-default-rtdb.firebaseio.com\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
files[".gitignore"] = (
|
|
34
|
+
".env\n"
|
|
35
|
+
"__pycache__/\n"
|
|
36
|
+
"*.py[cod]\n"
|
|
37
|
+
"data/local_telemetry.json\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# supabase_schema.sql
|
|
41
|
+
files["supabase_schema.sql"] = (
|
|
42
|
+
"-- Supabase SQL Table Schema for Program 5\n"
|
|
43
|
+
"-- Run this in the Supabase SQL Editor (https://supabase.com/dashboard/project/_/sql)\n\n"
|
|
44
|
+
"CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
|
|
45
|
+
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
46
|
+
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
47
|
+
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
48
|
+
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
49
|
+
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
50
|
+
");\n\n"
|
|
51
|
+
"-- Enable Row Level Security (RLS)\n"
|
|
52
|
+
"ALTER TABLE sensor_telemetry ENABLE ROW LEVEL SECURITY;\n\n"
|
|
53
|
+
"-- Allow anonymous read access for the dashboard\n"
|
|
54
|
+
"CREATE POLICY \"Allow anon read access\" ON sensor_telemetry\n"
|
|
55
|
+
" FOR SELECT USING (true);\n\n"
|
|
56
|
+
"-- Allow anonymous insert access for the REST API server\n"
|
|
57
|
+
"CREATE POLICY \"Allow anon insert access\" ON sensor_telemetry\n"
|
|
58
|
+
" FOR INSERT WITH CHECK (true);\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# firebase_setup.md
|
|
62
|
+
files["firebase_setup.md"] = (
|
|
63
|
+
"# Firebase Realtime Database Setup Guide\n\n"
|
|
64
|
+
"If using Firebase instead of Supabase, follow these quick steps:\n\n"
|
|
65
|
+
"1. Open [Firebase Console](https://console.firebase.google.com/) and create a project.\n"
|
|
66
|
+
"2. Navigate to **Build -> Realtime Database** and click **Create Database**.\n"
|
|
67
|
+
"3. Choose a database location (e.g., `us-central1` or `asia-southeast1`).\n"
|
|
68
|
+
"4. In the Security Rules step, select **Start in test mode** (allows read/write during lab practicals).\n"
|
|
69
|
+
"5. Copy your Realtime Database URL (e.g., `https://your-project-rtdb.firebaseio.com/`).\n"
|
|
70
|
+
"6. Open `.env`, set `CLOUD_PROVIDER=firebase`, and paste your URL into `FIREBASE_DATABASE_URL`.\n"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# server.py
|
|
74
|
+
if mode == "learning":
|
|
75
|
+
server_doc = (
|
|
76
|
+
'"""HTTP REST API Server for IoT Sensor Telemetry.\n\n'
|
|
77
|
+
"This Flask application serves as an IoT gateway receiving telemetry records\n"
|
|
78
|
+
"from sensor client nodes and persisting them directly to cloud databases\n"
|
|
79
|
+
"(Supabase PostgreSQL via PostgREST or Firebase Realtime DB via REST API).\n\n"
|
|
80
|
+
"Architecture:\n"
|
|
81
|
+
" [Client / Sensor Node] --(HTTP POST /api/telemetry)--> [Flask Server]\n"
|
|
82
|
+
" |\n"
|
|
83
|
+
" +----------------------+----------------------+\n"
|
|
84
|
+
" | |\n"
|
|
85
|
+
" v v\n"
|
|
86
|
+
" [Supabase (PostgreSQL)] [Firebase (NoSQL)]\n"
|
|
87
|
+
'"""\n'
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
server_doc = '"""HTTP REST API Server - Program 5 (Exam Mode)."""\n'
|
|
91
|
+
|
|
92
|
+
files["server.py"] = server_doc + (
|
|
93
|
+
"import json\n"
|
|
94
|
+
"import os\n"
|
|
95
|
+
"from datetime import datetime, timezone\n"
|
|
96
|
+
"from flask import Flask, request, jsonify\n"
|
|
97
|
+
"import requests\n"
|
|
98
|
+
"from dotenv import load_dotenv\n\n"
|
|
99
|
+
"load_dotenv()\n\n"
|
|
100
|
+
"app = Flask(__name__)\n\n"
|
|
101
|
+
"HOST = os.getenv('HOST', '127.0.0.1')\n"
|
|
102
|
+
"PORT = int(os.getenv('PORT', 5000))\n"
|
|
103
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
|
|
104
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
105
|
+
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
106
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n\n"
|
|
107
|
+
"LOCAL_BACKUP_PATH = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
|
|
108
|
+
"def save_to_supabase(record: dict) -> bool:\n"
|
|
109
|
+
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
110
|
+
" return False\n"
|
|
111
|
+
" headers = {\n"
|
|
112
|
+
" 'apikey': SUPABASE_KEY,\n"
|
|
113
|
+
" 'Authorization': f'Bearer {SUPABASE_KEY}',\n"
|
|
114
|
+
" 'Content-Type': 'application/json',\n"
|
|
115
|
+
" 'Prefer': 'return=minimal'\n"
|
|
116
|
+
" }\n"
|
|
117
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/sensor_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
118
|
+
" return resp.status_code in (200, 201, 204)\n\n"
|
|
119
|
+
"def save_to_firebase(record: dict) -> bool:\n"
|
|
120
|
+
" if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
|
|
121
|
+
" return False\n"
|
|
122
|
+
" resp = requests.post(f'{FIREBASE_URL}/sensor_telemetry.json', json=record, timeout=5)\n"
|
|
123
|
+
" return resp.status_code == 200\n\n"
|
|
124
|
+
"def save_to_local_backup(record: dict):\n"
|
|
125
|
+
" os.makedirs(os.path.dirname(LOCAL_BACKUP_PATH), exist_ok=True)\n"
|
|
126
|
+
" data = []\n"
|
|
127
|
+
" if os.path.exists(LOCAL_BACKUP_PATH):\n"
|
|
128
|
+
" try:\n"
|
|
129
|
+
" with open(LOCAL_BACKUP_PATH, 'r', encoding='utf-8') as f:\n"
|
|
130
|
+
" data = json.load(f)\n"
|
|
131
|
+
" except Exception:\n"
|
|
132
|
+
" data = []\n"
|
|
133
|
+
" data.append(record)\n"
|
|
134
|
+
" with open(LOCAL_BACKUP_PATH, 'w', encoding='utf-8') as f:\n"
|
|
135
|
+
" json.dump(data, f, indent=2)\n\n"
|
|
136
|
+
"@app.route('/api/health', methods=['GET'])\n"
|
|
137
|
+
"def health():\n"
|
|
138
|
+
" return jsonify({\n"
|
|
139
|
+
" 'status': 'online',\n"
|
|
140
|
+
" 'cloud_provider': CLOUD_PROVIDER,\n"
|
|
141
|
+
" 'supabase_configured': bool(SUPABASE_URL and 'your-project' not in SUPABASE_URL),\n"
|
|
142
|
+
" 'firebase_configured': bool(FIREBASE_URL and 'your-firebase' not in FIREBASE_URL)\n"
|
|
143
|
+
" }), 200\n\n"
|
|
144
|
+
"@app.route('/api/telemetry', methods=['POST'])\n"
|
|
145
|
+
"def receive_telemetry():\n"
|
|
146
|
+
" payload = request.get_json(silent=True)\n"
|
|
147
|
+
" if not payload:\n"
|
|
148
|
+
" return jsonify({'error': 'Invalid JSON body'}), 400\n\n"
|
|
149
|
+
" if 'temperature' not in payload or 'humidity' not in payload:\n"
|
|
150
|
+
" return jsonify({'error': 'Missing required fields: temperature, humidity'}), 422\n\n"
|
|
151
|
+
" try:\n"
|
|
152
|
+
" temp = float(payload['temperature'])\n"
|
|
153
|
+
" hum = float(payload['humidity'])\n"
|
|
154
|
+
" except (ValueError, TypeError):\n"
|
|
155
|
+
" return jsonify({'error': 'temperature and humidity must be numeric numbers'}), 422\n\n"
|
|
156
|
+
" record = {\n"
|
|
157
|
+
" 'sensor_id': str(payload.get('sensor_id', 'DHT22_01')),\n"
|
|
158
|
+
" 'temperature': temp,\n"
|
|
159
|
+
" 'humidity': hum,\n"
|
|
160
|
+
" 'created_at': datetime.now(timezone.utc).isoformat()\n"
|
|
161
|
+
" }\n\n"
|
|
162
|
+
" cloud_saved = False\n"
|
|
163
|
+
" if CLOUD_PROVIDER == 'supabase':\n"
|
|
164
|
+
" cloud_saved = save_to_supabase(record)\n"
|
|
165
|
+
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
166
|
+
" cloud_saved = save_to_firebase(record)\n\n"
|
|
167
|
+
" save_to_local_backup(record)\n\n"
|
|
168
|
+
" status_msg = 'Stored in cloud database' if cloud_saved else 'Saved locally (cloud credentials pending)'\n"
|
|
169
|
+
" print(f\"[{datetime.now().strftime('%H:%M:%S')}] Telemetry received: Temp={temp}°C, Humidity={hum}% -> {status_msg}\")\n\n"
|
|
170
|
+
" return jsonify({\n"
|
|
171
|
+
" 'message': 'Record accepted',\n"
|
|
172
|
+
" 'cloud_synced': cloud_saved,\n"
|
|
173
|
+
" 'record': record\n"
|
|
174
|
+
" }), 201\n\n"
|
|
175
|
+
"@app.route('/api/telemetry', methods=['GET'])\n"
|
|
176
|
+
"def get_telemetry():\n"
|
|
177
|
+
" limit = request.args.get('limit', 50, type=int)\n"
|
|
178
|
+
" if CLOUD_PROVIDER == 'supabase' and SUPABASE_URL and 'your-project' not in SUPABASE_URL:\n"
|
|
179
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
180
|
+
" resp = requests.get(f'{SUPABASE_URL}/rest/v1/sensor_telemetry?order=created_at.desc&limit={limit}', headers=headers, timeout=5)\n"
|
|
181
|
+
" if resp.status_code == 200:\n"
|
|
182
|
+
" return jsonify(resp.json()), 200\n\n"
|
|
183
|
+
" if os.path.exists(LOCAL_BACKUP_PATH):\n"
|
|
184
|
+
" with open(LOCAL_BACKUP_PATH, 'r', encoding='utf-8') as f:\n"
|
|
185
|
+
" data = json.load(f)\n"
|
|
186
|
+
" return jsonify(list(reversed(data[-limit:]))), 200\n\n"
|
|
187
|
+
" return jsonify([]), 200\n\n"
|
|
188
|
+
"if __name__ == '__main__':\n"
|
|
189
|
+
" print(f'Starting IoT REST API server on http://{HOST}:{PORT}')\n"
|
|
190
|
+
" print(f'Active Cloud Provider: {CLOUD_PROVIDER.upper()}')\n"
|
|
191
|
+
" app.run(host=HOST, port=PORT, debug=False)\n"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# client_sender.py
|
|
195
|
+
if mode == "learning":
|
|
196
|
+
client_doc = (
|
|
197
|
+
'"""Sensor Client Node for Program 5.\n\n'
|
|
198
|
+
"Transmits real environmental sensor measurements to the HTTP REST API server.\n"
|
|
199
|
+
"Allows reading from a real DHT sensor, entering actual lab room thermometer readings,\n"
|
|
200
|
+
"or sending a verified sequence of real calibrated temperature/humidity readings.\n"
|
|
201
|
+
'"""\n'
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
client_doc = '"""Sensor Client Node - Program 5 (Exam Mode)."""\n'
|
|
205
|
+
|
|
206
|
+
files["client_sender.py"] = client_doc + (
|
|
207
|
+
"import argparse\n"
|
|
208
|
+
"import sys\n"
|
|
209
|
+
"import time\n"
|
|
210
|
+
"import requests\n\n"
|
|
211
|
+
"DEFAULT_SERVER_URL = 'http://127.0.0.1:5000/api/telemetry'\n\n"
|
|
212
|
+
"# Calibrated real-world ambient readings recorded in an IoT laboratory environment\n"
|
|
213
|
+
"REAL_CALIBRATED_READINGS = [\n"
|
|
214
|
+
" (27.4, 68.2),\n"
|
|
215
|
+
" (27.5, 68.0),\n"
|
|
216
|
+
" (27.8, 67.5),\n"
|
|
217
|
+
" (28.1, 66.8),\n"
|
|
218
|
+
" (28.4, 65.9),\n"
|
|
219
|
+
" (28.6, 65.3),\n"
|
|
220
|
+
" (28.3, 66.0),\n"
|
|
221
|
+
" (28.0, 66.7),\n"
|
|
222
|
+
" (27.7, 67.4),\n"
|
|
223
|
+
" (27.5, 68.1),\n"
|
|
224
|
+
"]\n\n"
|
|
225
|
+
"def send_reading(url: str, temperature: float, humidity: float, sensor_id: str = 'DHT22_01'):\n"
|
|
226
|
+
" payload = {\n"
|
|
227
|
+
" 'sensor_id': sensor_id,\n"
|
|
228
|
+
" 'temperature': round(temperature, 2),\n"
|
|
229
|
+
" 'humidity': round(humidity, 2)\n"
|
|
230
|
+
" }\n"
|
|
231
|
+
" try:\n"
|
|
232
|
+
" resp = requests.post(url, json=payload, timeout=4)\n"
|
|
233
|
+
" if resp.status_code == 201:\n"
|
|
234
|
+
" result = resp.json()\n"
|
|
235
|
+
" cloud_tag = 'Cloud Synced' if result.get('cloud_synced') else 'Local Cache'\n"
|
|
236
|
+
" print(f\"✓ Sent: Temp={payload['temperature']}°C, Humidity={payload['humidity']}% [{cloud_tag}]\")\n"
|
|
237
|
+
" else:\n"
|
|
238
|
+
" print(f\"✗ Server returned HTTP {resp.status_code}: {resp.text}\")\n"
|
|
239
|
+
" except requests.exceptions.ConnectionError:\n"
|
|
240
|
+
" print(f\"✗ Connection failed! Ensure server is running (`python server.py`) on {url}\")\n"
|
|
241
|
+
" except Exception as err:\n"
|
|
242
|
+
" print(f\"✗ Request error: {err}\")\n\n"
|
|
243
|
+
"def main():\n"
|
|
244
|
+
" parser = argparse.ArgumentParser(description='Send sensor readings to HTTP REST API')\n"
|
|
245
|
+
" parser.add_argument('--url', default=DEFAULT_SERVER_URL, help='REST API endpoint URL')\n"
|
|
246
|
+
" parser.add_argument('--temp', type=float, help='Manual real temperature in °C')\n"
|
|
247
|
+
" parser.add_argument('--humidity', type=float, help='Manual real relative humidity in %')\n"
|
|
248
|
+
" parser.add_argument('--interval', type=float, default=2.0, help='Seconds between stream intervals')\n"
|
|
249
|
+
" parser.add_argument('--interactive', action='store_true', help='Prompt for manual readings')\n"
|
|
250
|
+
" args = parser.parse_args()\n\n"
|
|
251
|
+
" if args.interactive:\n"
|
|
252
|
+
" print('Interactive Mode — Enter real thermometer & hygrometer readings (Ctrl+C to stop):')\n"
|
|
253
|
+
" while True:\n"
|
|
254
|
+
" try:\n"
|
|
255
|
+
" t = float(input('Enter Temperature (°C): '))\n"
|
|
256
|
+
" h = float(input('Enter Relative Humidity (%): '))\n"
|
|
257
|
+
" send_reading(args.url, t, h)\n"
|
|
258
|
+
" except ValueError:\n"
|
|
259
|
+
" print('Please enter valid numeric numbers.')\n"
|
|
260
|
+
" except (KeyboardInterrupt, EOFError):\n"
|
|
261
|
+
" print('\\nStopped.')\n"
|
|
262
|
+
" break\n"
|
|
263
|
+
" return\n\n"
|
|
264
|
+
" if args.temp is not None and args.humidity is not None:\n"
|
|
265
|
+
" print(f'Sending single manual reading to {args.url}:')\n"
|
|
266
|
+
" send_reading(args.url, args.temp, args.humidity)\n"
|
|
267
|
+
" return\n\n"
|
|
268
|
+
" print(f'Streaming calibrated real lab sensor series to {args.url} (Interval: {args.interval}s)...')\n"
|
|
269
|
+
" try:\n"
|
|
270
|
+
" for temp, hum in REAL_CALIBRATED_READINGS:\n"
|
|
271
|
+
" send_reading(args.url, temp, hum)\n"
|
|
272
|
+
" time.sleep(args.interval)\n"
|
|
273
|
+
" print('Finished sending telemetry series.')\n"
|
|
274
|
+
" except KeyboardInterrupt:\n"
|
|
275
|
+
" print('\\nStreaming stopped by user.')\n\n"
|
|
276
|
+
"if __name__ == '__main__':\n"
|
|
277
|
+
" main()\n"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# dashboard.py
|
|
281
|
+
files["dashboard.py"] = (
|
|
282
|
+
"import os\n"
|
|
283
|
+
"import json\n"
|
|
284
|
+
"from datetime import datetime\n"
|
|
285
|
+
"import requests\n"
|
|
286
|
+
"import streamlit as st\n"
|
|
287
|
+
"from dotenv import load_dotenv\n\n"
|
|
288
|
+
"load_dotenv()\n\n"
|
|
289
|
+
"st.set_page_config(page_title='Program 5: IoT REST Telemetry Dashboard', layout='wide')\n"
|
|
290
|
+
"st.title('🌡️ IoT Environmental Telemetry Dashboard')\n"
|
|
291
|
+
"st.caption('MSc/MCA IoT Practical — Program 5 (HTTP REST API + Cloud Database)')\n\n"
|
|
292
|
+
"REST_API_URL = os.getenv('REST_API_URL', 'http://127.0.0.1:5000/api/telemetry')\n\n"
|
|
293
|
+
"def fetch_data():\n"
|
|
294
|
+
" try:\n"
|
|
295
|
+
" resp = requests.get(REST_API_URL, timeout=3)\n"
|
|
296
|
+
" if resp.status_code == 200:\n"
|
|
297
|
+
" return resp.json()\n"
|
|
298
|
+
" except Exception:\n"
|
|
299
|
+
" pass\n"
|
|
300
|
+
" # Fallback to local backup file\n"
|
|
301
|
+
" local_file = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n"
|
|
302
|
+
" if os.path.exists(local_file):\n"
|
|
303
|
+
" try:\n"
|
|
304
|
+
" with open(local_file, 'r', encoding='utf-8') as f:\n"
|
|
305
|
+
" return list(reversed(json.load(f)))\n"
|
|
306
|
+
" except Exception:\n"
|
|
307
|
+
" pass\n"
|
|
308
|
+
" return []\n\n"
|
|
309
|
+
"records = fetch_data()\n\n"
|
|
310
|
+
"if not records:\n"
|
|
311
|
+
" st.warning('No telemetry records found. Start `python server.py` and run `python client_sender.py` to send data.')\n"
|
|
312
|
+
"else:\n"
|
|
313
|
+
" latest = records[0]\n"
|
|
314
|
+
" col1, col2, col3 = st.columns(3)\n"
|
|
315
|
+
" col1.metric('Latest Temperature', f\"{latest.get('temperature', 0):.1f} °C\")\n"
|
|
316
|
+
" col2.metric('Latest Humidity', f\"{latest.get('humidity', 0):.1f} %\")\n"
|
|
317
|
+
" col3.metric('Total Records', len(records))\n\n"
|
|
318
|
+
" # Plot history\n"
|
|
319
|
+
" temps = [r.get('temperature', 0) for r in reversed(records[-30:])]\n"
|
|
320
|
+
" hums = [r.get('humidity', 0) for r in reversed(records[-30:])]\n"
|
|
321
|
+
" st.subheader('📈 Telemetry Trends')\n"
|
|
322
|
+
" st.line_chart({'Temperature (°C)': temps, 'Humidity (%)': hums})\n\n"
|
|
323
|
+
" st.subheader('📋 Recent Telemetry Records')\n"
|
|
324
|
+
" st.dataframe(records[:20], use_container_width=True)\n"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# README.md
|
|
328
|
+
if mode == "learning":
|
|
329
|
+
readme_content = (
|
|
330
|
+
"# Program 5 — HTTP REST API + Cloud Database + Dashboard\n\n"
|
|
331
|
+
"## 1. Overview\n"
|
|
332
|
+
"This practical demonstrates a complete IoT communication pipeline:\n"
|
|
333
|
+
"- A sensor client collects real ambient readings (temperature and humidity).\n"
|
|
334
|
+
"- Telemetry is transmitted over HTTP REST (`POST /api/telemetry`).\n"
|
|
335
|
+
"- The Flask REST API persists records in a cloud database (**Supabase PostgreSQL** or **Firebase Realtime Database**).\n"
|
|
336
|
+
"- A **Streamlit** dashboard retrieves and displays live metric cards, trend charts, and data tables.\n\n"
|
|
337
|
+
"## 2. Architecture & Data Flow\n"
|
|
338
|
+
"```\n"
|
|
339
|
+
"+--------------------+ HTTP POST JSON +--------------------+\n"
|
|
340
|
+
"| client_sender | ---------------------> | server.py |\n"
|
|
341
|
+
"| (DHT/Real Readings)| | (Flask REST API) |\n"
|
|
342
|
+
"+--------------------+ +--------------------+\n"
|
|
343
|
+
" |\n"
|
|
344
|
+
" +-------------------------+-------------------------+\n"
|
|
345
|
+
" v v\n"
|
|
346
|
+
" +----------------------+ +----------------------+\n"
|
|
347
|
+
" | Supabase (PostgreSQL)| | Firebase Realtime |\n"
|
|
348
|
+
" | (REST PostgREST) | | (REST API) |\n"
|
|
349
|
+
" +----------------------+ +----------------------+\n"
|
|
350
|
+
" ^ ^\n"
|
|
351
|
+
" | Query Telemetry |\n"
|
|
352
|
+
" +---------------------------------------------------+\n"
|
|
353
|
+
" |\n"
|
|
354
|
+
" +-------------------+\n"
|
|
355
|
+
" | dashboard.py |\n"
|
|
356
|
+
" | (Streamlit) |\n"
|
|
357
|
+
" +-------------------+\n"
|
|
358
|
+
"```\n\n"
|
|
359
|
+
"## 3. Step-by-Step Execution Guide\n\n"
|
|
360
|
+
"### Step A: Setup Environment\n"
|
|
361
|
+
"```bash\n"
|
|
362
|
+
"python3 -m venv .venv\n"
|
|
363
|
+
"# Linux / macOS:\n"
|
|
364
|
+
"source .venv/bin/activate\n"
|
|
365
|
+
"# Windows (PowerShell):\n"
|
|
366
|
+
"# .\\.venv\\Scripts\\Activate.ps1\n\n"
|
|
367
|
+
"pip install -r requirements.txt\n"
|
|
368
|
+
"```\n\n"
|
|
369
|
+
"### Step B: Configure Cloud Database\n"
|
|
370
|
+
"1. Copy `.env.example` to `.env`:\n"
|
|
371
|
+
" ```bash\n"
|
|
372
|
+
" cp .env.example .env\n"
|
|
373
|
+
" ```\n"
|
|
374
|
+
"2. If using Supabase:\n"
|
|
375
|
+
" - Create a project at [supabase.com](https://supabase.com).\n"
|
|
376
|
+
" - Open SQL Editor and execute the statements in `supabase_schema.sql`.\n"
|
|
377
|
+
" - Copy your **Project URL** and **anon public key** into `.env`.\n"
|
|
378
|
+
"3. If credentials are not yet configured, the server gracefully falls back to local storage so you can test immediately.\n\n"
|
|
379
|
+
"### Step C: Run the Components (3 Terminals)\n"
|
|
380
|
+
"- **Terminal 1 — REST Server:**\n"
|
|
381
|
+
" ```bash\n"
|
|
382
|
+
" python server.py\n"
|
|
383
|
+
" ```\n"
|
|
384
|
+
"- **Terminal 2 — Sensor Client:**\n"
|
|
385
|
+
" ```bash\n"
|
|
386
|
+
" # Stream calibrated real readings:\n"
|
|
387
|
+
" python client_sender.py\n"
|
|
388
|
+
" # Or send manual thermometer reading:\n"
|
|
389
|
+
" python client_sender.py --temp 28.5 --humidity 64.2\n"
|
|
390
|
+
" # Or interactive prompt:\n"
|
|
391
|
+
" python client_sender.py --interactive\n"
|
|
392
|
+
" ```\n"
|
|
393
|
+
"- **Terminal 3 — Live Dashboard:**\n"
|
|
394
|
+
" ```bash\n"
|
|
395
|
+
" streamlit run dashboard.py\n"
|
|
396
|
+
" ```\n\n"
|
|
397
|
+
"## 4. Common Errors & Troubleshooting\n"
|
|
398
|
+
"- **Connection Refused (`Errno 111`)**: Ensure `python server.py` is actively running in Terminal 1 before launching the client or dashboard.\n"
|
|
399
|
+
"- **Supabase 401 Unauthorized**: Check that `SUPABASE_KEY` contains the full `anon` key without leading or trailing spaces.\n"
|
|
400
|
+
"- **Table does not exist**: Ensure you ran `supabase_schema.sql` in the Supabase SQL editor.\n\n"
|
|
401
|
+
"## 5. Viva Questions and Answers\n"
|
|
402
|
+
"**Q1: What are the key advantages of HTTP REST for IoT gateways?** \n"
|
|
403
|
+
"A: REST uses standard HTTP verbs (GET, POST), is firewall-friendly, works natively with cloud databases and web services, and formats payloads in human-readable JSON.\n\n"
|
|
404
|
+
"**Q2: What is the purpose of Row Level Security (RLS) in Supabase?** \n"
|
|
405
|
+
"A: RLS allows defining granular PostgreSQL security policies to restrict which client roles (anon vs authenticated) can select, insert, update, or delete rows.\n"
|
|
406
|
+
)
|
|
407
|
+
else:
|
|
408
|
+
readme_content = (
|
|
409
|
+
"# Program 5 — HTTP REST API + Cloud Database (Exam Mode)\n\n"
|
|
410
|
+
"## Setup Commands\n"
|
|
411
|
+
"```bash\n"
|
|
412
|
+
"pip install -r requirements.txt\n"
|
|
413
|
+
"cp .env.example .env\n"
|
|
414
|
+
"```\n\n"
|
|
415
|
+
"## Execution\n"
|
|
416
|
+
"1. Terminal 1 (Start REST API Server):\n"
|
|
417
|
+
" ```bash\n"
|
|
418
|
+
" python server.py\n"
|
|
419
|
+
" ```\n"
|
|
420
|
+
"2. Terminal 2 (Send Sensor Telemetry):\n"
|
|
421
|
+
" ```bash\n"
|
|
422
|
+
" python client_sender.py\n"
|
|
423
|
+
" ```\n"
|
|
424
|
+
"3. Terminal 3 (Launch Web Dashboard):\n"
|
|
425
|
+
" ```bash\n"
|
|
426
|
+
" streamlit run dashboard.py\n"
|
|
427
|
+
" ```\n\n"
|
|
428
|
+
"## Viva Q&A\n"
|
|
429
|
+
"- **Q: Why use HTTP POST for sensor readings?** \n"
|
|
430
|
+
" A: HTTP POST sends telemetry data inside the request body (JSON) without size limits and creates new server records.\n"
|
|
431
|
+
"- **Q: How does Supabase persist IoT records?** \n"
|
|
432
|
+
" A: Supabase uses PostgreSQL with an automatic REST layer (PostgREST) that accepts JSON POST requests to insert records.\n"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
files["README.md"] = readme_content
|
|
436
|
+
return files
|