iotlabkit 1.0.0__tar.gz → 1.0.2__tar.gz
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-1.0.0/src/iotlabkit.egg-info → iotlabkit-1.0.2}/PKG-INFO +1 -1
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/pyproject.toml +1 -1
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/__init__.py +1 -1
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/cli.py +12 -12
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/doctor.py +6 -6
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/generator.py +4 -4
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/registry.py +3 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_05.py +282 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_06.py +277 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_07.py +117 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_08.py +129 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_09.py +161 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/templates/program_10.py +125 -90
- iotlabkit-1.0.2/src/iotlabkit/templates/program_11.py +188 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_12.py +164 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_13.py +145 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_14.py +141 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_15.py +192 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_16.py +193 -0
- iotlabkit-1.0.2/src/iotlabkit/templates/program_17.py +203 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2/src/iotlabkit.egg-info}/PKG-INFO +1 -1
- iotlabkit-1.0.0/src/iotlabkit/templates/program_05.py +0 -436
- iotlabkit-1.0.0/src/iotlabkit/templates/program_06.py +0 -404
- iotlabkit-1.0.0/src/iotlabkit/templates/program_07.py +0 -249
- iotlabkit-1.0.0/src/iotlabkit/templates/program_08.py +0 -278
- iotlabkit-1.0.0/src/iotlabkit/templates/program_09.py +0 -329
- iotlabkit-1.0.0/src/iotlabkit/templates/program_11.py +0 -347
- iotlabkit-1.0.0/src/iotlabkit/templates/program_12.py +0 -317
- iotlabkit-1.0.0/src/iotlabkit/templates/program_13.py +0 -272
- iotlabkit-1.0.0/src/iotlabkit/templates/program_14.py +0 -287
- iotlabkit-1.0.0/src/iotlabkit/templates/program_15.py +0 -411
- iotlabkit-1.0.0/src/iotlabkit/templates/program_16.py +0 -382
- iotlabkit-1.0.0/src/iotlabkit/templates/program_17.py +0 -422
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/LICENSE +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/README.md +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/setup.cfg +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/coap_util.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/beijing_air/beijing_sample.csv +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/india_air/city_day_sample.csv +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit/data/occupancy/datatraining.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/SOURCES.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/dependency_links.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/entry_points.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/requires.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/src/iotlabkit.egg-info/top_level.txt +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_api_parsing.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_cli.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_coap_util.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_doctor.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_generator.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_ml_pipeline.py +0 -0
- {iotlabkit-1.0.0 → iotlabkit-1.0.2}/tests/test_registry.py +0 -0
|
@@ -4,7 +4,7 @@ Designed for MSc/MCA Semester 3 IoT Laboratory practicals (Programs 5-17).
|
|
|
4
4
|
Generates clean, working, self-contained project templates in Learning and Exam modes.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "1.0.
|
|
7
|
+
__version__ = "1.0.2"
|
|
8
8
|
__author__ = "IoT Lab Contributors"
|
|
9
9
|
__license__ = "MIT"
|
|
10
10
|
|
|
@@ -70,14 +70,14 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
70
70
|
program_id = args.program_id
|
|
71
71
|
|
|
72
72
|
if program_id in EXCLUDED_PROGRAM_IDS:
|
|
73
|
-
print(f"\
|
|
73
|
+
print(f"\nError: Program {program_id} is outside the supported syllabus scope.")
|
|
74
74
|
print(" Only Programs 5 through 17 are included in iotlabkit.\n")
|
|
75
75
|
return 1
|
|
76
76
|
|
|
77
77
|
try:
|
|
78
78
|
p = get_program(program_id)
|
|
79
79
|
except ValueError as err:
|
|
80
|
-
print(f"\
|
|
80
|
+
print(f"\nError: {err}\n")
|
|
81
81
|
return 1
|
|
82
82
|
|
|
83
83
|
print("\n" + "=" * 75)
|
|
@@ -89,11 +89,11 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
89
89
|
print(f"Data Source / API: {p.source_info}")
|
|
90
90
|
|
|
91
91
|
print("\nOperational Flags:")
|
|
92
|
-
print(f"
|
|
93
|
-
print(f"
|
|
94
|
-
print(f"
|
|
95
|
-
print(f"
|
|
96
|
-
print(f"
|
|
92
|
+
print(f" - Internet Required : {'Yes' if p.requires_internet else 'No'}")
|
|
93
|
+
print(f" - Cloud Credentials : {'Yes' if p.requires_credentials else 'No'}")
|
|
94
|
+
print(f" - External Broker : {'Yes' if p.requires_broker else 'No'}")
|
|
95
|
+
print(f" - Local Dataset : {'Yes' if p.requires_dataset else 'No'}")
|
|
96
|
+
print(f" - Local Server : {'Yes' if p.requires_server else 'No'}")
|
|
97
97
|
|
|
98
98
|
print("\nRequired Python Dependencies:")
|
|
99
99
|
if p.dependencies:
|
|
@@ -111,7 +111,7 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
111
111
|
|
|
112
112
|
print("\nGenerated Files:")
|
|
113
113
|
for gf in p.generated_files:
|
|
114
|
-
print(f"
|
|
114
|
+
print(f" - {gf}")
|
|
115
115
|
|
|
116
116
|
if p.viva_preview:
|
|
117
117
|
print("\nSample Viva Questions & Answers:")
|
|
@@ -121,8 +121,8 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
121
121
|
|
|
122
122
|
print("=" * 75)
|
|
123
123
|
print(f"To generate this practical:")
|
|
124
|
-
print(f" iotlabkit create {p.id} --mode learning
|
|
125
|
-
print(f" iotlabkit create {p.id} --mode exam
|
|
124
|
+
print(f" iotlabkit create {p.id} --mode learning (detailed guide)")
|
|
125
|
+
print(f" iotlabkit create {p.id} --mode exam (concise exam code)")
|
|
126
126
|
print("=" * 75 + "\n")
|
|
127
127
|
return 0
|
|
128
128
|
|
|
@@ -132,7 +132,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
132
132
|
program_id = args.program_id
|
|
133
133
|
|
|
134
134
|
if program_id in EXCLUDED_PROGRAM_IDS:
|
|
135
|
-
print(f"\
|
|
135
|
+
print(f"\nError: Program {program_id} is outside the supported syllabus scope.")
|
|
136
136
|
print(" Only Programs 5 through 17 are included in iotlabkit.\n")
|
|
137
137
|
return 1
|
|
138
138
|
|
|
@@ -146,7 +146,7 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
146
146
|
)
|
|
147
147
|
return 0
|
|
148
148
|
except Exception as err:
|
|
149
|
-
print(f"\
|
|
149
|
+
print(f"\nError generating practical {program_id}: {err}\n")
|
|
150
150
|
return 1
|
|
151
151
|
|
|
152
152
|
|
|
@@ -62,7 +62,7 @@ def run_doctor() -> int:
|
|
|
62
62
|
# 1. Python Environment
|
|
63
63
|
print("\n[1] Python Runtime:")
|
|
64
64
|
py_ok, py_msg = check_python_version()
|
|
65
|
-
status_icon = "
|
|
65
|
+
status_icon = "[OK]" if py_ok else "[X]"
|
|
66
66
|
print(f" {status_icon} Python Version : {py_msg}")
|
|
67
67
|
print(f" Platform : {platform.system()} {platform.release()} ({platform.machine()})")
|
|
68
68
|
if not py_ok:
|
|
@@ -78,7 +78,7 @@ def run_doctor() -> int:
|
|
|
78
78
|
]
|
|
79
79
|
for binary, desc in tools:
|
|
80
80
|
found, msg = check_tool(binary)
|
|
81
|
-
icon = "
|
|
81
|
+
icon = "[OK]" if found else "[ ]"
|
|
82
82
|
print(f" {icon} {binary:<16}: {msg} - {desc}")
|
|
83
83
|
|
|
84
84
|
# 3. Layer Dependencies
|
|
@@ -112,7 +112,7 @@ def run_doctor() -> int:
|
|
|
112
112
|
print(f" -- {group_name} --")
|
|
113
113
|
for pkg_name, imp_name in pkgs:
|
|
114
114
|
installed, ver_info = check_python_package(pkg_name, imp_name)
|
|
115
|
-
icon = "
|
|
115
|
+
icon = "[OK]" if installed else "[ ]"
|
|
116
116
|
print(f" {icon} {pkg_name:<16}: {ver_info}")
|
|
117
117
|
if not installed:
|
|
118
118
|
missing_packages.append(pkg_name)
|
|
@@ -129,7 +129,7 @@ def run_doctor() -> int:
|
|
|
129
129
|
]
|
|
130
130
|
for var_name, desc in env_vars:
|
|
131
131
|
is_set, safe_info = check_env_var_safe(var_name)
|
|
132
|
-
icon = "
|
|
132
|
+
icon = "[OK]" if is_set else "[-]"
|
|
133
133
|
print(f" {icon} {var_name:<26}: {safe_info} ({desc})")
|
|
134
134
|
|
|
135
135
|
# 5. Diagnostic Summary
|
|
@@ -137,7 +137,7 @@ def run_doctor() -> int:
|
|
|
137
137
|
print(" Summary & Recommendations")
|
|
138
138
|
print("=" * 70)
|
|
139
139
|
if missing_packages:
|
|
140
|
-
print(f"\
|
|
140
|
+
print(f"\nNote: {len(missing_packages)} optional packages are currently uninstalled.")
|
|
141
141
|
print(" To install packages for specific practicals, you can run:")
|
|
142
142
|
print(" pip install iotlabkit[analytics] # For Data Analytics practicals (11-14)")
|
|
143
143
|
print(" pip install iotlabkit[ml] # For Machine Learning practicals (15-17)")
|
|
@@ -145,7 +145,7 @@ def run_doctor() -> int:
|
|
|
145
145
|
print(" pip install iotlabkit[dashboard] # For Streamlit dashboards")
|
|
146
146
|
print(" pip install iotlabkit[all] # To install all dependencies at once")
|
|
147
147
|
else:
|
|
148
|
-
print("\
|
|
148
|
+
print("\nAll Python packages for all 13 practicals are installed and ready!")
|
|
149
149
|
|
|
150
150
|
print(
|
|
151
151
|
"\nReminder: Programs 8 (CoAP) runs with zero external packages (pure standard library).\n"
|
|
@@ -21,7 +21,7 @@ def prompt_overwrite_confirmation(directory: str) -> bool:
|
|
|
21
21
|
if not existing_files:
|
|
22
22
|
return True
|
|
23
23
|
|
|
24
|
-
print(f"\
|
|
24
|
+
print(f"\nWarning: Target directory '{directory}' already exists and contains {len(existing_files)} file(s):")
|
|
25
25
|
for f in existing_files[:5]:
|
|
26
26
|
print(f" - {f}")
|
|
27
27
|
if len(existing_files) > 5:
|
|
@@ -77,7 +77,7 @@ def generate_program(
|
|
|
77
77
|
generator_fn = prog_info.get_generator()
|
|
78
78
|
files_to_write: Dict[str, str] = generator_fn(target_abs, clean_mode)
|
|
79
79
|
|
|
80
|
-
print(f"\
|
|
80
|
+
print(f"\nGenerating Program {prog_info.id}: {prog_info.title}")
|
|
81
81
|
print(f" Mode: {clean_mode.upper()}")
|
|
82
82
|
print(f" Target Directory: {target_abs}\n")
|
|
83
83
|
|
|
@@ -89,10 +89,10 @@ def generate_program(
|
|
|
89
89
|
|
|
90
90
|
with open(full_path, "w", encoding="utf-8") as f:
|
|
91
91
|
f.write(content)
|
|
92
|
-
print(f"
|
|
92
|
+
print(f" Created: {rel_path}")
|
|
93
93
|
|
|
94
94
|
print("\n" + "=" * 70)
|
|
95
|
-
print(f"
|
|
95
|
+
print(f"Project generated in '{target_dir}'.")
|
|
96
96
|
print("=" * 70)
|
|
97
97
|
print("\nNext steps to run this practical:")
|
|
98
98
|
print(f" 1. Navigate to the project directory:")
|
|
@@ -81,6 +81,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
|
|
|
81
81
|
"dashboard.py",
|
|
82
82
|
"supabase_schema.sql",
|
|
83
83
|
"firebase_setup.md",
|
|
84
|
+
"init_db.py",
|
|
84
85
|
],
|
|
85
86
|
prerequisites=[
|
|
86
87
|
"Python 3.9+",
|
|
@@ -130,6 +131,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
|
|
|
130
131
|
"publisher.py",
|
|
131
132
|
"dashboard.py",
|
|
132
133
|
"supabase_schema.sql",
|
|
134
|
+
"init_db.py",
|
|
133
135
|
],
|
|
134
136
|
prerequisites=[
|
|
135
137
|
"Python 3.9+",
|
|
@@ -323,6 +325,7 @@ PROGRAMS: Dict[int, ProgramInfo] = {
|
|
|
323
325
|
"dashboard.py",
|
|
324
326
|
"supabase_schema.sql",
|
|
325
327
|
"coap_core.py",
|
|
328
|
+
"init_db.py",
|
|
326
329
|
],
|
|
327
330
|
prerequisites=[
|
|
328
331
|
"Python 3.9+",
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
files["requirements.txt"] = (
|
|
10
|
+
"requests>=2.28.0\n"
|
|
11
|
+
"python-dotenv>=1.0.0\n"
|
|
12
|
+
"flask>=2.2.0\n"
|
|
13
|
+
"streamlit>=1.25.0\n"
|
|
14
|
+
"psycopg2-binary>=2.9.0\n"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
files[".env.example"] = (
|
|
18
|
+
"PORT=5000\n"
|
|
19
|
+
"HOST=127.0.0.1\n"
|
|
20
|
+
"CLOUD_PROVIDER=supabase\n"
|
|
21
|
+
"SUPABASE_URL=https://your-project.supabase.co\n"
|
|
22
|
+
"SUPABASE_KEY=your-supabase-key\n"
|
|
23
|
+
"SUPABASE_DB_PASSWORD=your-database-password\n"
|
|
24
|
+
"FIREBASE_DATABASE_URL=https://your-project.firebaseio.com\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
files[".gitignore"] = (
|
|
28
|
+
".env\n"
|
|
29
|
+
"__pycache__/\n"
|
|
30
|
+
"*.py[cod]\n"
|
|
31
|
+
"data/local_telemetry.json\n"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
files["supabase_schema.sql"] = (
|
|
35
|
+
"CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
|
|
36
|
+
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
37
|
+
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
38
|
+
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
39
|
+
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
40
|
+
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
41
|
+
");\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
files["firebase_setup.md"] = (
|
|
45
|
+
"# Firebase Setup Guide\n\n"
|
|
46
|
+
"1. Open https://console.firebase.google.com/ and create a project.\n"
|
|
47
|
+
"2. Go to Build -> Realtime Database -> Create Database.\n"
|
|
48
|
+
"3. Select test mode rules.\n"
|
|
49
|
+
"4. Copy database URL to .env under FIREBASE_DATABASE_URL.\n"
|
|
50
|
+
"5. Set CLOUD_PROVIDER=firebase in .env.\n"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# server.py
|
|
54
|
+
files["server.py"] = (
|
|
55
|
+
"import json\n"
|
|
56
|
+
"import os\n"
|
|
57
|
+
"from datetime import datetime, timezone\n"
|
|
58
|
+
"from flask import Flask, request, jsonify\n"
|
|
59
|
+
"import requests\n"
|
|
60
|
+
"from dotenv import load_dotenv\n\n"
|
|
61
|
+
"load_dotenv()\n\n"
|
|
62
|
+
"app = Flask(__name__)\n\n"
|
|
63
|
+
"HOST = os.getenv('HOST', '127.0.0.1')\n"
|
|
64
|
+
"PORT = int(os.getenv('PORT', 5000))\n"
|
|
65
|
+
"CLOUD_PROVIDER = os.getenv('CLOUD_PROVIDER', 'supabase').lower().strip()\n"
|
|
66
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
67
|
+
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
68
|
+
"FIREBASE_URL = os.getenv('FIREBASE_DATABASE_URL', '').rstrip('/')\n"
|
|
69
|
+
"LOCAL_BACKUP = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
|
|
70
|
+
"def save_to_supabase(record):\n"
|
|
71
|
+
" if not SUPABASE_URL or not SUPABASE_KEY or 'your-project' in SUPABASE_URL:\n"
|
|
72
|
+
" return False\n"
|
|
73
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}', 'Content-Type': 'application/json'}\n"
|
|
74
|
+
" resp = requests.post(f'{SUPABASE_URL}/rest/v1/sensor_telemetry', json=record, headers=headers, timeout=5)\n"
|
|
75
|
+
" return resp.status_code in (200, 201, 204)\n\n"
|
|
76
|
+
"def save_to_firebase(record):\n"
|
|
77
|
+
" if not FIREBASE_URL or 'your-firebase' in FIREBASE_URL:\n"
|
|
78
|
+
" return False\n"
|
|
79
|
+
" resp = requests.post(f'{FIREBASE_URL}/sensor_telemetry.json', json=record, timeout=5)\n"
|
|
80
|
+
" return resp.status_code == 200\n\n"
|
|
81
|
+
"def save_local(record):\n"
|
|
82
|
+
" os.makedirs(os.path.dirname(LOCAL_BACKUP), exist_ok=True)\n"
|
|
83
|
+
" data = []\n"
|
|
84
|
+
" if os.path.exists(LOCAL_BACKUP):\n"
|
|
85
|
+
" try:\n"
|
|
86
|
+
" with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
|
|
87
|
+
" data = json.load(f)\n"
|
|
88
|
+
" except Exception:\n"
|
|
89
|
+
" data = []\n"
|
|
90
|
+
" data.append(record)\n"
|
|
91
|
+
" with open(LOCAL_BACKUP, 'w', encoding='utf-8') as f:\n"
|
|
92
|
+
" json.dump(data, f, indent=2)\n\n"
|
|
93
|
+
"@app.route('/api/telemetry', methods=['POST'])\n"
|
|
94
|
+
"def receive_telemetry():\n"
|
|
95
|
+
" payload = request.get_json(silent=True) or {}\n"
|
|
96
|
+
" temp = float(payload.get('temperature', 25.0))\n"
|
|
97
|
+
" hum = float(payload.get('humidity', 60.0))\n"
|
|
98
|
+
" record = {\n"
|
|
99
|
+
" 'sensor_id': str(payload.get('sensor_id', 'DHT22_01')),\n"
|
|
100
|
+
" 'temperature': temp,\n"
|
|
101
|
+
" 'humidity': hum,\n"
|
|
102
|
+
" 'created_at': datetime.now(timezone.utc).isoformat()\n"
|
|
103
|
+
" }\n"
|
|
104
|
+
" saved_cloud = False\n"
|
|
105
|
+
" if CLOUD_PROVIDER == 'supabase':\n"
|
|
106
|
+
" saved_cloud = save_to_supabase(record)\n"
|
|
107
|
+
" elif CLOUD_PROVIDER == 'firebase':\n"
|
|
108
|
+
" saved_cloud = save_to_firebase(record)\n"
|
|
109
|
+
" save_local(record)\n"
|
|
110
|
+
" status = 'Cloud Stored' if saved_cloud else 'Saved Locally'\n"
|
|
111
|
+
" print(f\"[{datetime.now().strftime('%H:%M:%S')}] Telemetry: Temp={temp}C, Humidity={hum}% -> {status}\")\n"
|
|
112
|
+
" return jsonify({'message': 'Accepted', 'cloud_synced': saved_cloud, 'record': record}), 201\n\n"
|
|
113
|
+
"@app.route('/api/telemetry', methods=['GET'])\n"
|
|
114
|
+
"def get_telemetry():\n"
|
|
115
|
+
" if os.path.exists(LOCAL_BACKUP):\n"
|
|
116
|
+
" with open(LOCAL_BACKUP, 'r', encoding='utf-8') as f:\n"
|
|
117
|
+
" data = json.load(f)\n"
|
|
118
|
+
" return jsonify(list(reversed(data[-50:]))), 200\n"
|
|
119
|
+
" return jsonify([]), 200\n\n"
|
|
120
|
+
"if __name__ == '__main__':\n"
|
|
121
|
+
" try:\n"
|
|
122
|
+
" import init_db\n"
|
|
123
|
+
" init_db.init_table()\n"
|
|
124
|
+
" except Exception:\n"
|
|
125
|
+
" pass\n"
|
|
126
|
+
" print(f'Starting REST API server on http://{HOST}:{PORT}')\n"
|
|
127
|
+
" app.run(host=HOST, port=PORT, debug=False)\n"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
files["init_db.py"] = (
|
|
131
|
+
"import os\n"
|
|
132
|
+
"import re\n"
|
|
133
|
+
"import requests\n"
|
|
134
|
+
"from dotenv import load_dotenv\n\n"
|
|
135
|
+
"load_dotenv()\n\n"
|
|
136
|
+
"SUPABASE_URL = os.getenv('SUPABASE_URL', '').rstrip('/')\n"
|
|
137
|
+
"SUPABASE_KEY = os.getenv('SUPABASE_KEY', '')\n"
|
|
138
|
+
"DATABASE_URL = os.getenv('DATABASE_URL', '')\n"
|
|
139
|
+
"DB_PASSWORD = os.getenv('SUPABASE_DB_PASSWORD', '')\n"
|
|
140
|
+
"ACCESS_TOKEN = os.getenv('SUPABASE_ACCESS_TOKEN', '')\n\n"
|
|
141
|
+
"SQL_QUERY = '''\n"
|
|
142
|
+
"CREATE TABLE IF NOT EXISTS sensor_telemetry (\n"
|
|
143
|
+
" id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n"
|
|
144
|
+
" created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('utc'::text, NOW()) NOT NULL,\n"
|
|
145
|
+
" sensor_id TEXT NOT NULL DEFAULT 'DHT22_01',\n"
|
|
146
|
+
" temperature DOUBLE PRECISION NOT NULL,\n"
|
|
147
|
+
" humidity DOUBLE PRECISION NOT NULL\n"
|
|
148
|
+
");\n"
|
|
149
|
+
"ALTER TABLE sensor_telemetry ENABLE ROW LEVEL SECURITY;\n"
|
|
150
|
+
"CREATE POLICY \"Allow anon read\" ON sensor_telemetry FOR SELECT USING (true);\n"
|
|
151
|
+
"CREATE POLICY \"Allow anon insert\" ON sensor_telemetry FOR INSERT WITH CHECK (true);\n"
|
|
152
|
+
"'''\n\n"
|
|
153
|
+
"def init_table():\n"
|
|
154
|
+
" if not SUPABASE_URL or 'your-project' in SUPABASE_URL:\n"
|
|
155
|
+
" return False\n"
|
|
156
|
+
" headers = {'apikey': SUPABASE_KEY, 'Authorization': f'Bearer {SUPABASE_KEY}'}\n"
|
|
157
|
+
" try:\n"
|
|
158
|
+
" r = requests.get(f'{SUPABASE_URL}/rest/v1/sensor_telemetry?limit=1', headers=headers, timeout=4)\n"
|
|
159
|
+
" if r.status_code == 200:\n"
|
|
160
|
+
" print('Supabase: Table sensor_telemetry is ready.')\n"
|
|
161
|
+
" return True\n"
|
|
162
|
+
" except Exception:\n"
|
|
163
|
+
" pass\n"
|
|
164
|
+
" m = re.search(r'https://([a-z0-9_-]+)\\.supabase\\.co', SUPABASE_URL)\n"
|
|
165
|
+
" ref = m.group(1) if m else None\n"
|
|
166
|
+
" if ACCESS_TOKEN and ref:\n"
|
|
167
|
+
" try:\n"
|
|
168
|
+
" resp = requests.post(f'https://api.supabase.com/v1/projects/{ref}/database/query', json={'query': SQL_QUERY}, headers={'Authorization': f'Bearer {ACCESS_TOKEN}', 'Content-Type': 'application/json'}, timeout=8)\n"
|
|
169
|
+
" if resp.status_code in (200, 201):\n"
|
|
170
|
+
" print('Supabase: Table sensor_telemetry created via API.')\n"
|
|
171
|
+
" return True\n"
|
|
172
|
+
" except Exception:\n"
|
|
173
|
+
" pass\n"
|
|
174
|
+
" conn_str = DATABASE_URL\n"
|
|
175
|
+
" if not conn_str and DB_PASSWORD and ref:\n"
|
|
176
|
+
" conn_str = f'postgresql://postgres:{DB_PASSWORD}@db.{ref}.supabase.co:5432/postgres'\n"
|
|
177
|
+
" if conn_str:\n"
|
|
178
|
+
" try:\n"
|
|
179
|
+
" import psycopg2\n"
|
|
180
|
+
" conn = psycopg2.connect(conn_str, connect_timeout=6)\n"
|
|
181
|
+
" cur = conn.cursor()\n"
|
|
182
|
+
" cur.execute(SQL_QUERY)\n"
|
|
183
|
+
" conn.commit()\n"
|
|
184
|
+
" cur.close()\n"
|
|
185
|
+
" conn.close()\n"
|
|
186
|
+
" print('Supabase: Table sensor_telemetry created via PostgreSQL.')\n"
|
|
187
|
+
" return True\n"
|
|
188
|
+
" except Exception as e:\n"
|
|
189
|
+
" print(f'Supabase PostgreSQL note: {e}')\n"
|
|
190
|
+
" print('Supabase: Table not found. To create via code, add SUPABASE_DB_PASSWORD in .env, or paste supabase_schema.sql in Supabase SQL editor.')\n"
|
|
191
|
+
" return False\n\n"
|
|
192
|
+
"if __name__ == '__main__':\n"
|
|
193
|
+
" init_table()\n"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# client_sender.py
|
|
197
|
+
files["client_sender.py"] = (
|
|
198
|
+
"import argparse\n"
|
|
199
|
+
"import time\n"
|
|
200
|
+
"import requests\n\n"
|
|
201
|
+
"DEFAULT_URL = 'http://127.0.0.1:5000/api/telemetry'\n"
|
|
202
|
+
"READINGS = [\n"
|
|
203
|
+
" (24.5, 62.0), (24.8, 61.5), (25.1, 60.8), (25.4, 59.9), (25.7, 58.5)\n"
|
|
204
|
+
"]\n\n"
|
|
205
|
+
"def send_reading(url, temp, hum):\n"
|
|
206
|
+
" payload = {'sensor_id': 'DHT22_01', 'temperature': temp, 'humidity': hum}\n"
|
|
207
|
+
" try:\n"
|
|
208
|
+
" r = requests.post(url, json=payload, timeout=3)\n"
|
|
209
|
+
" print(f\"Sent: Temp={temp}C, Hum={hum}% | Response: {r.status_code}\")\n"
|
|
210
|
+
" except Exception as e:\n"
|
|
211
|
+
" print('Send error:', e)\n\n"
|
|
212
|
+
"def main():\n"
|
|
213
|
+
" parser = argparse.ArgumentParser()\n"
|
|
214
|
+
" parser.add_argument('--url', default=DEFAULT_URL)\n"
|
|
215
|
+
" parser.add_argument('--temp', type=float)\n"
|
|
216
|
+
" parser.add_argument('--humidity', type=float)\n"
|
|
217
|
+
" args = parser.parse_args()\n\n"
|
|
218
|
+
" if args.temp is not None and args.humidity is not None:\n"
|
|
219
|
+
" send_reading(args.url, args.temp, args.humidity)\n"
|
|
220
|
+
" return\n\n"
|
|
221
|
+
" print('Streaming sample sensor readings...')\n"
|
|
222
|
+
" for t, h in READINGS:\n"
|
|
223
|
+
" send_reading(args.url, t, h)\n"
|
|
224
|
+
" time.sleep(1.5)\n\n"
|
|
225
|
+
"if __name__ == '__main__':\n"
|
|
226
|
+
" main()\n"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# dashboard.py
|
|
230
|
+
files["dashboard.py"] = (
|
|
231
|
+
"import os\n"
|
|
232
|
+
"import json\n"
|
|
233
|
+
"import requests\n"
|
|
234
|
+
"import streamlit as st\n\n"
|
|
235
|
+
"st.title('Sensor Telemetry Dashboard')\n\n"
|
|
236
|
+
"REST_API_URL = os.getenv('REST_API_URL', 'http://127.0.0.1:5000/api/telemetry')\n"
|
|
237
|
+
"LOCAL_FILE = os.path.join(os.path.dirname(__file__), 'data', 'local_telemetry.json')\n\n"
|
|
238
|
+
"def fetch_data():\n"
|
|
239
|
+
" try:\n"
|
|
240
|
+
" resp = requests.get(REST_API_URL, timeout=3)\n"
|
|
241
|
+
" if resp.status_code == 200:\n"
|
|
242
|
+
" return resp.json()\n"
|
|
243
|
+
" except Exception:\n"
|
|
244
|
+
" pass\n"
|
|
245
|
+
" if os.path.exists(LOCAL_FILE):\n"
|
|
246
|
+
" try:\n"
|
|
247
|
+
" with open(LOCAL_FILE, 'r', encoding='utf-8') as f:\n"
|
|
248
|
+
" return list(reversed(json.load(f)))\n"
|
|
249
|
+
" except Exception:\n"
|
|
250
|
+
" pass\n"
|
|
251
|
+
" return []\n\n"
|
|
252
|
+
"records = fetch_data()\n"
|
|
253
|
+
"if not records:\n"
|
|
254
|
+
" st.write('No telemetry records found. Start python server.py and send data.')\n"
|
|
255
|
+
"else:\n"
|
|
256
|
+
" latest = records[0]\n"
|
|
257
|
+
" st.write(f\"Latest Reading: Temperature = {latest.get('temperature', 0)} C | Humidity = {latest.get('humidity', 0)} %\")\n"
|
|
258
|
+
" temps = [r.get('temperature', 0) for r in reversed(records[-30:])]\n"
|
|
259
|
+
" hums = [r.get('humidity', 0) for r in reversed(records[-30:])]\n"
|
|
260
|
+
" st.line_chart({'Temperature': temps, 'Humidity': hums})\n"
|
|
261
|
+
" st.write('Recent Records:')\n"
|
|
262
|
+
" st.dataframe(records[:20])\n"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# README.md
|
|
266
|
+
files["README.md"] = (
|
|
267
|
+
"# Program 5 — HTTP REST API + Cloud DB + Dashboard\n\n"
|
|
268
|
+
"## Run\n"
|
|
269
|
+
"```bash\n"
|
|
270
|
+
"# Terminal 1: Start REST Server\n"
|
|
271
|
+
"python server.py\n\n"
|
|
272
|
+
"# Terminal 2: Send Sensor Data\n"
|
|
273
|
+
"python client_sender.py\n\n"
|
|
274
|
+
"# Terminal 3: View Dashboard\n"
|
|
275
|
+
"streamlit run dashboard.py\n"
|
|
276
|
+
"```\n\n"
|
|
277
|
+
"## Viva Q&A\n"
|
|
278
|
+
"- Q: What is REST?\n"
|
|
279
|
+
" A: Representational State Transfer, a stateless HTTP-based architecture using methods like GET and POST.\n"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return files
|