docs: add project description and server setup instructions to README
This commit is contained in:
144
esp/microcoapy/observe_manager.py
Normal file
144
esp/microcoapy/observe_manager.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
CoAP Observe Manager (RFC 7641)
|
||||
|
||||
Manages observer registrations for CoAP resources on the ESP server side.
|
||||
Each resource can have multiple observers. When the resource state changes,
|
||||
all registered observers receive a notification.
|
||||
|
||||
Observer entry structure:
|
||||
{
|
||||
"ip": str, # Observer IP address
|
||||
"port": int, # Observer UDP port
|
||||
"token": bytearray, # Token from the original GET request (for matching)
|
||||
}
|
||||
"""
|
||||
|
||||
try:
|
||||
import time
|
||||
except ImportError:
|
||||
import utime as time
|
||||
|
||||
|
||||
class ObserveManager:
|
||||
"""Manages CoAP Observe subscriptions per resource URI."""
|
||||
|
||||
# Maximum observers per resource (memory constrained on ESP)
|
||||
MAX_OBSERVERS_PER_RESOURCE = 4
|
||||
# Maximum total observers across all resources
|
||||
MAX_TOTAL_OBSERVERS = 8
|
||||
|
||||
def __init__(self, debug=True):
|
||||
# Dict of resource_url -> list of observer entries
|
||||
self._observers = {}
|
||||
# Per-resource sequence counter (24-bit, wraps at 0xFFFFFF)
|
||||
self._sequence = {}
|
||||
self.debug = debug
|
||||
|
||||
def log(self, s):
|
||||
if self.debug:
|
||||
print("[observe]: " + s)
|
||||
|
||||
def register(self, resource_url, ip, port, token):
|
||||
"""Register an observer for a resource.
|
||||
|
||||
Returns True if successfully registered, False if limits exceeded.
|
||||
"""
|
||||
if resource_url not in self._observers:
|
||||
self._observers[resource_url] = []
|
||||
self._sequence[resource_url] = 0
|
||||
|
||||
observers = self._observers[resource_url]
|
||||
|
||||
# Check if this observer is already registered (same ip+port+token)
|
||||
for obs in observers:
|
||||
if obs["ip"] == ip and obs["port"] == port:
|
||||
# Update the token (re-registration)
|
||||
obs["token"] = token
|
||||
self.log("Re-registered observer {}:{} for {}".format(ip, port, resource_url))
|
||||
return True
|
||||
|
||||
# Check limits
|
||||
total = sum(len(v) for v in self._observers.values())
|
||||
if total >= self.MAX_TOTAL_OBSERVERS:
|
||||
self.log("Max total observers reached, rejecting registration")
|
||||
return False
|
||||
|
||||
if len(observers) >= self.MAX_OBSERVERS_PER_RESOURCE:
|
||||
self.log("Max observers for {} reached, rejecting".format(resource_url))
|
||||
return False
|
||||
|
||||
observers.append({
|
||||
"ip": ip,
|
||||
"port": port,
|
||||
"token": token,
|
||||
})
|
||||
|
||||
self.log("Registered observer {}:{} for {} (token={})".format(
|
||||
ip, port, resource_url, token
|
||||
))
|
||||
return True
|
||||
|
||||
def deregister(self, resource_url, ip, port):
|
||||
"""Remove an observer for a resource."""
|
||||
if resource_url not in self._observers:
|
||||
return
|
||||
|
||||
observers = self._observers[resource_url]
|
||||
self._observers[resource_url] = [
|
||||
obs for obs in observers
|
||||
if not (obs["ip"] == ip and obs["port"] == port)
|
||||
]
|
||||
|
||||
self.log("Deregistered observer {}:{} from {}".format(ip, port, resource_url))
|
||||
|
||||
def deregister_by_token(self, resource_url, token):
|
||||
"""Remove an observer by token (used when RST is received)."""
|
||||
if resource_url not in self._observers:
|
||||
return
|
||||
|
||||
observers = self._observers[resource_url]
|
||||
self._observers[resource_url] = [
|
||||
obs for obs in observers
|
||||
if obs["token"] != token
|
||||
]
|
||||
|
||||
def deregister_all(self, resource_url):
|
||||
"""Remove all observers for a resource."""
|
||||
if resource_url in self._observers:
|
||||
del self._observers[resource_url]
|
||||
del self._sequence[resource_url]
|
||||
|
||||
def get_observers(self, resource_url):
|
||||
"""Get the list of observers for a resource."""
|
||||
return self._observers.get(resource_url, [])
|
||||
|
||||
def has_observers(self, resource_url):
|
||||
"""Check if a resource has any registered observers."""
|
||||
return len(self._observers.get(resource_url, [])) > 0
|
||||
|
||||
def next_sequence(self, resource_url):
|
||||
"""Get and increment the sequence number for a resource (24-bit wrap)."""
|
||||
if resource_url not in self._sequence:
|
||||
self._sequence[resource_url] = 0
|
||||
|
||||
seq = self._sequence[resource_url]
|
||||
self._sequence[resource_url] = (seq + 1) & 0xFFFFFF
|
||||
return seq
|
||||
|
||||
def get_all_resources(self):
|
||||
"""Get all resource URLs that have observers."""
|
||||
return list(self._observers.keys())
|
||||
|
||||
def observer_count(self, resource_url=None):
|
||||
"""Get observer count. If resource_url is None, returns total count."""
|
||||
if resource_url:
|
||||
return len(self._observers.get(resource_url, []))
|
||||
return sum(len(v) for v in self._observers.values())
|
||||
|
||||
def summary(self):
|
||||
"""Return a summary string of all observer registrations."""
|
||||
parts = []
|
||||
for url, observers in self._observers.items():
|
||||
addrs = ["{}:{}".format(o["ip"], o["port"]) for o in observers]
|
||||
parts.append("{} -> [{}]".format(url, ", ".join(addrs)))
|
||||
return "; ".join(parts) if parts else "(no observers)"
|
||||
Reference in New Issue
Block a user