Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b475542

Browse files
committedFeb 17, 2025
Initial commit.
0 parents  commit b475542

33 files changed

+2222
-0
lines changed
 

‎LICENSE.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright (c) Quectel Wireless Solution, Co., Ltd.All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

‎code/ai_main.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import sim
2+
from usr.ui import SelectWindow, ChatWindow
3+
from usr.jobs import scheduler
4+
from usr import pypubsub as pub
5+
6+
import dataCall
7+
import utime as time
8+
import TiktokRTC
9+
import atcmd
10+
import _thread
11+
12+
from machine import Pin
13+
14+
PA = Pin.GPIO39
15+
16+
from machine import ExtInt
17+
from queue import Queue
18+
19+
def key1(args):
20+
global rtc_queue
21+
rtc_queue.put(1)
22+
23+
def key2(args):
24+
global rtc_queue
25+
rtc_queue.put(2)
26+
27+
def enable_pid2():
28+
resp = bytearray(50)
29+
atcmd.sendSync('AT+qicsgp=2,1,3gnet,"","",0\r\n',resp,'',20)
30+
atcmd.sendSync('at+cgact=1,2\r\n',resp,'',20)
31+
32+
def ai_callback(args):
33+
global GPIO39
34+
event = args[0]
35+
msg = args[1]
36+
global chat_win
37+
if event == 1:
38+
print('TIKTOK_RTC_EVENT_START')
39+
GPIO39.write(1)
40+
chat_win.update_status("Please speak to me")
41+
elif event == 2:
42+
print('TIKTOK_RTC_EVENT_STOP')
43+
GPIO39.write(0)
44+
elif event == 3:
45+
#chat_win.update_status("AI speaking . . .")
46+
print('TIKTOK_RTC_EVENT_TTS_TEXT {}'.format(msg))
47+
#call.stopAudioService()
48+
elif event == 4:
49+
#chat_win.update_status("AI listening . . .")
50+
print('TIKTOK_RTC_EVENT_ASR_TEXT {}'.format(msg))
51+
#call.stopAudioService()
52+
elif event == 5:
53+
print('TIKTOK_RTC_EVENT_ERROR {}'.format(msg))
54+
else:
55+
print('TIKTOK_RTC_EVENT UNKNOWN {}'.format(event))
56+
57+
def update_status_with_animation(chat_win, base_message, steps=3, delay_ms=400, final_wait=2):
58+
# 更新动画
59+
for i in range(steps + 1):
60+
chat_win.update_status(base_message + " " + " ." * i)
61+
time.sleep_ms(delay_ms)
62+
time.sleep(final_wait)
63+
64+
def perform_initialization(chat_win, tiktok):
65+
# 初始化动画加载状态
66+
print('start rtc')
67+
chat_win.show()
68+
tiktok.active(True)
69+
70+
# 需要展示的状态列表
71+
status_list = [
72+
"Connecting to the server",
73+
"Building the AI engine",
74+
"Joining the AI room",
75+
"Loading AI personality",
76+
"Creating AI characters"
77+
]
78+
79+
# 依次遍历状态并显示动画
80+
for status in status_list:
81+
update_status_with_animation(chat_win, status)
82+
def ai_task():
83+
global rtc_queue
84+
global extint1
85+
global extint2
86+
global tiktok
87+
global chat_win
88+
global selsct_win
89+
while True:
90+
lte = dataCall.getInfo(1, 0)
91+
if lte[2][0] == 1:
92+
print('lte network normal')
93+
#pub.publish('update_status', status="ready")
94+
break
95+
print('wait lte network normal...')
96+
pub.publish('update_status', status="connect network")
97+
time.sleep(3)
98+
99+
extint1.enable()
100+
extint2.enable()
101+
print('ai task running')
102+
while True:
103+
data = rtc_queue.get()
104+
print('rtc_queue key event {}'.format(data))
105+
if data == 1:
106+
perform_initialization(chat_win, tiktok)
107+
elif data == 2:
108+
print('stop rtc')
109+
selsct_win.show()
110+
tiktok.active(False)
111+
112+
113+
if __name__ == "__main__":
114+
115+
enable_pid2()
116+
117+
# 使能sim卡热插拔
118+
sim.setSimDet(1, 1)
119+
120+
# 设置按键中断
121+
extint1 = ExtInt(ExtInt.GPIO13, ExtInt.IRQ_FALLING, ExtInt.PULL_PU, key1, filter_time=50)
122+
extint2 = ExtInt(ExtInt.GPIO12, ExtInt.IRQ_FALLING, ExtInt.PULL_PU, key2, filter_time=50)
123+
124+
rtc_queue = Queue()
125+
126+
# 初始化界面
127+
selsct_win = SelectWindow()
128+
selsct_win.show()
129+
130+
chat_win = ChatWindow()
131+
132+
# 启动后台任务调度器
133+
scheduler.start()
134+
135+
print('window show over')
136+
137+
tiktok = TiktokRTC(300000, ai_callback)
138+
GPIO39 = Pin(PA, Pin.OUT, Pin.PULL_DISABLE, 0)
139+
tiktok.config(volume=6)
140+
print('volume: {}'.format(tiktok.config('volume')))
141+
142+
_thread.start_new_thread(ai_task, ())
143+
144+
145+
146+

‎code/datetime.py

