tcp/ip学习之20: LwM2M协议和解析

一、LwM2M协议

LwM2M( Lightweight Machine-To-Machine )协议是由Open Mobile Alliance(OMA)定义的一套适用于物联网的轻量级协议。它使用RESTful接口,提供设备的接入、管理和通信功能,也适用于资源比较紧张的设备。LwM2M协议的架构如下图所示:
image

LwM2M协议底层使用CoAP协议传输数据和信令。而在LwM2M协议的架构中,CoAP协议可以运行在UDP或者SMS(短信)之上,通过DTLS(数据报传输层安全)来实现数据的安全传输。

LwM2M协议架构主要包含3种实体——LwM2M Bootstrap Server、LwM2M Server和LwM2M Client。

LwM2M Bootstrap Server负责引导LwM2M Client注册并接入LwM2M Server,之后LwM2M Server和LwM2M Client就可以通过协议指定的接口进行交互了。

二、编写代码

lwm2m_server.py

#!/usr/bin/env python3
import asyncio, json, time
from aiocoap import Context, Message, CHANGED, CONTENT, BAD_REQUEST
from aiocoap.resource import Resource, Site

LOCATION_COUNTER = 1
CLIENTS = {}          # location -> {ep, lt, addr, path_dict, last_seen}

class CatchAll(Resource):
    async def render_post(self, request):
        print('[CatchAll] uri_path=', request.opt.uri_path,
              'query=', request.opt.uri_query)
        return Message(code=CHANGED)

class RdResource(Resource):
    """统一处理  /rd  和  /rd/<loc>  """
    async def render_post(self, request):
        print(f'[S] raw uri_path = {request.opt.uri_path}')
        print(f'[S] raw uri_query = {request.opt.uri_query}')
        request.opt.uri_path = '/rd'
        if len(request.opt.uri_path) == 1:          # /rd
            return await self.register(request)
        elif len(request.opt.uri_path) == 2:        # /rd/<loc>
            return await self.update(request)
        return Message(code=BAD_REQUEST)


    async def render_get(self, request):
        if len(request.opt.uri_path) == 2:          # /rd/<loc>
            return await self.discover(request)
        return Message(code=BAD_REQUEST)

    # ---------- 内部 ----------
    async def register(self, request):
        global LOCATION_COUNTER, CLIENTS
        # 安全解析 query
        qs = {}
        for item in request.opt.uri_query:
            if '=' in item:
                k, v = item.split('=', 1)
                qs[k] = v
        ep = qs.get('ep')
        lt = int(qs.get('lt', 300))
        if not ep:
            return Message(code=BAD_REQUEST)

        location = f'rd{LOCATION_COUNTER}'
        LOCATION_COUNTER += 1

        # 安全解析 payload
        try:
            link = json.loads(request.payload) if request.payload else {}
        except json.JSONDecodeError:
            link = {}

        addr = str(request.remote)  # 兼容 0.4.7
        CLIENTS[location] = dict(ep=ep, lt=lt, addr=addr,
                                 path_dict=link, last_seen=time.time())
        print(f"[S] Registered {ep=} -> location={location}  addr={addr}")
        return Message(code=CHANGED, location_path=[location])


    async def update(self, request):
        loc = request.opt.uri_path[1]
        if loc not in CLIENTS:
            return Message(code=BAD_REQUEST)
        CLIENTS[loc]['last_seen'] = time.time()
        print(f"[S] Update from {loc}")
        return Message(code=CHANGED)

    async def discover(self, request):
        loc = request.opt.uri_path[1]
        if loc not in CLIENTS:
            return Message(code=BAD_REQUEST)
        payload = json.dumps(CLIENTS[loc]['path_dict']).encode()
        return Message(code=CONTENT, payload=payload)

class Obj3311(Resource):
    """GET /3311/0/5700"""
    async def render_get(self, request):
        print("Obj3311  render_get")
        for loc, info in CLIENTS.items():
            if info['path_dict'].get("3311/0/5700"):
                val = info.get('sensor_value', 23.5)
                return Message(code=CONTENT, payload=str(val).encode())
        return Message(code=BAD_REQUEST)

async def main():
    site = Site()
    site.add_resource(['rd'], RdResource())
    site.add_resource(['3311', '0', '5700'], Obj3311())
    site.add_resource([], CatchAll)
    await Context.create_server_context(site, bind=("127.0.0.1", 5684))
    print("[S] LwM2M server on udp:5684")
    await asyncio.get_running_loop().create_future()

if __name__ == "__main__":
    asyncio.run(main())

lwm2m_client.py

#!/usr/bin/env python3
import asyncio, json, time
from aiocoap import Context, Message, POST, GET

SERVER   = "coap://127.0.0.1:5684"
ENDPOINT = "python-client-01"
LOCATION = None

# ---------------- 资源模型 ----------------
class Sensor:
    value = 23.5
    unit  = "°C"
    min_v = 0.0
    max_v = 100.0
sensor = Sensor()

def build_link():
    return {"3311/0/5700": {"value": sensor.value},
            "3311/0/5701": {"value": sensor.unit},
            "3311/0/5601": {"value": sensor.min_v, "writable": True},
            "3311/0/5602": {"value": sensor.max_v, "writable": True}}

# ---------------- 协议流程 ----------------
async def register(ctx):
    global LOCATION
    payload = json.dumps(build_link()).encode()
    req = Message(code=POST,
                  uri='coap://127.0.0.1:5684/rd',
                  payload=payload,
                  uri_query=['ep=python-client-01', 'lt=60', 'lwm2m=1.0'],
                  token=b'')  # ← 强制空 Token
    print('[C] sending to', req.opt.uri_path, req.opt.uri_query)  # 调试用
    rsp = await ctx.request(req).response
    if rsp.code != 2.01:
        # raise RuntimeError(f'registration failed: {rsp.code}')
        print(f"warnning!!! rsp.code = {rsp.code}")
        pass
    LOCATION = '/'.join(rsp.opt.location_path)
    print(f'[C] Registered, location={LOCATION}')

async def update(ctx):
    while True:
        await asyncio.sleep(30)
        req = Message(code=POST, uri=f"{SERVER}/{LOCATION}")
        await ctx.request(req).response
        print("[C] Update sent")

async def read_sensor(ctx):
    while True:
        await asyncio.sleep(10)
        sensor.value = round(sensor.value + 0.5, 1)
        print(f"[C] sensor now {sensor.value}{sensor.unit}")

async def main():
    ctx = await Context.create_client_context()
    await register(ctx)
    await asyncio.gather(update(ctx), read_sensor(ctx))

if __name__ == "__main__":
    asyncio.run(main())


posted @ 2025-10-20 09:00  cupid8505  阅读(160)  评论(0)    收藏  举报