本地运行

micropython 支持多平台,可以在本地运行测试APP基础代码逻辑。

使用系统

cat /etc/issue
Ubuntu 22.04 LTS \n \l

uname -a
Linux VM-0-9-ubuntu 5.15.0-106-generic

下载源码

git clone --branch v1.24.1 --depth 1 https://github.com/micropython/micropython.git
git submodule update --init --recursive

安装依赖

sudo apt update
sudo apt install -y build-essential git pkg-config \
    libffi-dev libssl-dev libbz2-dev liblzma-dev \
    libreadline-dev libsqlite3-dev libgdbm-dev \
    libncurses5-dev zlib1g-dev libmpdec-dev \
    libmbedtls-dev libdb5.3-dev uuid-dev

编译构建

cd micropython/ports/unix
make clean
make -j$(nproc) MICROPY_PY_USER_CMODULE=0


# LINK build-standard/micropython
#   text       data     bss     dec     hex filename
# 764607      69328    7088  841023   cd53f build-standard/micropython

查看版本

./build-standard/micropython --version

# MicroPython v1.24.1 on 2025-09-09; linux [GCC 11.2.0] version

准备脚本

准备hello.py,脚本内容如下

echo 'print("hello")' > hello.py

运行脚本

./build-standard/micropython hello.py
hello

Cassiablue API Mock

需要调试Cassiablue API相关的代码,可以参考下面使用网关RESTful API Mock Cassiablue API实现,用于快速测试基本逻辑功能。

备注

  • 仅仅用于本地调试代码基本逻辑,勿用于压力测和稳定性测试

  • 脚本实际运行结果,请以M2000网关内运行为准

  1"""
  2Module name: cassiablue.py
  3Purpose: Local MicroPython debugging mock. Exposes the same high-level API as the real gateway by forwarding calls to its REST + Server-Sent-Events interface, so you can develop and test your MicroPython logic on a PC without flashing firmware.
  4MicroPython compatibility: v1.24.1
  5Important: Do **not** copy this file into the gateway itself—the gateway already contains a native C implementation. This module is **only** for convenient desktop debugging and has no performance guarantees.
  6Usage: Change the variable `GATEWAY` to the actual IP address of your gateway.
  7"""
  8
  9GATEWAY = ""
 10
 11import json
 12import asyncio
 13
 14from cassia_log import get_logger
 15import aiohttp
 16
 17try:
 18    from typing import Optional
 19except ImportError:
 20    pass
 21
 22
 23log = get_logger("__mock_cassiablue__")
 24
 25
 26class AsyncQueue:
 27    def __init__(self):
 28        self._buffer = []
 29        self._flag = asyncio.ThreadSafeFlag()
 30
 31    def put_nowait(self, item):
 32        self._buffer.append(item)
 33        self._flag.set()
 34
 35    async def get(self):
 36        while not self._buffer:
 37            await self._flag.wait()
 38        return self._buffer.pop(0)
 39
 40
 41async def send_cmd(url, method="GET", query=None, body=None):
 42    global GATEWAY
 43    url = f"http://{GATEWAY}{url}"
 44
 45    print("send cmd start:", method, url, query, body)
 46
 47    async with aiohttp.ClientSession() as session:
 48        if body is not None:
 49            async with session.request(
 50                method=method, url=url, json=json.loads(body)
 51            ) as resp:
 52                if resp.status == 200:
 53                    text = await resp.text()
 54                    return True, text
 55                else:
 56                    text = await resp.text()
 57                    return False, text
 58        else:
 59            async with session.request(method=method, url=url) as resp:
 60                if resp.status == 200:
 61                    text = await resp.text()
 62                    return True, text
 63                else:
 64                    text = await resp.text()
 65                    return False, text
 66
 67
 68# address is a string like "00:11:22:33:44:55"
 69# params is a json string like '{"param1": "value1", "param2": "value2"}'
 70async def connect(addr, params=None):
 71    if not addr:
 72        raise ValueError("Address cannot be empty")
 73    url = "/gap/nodes/{}/connection".format(addr)
 74    return await send_cmd(url, "POST", None, body=params)
 75
 76
 77# address is a string like "00:11:22:33:44:55"
 78async def disconnect(addr):
 79    if not addr:
 80        raise ValueError("Address cannot be empty")
 81    url = "/gap/nodes/{}/connection".format(addr)
 82    return await send_cmd(url, "DELETE")
 83
 84
 85async def get_connected_devices():
 86    return await send_cmd("/gap/nodes", "GET")
 87
 88
 89async def gatt_discover(addr):
 90    if not addr:
 91        raise ValueError("Address cannot be empty")
 92    url = "/gatt/nodes/{}/services/characteristics/descriptors".format(addr)
 93    return await send_cmd(url, "GET")
 94
 95
 96async def gatt_read(addr, handle):
 97    if not addr or not handle:
 98        raise ValueError("Address and handle cannot be empty")
 99    url = "/gatt/nodes/{}/handle/{}/value".format(addr, handle)
