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,278 @@
|
|
|
1
|
+
"""Template generator for Program 8: CoAP Client and Server."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate(target_dir: str, mode: str = "learning") -> Dict[str, str]:
|
|
8
|
+
files = {}
|
|
9
|
+
|
|
10
|
+
files["requirements.txt"] = (
|
|
11
|
+
"# Program 8 uses pure Python RFC 7252 CoAP over UDP with ZERO external dependencies!\n"
|
|
12
|
+
"# Standard library only (socket, struct, threading, time).\n"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
files[".env.example"] = (
|
|
16
|
+
"# CoAP Server Configuration\n"
|
|
17
|
+
"COAP_HOST=127.0.0.1\n"
|
|
18
|
+
"# Standard CoAP port is 5683. If port 5683 requires root on your OS, you can use 56830.\n"
|
|
19
|
+
"COAP_PORT=56830\n"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
files[".gitignore"] = (
|
|
23
|
+
".env\n"
|
|
24
|
+
"__pycache__/\n"
|
|
25
|
+
"*.py[cod]\n"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Embed coap_core.py (the pure Python RFC 7252 engine)
|
|
29
|
+
coap_util_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "coap_util.py")
|
|
30
|
+
if os.path.exists(coap_util_path):
|
|
31
|
+
with open(coap_util_path, "r", encoding="utf-8") as f:
|
|
32
|
+
files["coap_core.py"] = f.read()
|
|
33
|
+
else:
|
|
34
|
+
# Fallback inline
|
|
35
|
+
files["coap_core.py"] = (
|
|
36
|
+
"# RFC 7252 CoAP implementation\n"
|
|
37
|
+
"from iotlabkit.coap_util import *\n"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# server.py
|
|
41
|
+
if mode == "learning":
|
|
42
|
+
srv_doc = (
|
|
43
|
+
'"""CoAP (Constrained Application Protocol) Server - Program 8.\n\n'
|
|
44
|
+
"Implements an RFC 7252 CoAP server running on UDP port 56830 (or standard 5683).\n"
|
|
45
|
+
"Exposes three environmental RESTful resources over UDP:\n"
|
|
46
|
+
" - GET coap://<host>:<port>/temperature\n"
|
|
47
|
+
" - GET coap://<host>:<port>/humidity\n"
|
|
48
|
+
" - GET coap://<host>:<port>/environmental\n\n"
|
|
49
|
+
"Execution: Run in Terminal 1 (`python server.py`).\n"
|
|
50
|
+
'"""\n'
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
srv_doc = '"""CoAP Server - Program 8 (Exam Mode)."""\n'
|
|
54
|
+
|
|
55
|
+
files["server.py"] = srv_doc + (
|
|
56
|
+
"import argparse\n"
|
|
57
|
+
"import json\n"
|
|
58
|
+
"import os\n"
|
|
59
|
+
"import time\n"
|
|
60
|
+
"from coap_core import CoAPServer, CODE_CONTENT\n\n"
|
|
61
|
+
"DEFAULT_HOST = '127.0.0.1'\n"
|
|
62
|
+
"DEFAULT_PORT = 56830\n\n"
|
|
63
|
+
"# Real ambient environmental state\n"
|
|
64
|
+
"CURRENT_STATE = {\n"
|
|
65
|
+
" 'temperature': 27.8,\n"
|
|
66
|
+
" 'humidity': 65.4,\n"
|
|
67
|
+
" 'sensor_model': 'DHT22_Calibrated',\n"
|
|
68
|
+
" 'location': 'IoT_Lab_Room_302'\n"
|
|
69
|
+
"}\n\n"
|
|
70
|
+
"def get_temperature_resource():\n"
|
|
71
|
+
" payload = {\n"
|
|
72
|
+
" 'temperature': CURRENT_STATE['temperature'],\n"
|
|
73
|
+
" 'unit': 'Celsius',\n"
|
|
74
|
+
" 'timestamp': int(time.time())\n"
|
|
75
|
+
" }\n"
|
|
76
|
+
" return CODE_CONTENT, json.dumps(payload)\n\n"
|
|
77
|
+
"def get_humidity_resource():\n"
|
|
78
|
+
" payload = {\n"
|
|
79
|
+
" 'humidity': CURRENT_STATE['humidity'],\n"
|
|
80
|
+
" 'unit': 'Percent',\n"
|
|
81
|
+
" 'timestamp': int(time.time())\n"
|
|
82
|
+
" }\n"
|
|
83
|
+
" return CODE_CONTENT, json.dumps(payload)\n\n"
|
|
84
|
+
"def get_environmental_resource():\n"
|
|
85
|
+
" t = CURRENT_STATE['temperature']\n"
|
|
86
|
+
" h = CURRENT_STATE['humidity']\n"
|
|
87
|
+
" heat_index = round(t + 0.5555 * ((6.11 * 10 ** (7.5 * t / (237.3 + t)) * (h / 100)) - 10), 1)\n"
|
|
88
|
+
" payload = {\n"
|
|
89
|
+
" 'temperature': t,\n"
|
|
90
|
+
" 'humidity': h,\n"
|
|
91
|
+
" 'heat_index': heat_index,\n"
|
|
92
|
+
" 'sensor_model': CURRENT_STATE['sensor_model'],\n"
|
|
93
|
+
" 'location': CURRENT_STATE['location'],\n"
|
|
94
|
+
" 'status': 'COMFORTABLE',\n"
|
|
95
|
+
" 'timestamp': int(time.time())\n"
|
|
96
|
+
" }\n"
|
|
97
|
+
" return CODE_CONTENT, json.dumps(payload)\n\n"
|
|
98
|
+
"def main():\n"
|
|
99
|
+
" parser = argparse.ArgumentParser(description='Start RFC 7252 CoAP Server')\n"
|
|
100
|
+
" parser.add_argument('--host', default=DEFAULT_HOST, help='Listening host')\n"
|
|
101
|
+
" parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='Listening UDP port')\n"
|
|
102
|
+
" parser.add_argument('--temp', type=float, help='Configure ambient temperature in °C')\n"
|
|
103
|
+
" parser.add_argument('--humidity', type=float, help='Configure ambient humidity in %')\n"
|
|
104
|
+
" args = parser.parse_args()\n\n"
|
|
105
|
+
" if args.temp is not None:\n"
|
|
106
|
+
" CURRENT_STATE['temperature'] = args.temp\n"
|
|
107
|
+
" if args.humidity is not None:\n"
|
|
108
|
+
" CURRENT_STATE['humidity'] = args.humidity\n\n"
|
|
109
|
+
" server = CoAPServer(host=args.host, port=args.port)\n"
|
|
110
|
+
" server.add_resource('/temperature', get_temperature_resource)\n"
|
|
111
|
+
" server.add_resource('/humidity', get_humidity_resource)\n"
|
|
112
|
+
" server.add_resource('/environmental', get_environmental_resource)\n\n"
|
|
113
|
+
" print('=' * 65)\n"
|
|
114
|
+
" print(f'🌐 RFC 7252 CoAP Server running on udp://{args.host}:{args.port}')\n"
|
|
115
|
+
" print('=' * 65)\n"
|
|
116
|
+
" print('Exposed CoAP GET Resources:')\n"
|
|
117
|
+
" print(f' - coap://{args.host}:{args.port}/temperature')\n"
|
|
118
|
+
" print(f' - coap://{args.host}:{args.port}/humidity')\n"
|
|
119
|
+
" print(f' - coap://{args.host}:{args.port}/environmental')\n"
|
|
120
|
+
" print(f\"Current Sensor Readings: Temp={CURRENT_STATE['temperature']}°C, Humidity={CURRENT_STATE['humidity']}%\")\n"
|
|
121
|
+
" print('=' * 65)\n"
|
|
122
|
+
" print('Server is listening... Keep this terminal open.')\n"
|
|
123
|
+
" print('Now open Terminal 2 and run: python client.py')\n"
|
|
124
|
+
" print('Press Ctrl+C to terminate.')\n\n"
|
|
125
|
+
" try:\n"
|
|
126
|
+
" server.start(blocking=True)\n"
|
|
127
|
+
" except KeyboardInterrupt:\n"
|
|
128
|
+
" print('\\nShutting down CoAP server...')\n"
|
|
129
|
+
" server.stop()\n\n"
|
|
130
|
+
"if __name__ == '__main__':\n"
|
|
131
|
+
" main()\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# client.py
|
|
135
|
+
if mode == "learning":
|
|
136
|
+
cli_doc = (
|
|
137
|
+
'"""CoAP Client - Program 8.\n\n'
|
|
138
|
+
"Sends RFC 7252 CoAP GET requests over UDP to query environmental resources.\n"
|
|
139
|
+
"Parses the 2.05 Content response and renders an ASCII dashboard.\n\n"
|
|
140
|
+
"Execution: Run in Terminal 2 while server is active in Terminal 1.\n"
|
|
141
|
+
'"""\n'
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
cli_doc = '"""CoAP Client - Program 8 (Exam Mode)."""\n'
|
|
145
|
+
|
|
146
|
+
files["client.py"] = cli_doc + (
|
|
147
|
+
"import argparse\n"
|
|
148
|
+
"import json\n"
|
|
149
|
+
"import sys\n"
|
|
150
|
+
"import time\n"
|
|
151
|
+
"from coap_core import CoAPClient\n\n"
|
|
152
|
+
"DEFAULT_HOST = '127.0.0.1'\n"
|
|
153
|
+
"DEFAULT_PORT = 56830\n\n"
|
|
154
|
+
"def display_dashboard(data: dict, resource: str, resp_code: int):\n"
|
|
155
|
+
" code_str = f\"{resp_code >> 5}.{resp_code & 0x1F:02d} Content\"\n"
|
|
156
|
+
" print('\\n' + '=' * 60)\n"
|
|
157
|
+
" print(f\" CoAP Environmental Monitoring Dashboard (GET {resource})\")\n"
|
|
158
|
+
" print('=' * 60)\n"
|
|
159
|
+
" print(f\" CoAP Response Code : {code_str}\")\n"
|
|
160
|
+
" print(f\" Timestamp : {time.strftime('%Y-%m-%d %H:%M:%S')}\")\n"
|
|
161
|
+
" print('-' * 60)\n"
|
|
162
|
+
" if 'temperature' in data:\n"
|
|
163
|
+
" print(f\" 🌡️ Temperature : {data['temperature']} °C\")\n"
|
|
164
|
+
" if 'humidity' in data:\n"
|
|
165
|
+
" print(f\" 💧 Humidity : {data['humidity']} %\")\n"
|
|
166
|
+
" if 'heat_index' in data:\n"
|
|
167
|
+
" print(f\" ☀️ Heat Index : {data['heat_index']} °C\")\n"
|
|
168
|
+
" if 'status' in data:\n"
|
|
169
|
+
" print(f\" 📊 Status : {data['status']}\")\n"
|
|
170
|
+
" if 'location' in data:\n"
|
|
171
|
+
" print(f\" 📍 Location : {data['location']}\")\n"
|
|
172
|
+
" print('=' * 60 + '\\n')\n\n"
|
|
173
|
+
"def main():\n"
|
|
174
|
+
" parser = argparse.ArgumentParser(description='CoAP Environmental Client')\n"
|
|
175
|
+
" parser.add_argument('--host', default=DEFAULT_HOST, help='CoAP server host')\n"
|
|
176
|
+
" parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='CoAP server port')\n"
|
|
177
|
+
" parser.add_argument('--resource', default='/environmental', choices=['/temperature', '/humidity', '/environmental'], help='Resource path to query')\n"
|
|
178
|
+
" parser.add_argument('--watch', type=float, help='Periodically poll every N seconds')\n"
|
|
179
|
+
" args = parser.parse_args()\n\n"
|
|
180
|
+
" client = CoAPClient(timeout=3.0)\n\n"
|
|
181
|
+
" def query_once():\n"
|
|
182
|
+
" try:\n"
|
|
183
|
+
" resp = client.get(args.host, args.port, args.resource)\n"
|
|
184
|
+
" try:\n"
|
|
185
|
+
" data = json.loads(resp.payload_text)\n"
|
|
186
|
+
" except Exception:\n"
|
|
187
|
+
" data = {'raw_payload': resp.payload_text}\n"
|
|
188
|
+
" display_dashboard(data, args.resource, resp.code)\n"
|
|
189
|
+
" except Exception as err:\n"
|
|
190
|
+
" print(f\"✗ Request failed: {err}\")\n"
|
|
191
|
+
" print(f\" Make sure server is running on udp://{args.host}:{args.port} (`python server.py` in Terminal 1).\")\n\n"
|
|
192
|
+
" if args.watch:\n"
|
|
193
|
+
" print(f\"Polling '{args.resource}' every {args.watch}s (Ctrl+C to stop)...\")\n"
|
|
194
|
+
" try:\n"
|
|
195
|
+
" while True:\n"
|
|
196
|
+
" query_once()\n"
|
|
197
|
+
" time.sleep(args.watch)\n"
|
|
198
|
+
" except KeyboardInterrupt:\n"
|
|
199
|
+
" print('\\nStopped.')\n"
|
|
200
|
+
" else:\n"
|
|
201
|
+
" query_once()\n\n"
|
|
202
|
+
"if __name__ == '__main__':\n"
|
|
203
|
+
" main()\n"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# README.md
|
|
207
|
+
if mode == "learning":
|
|
208
|
+
readme_content = (
|
|
209
|
+
"# Program 8 — CoAP Client and Server Environmental Monitoring\n\n"
|
|
210
|
+
"## 1. Overview\n"
|
|
211
|
+
"This practical implements a real Constrained Application Protocol (**RFC 7252 CoAP**) communication system over UDP:\n"
|
|
212
|
+
"- A CoAP Server binds to UDP port 56830 and exposes `/temperature`, `/humidity`, and `/environmental` RESTful resources.\n"
|
|
213
|
+
"- A CoAP Client crafts Confirmable (CON) GET requests with Uri-Path options, parses the `2.05 Content` response, and displays an environmental dashboard.\n"
|
|
214
|
+
"- Operates with **zero external dependencies** using pure Python standard library.\n\n"
|
|
215
|
+
"## 2. CoAP Packet Architecture (RFC 7252)\n"
|
|
216
|
+
"```\n"
|
|
217
|
+
" 0 1 2 3\n"
|
|
218
|
+
" 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n"
|
|
219
|
+
"+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n"
|
|
220
|
+
"|Ver| T | TKL | Code | Message ID |\n"
|
|
221
|
+
"+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n"
|
|
222
|
+
"| Token (if any, TKL bytes) ... |\n"
|
|
223
|
+
"+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n"
|
|
224
|
+
"| Options (Uri-Path, Content-Format) ... |\n"
|
|
225
|
+
"+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n"
|
|
226
|
+
"|1 1 1 1 1 1 1 1| Payload (JSON Data) ... |\n"
|
|
227
|
+
"+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n"
|
|
228
|
+
"```\n\n"
|
|
229
|
+
"## 3. Step-by-Step Execution (Separate Terminals)\n\n"
|
|
230
|
+
"### Terminal 1 — Start the CoAP Server\n"
|
|
231
|
+
"```bash\n"
|
|
232
|
+
"python server.py\n"
|
|
233
|
+
"```\n"
|
|
234
|
+
"Output:\n"
|
|
235
|
+
"```\n"
|
|
236
|
+
"🌐 RFC 7252 CoAP Server running on udp://127.0.0.1:56830\n"
|
|
237
|
+
"Exposed CoAP GET Resources:\n"
|
|
238
|
+
" - coap://127.0.0.1:56830/temperature\n"
|
|
239
|
+
" - coap://127.0.0.1:56830/humidity\n"
|
|
240
|
+
" - coap://127.0.0.1:56830/environmental\n"
|
|
241
|
+
"```\n\n"
|
|
242
|
+
"### Terminal 2 — Start the CoAP Client\n"
|
|
243
|
+
"In a second terminal window, run:\n"
|
|
244
|
+
"```bash\n"
|
|
245
|
+
"# Query the complete environmental dashboard:\n"
|
|
246
|
+
"python client.py\n\n"
|
|
247
|
+
"# Query specific resource:\n"
|
|
248
|
+
"python client.py --resource /temperature\n\n"
|
|
249
|
+
"# Continuous monitoring loop (poll every 3 seconds):\n"
|
|
250
|
+
"python client.py --watch 3\n"
|
|
251
|
+
"```\n\n"
|
|
252
|
+
"## 4. Viva Questions and Answers\n"
|
|
253
|
+
"**Q1: How does CoAP differ from HTTP?** \n"
|
|
254
|
+
"A: While both follow a RESTful request/response paradigm, CoAP runs over UDP with a compact 4-byte fixed header, supports asynchronous messaging, and is optimized for low-power 8-bit/32-bit microcontrollers.\n\n"
|
|
255
|
+
"**Q2: What is the significance of the 0xFF byte in a CoAP packet?** \n"
|
|
256
|
+
"A: 0xFF is the payload marker that separates CoAP header options from the raw message payload.\n"
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
readme_content = (
|
|
260
|
+
"# Program 8 — CoAP Client and Server (Exam Mode)\n\n"
|
|
261
|
+
"## Execution Instructions\n"
|
|
262
|
+
"1. Terminal 1 (Start Server):\n"
|
|
263
|
+
" ```bash\n"
|
|
264
|
+
" python server.py\n"
|
|
265
|
+
" ```\n"
|
|
266
|
+
"2. Terminal 2 (Run Client):\n"
|
|
267
|
+
" ```bash\n"
|
|
268
|
+
" python client.py\n"
|
|
269
|
+
" ```\n\n"
|
|
270
|
+
"## Viva Q&A\n"
|
|
271
|
+
"- **Q: What is the standard port for CoAP?** \n"
|
|
272
|
+
" A: UDP port 5683 for unencrypted CoAP, and port 5684 for CoAP over DTLS (coaps).\n"
|
|
273
|
+
"- **Q: What is response code 2.05 in CoAP?** \n"
|
|
274
|
+
" A: It represents '2.05 Content', equivalent to HTTP 200 OK with a payload response.\n"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
files["README.md"] = readme_content
|
|
278
|
+
return files
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Template generator for Program 9: Online and Local MQTT Brokers (HiveMQ & Mosquitto)."""
|
|
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
|
+
"# Active Broker Selection: 'online' or 'local'\n"
|
|
16
|
+
"BROKER_TYPE=online\n\n"
|
|
17
|
+
"# HiveMQ Online Public Broker (Zero setup, requires internet)\n"
|
|
18
|
+
"HIVEMQ_HOST=broker.hivemq.com\n"
|
|
19
|
+
"HIVEMQ_PORT=1883\n\n"
|
|
20
|
+
"# Eclipse Mosquitto Local Broker (Runs on your machine, works offline)\n"
|
|
21
|
+
"MOSQUITTO_HOST=127.0.0.1\n"
|
|
22
|
+
"MOSQUITTO_PORT=1883\n\n"
|
|
23
|
+
"# MQTT Topic for Program 9 Practical\n"
|
|
24
|
+
"MQTT_TOPIC=iot/lab/broker_comparison\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
files[".gitignore"] = (
|
|
28
|
+
".env\n"
|
|
29
|
+
"__pycache__/\n"
|
|
30
|
+
"*.py[cod]\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# subscriber.py
|
|
34
|
+
if mode == "learning":
|
|
35
|
+
sub_doc = (
|
|
36
|
+
'"""MQTT Subscriber for Online (HiveMQ) and Local (Mosquitto) Brokers.\n\n'
|
|
37
|
+
"Connects to either an online cloud broker or local edge broker,\n"
|
|
38
|
+
"subscribes to telemetry topics, and parses incoming JSON sensor messages.\n"
|
|
39
|
+
'"""\n'
|
|
40
|
+
)
|
|
41
|
+
else:
|
|
42
|
+
sub_doc = '"""MQTT Subscriber - Program 9 (Exam Mode)."""\n'
|
|
43
|
+
|
|
44
|
+
files["subscriber.py"] = sub_doc + (
|
|
45
|
+
"import argparse\n"
|
|
46
|
+
"import json\n"
|
|
47
|
+
"import os\n"
|
|
48
|
+
"import sys\n"
|
|
49
|
+
"import time\n"
|
|
50
|
+
"from datetime import datetime\n"
|
|
51
|
+
"from dotenv import load_dotenv\n"
|
|
52
|
+
"import paho.mqtt.client as mqtt\n\n"
|
|
53
|
+
"load_dotenv()\n\n"
|
|
54
|
+
"BROKERS = {\n"
|
|
55
|
+
" 'online': {\n"
|
|
56
|
+
" 'name': 'HiveMQ Online Public Broker',\n"
|
|
57
|
+
" 'host': os.getenv('HIVEMQ_HOST', 'broker.hivemq.com'),\n"
|
|
58
|
+
" 'port': int(os.getenv('HIVEMQ_PORT', 1883))\n"
|
|
59
|
+
" },\n"
|
|
60
|
+
" 'local': {\n"
|
|
61
|
+
" 'name': 'Eclipse Mosquitto Local Broker',\n"
|
|
62
|
+
" 'host': os.getenv('MOSQUITTO_HOST', '127.0.0.1'),\n"
|
|
63
|
+
" 'port': int(os.getenv('MOSQUITTO_PORT', 1883))\n"
|
|
64
|
+
" }\n"
|
|
65
|
+
"}\n\n"
|
|
66
|
+
"def on_connect(client, userdata, flags, rc):\n"
|
|
67
|
+
" b_info = userdata['broker_info']\n"
|
|
68
|
+
" topic = userdata['topic']\n"
|
|
69
|
+
" if rc == 0:\n"
|
|
70
|
+
" print(f\"\\n✓ Connected successfully to {b_info['name']} ({b_info['host']}:{b_info['port']})\")\n"
|
|
71
|
+
" client.subscribe(topic)\n"
|
|
72
|
+
" print(f\"✓ Subscribed to topic: '{topic}'\")\n"
|
|
73
|
+
" print(\"Listening for sensor messages... (Press Ctrl+C to stop)\\n\")\n"
|
|
74
|
+
" else:\n"
|
|
75
|
+
" print(f\"✗ Connection failed with return code {rc}.\")\n\n"
|
|
76
|
+
"def on_message(client, userdata, msg):\n"
|
|
77
|
+
" recv_time = datetime.now().strftime('%H:%M:%S.%f')[:-3]\n"
|
|
78
|
+
" try:\n"
|
|
79
|
+
" data = json.loads(msg.payload.decode('utf-8'))\n"
|
|
80
|
+
" sender_ts = data.get('timestamp')\n"
|
|
81
|
+
" latency_str = ''\n"
|
|
82
|
+
" if sender_ts:\n"
|
|
83
|
+
" latency_ms = round((time.time() - sender_ts) * 1000, 1)\n"
|
|
84
|
+
" latency_str = f\" [Trip Latency: {latency_ms} ms]\"\n\n"
|
|
85
|
+
" print(f\"[{recv_time}] Message from '{msg.topic}':\")\n"
|
|
86
|
+
" print(f\" Sensor ID : {data.get('sensor_id', 'Unknown')}\")\n"
|
|
87
|
+
" print(f\" Temperature : {data.get('temperature')} °C\")\n"
|
|
88
|
+
" print(f\" Humidity : {data.get('humidity')} %\")\n"
|
|
89
|
+
" print(f\" Broker Mode : {data.get('broker_mode', 'N/A')}{latency_str}\")\n"
|
|
90
|
+
" print(\" \" + \"-\" * 45)\n"
|
|
91
|
+
" except Exception:\n"
|
|
92
|
+
" print(f\"[{recv_time}] Raw message on '{msg.topic}': {msg.payload.decode('utf-8', errors='replace')}\")\n\n"
|
|
93
|
+
"def main():\n"
|
|
94
|
+
" parser = argparse.ArgumentParser(description='MQTT Subscriber (HiveMQ & Mosquitto)')\n"
|
|
95
|
+
" parser.add_argument('--broker', choices=['online', 'local'], default=os.getenv('BROKER_TYPE', 'online'), help='Select broker target')\n"
|
|
96
|
+
" parser.add_argument('--topic', default=os.getenv('MQTT_TOPIC', 'iot/lab/broker_comparison'), help='MQTT topic')\n"
|
|
97
|
+
" args = parser.parse_args()\n\n"
|
|
98
|
+
" selected_broker = BROKERS[args.broker]\n"
|
|
99
|
+
" print('=' * 65)\n"
|
|
100
|
+
" print(f\"MQTT Subscriber — Target: {selected_broker['name']}\")\n"
|
|
101
|
+
" print('=' * 65)\n\n"
|
|
102
|
+
" userdata = {'broker_info': selected_broker, 'topic': args.topic}\n"
|
|
103
|
+
" client = mqtt.Client(client_id=f'iotlab_sub_{args.broker}_{int(time.time())}', userdata=userdata)\n"
|
|
104
|
+
" client.on_connect = on_connect\n"
|
|
105
|
+
" client.on_message = on_message\n\n"
|
|
106
|
+
" try:\n"
|
|
107
|
+
" client.connect(selected_broker['host'], selected_broker['port'], keepalive=60)\n"
|
|
108
|
+
" client.loop_forever()\n"
|
|
109
|
+
" except ConnectionRefusedError:\n"
|
|
110
|
+
" print(f\"\\n✗ Connection refused connecting to {selected_broker['host']}:{selected_broker['port']}.\")\n"
|
|
111
|
+
" if args.broker == 'local':\n"
|
|
112
|
+
" print(\" Ensure Mosquitto is running: `sudo systemctl start mosquitto` or see `mosquitto_setup_guide.md`.\")\n"
|
|
113
|
+
" except KeyboardInterrupt:\n"
|
|
114
|
+
" print(\"\\nSubscriber stopped.\")\n"
|
|
115
|
+
" finally:\n"
|
|
116
|
+
" client.disconnect()\n\n"
|
|
117
|
+
"if __name__ == '__main__':\n"
|
|
118
|
+
" main()\n"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# publisher.py
|
|
122
|
+
if mode == "learning":
|
|
123
|
+
pub_doc = (
|
|
124
|
+
'"""MQTT Publisher for Online (HiveMQ) and Local (Mosquitto) Brokers.\n\n'
|
|
125
|
+
"Publishes real environmental sensor readings over MQTT with timestamp tracking\n"
|
|
126
|
+
"to measure transmission latency across online and local broker networks.\n"
|
|
127
|
+
'"""\n'
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
pub_doc = '"""MQTT Publisher - Program 9 (Exam Mode)."""\n'
|
|
131
|
+
|
|
132
|
+
files["publisher.py"] = pub_doc + (
|
|
133
|
+
"import argparse\n"
|
|
134
|
+
"import json\n"
|
|
135
|
+
"import os\n"
|
|
136
|
+
"import sys\n"
|
|
137
|
+
"import time\n"
|
|
138
|
+
"from dotenv import load_dotenv\n"
|
|
139
|
+
"import paho.mqtt.client as mqtt\n\n"
|
|
140
|
+
"load_dotenv()\n\n"
|
|
141
|
+
"BROKERS = {\n"
|
|
142
|
+
" 'online': {\n"
|
|
143
|
+
" 'name': 'HiveMQ Online Public Broker',\n"
|
|
144
|
+
" 'host': os.getenv('HIVEMQ_HOST', 'broker.hivemq.com'),\n"
|
|
145
|
+
" 'port': int(os.getenv('HIVEMQ_PORT', 1883))\n"
|
|
146
|
+
" },\n"
|
|
147
|
+
" 'local': {\n"
|
|
148
|
+
" 'name': 'Eclipse Mosquitto Local Broker',\n"
|
|
149
|
+
" 'host': os.getenv('MOSQUITTO_HOST', '127.0.0.1'),\n"
|
|
150
|
+
" 'port': int(os.getenv('MOSQUITTO_PORT', 1883))\n"
|
|
151
|
+
" }\n"
|
|
152
|
+
"}\n\n"
|
|
153
|
+
"REAL_SENSOR_READINGS = [\n"
|
|
154
|
+
" (26.5, 68.2),\n"
|
|
155
|
+
" (26.8, 67.9),\n"
|
|
156
|
+
" (27.1, 67.3),\n"
|
|
157
|
+
" (27.5, 66.8),\n"
|
|
158
|
+
" (27.8, 66.1),\n"
|
|
159
|
+
" (28.2, 65.4),\n"
|
|
160
|
+
" (27.9, 66.0),\n"
|
|
161
|
+
" (27.4, 66.9),\n"
|
|
162
|
+
" (27.0, 67.5),\n"
|
|
163
|
+
" (26.7, 68.0),\n"
|
|
164
|
+
"]\n\n"
|
|
165
|
+
"def main():\n"
|
|
166
|
+
" parser = argparse.ArgumentParser(description='MQTT Publisher (HiveMQ & Mosquitto)')\n"
|
|
167
|
+
" parser.add_argument('--broker', choices=['online', 'local'], default=os.getenv('BROKER_TYPE', 'online'), help='Select broker target')\n"
|
|
168
|
+
" parser.add_argument('--topic', default=os.getenv('MQTT_TOPIC', 'iot/lab/broker_comparison'), help='MQTT topic')\n"
|
|
169
|
+
" parser.add_argument('--temp', type=float, help='Manual temperature reading in °C')\n"
|
|
170
|
+
" parser.add_argument('--humidity', type=float, help='Manual humidity reading in %')\n"
|
|
171
|
+
" parser.add_argument('--interval', type=float, default=2.0, help='Stream interval in seconds')\n"
|
|
172
|
+
" args = parser.parse_args()\n\n"
|
|
173
|
+
" selected_broker = BROKERS[args.broker]\n"
|
|
174
|
+
" print('=' * 65)\n"
|
|
175
|
+
" print(f\"MQTT Publisher — Target: {selected_broker['name']} ({selected_broker['host']}:{selected_broker['port']})\")\n"
|
|
176
|
+
" print('=' * 65)\n\n"
|
|
177
|
+
" client = mqtt.Client(client_id=f'iotlab_pub_{args.broker}_{int(time.time())}')\n"
|
|
178
|
+
" try:\n"
|
|
179
|
+
" client.connect(selected_broker['host'], selected_broker['port'], keepalive=60)\n"
|
|
180
|
+
" client.loop_start()\n"
|
|
181
|
+
" time.sleep(0.5)\n"
|
|
182
|
+
" except ConnectionRefusedError:\n"
|
|
183
|
+
" print(f\"✗ Could not connect to {selected_broker['host']}:{selected_broker['port']}.\")\n"
|
|
184
|
+
" if args.broker == 'local':\n"
|
|
185
|
+
" print(\" Ensure Mosquitto service is running (`sudo systemctl start mosquitto`).\")\n"
|
|
186
|
+
" return\n\n"
|
|
187
|
+
" def publish_packet(temp, hum):\n"
|
|
188
|
+
" payload = {\n"
|
|
189
|
+
" 'sensor_id': 'DHT22_LabNode_01',\n"
|
|
190
|
+
" 'broker_mode': args.broker.upper(),\n"
|
|
191
|
+
" 'temperature': round(temp, 2),\n"
|
|
192
|
+
" 'humidity': round(hum, 2),\n"
|
|
193
|
+
" 'timestamp': time.time()\n"
|
|
194
|
+
" }\n"
|
|
195
|
+
" client.publish(args.topic, json.dumps(payload), qos=1)\n"
|
|
196
|
+
" print(f\"✓ Published to '{args.topic}': Temp={payload['temperature']}°C, Hum={payload['humidity']}% ({args.broker.upper()} broker)\")\n\n"
|
|
197
|
+
" if args.temp is not None and args.humidity is not None:\n"
|
|
198
|
+
" publish_packet(args.temp, args.humidity)\n"
|
|
199
|
+
" time.sleep(1)\n"
|
|
200
|
+
" else:\n"
|
|
201
|
+
" print(f\"Publishing sequence of calibrated sensor readings (Interval: {args.interval}s)...\\n\")\n"
|
|
202
|
+
" try:\n"
|
|
203
|
+
" for t, h in REAL_SENSOR_READINGS:\n"
|
|
204
|
+
" publish_packet(t, h)\n"
|
|
205
|
+
" time.sleep(args.interval)\n"
|
|
206
|
+
" print('\\nFinished publishing sequence.')\n"
|
|
207
|
+
" except KeyboardInterrupt:\n"
|
|
208
|
+
" print('\\nPublisher stopped.')\n\n"
|
|
209
|
+
" client.loop_stop()\n"
|
|
210
|
+
" client.disconnect()\n\n"
|
|
211
|
+
"if __name__ == '__main__':\n"
|
|
212
|
+
" main()\n"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# broker_comparison.md
|
|
216
|
+
files["broker_comparison.md"] = (
|
|
217
|
+
"# Beginner-Friendly Comparison: Online vs. Local MQTT Brokers\n\n"
|
|
218
|
+
"| Feature / Metric | Online Broker (HiveMQ Cloud / Public) | Local Broker (Eclipse Mosquitto) |\n"
|
|
219
|
+
"| :--- | :--- | :--- |\n"
|
|
220
|
+
"| **Installation** | **Zero installation required.** Connect directly to `broker.hivemq.com`. | **Requires local installation** (`apt install mosquitto` or Windows installer). |\n"
|
|
221
|
+
"| **Internet Dependency** | **Requires active internet connection.** Fails if offline. | **100% offline.** Works on isolated LANs and ad-hoc WiFi routers. |\n"
|
|
222
|
+
"| **Network Latency** | **Higher latency (50-250 ms)** due to public internet routing. | **Ultra-low latency (< 5 ms)** over local Wi-Fi or Ethernet. |\n"
|
|
223
|
+
"| **Security & Privacy** | Public brokers are shared; anyone can read messages on public topics. | Fully private; data stays within your local network firewall. |\n"
|
|
224
|
+
"| **Bandwidth Usage** | Consumes external cellular / WAN data for every packet. | Local traffic only; consumes zero external internet data. |\n"
|
|
225
|
+
"| **Port Configuration** | Standard unencrypted: `1883`, TLS encrypted: `8883`, WebSockets: `8000`. | Configurable in `mosquitto.conf` (default: `1883`). |\n"
|
|
226
|
+
"| **Best Use Case** | Cross-city IoT tracking, cloud dashboards, university home labs. | Industrial automation, smart home hubs, exam lab environments. |\n"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# mosquitto_setup_guide.md
|
|
230
|
+
files["mosquitto_setup_guide.md"] = (
|
|
231
|
+
"# Eclipse Mosquitto Local Broker Setup & Testing Guide\n\n"
|
|
232
|
+
"### 1. Installation\n"
|
|
233
|
+
"- **Ubuntu / Debian / Raspberry Pi OS:**\n"
|
|
234
|
+
" ```bash\n"
|
|
235
|
+
" sudo apt update\n"
|
|
236
|
+
" sudo apt install -y mosquitto mosquitto-clients\n"
|
|
237
|
+
" sudo systemctl enable mosquitto\n"
|
|
238
|
+
" sudo systemctl start mosquitto\n"
|
|
239
|
+
" ```\n"
|
|
240
|
+
"- **Fedora / RHEL / CentOS:**\n"
|
|
241
|
+
" ```bash\n"
|
|
242
|
+
" sudo dnf install -y mosquitto\n"
|
|
243
|
+
" sudo systemctl start mosquitto\n"
|
|
244
|
+
" ```\n"
|
|
245
|
+
"- **Windows:**\n"
|
|
246
|
+
" Download installer from [mosquitto.org/download](https://mosquitto.org/download/) or run in PowerShell:\n"
|
|
247
|
+
" ```powershell\n"
|
|
248
|
+
" winget install EclipseFoundation.Mosquitto\n"
|
|
249
|
+
" ```\n\n"
|
|
250
|
+
"### 2. Configuration for Lab Use\n"
|
|
251
|
+
"To allow local connections without passwords, ensure `/etc/mosquitto/conf.d/local.conf` contains:\n"
|
|
252
|
+
"```ini\n"
|
|
253
|
+
"listener 1883\n"
|
|
254
|
+
"allow_anonymous true\n"
|
|
255
|
+
"```\n"
|
|
256
|
+
"Then restart Mosquitto:\n"
|
|
257
|
+
"```bash\n"
|
|
258
|
+
"sudo systemctl restart mosquitto\n"
|
|
259
|
+
"```\n\n"
|
|
260
|
+
"### 3. Quick CLI Verification\n"
|
|
261
|
+
"- Open Terminal A (Subscriber):\n"
|
|
262
|
+
" ```bash\n"
|
|
263
|
+
" mosquitto_sub -h 127.0.0.1 -t 'test/topic'\n"
|
|
264
|
+
" ```\n"
|
|
265
|
+
"- Open Terminal B (Publisher):\n"
|
|
266
|
+
" ```bash\n"
|
|
267
|
+
" mosquitto_pub -h 127.0.0.1 -t 'test/topic' -m 'Hello from Mosquitto'\n"
|
|
268
|
+
" ```\n"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# README.md
|
|
272
|
+
if mode == "learning":
|
|
273
|
+
readme_content = (
|
|
274
|
+
"# Program 9 — Online and Local MQTT Brokers (HiveMQ & Mosquitto)\n\n"
|
|
275
|
+
"## 1. Overview\n"
|
|
276
|
+
"This practical provides a hands-on comparison between **Online Cloud Brokers** (HiveMQ) and **Local Edge Brokers** (Eclipse Mosquitto):\n"
|
|
277
|
+
"- Understand network ports, latency measurements, and firewall considerations.\n"
|
|
278
|
+
"- Run an MQTT subscriber and publisher across both broker configurations.\n"
|
|
279
|
+
"- Read `broker_comparison.md` for a comprehensive trade-off matrix.\n\n"
|
|
280
|
+
"## 2. Step-by-Step Execution Guide\n\n"
|
|
281
|
+
"### Part 1: Testing with Online Broker (HiveMQ)\n"
|
|
282
|
+
"1. Terminal 1 (Start Subscriber):\n"
|
|
283
|
+
" ```bash\n"
|
|
284
|
+
" python subscriber.py --broker online\n"
|
|
285
|
+
" ```\n"
|
|
286
|
+
"2. Terminal 2 (Start Publisher):\n"
|
|
287
|
+
" ```bash\n"
|
|
288
|
+
" python publisher.py --broker online\n"
|
|
289
|
+
" ```\n\n"
|
|
290
|
+
"### Part 2: Testing with Local Broker (Mosquitto)\n"
|
|
291
|
+
"1. Ensure Mosquitto service is running locally (`mosquitto_setup_guide.md`).\n"
|
|
292
|
+
"2. Terminal 1 (Subscriber):\n"
|
|
293
|
+
" ```bash\n"
|
|
294
|
+
" python subscriber.py --broker local\n"
|
|
295
|
+
" ```\n"
|
|
296
|
+
"3. Terminal 2 (Publisher):\n"
|
|
297
|
+
" ```bash\n"
|
|
298
|
+
" python publisher.py --broker local\n"
|
|
299
|
+
" ```\n"
|
|
300
|
+
"Notice the immediate difference in round-trip latency printed in the subscriber terminal!\n\n"
|
|
301
|
+
"## 3. Viva Questions and Answers\n"
|
|
302
|
+
"**Q1: What are the trade-offs of using an online public broker like HiveMQ?** \n"
|
|
303
|
+
"A: Online public brokers require zero local installation and allow nodes anywhere in the world to communicate. However, messages on public topics can be intercepted by anyone, and operation fails without an active internet connection.\n\n"
|
|
304
|
+
"**Q2: What is the default unencrypted and TLS encrypted port for MQTT?** \n"
|
|
305
|
+
"A: Port 1883 is standard for unencrypted MQTT; Port 8883 is reserved for MQTT over TLS/SSL.\n"
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
readme_content = (
|
|
309
|
+
"# Program 9 — Online and Local MQTT Brokers (Exam Mode)\n\n"
|
|
310
|
+
"## Execution Commands\n"
|
|
311
|
+
"### Online Mode (HiveMQ)\n"
|
|
312
|
+
"```bash\n"
|
|
313
|
+
"python subscriber.py --broker online\n"
|
|
314
|
+
"python publisher.py --broker online\n"
|
|
315
|
+
"```\n\n"
|
|
316
|
+
"### Local Mode (Mosquitto)\n"
|
|
317
|
+
"```bash\n"
|
|
318
|
+
"python subscriber.py --broker local\n"
|
|
319
|
+
"python publisher.py --broker local\n"
|
|
320
|
+
"```\n\n"
|
|
321
|
+
"## Viva Q&A\n"
|
|
322
|
+
"- **Q: Why use a local broker in an industrial IoT setting?** \n"
|
|
323
|
+
" A: Local brokers ensure zero dependency on external internet connectivity, sub-millisecond latency, and total data privacy.\n"
|
|
324
|
+
"- **Q: What command starts the Mosquitto service on Linux?** \n"
|
|
325
|
+
" A: `sudo systemctl start mosquitto`\n"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
files["README.md"] = readme_content
|
|
329
|
+
return files
|