Run Locally
micropython supports multiple platforms and can be used to run and test the basic APP logic locally.
System Information
cat /etc/issue
Ubuntu 22.04 LTS \n \l
uname -a
Linux VM-0-9-ubuntu 5.15.0-106-generic
Download Source Code
git clone --branch v1.24.1 --depth 1 https://github.com/micropython/micropython.git
git submodule update --init --recursive
Install Dependencies
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
Compile and Build
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
Check Version
./build-standard/micropython --version
# MicroPython v1.24.1 on 2025-09-09; linux [GCC 11.2.0] version
Prepare Script
Prepare hello.py with the following script content:
echo 'print("hello")' > hello.py
Run the Script
./build-standard/micropython hello.py
hello
Cassiablue API Mock
To debug code related to the Cassiablue API, you can refer to the following implementation using the gateway RESTful API to mock the Cassiablue API, which allows for quick testing of basic logic and functionality.
Note
仅用于本地调试代码基本逻辑,勿用于压力测试和稳定性测试
The actual script execution results should be based on operation within the M2000 gateway.
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
To debug code related to the Cassiamqtt API , you can refer to the following implementation using the mqtt_as API to mock the Cassiamqtt API , which allows for quick testing of basic logic and functionality.
Note
仅用于本地调试代码基本逻辑,勿用于压力测试和稳定性测试
The actual script execution results should be based on operation within the M2000 gateway.
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