100    return await send_cmd(url, "GET")
101
102
103async def gatt_write(addr, handle, value):
104    if not addr or not handle or value is None:
105        raise ValueError("Address, handle, and value cannot be empty")
106    url = "/gatt/nodes/{}/handle/{}/value/{}".format(addr, handle, value)
107    return await send_cmd(url, "GET")
108
109
110class SSEClient:
111    def __init__(
112        self,
113        host: str,
114        path: str,
115        reconnect_delay: int = 3,
116    ):
117        self.log = get_logger(self.__class__.__name__)
118        self.host = host
119        self.path = path
120        self.reconnect_delay = reconnect_delay
121        self.running = True
122        self.queue = AsyncQueue()
123        self.reader = None
124        self.writer = None
125
126    async def connect(self):
127        self.log.info(f"connect to sse start: {self.host}{self.path}")
128        self.reader, self.writer = await asyncio.open_connection(self.host, 80)
129        req = (
130            f"GET {self.path} HTTP/1.1\r\n"
131            f"Accept: text/event-stream\r\n"
132            f"Host: {self.host}\r\n"
133            f"Connection: keep-alive\r\n"
134            "\r\n"
135        )
136        self.writer.write(req.encode())
137        await self.writer.drain()
138        self.log.info(f"connect to sse ok: {self.host}{self.path}")
139
140    async def co_read(self):
141        while self.running:
142            try:
143                line = await self.reader.readline()
144
145                if not line:
146                    raise OSError("connection closed")
147
148                line = line.decode().strip()
149
150                if not line:
151                    continue
152
153                log.debug("raw line:", line)
154                if not (line[0] in ("{", "[") or line.startswith("data: {")):
155                    continue
156
157                log.debug("raw line:", line)
158                line = line.replace("data: ", "")
159                line = line.replace("\n", "")
160                line = line.replace("\r", "")
161                line = line.replace("\r\n", "")
162
163                try:
164                    data = json.loads(line)
165
166                    if "bdaddrs" in data:
167                        data["bdaddr"] = data["bdaddrs"][0]["bdaddr"]
168                        data["bdaddrType"] = data["bdaddrs"][0]["bdaddrType"]
169                        del data["bdaddrs"]
170
171                    self.queue.put_nowait(data)
172                except Exception as e:
173                    self.log.info("parse data error:", e, line)
174
175            except Exception as e:
176                self.log.error("sse disconnected:", e)
177                await asyncio.sleep(self.reconnect_delay)
178                self.log.info("reconnecting...")
179
180    async def stop(self):
181        self.running = False
182
183
184#######################
185# scan sse
186#######################
187
188scan_sse_client: Optional[SSEClient] = None
189
190
191class BLEScanResult:
192    def __aiter__(self):
193        return self
194
195    async def __anext__(self):
196        global scan_sse_client
197        item = await scan_sse_client.queue.get()
198        return item
199
200
201def scan_result():
202    return BLEScanResult()
203
204
205async def start_scan(query: str = None):
206    global scan_sse_client
207
208    qs = ""
209    if query is None:
210        qs = "event=1"
211    else:
212        if "event=1" not in query:
213            qs = qs + "event=1&"
214        qs = qs + query
215
216    log.info("start scan:", qs)
217
218    scan_sse_client = SSEClient(
219        host=GATEWAY,
220        path=f"/gap/nodes?{qs}",
221    )
222
223    await scan_sse_client.connect()
224
225    asyncio.create_task(scan_sse_client.co_read())
226
227    return True, "OK"
228
229
230#######################
231# notify sse
232#######################
233
234notify_sse_client: Optional[SSEClient] = None
235
236
237class BLENotifyResult:
238    def __aiter__(self):
239        return self
240
241    async def __anext__(self):
242        global notify_sse_client
243        item = await notify_sse_client.queue.get()
244        return item
245
246
247def notify_result():
248    return BLENotifyResult()
249
250
251async def start_recv_notify(query: str = None):
252    global notify_sse_client
253
254    qs = ""
255    if query is None:
256        qs = "event=1"
257    else:
258        if "event=1" not in query:
259            qs = qs + "event=1&"
260        qs = qs + query
261
262    log.info("start notify:", qs)
263
264    notify_sse_client = SSEClient(
265        host=GATEWAY,
266        path=f"/gatt/nodes?{qs}",
267    )
268
269    await notify_sse_client.connect()
270
271    asyncio.create_task(notify_sse_client.co_read())
272
273    return True, "OK"
274
275
276#######################
277# state sse
278#######################
279
280state_sse_client: Optional[SSEClient] = None
281
282
283class BLEConnectionResult:
284    def __aiter__(self):
285        return self
286
287    async def __anext__(self):
288        global state_sse_client
289        item = await state_sse_client.queue.get()
290        return item
291
292
293def connection_result():
294    return BLEConnectionResult()
295
296
297async def start_recv_connection_state():
298    global state_sse_client
299
300    log.info("start state")
301
302    state_sse_client = SSEClient(
303        host=GATEWAY,
304        path=f"/management/nodes/connection-state",
305    )
306
307    await state_sse_client.connect()
308    asyncio.create_task(state_sse_client.co_read())
309
310    return True, "OK"
311
312
313def set_gateway(ip: str):
314    global GATEWAY
315    GATEWAY = ip