Lines changed: 396 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
1+
import utime
2+
3+
4+
def is_leap_year(year):
5+
return not year % 4 if year % 100 else not year % 400
6+
7+
8+
def get_day_in_month(year, month):
9+
if month == 2:
10+
return 29 if is_leap_year(year) else 28
11+
else:
12+
return 31 if month in [1, 3, 5, 7, 8, 10, 12] else 30
13+
14+
15+
def validate_date(year, month, day):
16+
if not (isinstance(year, int) and 1 <= year <= 9999):
17+
raise ValueError("\"year\" is not valid.")
18+
if not (isinstance(month, int) and 1 <= month <= 12):
19+
raise ValueError("\"month\" is not valid.")
20+
if not (isinstance(day, int) and 1 <= day <= get_day_in_month(year, month)):
21+
raise ValueError("\"day\" is not valid.")
22+
23+
24+
def validate_time(hour, minute, second):
25+
if not (isinstance(hour, int) and 0 <= hour <= 23):
26+
raise ValueError("\"hour\" is not valid.")
27+
if not (isinstance(minute, int) and 0 <= minute <= 59):
28+
raise ValueError("\"minute\" is not valid.")
29+
if not (isinstance(second, int) and 0 <= second <= 59):
30+
raise ValueError("\"second\" is not valid.")
31+
32+
33+
class UTimeAdapter(object):
34+
"""QuecPython platform `utime` module adapter"""
35+
36+
@staticmethod
37+
def get_local_timetuple():
38+
temp = list(utime.localtime())
39+
temp[-2] = (temp[-2] + 1) % 7
40+
return tuple(temp)
41+
42+
@staticmethod
43+
def get_timestamp_from_timetuple(time_tuple):
44+
return utime.mktime(time_tuple)
45+
46+
@staticmethod
47+
def get_timetuple_from_timestamp(timestamp):
48+
temp = list(utime.localtime(timestamp))
49+
temp[-2] = (temp[-2] + 1) % 7
50+
return tuple(temp)
51+
52+
@staticmethod
53+
def get_local_timezone_offset():
54+
return utime.getTimeZone()
55+
56+
@staticmethod
57+
def set_local_timezone_offset(offset):
58+
return utime.setTimeZone(offset) == 0
59+
60+
61+
class TimeZone(object):
62+
63+
def __init__(self, offset=0, name="N/A"):
64+
self.__offset = self.__validate_offset(offset)
65+
self.__name = name
66+
67+
def __repr__(self):
68+
return "{}(offset={}, name={})".format(type(self).__name__, repr(self.offset), repr(self.name))
69+
70+
def __str__(self):
71+
return "UTC{:+02d}:00".format(self.offset)
72+
73+
@staticmethod
74+
def __validate_offset(offset):
75+
if not (isinstance(offset, int) and -12 <= offset <= 12):
76+
raise ValueError("offset should be int type and between [-12, 12].")
77+
return offset
78+
79+
@property
80+
def offset(self):
81+
return self.__offset
82+
83+
@property
84+
def name(self):
85+
return self.__name
86+
87+
88+
UTC = TimeZone(offset=0, name="UTC")
89+
90+
91+
class TimeDelta(object):
92+
93+
def __init__(self, days=0, seconds=0, hours=0, minutes=0, weeks=0):
94+
seconds += (minutes * 60 + (hours % 24 * 3600))
95+
self.__seconds = seconds % 86400
96+
self.__days = days + weeks * 7 + hours // 24 + seconds // 86400
97+
98+
def __str__(self):
99+
return "{} days, {} seconds".format(self.days, self.seconds)
100+
101+
def __add__(self, other):
102+
if isinstance(other, type(self)):
103+
return type(self)(
104+
days=self.days + other.days,
105+
seconds=self.seconds + other.seconds
106+
)
107+
elif isinstance(other, DateTime):
108+
return other + self
109+
else:
110+
raise TypeError("unsupported operand type(s) for +: \"{}\" and \"{}\"".format(type(self), type(other)))
111+
112+
def __sub__(self, other):
113+
if isinstance(other, type(self)):
114+
return type(self)(
115+
days=self.days - other.days,
116+
seconds=self.seconds - other.seconds
117+
)
118+
else:
119+
raise TypeError("unsupported operand type(s) for -: \"{}\" and \"{}\"".format(type(self), type(other)))
120+
121+
def __lt__(self, other):
122+
if not isinstance(other, type(self)):
123+
raise TypeError("unsupported operand type(s) for <: \"{}\" and \"{}\"".format(type(self), type(other)))
124+
return (self - other).total_seconds() < 0
125+
126+
def __le__(self, other):
127+
if not isinstance(other, type(self)):
128+
raise TypeError("unsupported operand type(s) for <=: \"{}\" and \"{}\"".format(type(self), type(other)))
129+
return (self - other).total_seconds() <= 0
130+
131+
def __gt__(self, other):
132+
if not isinstance(other, type(self)):
133+
raise TypeError("unsupported operand type(s) for >: \"{}\" and \"{}\"".format(type(self), type(other)))
134+
return (self - other).total_seconds() > 0
135+
136+
def __ge__(self, other):
137+
if not isinstance(other, type(self)):
138+
raise TypeError("unsupported operand type(s) for >=: \"{}\" and \"{}\"".format(type(self), type(other)))
139+
return (self - other).total_seconds() >= 0
140+
141+
def __eq__(self, other):
142+
if not isinstance(other, type(self)):
143+
raise TypeError("unsupported operand type(s) for ==: \"{}\" and \"{}\"".format(type(self), type(other)))
144+
return (self - other).total_seconds() == 0
145+
146+
def __ne__(self, other):
147+
if not isinstance(other, type(self)):
148+
raise TypeError("unsupported operand type(s) for !=: \"{}\" and \"{}\"".format(type(self), type(other)))
149+
return (self - other).total_seconds() != 0
150+
151+
@property
152+
def days(self):
153+
return self.__days
154+
155+
@property
156+
def seconds(self):
157+
return self.__seconds
158+
159+
def total_seconds(self):
160+
return self.days * 86400 + self.seconds
161+
162+
163+
class _Date(object):
164+
165+
def __init__(self, year, month=1, day=1):
166+
validate_date(year, month, day)
167+
self.__year = year
168+
self.__month = month
169+
self.__day = day
170+
171+
def __str__(self):
172+
return "{:04d}-{:02d}-{:02d}".format(self.year, self.month, self.day)
173+
174+
@property
175+
def year(self):
176+
return self.__year
177+
178+
@property
179+
def month(self):
180+
return self.__month
181+
182+
@property
183+
def day(self):
184+
return self.__day
185+
186+
187+
class _Time(object):
188+
189+
def __init__(self, hour=0, minute=0, second=0):
190+
validate_time(hour, minute, second)
191+
self.__hour = hour
192+
self.__minute = minute
193+
self.__second = second
194+
195+
def __str__(self):
196+
return "{:02d}:{:02d}:{:02d}".format(self.hour, self.minute, self.second)
197+
198+
@property
199+
def hour(self):
200+
return self.__hour
201+
202+
@property
203+
def minute(self):
204+
return self.__minute
205+
206+
@property
207+
def second(self):
208+
return self.__second
209+
210+
211+
class DateTime(object):
212+
213+
def __init__(self, year, month=1, day=1, hour=0, minute=0, second=0, weekday=None, yearday=None, tz=None):
214+
self.__date = _Date(year, month, day)
215+
self.__time = _Time(hour, minute, second)
216+
self.__tz = tz
217+
self.__weekday = weekday
218+
self.__yearday = yearday
219+
self.__timestamp = None
220+
221+
def __repr__(self):
222+
return "{}({}, {}, {}, {}, {}, {}, tz={})".format(
223+
type(self).__name__,
224+
self.year, self.month, self.day,
225+
self.hour, self.minute, self.second,
226+
repr(self.tz)
227+
)
228+
229+
def __str__(self):
230+
return "{} {}".format(self.date, self.time) + (" {}".format(self.tz) if self.tz else "")
231+
232+
@property
233+
def year(self):
234+
return self.date.year
235+
236+
@property
237+
def month(self):
238+
return self.date.month
239+
240+
@property
241+
def day(self):
242+
return self.date.day
243+
244+
@property
245+
def hour(self):
246+
return self.time.hour
247+
248+
@property
249+
def minute(self):
250+
return self.time.minute
251+
252+
@property
253+
def second(self):
254+
return self.time.second
255+
256+
@property
257+
def date(self):
258+
return self.__date
259+
260+
@property
261+
def time(self):
262+
return self.__time
263+
264+
@property
265+
def tz(self):
266+
return self.__tz
267+
268+
def __get_year_and_weekday(self):
269+
days = 0
270+
for m in range(1, self.month):
271+
if m == 2:
272+
days += (29 if is_leap_year(self.year) else 28)
273+
else:
274+
days += (31 if m in [1, 3, 5, 7, 8, 10, 12] else 30)
275+
self.__yearday = days + self.day
276+
year = self.year - 1
277+
self.__weekday = (year + year // 4 - year // 100 + year // 400 + self.__yearday) % 7
278+
279+
@property
280+
def weekday(self):
281+
if self.__weekday is None:
282+
self.__get_year_and_weekday()
283+
return self.__weekday
284+
285+
@property
286+
def yearday(self):
287+
if self.__yearday is None:
288+
self.__get_year_and_weekday()
289+
return self.__yearday
290+
291+
@property
292+
def timetuple(self):
293+
return self.year, self.month, self.day, self.hour, self.minute, self.second
294+
295+
@property
296+
def timestamp(self):
297+
if self.__timestamp is None:
298+
self.__timestamp = UTimeAdapter.get_timestamp_from_timetuple(self.timetuple + (None, None))
299+
return self.__timestamp
300+
301+
@classmethod
302+
def from_timestamp(cls, timestamp, tz=None):
303+
time_tuple = UTimeAdapter.get_timetuple_from_timestamp(timestamp)
304+
return cls(*time_tuple, tz=tz)
305+
306+
@classmethod
307+
def utc_now(cls):
308+
return cls.now(tz=UTC)
309+
310+
@classmethod
311+
def now(cls, tz=None):
312+
if not isinstance(tz, (TimeZone, type(None))):
313+
raise TypeError("\"tz\" should be TimeZone type or None.")
314+
timetuple = UTimeAdapter.get_local_timetuple()
315+
self = cls(
316+
*timetuple,
317+
tz=TimeZone(offset=UTimeAdapter.get_local_timezone_offset())
318+
)
319+
if tz:
320+
self = self.astimezone(tz)
321+
return self
322+
323+
def replace(self, year=None, month=None, day=None, hour=None,
324+
minute=None, second=None, tz=None):
325+
if year is None:
326+
year = self.year
327+
if month is None:
328+
month = self.month
329+
if day is None:
330+
day = self.day
331+
if hour is None:
332+
hour = self.hour
333+
if minute is None:
334+
minute = self.minute
335+
if second is None:
336+
second = self.second
337+
if tz is None:
338+
tz = self.tz
339+
return type(self)(year, month, day, hour, minute, second, tz=tz)
340+
341+
def astimezone(self, tz=None):
342+
if not (isinstance(tz, TimeZone) and isinstance(self.tz, TimeZone)):
343+
raise TypeError("can not convert without timezone information.")
344+
return (self - TimeDelta(hours=self.tz.offset) + TimeDelta(hours=tz.offset)).replace(tz=tz)
345+
346+
def __sub__(self, other):
347+
if isinstance(other, TimeDelta):
348+
time_tuple = UTimeAdapter.get_timetuple_from_timestamp(self.timestamp - other.total_seconds())
349+
return type(self)(
350+
*time_tuple,
351+
tz=self.tz
352+
)
353+
elif isinstance(other, type(self)):
354+
seconds = self.timestamp - other.timestamp
355+
if all((self.tz, other.tz)):
356+
seconds -= (self.tz.offset - other.tz.offset) * 3600
357+
return TimeDelta(seconds=seconds)
358+
else:
359+
raise TypeError("unsupported operand type(s) for -: \"{}\" and \"{}\"".format(type(self), type(other)))
360+
361+
def __add__(self, other):
362+
if isinstance(other, TimeDelta):
363+
time_tuple = UTimeAdapter.get_timetuple_from_timestamp(self.timestamp + other.total_seconds())
364+
return type(self)(*time_tuple, tz=self.tz)
365+
else:
366+
raise TypeError("unsupported operand type(s) for +: \"{}\" and \"{}\"".format(type(self), type(other)))
367+
368+
def __lt__(self, other):
369+
if not isinstance(other, type(self)):
370+
raise TypeError("unsupported operand type(s) for <: \"{}\" and \"{}\"".format(type(self), type(other)))
371+
return (self - other).total_seconds() < 0
372+
373+
def __le__(self, other):
374+
if not isinstance(other, type(self)):
375+
raise TypeError("unsupported operand type(s) for <=: \"{}\" and \"{}\"".format(type(self), type(other)))
376+
return (self - other).total_seconds() <= 0
377+
378+
def __gt__(self, other):
379+
if not isinstance(other, type(self)):
380+
raise TypeError("unsupported operand type(s) for >: \"{}\" and \"{}\"".format(type(self), type(other)))
381+
return (self - other).total_seconds() > 0
382+
383+
def __ge__(self, other):
384+
if not isinstance(other, type(self)):
385+
raise TypeError("unsupported operand type(s) for >=: \"{}\" and \"{}\"".format(type(self), type(other)))
386+
return (self - other).total_seconds() >= 0
387+
388+
def __eq__(self, other):
389+
if not isinstance(other, type(self)):
390+
raise TypeError("unsupported operand type(s) for ==: \"{}\" and \"{}\"".format(type(self), type(other)))
391+
return (self - other).total_seconds() == 0
392+
393+
def __ne__(self, other):
394+
if not isinstance(other, type(self)):
395+
raise TypeError("unsupported operand type(s) for !=: \"{}\" and \"{}\"".format(type(self), type(other)))
396+
return (self - other).total_seconds() != 0

‎code/img/battery/bat_00.png

350 Bytes
Loading

‎code/img/battery/bat_01.png

219 Bytes
Loading

‎code/img/battery/bat_02.png

221 Bytes
Loading

‎code/img/battery/bat_03.png

218 Bytes
Loading

‎code/img/battery/bat_04.png

221 Bytes
Loading

‎code/img/battery/bat_05.png

220 Bytes
Loading

‎code/img/battery/bat_06.png

219 Bytes
Loading

‎code/img/battery/bat_07.png

213 Bytes
Loading

‎code/img/battery/bat_08.png

276 Bytes
Loading

‎code/img/battery/bat_09.png

437 Bytes
Loading

‎code/img/image1.png

5.53 KB
Loading

‎code/img/image1_80.png

5.49 KB
Loading

‎code/img/image2.png

8.27 KB
Loading

‎code/img/image2_80.png

7.73 KB
Loading

‎code/img/signal/signal_00.png

364 Bytes
Loading

‎code/img/signal/signal_01.png

180 Bytes
Loading

‎code/img/signal/signal_02.png

179 Bytes
Loading

‎code/img/signal/signal_03.png

176 Bytes
Loading

‎code/img/signal/signal_04.png

172 Bytes
Loading

‎code/img/signal/signal_05.png

157 Bytes
Loading

‎code/jobs.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import net
2+
from usr import pypubsub as pub
3+
from usr.datetime import DateTime
4+
from usr.scheduler import Scheduler
5+
6+
7+
# 任务调度器
8+
scheduler = Scheduler()
9+
10+
11+
@scheduler.task(interval=10)
12+
def update_signal():
13+
result = net.csqQueryPoll() # 0~31
14+
csq = 0 if result == 99 or result == -1 else result
15+
pub.publish("update_signal", level=csq // 6)
16+
17+
18+
@scheduler.task(interval=5)
19+
def update_time():
20+
now = DateTime.now()
21+
pub.publish("update_time", time="{:02d}:{:02d}".format(now.hour, now.minute))

‎code/lcd_config.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
INIT_RAW_DATA = (
2+
2, 0, 120,
3+
0, 0, 0x11,
4+
0, 1, 0x36,
5+
1, 1, 0x00,
6+
# 0, 1, 0x36,
7+
# 1, 1, 0x00,
8+
0, 1, 0x3A,
9+
1, 1, 0x05,
10+
0, 0, 0x21,
11+
0, 5, 0xB2,
12+
1, 1, 0x05,
13+
1, 1, 0x05,
14+
1, 1, 0x00,
15+
1, 1, 0x33,
16+
1, 1, 0x33,
17+
0, 1, 0xB7,
18+
1, 1, 0x23,
19+
0, 1, 0xBB,
20+
1, 1, 0x22,
21+
0, 1, 0xC0,
22+
1, 1, 0x2C,
23+
0, 1, 0xC2,
24+
1, 1, 0x01,
25+
0, 1, 0xC3,
26+
1, 1, 0x13,
27+
0, 1, 0xC4,
28+
1, 1, 0x20,
29+
0, 1, 0xC6,
30+
1, 1, 0x0F,
31+
0, 2, 0xD0,
32+
1, 1, 0xA4,
33+
1, 1, 0xA1,
34+
0, 1, 0xD6,
35+
1, 1, 0xA1,
36+
0, 14, 0xE0,
37+
1, 1, 0x70,
38+
1, 1, 0x06,
39+
1, 1, 0x0C,
40+
1, 1, 0x08,
41+
1, 1, 0x09,
42+
1, 1, 0x27,
43+
1, 1, 0x2E,
44+
1, 1, 0x34,
45+
1, 1, 0x46,
46+
1, 1, 0x37,
47+
1, 1, 0x13,
48+
1, 1, 0x13,
49+
1, 1, 0x25,
50+
1, 1, 0x2A,
51+
0, 14, 0xE1,
52+
1, 1, 0x70,
53+
1, 1, 0x04,
54+
1, 1, 0x08,
55+
1, 1, 0x09,
56+
1, 1, 0x07,
57+
1, 1, 0x03,
58+
1, 1, 0x2C,
59+
1, 1, 0x42,
60+
1, 1, 0x42,
61+
1, 1, 0x38,
62+
1, 1, 0x14,
63+
1, 1, 0x14,
64+
1, 1, 0x27,
65+
1, 1, 0x2C,
66+
0, 0, 0x29,
67+
0, 4, 0x2a,
68+
1, 1, 0x00,
69+
1, 1, 0x00,
70+
1, 1, 0x00,
71+
1, 1, 0xef,
72+
0, 4, 0x2b,
73+
1, 1, 0x00,
74+
1, 1, 0x00,
75+
1, 1, 0x01,
76+
1, 1, 0x3f,
77+
0, 0, 0x2c,
78+
79+
)
80+
81+
XSTART_H = 0xf0
82+
XSTART_L = 0xf1
83+
YSTART_H = 0xf2
84+
YSTART_L = 0xf3
85+
XEND_H = 0xE0
86+
XEND_L = 0xE1
87+
YEND_H = 0xE2
88+
YEND_L = 0xE3
89+
90+
XSTART = 0xD0
91+
XEND = 0xD1
92+
YSTART = 0xD2
93+
YEND = 0xD3
94+
95+
96+
INIT_DATA = bytearray(INIT_RAW_DATA)
97+
98+
99+
INVALID_DATA = bytearray((
100+
0, 4, 0x2a,
101+
1, 1, XSTART_H,
102+
1, 1, XSTART_L,
103+
1, 1, XEND_H,
104+
1, 1, XEND_L,
105+
0, 4, 0x2b,
106+
1, 1, YSTART_H,
107+
1, 1, YSTART_L,
108+
1, 1, YEND_H,
109+
1, 1, YEND_L,
110+
0, 0, 0x2c,
111+
))
112+
113+
114+
DISPLAY_OFF_DATA = bytearray((
115+
0, 0, 0x28,
116+
2, 0, 120,
117+
0, 0, 0x10,
118+
))
119+
120+
121+
DISPLAY_ON_DATA = bytearray((
122+
0, 0, 0x11,
123+
2, 0, 20,
124+
0, 0, 0x29,
125+
))
126+
127+
128+
LCD_WIDTH = 240
129+
LCD_HEIGHT = 240

‎code/logging.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import utime
2+
import sys
3+
import uio as io
4+
import _thread
5+
6+
7+
class Level(object):
8+
DEBUG = 0
9+
INFO = 1
10+
WARN = 2
11+
ERROR = 3
12+
CRITICAL = 4
13+
14+
15+
_levelToName = {
16+
Level.CRITICAL: "CRITICAL",
17+
Level.ERROR: "ERROR",
18+
Level.WARN: "WARN",
19+
Level.INFO: "INFO",
20+
Level.DEBUG: "DEBUG"
21+
}
22+
23+
_nameToLevel = {
24+
"CRITICAL": Level.CRITICAL,
25+
"ERROR": Level.ERROR,
26+
"WARN": Level.WARN,
27+
"INFO": Level.INFO,
28+
"DEBUG": Level.DEBUG,
29+
}
30+
31+
32+
def getLevelName(level):
33+
if level not in _levelToName:
34+
raise ValueError("unknown level \"{}\", choose from <class Level>.".format(level))
35+
return _levelToName[level]
36+
37+
38+
def getNameLevel(name):
39+
temp = name.upper()
40+
if temp not in _nameToLevel:
41+
raise ValueError("\"{}\" is not valid. choose from [{}]".format(name, list(_nameToLevel.keys())))
42+
return _nameToLevel[temp]
43+
44+
45+
class BasicConfig(object):
46+
logger_register_table = {}
47+
basic_configure = {
48+
"level": Level.WARN,
49+
"debug": True,
50+
"stream": sys.stdout
51+
}
52+
53+
@classmethod
54+
def getLogger(cls, name):
55+
if name not in cls.logger_register_table:
56+
logger = Logger(name)
57+
cls.logger_register_table[name] = logger
58+
else:
59+
logger = cls.logger_register_table[name]
60+
return logger
61+
62+
@classmethod
63+
def update(cls, **kwargs):
64+
level = kwargs.pop("level", None)
65+
if level is not None:
66+
kwargs["level"] = getNameLevel(level)
67+
return cls.basic_configure.update(kwargs)
68+
69+
@classmethod
70+
def get(cls, key):
71+
return cls.basic_configure[key]
72+
73+
@classmethod
74+
def set(cls, key, value):
75+
if key == "level":
76+
value = getNameLevel(value)
77+
cls.basic_configure[key] = value
78+
79+
80+
class Logger(object):
81+
lock = _thread.allocate_lock()
82+
83+
def __init__(self, name):
84+
self.name = name
85+
86+
@staticmethod
87+
def __getFormattedTime():
88+
# (2023, 9, 30, 11, 11, 41, 5, 273)
89+
cur_time_tuple = utime.localtime()
90+
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
91+
cur_time_tuple[0],
92+
cur_time_tuple[1],
93+
cur_time_tuple[2],
94+
cur_time_tuple[3],
95+
cur_time_tuple[4],
96+
cur_time_tuple[5]
97+
)
98+
99+
def log(self, level, *message):
100+
if not BasicConfig.get("debug"):
101+
if level < BasicConfig.get("level"):
102+
return
103+
stream = BasicConfig.get("stream")
104+
prefix = "[{}][{}][{}]".format(
105+
self.__getFormattedTime(),
106+
getLevelName(level),
107+
self.name,
108+
)
109+
with self.lock:
110+
print(prefix, *message, file=stream)
111+
if isinstance(stream, io.TextIOWrapper):
112+
stream.flush()
113+
114+
def debug(self, *message):
115+
self.log(Level.DEBUG, *message)
116+
117+
def info(self, *message):
118+
self.log(Level.INFO, *message)
119+
120+
def warn(self, *message):
121+
self.log(Level.WARN, *message)
122+
123+
def error(self, *message):
124+
self.log(Level.ERROR, *message)
125+
126+
def critical(self, *message):
127+
self.log(Level.CRITICAL, *message)
128+
129+
130+
def getLogger(name):
131+
return BasicConfig.getLogger(name)

‎code/pypubsub.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""基于 QuecPython 的订阅/发布机制"""
2+
3+
4+
from usr.threading import Thread, Queue, Lock
5+
6+
7+
class Publisher(object):
8+
9+
def __init__(self):
10+
self.__q = Queue()
11+
self.__topic_manager_lock = Lock()
12+
self.__topic_manager = {}
13+
self.__listen_thread = Thread(target=self.__listen_worker)
14+
15+
def listen(self):
16+
self.__listen_thread.start()
17+
18+
def __listen_worker(self):
19+
while True:
20+
topic, messages = self.__q.get()
21+
# print("topic: {}, messages: {}".format(topic, messages))
22+
with self.__topic_manager_lock:
23+
for listener in self.__topic_manager.setdefault(topic, []):
24+
try:
25+
listener(**messages)
26+
except Exception as e:
27+
print("listener error:", str(e))
28+
29+
def publish(self, topic, **kwargs):
30+
self.__q.put((topic, kwargs))
31+
32+
def subscribe(self, topic, listener):
33+
with self.__topic_manager_lock:
34+
listener_list = self.__topic_manager.setdefault(topic, [])
35+
listener_list.append(listener)
36+
37+
def unsubscribe(self, topic, listener):
38+
with self.__topic_manager_lock:
39+
listener_list = self.__topic_manager.setdefault(topic, [])
40+
try:
41+
listener_list.remove(listener)
42+
except ValueError:
43+
pass
44+
45+
46+
# global publisher
47+
__publisher__ = None
48+
49+
50+
def get_default_publisher():
51+
global __publisher__
52+
if __publisher__ is None:
53+
__publisher__ = Publisher()
54+
__publisher__.listen()
55+
return __publisher__
56+
57+
58+
59+
def publish(topic, **kwargs):
60+
"""订阅消息"""
61+
pub = get_default_publisher()
62+
pub.publish(topic, **kwargs)
63+
64+
65+
def subscribe(topic, listener):
66+
"""订阅消息"""
67+
pub = get_default_publisher()
68+
pub.subscribe(topic, listener)
69+
70+
71+
def unsubscribe(topic, listener):
72+
"""取消订阅消息"""
73+
pub = get_default_publisher()
74+
pub.unsubscribe(topic, listener)

‎code/scheduler.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import sys
2+
from usr.threading import Thread, Condition, AsyncTask
3+
from usr.datetime import DateTime, TimeDelta
4+
5+
6+
class HeapAlgorithm(object):
7+
8+
@classmethod
9+
def __siftdown(cls, heap, startpos, pos):
10+
newitem = heap[pos]
11+
while pos > startpos:
12+
parentpos = (pos - 1) >> 1
13+
parent = heap[parentpos]
14+
if newitem < parent:
15+
heap[pos] = parent
16+
pos = parentpos
17+
continue
18+
break
19+
heap[pos] = newitem
20+
21+
@classmethod
22+
def __siftup(cls, heap, pos):
23+
endpos = len(heap)
24+
startpos = pos
25+
newitem = heap[pos]
26+
childpos = 2 * pos + 1
27+
while childpos < endpos:
28+
rightpos = childpos + 1
29+
if rightpos < endpos and not heap[childpos] < heap[rightpos]:
30+
childpos = rightpos
31+
heap[pos] = heap[childpos]
32+
pos = childpos
33+
childpos = 2 * pos + 1
34+
heap[pos] = newitem
35+
cls.__siftdown(heap, startpos, pos)
36+
37+
@classmethod
38+
def heapify(cls, sequence):
39+
if not isinstance(sequence, list):
40+
raise TypeError("`sequence` must be instance of list")
41+
n = len(sequence)
42+
for i in reversed(range(n // 2)):
43+
cls.__siftup(sequence, i)
44+
45+
@classmethod
46+
def pop(cls, heap):
47+
lastelt = heap.pop() # raise IndexError if heap is empty
48+
if heap:
49+
returnitem = heap[0]
50+
heap[0] = lastelt
51+
cls.__siftup(heap, 0)
52+
return returnitem
53+
return lastelt
54+
55+
@classmethod
56+
def push(cls, heap, item):
57+
heap.append(item)
58+
cls.__siftdown(heap, 0, len(heap) - 1)
59+
60+
@classmethod
61+
def poppush(cls, heap, item):
62+
"""more efficient than pop() followed by push()"""
63+
returnitem = heap[0] # raise IndexError if heap is empty
64+
heap[0] = item
65+
cls.__siftup(heap, 0)
66+
return returnitem
67+
68+
@classmethod
69+
def pushpop(cls, heap, item):
70+
"""more efficient than push() followed by pop()"""
71+
if heap and heap[0] < item:
72+
item, heap[0] = heap[0], item
73+
cls.__siftup(heap, 0)
74+
return item
75+
76+
77+
class BaseTrigger(object):
78+
MAX_CALIBRATION_TIMEOUT = 600
79+
80+
def __init__(self):
81+
self.next_run_time = None
82+
83+
def get_remaining_seconds(self):
84+
# WARN: max remaining is TimeDelta(seconds=0x20C49B)
85+
value = (self.next_run_time - DateTime.now()).total_seconds()
86+
# time calibration
87+
return value if value <= self.MAX_CALIBRATION_TIMEOUT else self.MAX_CALIBRATION_TIMEOUT
88+
89+
def update(self):
90+
"""overwritten to set self.next_run_time, if valid return True else False"""
91+
raise NotImplementedError("BaseTrigger.update must be overwritten by subclass")
92+
93+
94+
class IntervalTrigger(BaseTrigger):
95+
96+
def __init__(self, interval, start_time=None, end_time=None):
97+
self.interval = interval
98+
self.start_time = start_time
99+
self.end_time = end_time
100+
super().__init__()
101+
102+
def update(self):
103+
self.next_run_time = DateTime.now() + TimeDelta(seconds=self.interval)
104+
if self.start_time is not None and self.start_time > self.next_run_time:
105+
self.next_run_time = self.start_time
106+
return True if self.end_time is None else self.next_run_time <= self.end_time
107+
108+
109+
class DateTimeTrigger(BaseTrigger):
110+
111+
def __init__(self, datetime):
112+
self.datetime = datetime
113+
super().__init__()
114+
115+
def update(self):
116+
self.next_run_time = self.datetime
117+
return self.next_run_time > DateTime.now()
118+
119+
120+
class CronTrigger(BaseTrigger):
121+
122+
def __init__(self, hour, minute):
123+
self.hour = hour
124+
self.minute = minute
125+
super().__init__()
126+
127+
def update(self):
128+
current_time = DateTime.now()
129+
clock_time = current_time.replace(hour=self.hour, minute=self.minute, second=0)
130+
if clock_time > current_time:
131+
self.next_run_time = clock_time
132+
else:
133+
self.next_run_time = clock_time + TimeDelta(days=1)
134+
return True
135+
136+
137+
class TriggerFactory(object):
138+
139+
@staticmethod
140+
def create(interval=None, datetime=None, cron=(), start_time=None, end_time=None):
141+
if all([interval, datetime, cron]):
142+
raise ValueError("can only choose one from [interval, datetime, cron]")
143+
if isinstance(interval, int) and interval > 0:
144+
trigger = IntervalTrigger(interval, start_time=start_time, end_time=end_time)
145+
elif isinstance(datetime, DateTime):
146+
trigger = DateTimeTrigger(datetime)
147+
elif isinstance(cron, (tuple, list)):
148+
trigger = CronTrigger(hour=cron[0], minute=cron[1])
149+
else:
150+
raise ValueError("can not build trigger object according to params!!!")
151+
return trigger
152+
153+
154+
class Task(object):
155+
156+
def __init__(self, title="N/A", target=None, args=(), kwargs=None, sync=True, trigger=None):
157+
if not callable(target):
158+
raise TypeError("`target` must be callable")
159+
if not isinstance(trigger, (IntervalTrigger, DateTimeTrigger, CronTrigger)):
160+
raise TypeError("`trigger` must be instance of (IntervalTrigger, DateTimeTrigger, CronTrigger)")
161+
self.title = title
162+
self.__target = target
163+
self.__args = args
164+
self.__kwargs = kwargs or {}
165+
self.trigger = trigger
166+
self.sync = sync
167+
168+
def __str__(self):
169+
return "{}(title={})".format(type(self).__name__, repr(self.title))
170+
171+
def __lt__(self, other):
172+
return self.trigger.next_run_time < other.trigger.next_run_time
173+
174+
def run(self):
175+
try:
176+
if self.sync:
177+
self.__target(*self.__args, **self.__kwargs)
178+
else:
179+
AsyncTask(target=self.__target, args=self.__args, kwargs=self.__kwargs).delay()
180+
except Exception as e:
181+
sys.print_exception(e)
182+
return False
183+
else:
184+
return True
185+
186+
187+
class Scheduler(object):
188+
189+
def __init__(self):
190+
self.__heap = []
191+
self.__cond = Condition()
192+
self.__executor_thread = Thread(target=self.__executor_thread_worker)
193+
194+
def __task_processing(self):
195+
task = HeapAlgorithm.pop(self.__heap)
196+
remaining = task.trigger.get_remaining_seconds()
197+
if remaining <= 0:
198+
task.run()
199+
if task.trigger.update():
200+
HeapAlgorithm.push(self.__heap, task)
201+
return
202+
else:
203+
HeapAlgorithm.push(self.__heap, task)
204+
return remaining
205+
206+
def __executor_thread_worker(self):
207+
while True:
208+
with self.__cond:
209+
try:
210+
remaining = self.__task_processing()
211+
if remaining is not None:
212+
self.__cond.wait(remaining)
213+
except IndexError: # heap empty
214+
self.__cond.wait()
215+
216+
def start(self):
217+
self.__executor_thread.start()
218+
219+
def reload(self):
220+
with self.__cond:
221+
for task in self.__heap:
222+
if task.trigger.update():
223+
continue
224+
self.__heap.remove(task)
225+
HeapAlgorithm.heapify(self.__heap)
226+
self.__cond.notify()
227+
228+
def cancel(self, task):
229+
if not isinstance(task, Task):
230+
raise TypeError("`task` must be instance of Task")
231+
with self.__cond:
232+
if task in self.__heap:
233+
self.__heap.remove(task)
234+
HeapAlgorithm.heapify(self.__heap)
235+
self.__cond.notify()
236+
237+
def update(self, task, **kwargs):
238+
if not isinstance(task, Task):
239+
raise TypeError("`task` must be instance of Task")
240+
with self.__cond:
241+
task.trigger = TriggerFactory.create(**kwargs)
242+
if task.trigger.update() and task in self.__heap:
243+
HeapAlgorithm.heapify(self.__heap)
244+
self.__cond.notify()
245+
246+
def add(self, task):
247+
if not isinstance(task, Task):
248+
raise TypeError("`task` must be instance of Task")
249+
with self.__cond:
250+
if task in self.__heap:
251+
raise ValueError("task {} already scheduled".format(task))
252+
if task.trigger.update():
253+
HeapAlgorithm.push(self.__heap, task)
254+
self.__cond.notify()
255+
256+
def submit(self, title="N/A", target=None, args=(), kwargs=None, sync=True, interval=None, datetime=None, cron=(),
257+
start_time=None, end_time=None):
258+
task = Task(
259+
title=title,
260+
target=target,
261+
args=args,
262+
kwargs=kwargs,
263+
sync=sync,
264+
trigger=TriggerFactory.create(
265+
interval=interval,
266+
datetime=datetime,
267+
cron=cron,
268+
start_time=start_time,
269+
end_time=end_time
270+
)
271+
)
272+
self.add(task)
273+
return task
274+
275+
def task(self, title="N/A", args=(), kwargs=None, sync=True, interval=None, datetime=None, cron=(), start_time=None, end_time=None):
276+
def wrapper(target):
277+
return self.submit(
278+
title=title,
279+
target=target,
280+
args=args,
281+
kwargs=kwargs,
282+
sync=sync,
283+
interval=interval,
284+
datetime=datetime,
285+
cron=cron,
286+
start_time=start_time,
287+
end_time=end_time
288+
)
289+
return wrapper

‎code/threading.py

Lines changed: 571 additions & 0 deletions
Large diffs are not rendered by default.

‎code/ui.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
from usr import pypubsub as pub
2+
import lvgl as lv
3+
from machine import LCD
4+
from usr.lcd_config import *
5+
from usr.jobs import update_signal, update_time
6+
7+
8+
# init lcd
9+
lcd = LCD()
10+
lcd.lcd_init(INIT_DATA, 240, 320, 26000, 1, 4, 0, INVALID_DATA, DISPLAY_ON_DATA, DISPLAY_OFF_DATA, None)
11+
12+
# init lvgl
13+
lv.init()
14+
# init display driver
15+
disp_buf = lv.disp_draw_buf_t()
16+
buf_length = LCD_WIDTH * LCD_HEIGHT * 2
17+
disp_buf.init(bytearray(buf_length), None, buf_length)
18+
# disp_buf1.init(bytearray(buf_length), bytearray(buf_length), buf_length) # 双buffer缓冲,占用过多RAM
19+
disp_drv = lv.disp_drv_t()
20+
disp_drv.init()
21+
disp_drv.draw_buf = disp_buf
22+
disp_drv.flush_cb = lcd.lcd_write
23+
disp_drv.hor_res = LCD_WIDTH
24+
disp_drv.ver_res = LCD_HEIGHT
25+
# disp_drv.sw_rotate = 1 # 此处设置是否需要旋转
26+
# disp_drv.rotated = lv.DISP_ROT._270 # 旋转角度
27+
disp_drv.register()
28+
# image cache
29+
lv.img.cache_invalidate_src(None)
30+
lv.img.cache_set_size(50)
31+
# start lvgl thread
32+
lv.tick_inc(5)
33+
lv.task_handler()
34+
35+
36+
# 创建字体
37+
arial_12_style = lv.style_t()
38+
arial_12_style.init()
39+
arial_12_style.set_text_color(lv.color_white())
40+
arial_12_style.set_text_font_v2("arial_12.bin", 18, 0)
41+
42+
43+
arial_16_style = lv.style_t()
44+
arial_16_style.init()
45+
arial_16_style.set_text_color(lv.color_white())
46+
arial_16_style.set_text_font_v2("arial_16.bin", 24, 0)
47+
48+
arial_22_style = lv.style_t()
49+
arial_22_style.init()
50+
arial_22_style.set_text_color(lv.color_white())
51+
arial_22_style.set_text_font_v2("arial_22.bin", 33, 0)
52+
53+
54+
class SelectWindow(object):
55+
56+
def __init__(self):
57+
self.obj = lv.obj(None)
58+
self.obj.set_style_bg_color(lv.color_black(), lv.PART.MAIN)
59+
60+
self.batt = lv.img(self.obj)
61+
self.batt.set_src("U:/img/battery/bat_09.png")
62+
self.batt.align(lv.ALIGN.TOP_RIGHT, -10, 10)
63+
64+
self.signal = lv.img(self.obj)
65+
self.signal.set_size(16, 16)
66+
self.signal.set_src("U:/img/signal/signal_00.png")
67+
self.signal.align(lv.ALIGN.TOP_LEFT, 10, 10)
68+
self.signal.set_offset_y(-20 * 0)
69+
pub.subscribe("update_signal", lambda level: self.set_signal_level(level))
70+
update_signal.run() # first update signal
71+
72+
self.time = lv.label(self.obj)
73+
self.time.set_text("00:00")
74+
self.time.add_style(arial_16_style, lv.PART.MAIN | lv.STATE.DEFAULT)
75+
self.time.align(lv.ALIGN.TOP_MID, 0, 10)
76+
pub.subscribe("update_time", lambda time: self.time.set_text(time))
77+
update_time.run() # first update time
78+
79+
self.main_icon = lv.img(self.obj)
80+
self.main_icon.set_src("U:/img/image1_80.png")
81+
self.main_icon.set_size(80, 80)
82+
self.main_icon.align(lv.ALIGN.TOP_RIGHT, -20, 85)
83+
84+
self.main_icon1 = lv.img(self.obj)
85+
self.main_icon1.set_src("U:/img/image2_80.png")
86+
self.main_icon1.set_size(80, 80)
87+
self.main_icon1.align(lv.ALIGN.TOP_LEFT, 20, 85)
88+
89+
self.name = lv.label(self.obj)
90+
self.name.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
91+
self.name.set_style_text_color(lv.palette_main(lv.PALETTE.BLUE), lv.PART.MAIN | lv.STATE.DEFAULT)
92+
self.name.set_text("Ding")
93+
self.name.align_to(self.main_icon, lv.ALIGN.OUT_TOP_MID, 0, -10)
94+
95+
self.name = lv.label(self.obj)
96+
self.name.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
97+
self.name.set_style_text_color(lv.palette_main(lv.PALETTE.RED), lv.PART.MAIN | lv.STATE.DEFAULT)
98+
self.name.set_text("Wei")
99+
self.name.align_to(self.main_icon1, lv.ALIGN.OUT_TOP_MID, 0, -10)
100+
101+
self.choose = lv.label(self.obj)
102+
self.choose.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
103+
self.choose.set_style_text_color(lv.palette_main(lv.PALETTE.YELLOW), lv.PART.MAIN | lv.STATE.DEFAULT)
104+
self.choose.set_text("Choose her")
105+
self.choose.align_to(self.main_icon1, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
106+
107+
self.content = lv.label(self.obj)
108+
self.content.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
109+
self.content.set_text("")
110+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
111+
112+
pub.subscribe("update_status", self.update_status)
113+
114+
def update_status(self, status):
115+
self.content.set_text(status)
116+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
117+
118+
def show(self):
119+
lv.scr_load(self.obj)
120+
121+
def set_signal_level(self, level):
122+
"""level 分 6 档, [0, 5], 其中 0 表示无信号, 5表示满信号"""
123+
self.signal.set_src("U:/img/signal/signal_{:02d}.png".format(level))
124+
125+
def set_batt_level(self, level):
126+
"""level 分 10 档, [0, 9], 其中 0 表示电池异常馈电, 8表示满电池, 9表示电池充电"""
127+
self.batt.set_src("U:/img/battery/bat_{:02d}.png".format(level))
128+
def update_status(self, status):
129+
self.content.set_text(status)
130+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
131+
132+
def show(self):
133+
lv.scr_load(self.obj)
134+
135+
def set_signal_level(self, level):
136+
"""level 分 6 档, [0, 5], 其中 0 表示无信号, 5表示满信号"""
137+
self.signal.set_src("U:/img/signal/signal_{:02d}.png".format(level))
138+
139+
def set_batt_level(self, level):
140+
"""level 分 10 档, [0, 9], 其中 0 表示电池异常馈电, 8表示满电池, 9表示电池充电"""
141+
self.batt.set_src("U:/img/battery/bat_{:02d}.png".format(level))
142+
143+
144+
class ChatWindow(object):
145+
146+
def __init__(self):
147+
self.obj = lv.obj(None)
148+
self.obj.set_style_bg_color(lv.color_black(), lv.PART.MAIN)
149+
150+
self.batt = lv.img(self.obj)
151+
self.batt.set_src("U:/img/battery/bat_09.png")
152+
self.batt.align(lv.ALIGN.TOP_RIGHT, -10, 10)
153+
154+
self.signal = lv.img(self.obj)
155+
self.signal.set_size(16, 16)
156+
self.signal.set_src("U:/img/signal/signal_00.png")
157+
self.signal.align(lv.ALIGN.TOP_LEFT, 10, 10)
158+
self.signal.set_offset_y(-20 * 0)
159+
pub.subscribe("update_signal", lambda level: self.set_signal_level(level))
160+
update_signal.run() # first update signal
161+
162+
self.time = lv.label(self.obj)
163+
self.time.set_text("00:00")
164+
self.time.add_style(arial_16_style, lv.PART.MAIN | lv.STATE.DEFAULT)
165+
self.time.align(lv.ALIGN.TOP_MID, 0, 10)
166+
pub.subscribe("update_time", lambda time: self.time.set_text(time))
167+
update_time.run() # first update time
168+
169+
self.main_icon = lv.img(self.obj)
170+
self.main_icon.set_src("U:/img/image2.png")
171+
self.main_icon.set_size(128, 128)
172+
self.main_icon.align(lv.ALIGN.CENTER, 0, 10)
173+
174+
self.name = lv.label(self.obj)
175+
self.name.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
176+
self.name.set_style_text_color(lv.palette_main(lv.PALETTE.ORANGE), lv.PART.MAIN | lv.STATE.DEFAULT)
177+
self.name.set_text("Wei")
178+
self.name.align_to(self.main_icon, lv.ALIGN.OUT_TOP_MID, 0, -10)
179+
180+
self.content = lv.label(self.obj)
181+
self.content.add_style(arial_22_style, lv.PART.MAIN | lv.STATE.DEFAULT)
182+
self.content.set_text("")
183+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
184+
185+
pub.subscribe("update_status", self.update_status)
186+
187+
def update_status(self, status):
188+
self.content.set_text(status)
189+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
190+
191+
def show(self):
192+
lv.scr_load(self.obj)
193+
194+
def set_signal_level(self, level):
195+
"""level 分 6 档, [0, 5], 其中 0 表示无信号, 5表示满信号"""
196+
self.signal.set_src("U:/img/signal/signal_{:02d}.png".format(level))
197+
198+
def set_batt_level(self, level):
199+
"""level 分 10 档, [0, 9], 其中 0 表示电池异常馈电, 8表示满电池, 9表示电池充电"""
200+
self.batt.set_src("U:/img/battery/bat_{:02d}.png".format(level))
201+
def update_status(self, status):
202+
self.content.set_text(status)
203+
self.content.align_to(self.main_icon, lv.ALIGN.OUT_BOTTOM_MID, 0, 10)
204+
205+
def show(self):
206+
lv.scr_load(self.obj)
207+
208+
def set_signal_level(self, level):
209+
"""level 分 6 档, [0, 5], 其中 0 表示无信号, 5表示满信号"""
210+
self.signal.set_src("U:/img/signal/signal_{:02d}.png".format(level))
211+
212+
def set_batt_level(self, level):
213+
"""level 分 10 档, [0, 9], 其中 0 表示电池异常馈电, 8表示满电池, 9表示电池充电"""
214+
self.batt.set_src("U:/img/battery/bat_{:02d}.png".format(level))
215+
216+
217+
if __name__ == '__main__':
218+
selsct_win = SelectWindow()
219+
chat_win = ChatWindow()
220+
selsct_win.show()
221+
chat_win.show()

‎docs/zh/media/wire_connection.jpg

546 KB
Loading

‎example/example_ai.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import dataCall
2+
import utime as time
3+
import TiktokRTC
4+
import atcmd
5+
import _thread
6+
7+
from machine import Pin
8+
9+
PA = Pin.GPIO39
10+
11+
def enable_pid2():
12+
resp = bytearray(50)
13+
atcmd.sendSync('AT+qicsgp=2,1,3gnet,"","",0\r\n',resp,'',20)
14+
atcmd.sendSync('at+cgact=1,2\r\n',resp,'',20)
15+
16+
def ai_callback(args):
17+
global GPIO39
18+
event = args[0]
19+
msg = args[1]
20+
if event == 1:
21+
print('TIKTOK_RTC_EVENT_START')
22+
GPIO39.write(1)
23+
elif event == 2:
24+
print('TIKTOK_RTC_EVENT_STOP')
25+
GPIO39.write(0)
26+
elif event == 3:
27+
print('TIKTOK_RTC_EVENT_TTS_TEXT {}'.format(msg))
28+
#call.stopAudioService()
29+
elif event == 4:
30+
print('TIKTOK_RTC_EVENT_ASR_TEXT {}'.format(msg))
31+
#call.stopAudioService()
32+
elif event == 5:
33+
print('TIKTOK_RTC_EVENT_ERROR {}'.format(msg))
34+
else:
35+
print('TIKTOK_RTC_EVENT UNKNOWN {}'.format(event))
36+
37+
38+
if __name__ == '__main__':
39+
while True:
40+
lte = dataCall.getInfo(1, 0)
41+
if lte[2][0] == 1:
42+
print('lte network normal')
43+
enable_pid2()
44+
break
45+
print('wait lte network normal...')
46+
time.sleep(3)
47+
48+
tiktok = TiktokRTC(3000000, ai_callback)
49+
GPIO39 = Pin(PA, Pin.OUT, Pin.PULL_DISABLE, 0)
50+
time.sleep(2)
51+
52+
tiktok.config(volume=6)
53+
print('volume: {}'.format(tiktok.config('volume')))
54+
55+
tiktok.active(True)

‎readme.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# QuecPython 基于豆包 webRTC 的 AI 聊天机器人
2+
3+
## 目录
4+
5+
- [介绍](#介绍)
6+
- [功能特性](#功能特性)
7+
- [快速开始](#快速开始)
8+
- [先决条件](#先决条件)
9+
- [安装](#安装)
10+
- [运行应用程序](#运行应用程序)
11+
- [目录结构](#目录结构)
12+
- [贡献](#贡献)
13+
- [许可证](#许可证)
14+
- [支持](#支持)
15+
16+
## 介绍
17+
18+
QuecPython 推出了基于豆包 webRTC 的 AI 聊天机器人解决方案。该方案基于火山的 RTC 库,并且只能使用支持 TiktokRTC 功能的固件。
19+
20+
支持该功能的模组型号如下:
21+
22+
| 系列 | 型号 |
23+
| :----- | :----------------------------------------------------------- |
24+
| EC600M | EC600MCN_LE |
25+
| EC800M | EC800MCN_LE、EC800MCN_GB |
26+
| EG810M | EG810MCN_GA_VOLTE |
27+
28+
## 功能特性
29+
30+
- 支持智能体切换。
31+
- 支持音色切换。
32+
- 支持ASR字幕。
33+
- 支持TTS字幕。
34+
- 支持语音中断/打断。
35+
- 支持服务器地址切换。
36+
- 支持语音唤醒。
37+
- 使用 Python 语言,便于二次开发。
38+
39+
## 快速开始
40+
41+
### 先决条件
42+
43+
在开始之前,请确保您具备以下先决条件:
44+
45+
- **硬件:**
46+
- [EC600MCNLE QuecPython 标准开发板](https://python.quectel.com/doc/Getting_started/zh/evb/ec600x-evb.html)(含天线、Type-C 数据线等)
47+
> 点击查看开发板的[原理图](https://images.quectel.com/python/2023/05/EC600X_EVB_V3.2-SCH.pdf)[丝印图](https://images.quectel.com/python/2023/05/EC600X_EVB_V3.2-丝印.pdf)文档。
48+
- 电脑(Windows 7、Windows 10 或 Windows 11)
49+
- LCD 显示屏
50+
- 型号:ST7789
51+
- 分辨率:240×240
52+
- 喇叭
53+
- 任意 2-5W 功率的喇叭即可
54+
55+
- **软件:**
56+
- QuecPython 模块的 USB 驱动:[QuecPython_USB_Driver_Win10_ASR](https://images.quectel.com/python/2023/04/Quectel_Windows_USB_DriverA_Customer_V1.1.13.zip)
57+
- 调试工具 [QPYcom](https://images.quectel.com/python/2022/12/QPYcom_V3.6.0.zip)
58+
- QuecPython [固件](https://github.com/QuecPython/AIChatBot-Volcengine-webRTC/releases/download/v1.0.0/EC600MCNLER06A01M08_OCPU_QPY_TEST0213.zip)
59+
- Python 文本编辑器(例如,[VSCode](https://code.visualstudio.com/)[Pycharm](https://www.jetbrains.com/pycharm/download/)
60+
61+
### 安装
62+
63+
1. **克隆仓库**
64+
```bash
65+
git clone https://github.com/QuecPython/AIChatBot-Volcengine-webRTC.git
66+
cd AIChatBot-Volcengine-webRTC
67+
```
68+
69+
2. **安装 USB 驱动**
70+
71+
3. **烧录固件:**
72+
按照[说明](https://python.quectel.com/doc/Application_guide/zh/dev-tools/QPYcom/qpycom-dw.html#%E4%B8%8B%E8%BD%BD%E5%9B%BA%E4%BB%B6)将固件烧录到开发板上。
73+
74+
> 注意:固件内火山对话 token 临时测试使用,随时可能取消,使用体验可以联系移远技术支持。
75+
> 如果自己有火山 token,可以直接通过`tiktok.config`接口配置即可。
76+
77+
### 运行应用程序
78+
79+
1. **连接硬件:**
80+
按照下图进行硬件连接:
81+
<img src="./docs/zh/media/wire_connection.jpg" style="zoom:67%;" />
82+
1. 将喇叭连接至图中标识有`SPK+``SPK-`的排针上。
83+
2. 将 LCD 屏连接至标识有 `LCD` 字样的排针上。
84+
3. 在图示位置插入可用的 Nano SIM 卡。
85+
4. 将天线连接至标识有`LTE`字样的天线连接座上。
86+
5. 使用 Type-C 数据线连接开发板和电脑。
87+
88+
2. **将代码下载到设备:**
89+
- 启动 QPYcom 调试工具。
90+
- 将数据线连接到计算机。
91+
- 按下开发板上的 **PWRKEY** 按钮启动设备。
92+
- 按照[说明](https://python.quectel.com/doc/Application_guide/zh/dev-tools/QPYcom/qpycom-dw.html#%E4%B8%8B%E8%BD%BD%E8%84%9A%E6%9C%AC)`code` 文件夹中的所有文件导入到模块的文件系统中,保留目录结构。
93+
94+
3. **运行应用程序:**
95+
- 选择 `File` 选项卡。
96+
- 选择 `ai_main.py` 脚本。
97+
- 右键单击并选择 `Run` 或使用`运行`快捷按钮执行脚本。
98+
99+
4. **参考运行日志:**
100+
```python
101+
import example
102+
>>> example.exec('/usr/ai_main.py')
103+
window show over
104+
volume: 6
105+
>>> lte network normal
106+
ai task running
107+
108+
# 按KEY1键进入智能体
109+
rtc_queue key event 1
110+
start rtc
111+
TIKTOK_RTC_EVENT_START
112+
TIKTOK_RTC_EVENT_TTS_TEXT
113+
TIKTOK_RTC_EVENT_TTS_TEXT 你好
114+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有
115+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什
116+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么
117+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可
118+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以
119+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以帮
120+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以帮到
121+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以帮到你
122+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以帮到你的
123+
TIKTOK_RTC_EVENT_TTS_TEXT 你好有什么可以帮到你的吗
124+
125+
# 按KEY2键退出智能体
126+
rtc_queue key event 2
127+
stop rtc
128+
```
129+
130+
## 目录结构
131+
132+
```plaintext
133+
solution-AI/
134+
├── code/
135+
│   ├── ai_main.py
136+
│   ├── datetime.py
137+
│   ├── ...
138+
│   └── img/
139+
│      ├── battery/
140+
│      │   ├── bat_00.png
141+
│      │   ├── bat_01.png
142+
│      │   └── ...
143+
│      ├── signal/
144+
│      │   ├── signal_00.png
145+
│      │   ├── signal_01.png
146+
│      │   └── ...
147+
│      ├── image1.png
148+
│      ├── image2.png
149+
│      └── ...
150+
├── examples/
151+
│   └── examples_ai.py
152+
├── docs/zh/media/
153+
│         └── wire_connection.jpg
154+
├── EC600MCNLER06A01M08_OCPU_QPY_TEST0213.zip
155+
├── LICENSE
156+
├── readme.md
157+
└── readme_zh.md
158+
```
159+
160+
## 贡献
161+
162+
我们欢迎对本项目的改进做出贡献!请按照以下步骤进行贡献:
163+
164+
1. Fork 此仓库。
165+
2. 创建一个新分支(`git checkout -b feature/your-feature`)。
166+
3. 提交您的更改(`git commit -m 'Add your feature'`)。
167+
4. 推送到分支(`git push origin feature/your-feature`)。
168+
5. 打开一个 Pull Request。
169+
170+
## 许可证
171+
172+
本项目使用 Apache 许可证。详细信息请参阅 [LICENSE](LICENSE) 文件。
173+
174+
## 支持
175+
176+
如果您有任何问题或需要支持,请参阅 [QuecPython 文档](https://python.quectel.com/doc) 或在本仓库中打开一个 issue。

0 commit comments

Comments
 (0)
Please sign in to comment.