本地运行
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
Cassiamqtt API Mock
需要调试 Cassiamqtt API 相关的代码,可以参考下面使用 mqtt_as 实现,用于快速测试基本逻辑功能。
备注
仅用于本地调试代码基本逻辑,勿用于压力测试和稳定性测试
脚本实际运行结果,请以M2000网关内运行为准
1"""
2Module name: cassiamqtt.py
3Purpose: Local MicroPython debugging mock. Exposes the same high-level API as the real gateway by forwarding calls to mqtt_as 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.
6"""
7
8import asyncio
9
10from mqtt_as import MQTTClient
11from cassia_log import get_logger
12
13
14def _parse_uri(uri):
15 try:
16 host_port = uri.split("://", 1)[1]
17 host, port = host_port.rsplit(":", 1)
18 return {
19 "host": host,
20 "port": port,
21 }
22 except (IndexError, ValueError):
23 return None
24
25
26class CassiaMQTTClient:
27 def __init__(self, uri, username=None, password=None, client_id=None):
28 self.log = get_logger("__mock_cassiamqtt__")
29
30 addr = _parse_uri(uri)
31 if addr is None:
32 self.log.warn(f"parse uri failed: {uri}")
33 return
34
35 config = {
36 "client_id": client_id,
37 "server": addr.get("host", None),
38 "port": addr.get("port", None),
39 "user": username,
40 "password": password,
41 "keepalive": 60,
42 "ping_interval": 0,
43 "ssl": False,
44 "ssl_params": {},
45 "response_time": 10,
46 "clean_init": True,
47 "clean": True,
48 "max_repubs": 4,
49 "will": None,
50 "subs_cb": lambda *_: None,
51 "ssid": None,
52 "wifi_pw": None,
53 "queue_len": 64,
54 "gateway": False,
55 "mqttv5": False,
56 "mqttv5_con_props": None,
57 }
58
59 MQTTClient.DEBUG = True
60 self.client = MQTTClient(config=config)
61
62 async def __aenter__(self):
63 while True:
64 try:
65 self.log.info("connect start...")
66 await self.client.connect()
67 self.log.info("connect ok")
68 break
69 except Exception as e:
70 self.log.error(f"connect failed: {e}, wait next retry...")
71 await asyncio.sleep(3)
72 return self
73
74 async def __aexit__(self, exc_type, exc_val, exc_tb):
75 self.log.info("disconnect start")
76 await self.client.disconnect()
77 self.client = None
78 self.log.info("disconnect ok")
79
80 def __aiter__(self):
81 return self
82
83 async def __anext__(self):
84 if self.client is None:
85 raise StopAsyncIteration
86
87 (topic, msg, retained) = await self.client.queue.__anext__()
88
89 return {
90 "topic": topic.decode(),
91 "payload": msg.decode(),
92 "qos": 0,
93 }
94
95 async def publish(self, topic, payload: str, qos=0, retain=False):
96 try:
97 self.log.info(f"pub start: {topic} {qos} {retain} {payload[:32]}...")
98 await self.client.publish(topic, payload, qos=qos, retain=retain)
99 self.log.info(f"pub ok")
100 return True, ""
101 except Exception as e:
102 self.log.warn(f"pub failed: {e}")
103 return False, e
104
105 async def subscribe(self, topic, qos=0):
106 try:
107 self.log.info(f"sub start: {topic} {qos}")
108 await self.client.subscribe(topic, qos=qos)
109 self.log.info(f"sub ok")
110 return True, ""
111 except Exception as e:
112 self.log.warn(f"sub failed: {e}")
113 return False, e
114
115 async def unsubscribe(self, topic):
116 try:
117 self.log.info(f"unsub start: {topic}")
118 await self.client.unsubscribe(topic)
119 self.log.info(f"unsub ok")
120 return True, ""
121 except Exception as e:
122 self.log.warn(f"unsub failed: {e}")
123 return False, e