-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAssignment2.py
More file actions
234 lines (194 loc) · 9.57 KB
/
Copy pathAssignment2.py
File metadata and controls
234 lines (194 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#cs3
#Christopher Luddy
#imports
import threading
import time
import random
import datetime
def log(message):
"""Helper function for timestamped debug logs."""
print(f"[{datetime.datetime.now()}] {message}")
class NeighbourManager:
"""NeighbourManager Class for managing neighbours and ETX metrics."""
def __init__(self):
self.neighbour_table = {} # Dictionary to store neighbour state
self.lock = threading.Lock() # Lock for thread safety
self.probe_interval = 5 # Base interval for probing in seconds
self.current_neighbour = None # Track the neighbour being probed
self.stop_probing = False # Control flag to stop probing
self.rank = 999 # Initialize the node rank to infinity
self.potential_parents = {} # Dictionary of potential parents
def add_or_update_neighbour(self, node_id):
"""Adds or updates a neighbour's state, including resetting the timeout timer."""
with self.lock:
if node_id in self.neighbour_table:
log(f"Updating neighbour {node_id}")
self.neighbour_table[node_id]['timeout_timer'].cancel()
else:
log(f"Adding new neighbour {node_id}")
self.neighbour_table[node_id] = {
'distance': 1, # Start at a distance of 1
'rank': None,
'timeout_timer': None,
'resend_timer': None,
'tx_cost': 0
}
# Set or reset the timeout timer to 15 seconds
timer = threading.Timer(15, self.remove_neighbour, args=[node_id])
self.neighbour_table[node_id]['timeout_timer'] = timer
timer.start()
def remove_neighbour(self, node_id):
"""Removes a neighbour when the timeout timer expires."""
with self.lock:
if node_id in self.neighbour_table:
log(f"Timeout expired: Removing neighbour {node_id}")
self.neighbour_table[node_id]['timeout_timer'].cancel()
if self.neighbour_table[node_id]['resend_timer']:
self.neighbour_table[node_id]['resend_timer'].cancel()
del self.neighbour_table[node_id]
# If this was a potential parent, update the parent list
if node_id in self.potential_parents:
log(f"Removed {node_id} from potential parents due to timeout.")
del self.potential_parents[node_id]
self.update_parents()
else:
log(f"Node {node_id} not found for removal.")
def probe_neighbours(self):
"""Probes neighbours periodically and calculates ETX."""
while not self.stop_probing:
with self.lock:
if not self.neighbour_table:
log("No neighbours to probe.")
time.sleep(self.probe_interval)
continue
# Select the next neighbour in sequence
neighbours = list(self.neighbour_table.keys())
if self.current_neighbour is None or self.current_neighbour not in self.neighbour_table:
self.current_neighbour = neighbours[0]
else:
current_index = neighbours.index(self.current_neighbour)
self.current_neighbour = neighbours[(current_index + 1) % len(neighbours)]
log(f"Probing neighbour: {self.current_neighbour}")
# Simulate sending a PROBE
self.send_probe(self.current_neighbour)
# Wait for the next probe interval with a small random offset
time.sleep(self.probe_interval + random.uniform(0, 1))
def send_probe(self, node_id):
"""Simulate sending a PROBE message."""
with self.lock:
if node_id not in self.neighbour_table:
return
tx_cost = self.neighbour_table[node_id]['tx_cost']
# Prevent retrying if max retries are reached
if tx_cost >= 5:
log(f"Max retries reached for {node_id}. Skipping PROBE.")
return
tx_cost += 1
self.neighbour_table[node_id]['tx_cost'] = tx_cost
log(f"Sent PROBE to {node_id}. TX cost: {tx_cost}")
# Set a resend timer for this neighbour
resend_timer = threading.Timer(0.5, self.resend_probe, args=[node_id])
self.neighbour_table[node_id]['resend_timer'] = resend_timer
resend_timer.start()
def resend_probe(self, node_id):
"""Resend a PROBE if no ACK is received."""
with self.lock:
if node_id in self.neighbour_table:
# Limit retries to avoid infinite resends
if self.neighbour_table[node_id]['tx_cost'] >= 5:
log(f"Max retries reached for {node_id}. Stopping PROBE resends.")
return
tx_cost = self.neighbour_table[node_id]['tx_cost']
tx_cost += 1
self.neighbour_table[node_id]['tx_cost'] = tx_cost
log(f"Resending PROBE to {node_id}. TX cost: {tx_cost}")
# Reschedule resend timer
resend_timer = threading.Timer(0.5, self.resend_probe, args=[node_id])
self.neighbour_table[node_id]['resend_timer'] = resend_timer
resend_timer.start()
def receive_probe_ack(self, node_id):
"""Handle receipt of a PROBE_ACK."""
with self.lock:
if node_id in self.neighbour_table:
# reset timeout timer
self.add_or_update_neighbour(node_id)
# Cancel the resend timer
resend_timer = self.neighbour_table[node_id]['resend_timer']
if resend_timer:
resend_timer.cancel()
self.neighbour_table[node_id]['resend_timer'] = None # Clear timer reference
self.neighbour_table[node_id]['tx_cost'] = 0
# Update the distance using the ETX formula
old_distance = self.neighbour_table[node_id]['distance']
new_distance = (old_distance * 0.5) + (1 * 0.5) # Tx cost of 1 for successful probe
self.neighbour_table[node_id]['distance'] = new_distance
log(f"Updated distance for {node_id}: {new_distance}")
# Check for potential parent update
self.add_potential_parent(node_id, new_distance)
else:
log(f'Recieved PROBE_ACK from unknown node {node_id}. Ignoring.')
def add_potential_parent(self, node_id, distance):
"""Add or update potential parents based on rank and distance."""
if node_id not in self.potential_parents or distance < self.potential_parents[node_id]['distance']:
self.potential_parents[node_id] = {'distance': distance}
log(f"Added {node_id} to potential parents with distance {distance}")
self.update_parents()
def update_parents(self):
"""Update the preferred parent based on potential parents."""
if not self.potential_parents:
log("No potential parents available. Setting rank to infinity.")
self.rank = 999
else:
# Select the parent with the smallest distance
best_parent = min(self.potential_parents, key=lambda node: self.potential_parents[node]['distance'])
self.rank = self.potential_parents[best_parent]['distance'] + 1
log(f"Selected {best_parent} as the parent with distance {self.potential_parents[best_parent]['distance']}")
def stop(self):
"""Stop the probing process."""
self.stop_probing = True
with self.lock:
for node_id, details in self.neighbour_table.items():
if details['timeout_timer']:
details['timeout_timer'].cancel()
if details['resend_timer']:
details['resend_timer'].cancel()
def print_neighbour_table(self):
"""Print the current neighbour table."""
with self.lock:
log("\n--- Current Neighbour Table ---")
if not self.neighbour_table:
log("No neighbours present.")
else:
for node_id, details in self.neighbour_table.items():
log(f"Node {node_id}: Distance = {details['distance']}, TX Cost = {details['tx_cost']}")
log("--------------------------------\n")
# Testing the Code + debugging
if __name__ == "__main__":
manager = NeighbourManager()
# Add 50 nodes dynamically
for i in range(1, 51): # Loop from 1 to 50
node_id = f"Node{i}" # Generate node ID (Node1, Node2, ..., Node50)
manager.add_or_update_neighbour(node_id)
time.sleep(0.1) # A brief sleep to avoid system crash
# Print initial state
manager.print_neighbour_table()
# Allow a brief delay to ensure the neighbours are fully added
time.sleep(2)
# Start probing neighbours
probe_thread = threading.Thread(target=manager.probe_neighbours)
probe_thread.start()
# Simulate receiving PROBE_ACK messages after some time
time.sleep(3)
manager.receive_probe_ack("Node1")
time.sleep(3)
manager.receive_probe_ack("Node2")
# Periodically print the table to observe updates
for _ in range(3):
time.sleep(5)
manager.print_neighbour_table()
# Stop the probing after 20 seconds
time.sleep(20)
# Stop the probing and cleanup
manager.stop()
probe_thread.join()
log("Program exited cleanly.")