نسخه 0.4.0 — C++17 — Open Source

کتابخانه GSM

درایور غیرمسدودکننده و کاملاً ماژولار برای مودم‌های سلولارSIM800 / SIM900 / Quectel M08 / M08R

این سایت، مستندات یکپارچه و کامل کتابخانه است: از معماری لایه‌ای و ماشین حالت گرفته تا جزئیات هر فایل، هر تایمر و هر ترفند میدانی. ساخته‌شده برای سیستم‌های امبدد جدی — بدون heap، بدون STL، بدون استثنا، با پشتیبانی کامل از پیامک فارسی.

Zero Dynamic AllocationNo STL-fno-exceptions -fno-rttiNon-blocking tick()پیامک فارسی UCS2سرپرستی تماسUSSD هوشمندنگه‌بانی صنعتی ۲۴/۷
minimal — STM32
static port::Stm32Uart uart(&huart2);
static port::Stm32Clock clk;
static port::Stm32PowerControl pwr(GPIOB, GPIO_PIN_12);
static Modem modem(uart, clk, kSim800, &pwr);
modem.set_on_sms(...);
modem.begin();
while (true) modem.tick(); // همین!
PHILOSOPHY

اصول طراحی — چرا این کتابخانه متفاوت است؟

کتابخانه بر اساس اصول مهندسی سیستم‌های امبدد صنعتی طراحی شده است؛ هر تصمیم معماری به یک محدودیت واقعی تولید پاسخ می‌دهد: حافظه کم، حلقه اصلی حساس به تأخیر، و نیاز به کارکرد ۲۴/۷ بدون دخالت انسان.

Zero Dynamic Allocation — بدون heap

هیچ جایی از کد از malloc، new یا کانتینرهای پویا استفاده نمی‌کند. تمام بافرها (بافر حلقوی RX، خطوط AT، متن پیامک، صف کارها) در زمان کامپایل و به‌صورت استاتیک رزرو می‌شوند. نتیجه: هیچ تکه‌تکه شدن حافظه، هیچ شکست تخصیص در زمان اجرا، و رفتار قطعی و قابل پیش‌بینی.

بدون وابستگی به STL

از std::vector، std::string و std::function خبری نیست. جایگزین‌ها ابزارهای اختصاصی همین کتابخانه‌اند: StaticString با اندازه ثابت، RingBuffer ایستا و Delegate سبک‌وزن. کامپایل روی هر کامپایلر C++17 بدون نیاز به کتابخانه استاندارد اجرایی.

Non-blocking Tick Architecture

کل کتابخانه با فراخوانی ساده modem.tick() در هر چرخه زنده می‌ماند. هیچ sleep، هیچ wait فعال و هیچ بلاک‌شدنی وجود ندارد — حلقه اصلی یا FreeRTOS هرگز متوقف نمی‌شود. حتی ارسال پیامک که چند مرحله AT دارد، همیشه با ماشین گام‌به‌گام اداره می‌شود.

No Exceptions / No RTTI

کاملاً سازگار با -fno-exceptions -fno-rtti. خطاها با کدهای عددی (kErrSmsTooLong، kErrJobStalled و کدهای CME/CMS مودم) و کال‌بک‌ها گزارش می‌شوند؛ نه با پرتاب استثنا. این یعنی سربار باینری حداقل و رفتار قابل پیش‌بینی در محیط‌های حساس.

پیامک فارسی به‌صورت پیش‌فرض سیستم

پیش‌فرض کل سیستم روی UCS2 در Text Mode قفل شده است: ارسال، خواندن، اسکن و پاسخ USSD همه با رمزگذاری خودکار UTF-8↔UCS2 انجام می‌شود. حتی شماره گیرنده به هگز UCS2 تبدیل می‌شود چون مودم‌ها در حالت UCS2 شماره ASCII را قبول نمی‌کنند — درسی که از لاگ‌های میدانی آموخته شد.

معماری لایه‌ای با قاعده طلایی

هیچ لایه‌ای به لایه بالاتر وابسته نیست و فقط AtEngine با UART حرف می‌زند. اضافه کردن مودم جدید = تعریف یک DeviceTraits؛ اضافه کردن سکوی جدید = پیاده‌سازی ۳ اینترفیس HAL. هسته هرگز برای توسعه دست نمی‌خورد.

OVERVIEW

کتابخانه در یک نگاه

اعداد کلیدی که مقیاس و طراحی کتابخانه را نشان می‌دهند.

25فایل منبع (.cpp) در هسته
25هدر عمومی در include/
11ماشین حالت (حالت‌های مودم)
3–4KBمصرف RAM با تنظیمات پیش‌فرض
60سقف نویسه پیامک (حاشیه امن UCS2)
20sتایمر قطع خودکار تماس
1h / 50دو ریست‌کننده نگه‌بانی
3پلتفرم: STM32 / Arduino / Host

ماژول‌های پشتیبانی‌شده

SIM800/SIM900 با پروفایل kSim800، Quectel M08/M66 با kQuectelM08 و نسخه اصلاح‌شده M08R با پروفایل اختصاصی.

بیلد چندسکویی

CMake برای میزبان/STM32 و library.json برای PlatformIO (ststm32، atmelavr، espressif32، native).

تست دود میزبان

host_smoke.cpp همه چیز را بدون سخت‌افزار می‌آزماید: کدک، بوت، USSD، صف، تماس، بازیابی SIM و واچ‌داگ.

ARCHITECTURE

معماری لایه‌ای — از سخت‌افزار تا اپلیکیشن

کتابخانه در پنج لایه L0 تا L4 سازمان یافته است. روی هر لایه کلیک یا هاور کنید تا توضیح و جریان داده همان لایه را ببینید. قاعده طلایی: هیچ لایه‌ای به لایه بالاتر وابسته نیست و فقط AtEngine با UART حرف می‌زند.

قاعده طلایی: هیچ لایه‌ای به لایه بالاتر وابسته نیست
L3

مدیریت‌گر مودم و سرویس‌ها

هوش کتابخانه اینجاست: ماشین حالت اصلی + زیرحالت‌ها داخل سرویس‌ها، صف کار با اولویت و retry، گیت تماس و نگه‌بانی صنعتی.

data flow
AppL3UARTModem

پورت جدید = سه اینترفیس

برای انتقال به هر سخت‌افزار تازه فقط باید IUart، IClock و (اختیاری) IPowerControl را پیاده‌سازی کنید. پورت STM32 از DMA با حالت ReceiveToIdle استفاده می‌کند، پورت Arduino روی هر Stream و پورت Host برای تست دسکتاپ طراحی شده است.

ماژول جدید = یک DeviceTraits

هر مودم با یک ساختار traits توصیف می‌شود: فرمان قطع تماس، پارامتر CSMP، مخزن پیامک، تایم‌اوت بوت و رفتارهای خاص (مثل اطمینان به +CLCC در M08R). اضافه کردن M66 یا SIM7600 یعنی فقط تعریف یک constexpr جدید — دست زدن به هسته لازم نیست.

STATE MACHINE

ماشین حالت اصلی — قلب کتابخانه

مودم همیشه دقیقاً در یکی از ۱۱ حالت زیر است. زیرحالت‌ها (مثل مراحل ارسال پیامک) داخل سرویس‌ها زندگی می‌کنند و از این ماشین جدا هستند. نمودار زیر مسیر کامل از روشن‌شدن تا آماده‌سازی و کار را نشان می‌دهد.

Off
خاموش
begin()
Booting
بوت
پاسخ ندادن ×۳
PowerPulsing
پالس پاور
پالس PWRKEY ۱.۲s ×۲↺ بازگشت به Booting
AT → OK
WaitingRegistration
انتظار شبکه
رجیستر شدن (+CREG: 1/5)
Configuring
پیکربندی
پایان جدول بوت
Ready
آماده
Job در صف
Working
در حال کار
تماس ورودی (RING)
Ringing
زنگ ورودی
answer()
InCall
تماس فعال
پایان تماس ↩ بازگشت به Ready
سیم‌کارت خارج شد → SimMissing → بازیابی خودکار CPIN/CFUN
تایم‌اوت شبکه / شکست پروب → Fault → reset() یا ریست خودکار ۹۰s
جدول کامل حالت‌ها
حالتنام رشته‌ایمعنیخروج
Off"off"مودم شروع نشده یا خودش را خاموش اعلام کرده است. هیچ فعالیتی وجود ندارد.فراخوانی begin()
Booting"booting"ارسال مکرر پروب «AT» تا جواب دادن ماژول؛ در صورت بی‌پاسخی، عبور به PowerPulsing.دریافت OK از پروب
PowerPulsing"pwrkey-pulsing"پالس زدن پایه PWRKEY (حداکثر ۲ بار) تا ماژول سخت‌افزاری روشن شود.بازگشت به Booting یا Fault
WaitingRegistration"waiting-reg"هر ۲ ثانیه استعلام AT+CREG? یا گوش‌دادن به URC شبکه؛ تا اتصال به شبکه صبر می‌کند.رجیستر شدن (1/5) یا تایم‌اوت ۴۵s
Configuring"configuring"اجرای جدول بوت: ATE0، CMEE، CLIP، CNMI، CMGF، CSCS، CSMP، CPMS.پایان جدول
Ready"ready"بیکار؛ فقط اینجا housekeeping (CSQ، Sweep، اسکن پیامک، پاک‌سازی) اجرا می‌شود.آمدن Job یا تماس ورودی
Working"working"یک Job از صف در جریان است (ارسال پیامک، USSD، استعلام وضعیت، تماس خروجی و ...).پایان Job
Ringing"ringing"تماس ورودی در انتظار پاسخ است؛ اگر ۲۰ ثانیه جواب داده نشود خودکار رد می‌شود.answer() / hang_up() / NO CARRIER
InCall"in-call"تماس صوتی برقرار است؛ جاب‌های غیرتماسی در صف نگه داشته می‌شوند (گیت تماس).قطع تماس از هر طرف
SimMissing"sim-missing"سیم‌کارت تشخیص داده نشده؛ پروب CPIN و چرخه CFUN=0/1 برای بازیابی اجرا می‌شود.بازگشت سیم‌کارت (+CPIN: READY)
Fault"fault"تسلیم؛ مثلا تایم‌اوت رجیستر یا شکست پروب‌ها. برای خروج، reset() یا power_cycle() لازم است.reset() / ریست خودکار ۹۰ ثانیه‌ای
BOOT & RECOVERY

راه‌اندازی هوشمند، رجیستر شبکه و بازیابی سیم‌کارت

فرآیند بوت هرگز به بنرهای بوت مودم (RDY، Call Ready، SMS Ready) اعتماد نمی‌کند؛ خودش با پروب AT مودم را می‌آزماید، در صورت نیاز PWRKEY را پالس می‌زند، صبر می‌کند شبکه رجیستر شود و فقط بعد از آن پیکربندی را آغاز می‌کند.

۱) بوت و مکانیزم PWRKEY

  1. ۱ مودم تا ۱۰ بار پروب AT می‌فرستد و پاسخ می‌گیرد.
  2. ۲ اگر ماژول جواب ندهد (مثلاً خاموش باشد) و IPowerControl متصل باشد، وارد حالت PowerPulsing می‌شود و پایه PWRKEY را پالس می‌زند.
  3. ۳ دوباره پروب ارسال می‌شود؛ در صورت بی‌پاسخی یک پالس دیگر و سپس وضعیت Fault.
  4. ۴ اگر power برابر nullptr باشد، همه این مراحل نرم‌افزاری و بدون خطا هستند.

۲) انتظار رجیستر شبکه

  1. ۱ هر ۲ ثانیه دستور AT+CREG? ارسال یا به URC شبکه گوش داده می‌شود.
  2. ۲ کد 1 یعنی شبکه خانگی و کد 5 یعنی رومینگ — هر دو موفقیت‌اند.
  3. ۳ اگر تا ۴۵ ثانیه رجیستر نشد، مودم وارد Fault می‌شود و لاگ خطای شبکه ثبت می‌کند.
  4. ۴ فقط بعد از رجیستر شدن، مرحله پیکربندی آغاز می‌شود — نه یک لحظه زودتر.
جدول پیکربندی بوت (kBoot) — ترتیب دقیق فرمان‌ها
فرمانچرا؟وضعیت شکست
ATE0خاموش کردن اکو — پارسر حتی با اکو روشن هم مقاوم استFault
AT+CMEE=2گزارش خطاهای CME/CMS با متن راهنما (اختیاری)ادامه می‌دهد
AT+CLIP=1نمایش شماره تماس‌گیرنده (اختیاری)ادامه می‌دهد
AT+CNMI=2,1,0,0,0اعلان پیامک جدید با +CMTI (اختیاری)ادامه می‌دهد
AT+CMGF=1حالت متنی پیامک (اجباری — شکست یعنی Fault)Fault
AT+CSCS="UCS2"قفل charset روی UCS2 برای پیامک فارسی (اجباری)Fault
AT+CSMP=…پارامترهای پیامک از DeviceTraits هر ماژول (اجباری)Fault
AT+CPMS=…انتخاب مخزن پیامک SM یا ME از DeviceTraits (اجباری)Fault

۳) تشخیص زنده سیم‌کارت

روتر URC به‌صورت پویا وضعیت سیم‌کارت را پایش می‌کند. با دیدن +CPIN: NOT INSERTED مودم وارد حالت SimMissing می‌شود، کال‌بک on_sim با SimState::Removed صدا زده می‌شود و فرآیندهای پس‌زمینه متوقف می‌شوند. با برگشتن سیم‌کارت (+CPIN: READY) به‌صورت خودکار به WaitingRegistration برمی‌گردد و چرخه را کامل می‌کند.

۴) بازیابی صنعتی با چرخه CFUN

در Quectel و SIM800 بعد از جا زدن مجدد سیم‌کارت، ماژول تا ریست لایه بیس‌باند خطایSIM not inserted می‌دهد. کتابخانه هر ۱۰ ثانیه پروب CPIN می‌زند و اگر سیم نیامده باشد، هر ۲۵ ثانیه چرخه AT+CFUN=0 سپسAT+CFUN=1 را اجرا می‌کند تا ماژول مجبور به اسکن فیزیکی پین‌های سیم‌کارت شود.

TICK PIPELINE

یک تیک چگونه می‌گذرد؟ — حلقه حیاتی ۹ مرحله‌ای

هر فراخوانی Modem::tick() یک مسیر دقیق و ترتیب‌دار را طی می‌کند. این ترتیب تضمین می‌کند سرپرستی تماس همیشه به‌موقع باشد، صف هرگز بند نیاید و پروب‌های سلامت فقط در بیکاری کامل اجرا شوند.

Modem::tick() — هر چرخه
1
at_.tick()
خواندن UART، خط‌سازی، تشخیص پاسخ نهایی و URC
2
flush_cancelled()
تحویل کال‌بک لغوشده‌ها
3
check_watchdog()
پاک‌سازی Jobهای کهنه + بازیابی صنعتی (۳ تایم‌اوت، ۹۰s گیر)
4
call_.monitor() + pump()
سرپرستی تماس: تایمر قطع خودکار و تور ایمنی +CLCC
5
maintenance_service()
دو ریست‌کننده دوره‌ای (۱ ساعت / ۵۰ عملیات)
6
FSM
اجرای منطق حالت: Booting / Reg / Configuring / بازیابی SIM
7
stat_.pump() + pump_services()
اجرای صف (با گیت تماس) و به‌روزرسانی وضعیت
8
pump_housekeeping()
CSQ، Sweep، اسکن پیامک، پاک‌سازی
9
watchdog_.pump()
پروب‌های سلامت فقط در بیکاری کامل

چرا ترتیب مهم است؟

  • سرپرستی تماس قبل از FSM و pump_services صدا زده می‌شود تا تایمرهای قطع و پرس‌وجوی +CLCC همیشه به‌موقع برسند، حتی وقتی صف شلوغ است.
  • پروب‌های سلامت watchdog فقط وقتی اجرا می‌شوند که همه‌چیز بیکار است — هیچ تداخلی با تماس یا Job جاری رخ نمی‌دهد.
  • پاک‌سازی Jobهای کهنه قبل از FSM انجام می‌شود تا صف همیشه پیش از شروع کار جدید تمیز باشد.
  • خانه‌داری (CSQ، اسکن پیامک و …) فقط در حالت Ready و وقتی گیت تماس باز است اجرا می‌شود.
JOB QUEUE

صف کارها — ستون فقرات اجرای عملیات

هر عملیات (ارسال پیامک، تماس، USSD، استعلام وضعیت) یک Job است که در صف FIFO ایستا با ظرفیت ثابت GSM_QUEUE_DEPTH (پیش‌فرض ۶) قرار می‌گیرد. هیچ تخصیص حافظه‌ای در صف رخ نمی‌دهد و هیچ Jobای بی‌جواب نمی‌ماند.

اولویت Urgent

کارهای اضطراری مثل hang_up() و answer() از جلو صف وارد می‌شوند تا حتی وقتی صف شلوغ است هم فوراً اجرا شوند. بقیه کارها از انتهای صف.

تلاش مجدد هوشمند

خطای موقت ماژول یا شبکه باعث retry تا max_attempts می‌شود (ارسال پیامک پیش‌فرض ۲ بار). اما خطاهای سیاستی مثل kErrSmsTooLong و kErrJobStalled هرگز retry نمی‌شوند.

ضد انباشت و گیرکردن

Jobهای معلق بیش از ۴۵ ثانیه با purge_stale() لغو می‌شوند. اگر سرویسی از شروع Job امتناع کند، بعد از ۳ تلاش با kErrJobStalled تمام می‌شود — صف هرگز نمی‌ایستد.

انواع Job (JobKind)

SmsSendSmsReadSmsScanSmsDeleteReadSmsDeleteIndexCallDialCallAnswerCallHangupStatusRefreshUssdQueryUssdReply

هر Job شامل kind، وضعیت (Queued/Running/Succeeded/Failed/Cancelled)، بودجه تلاش (attempts)، زمان ورود، پرچم deferred و نتیجه (کد خطا + خروجی مثل شماره مرجع پیامک یا پاسخ USSD) است.

کنترل جامع صف

queue control
modem.pause_queue(); // توقف پردازش صف
modem.resume_queue(); // ادامه از همان‌جا
modem.stop_all(); // لغو جاری با ESC/ATH + پاک‌سازی صف
modem.cancel(id); // لغو تکی با فراخوانی کال‌بک
// مشاهده:
modem.queue_size();
modem.queued(i); // i-امین Job در انتظار
modem.current_job(); // Job در حال اجرا

گیت تماس — حل مشکل «مرگ صف»

وقتی تماسی در جریان است (Dialing / Outgoing / Incoming / Active / Releasing)، جاب‌های غیرتماسی اجرا نمی‌شوند و در صف نگه داشته می‌شوند. به محض پایان تماس، صف از همان‌جا ادامه پیدا می‌کند.
  • ✔ جاب‌های تماس (Dial/Answer/Hangup) همیشه از گیت عبور می‌کنند.
  • ✔ تماس ورودی وسط ارسال پیامک → preempt_for_call() جاب را با ESC به‌صورت امن لغو و deferred می‌کند؛ بعد از تماس بدون مصرف بودجه retry ادامه می‌یابد.
  • ✔ دقیقاً همان‌جایی است که M08R قبلاً هنگ می‌کرد و «پیام خالی» می‌فرستاد.

مسیر یک Job از enqueue تا callback

enqueue
JobId
pop از صف
گیت تماس؟
hold
start_job
svc start
pump
گام‌ها
finish_job
retry?
callback

اگر start سه بار شکست بخورد، Job با kErrJobStalled تمام می‌شود؛ اگر finish با خطای retry-پذیر روبه‌رو شود، Job دوباره به صف برمی‌گردد. پس از نهایی شدن، watchdog_.note_operation() بودجه ریست دوم را تغذیه می‌کند.

AT ENGINE

هسته AT — موتور تک‌فرمانی غیرهمزمان

AtEngine زیربنای همه سرویس‌هاست: هر لحظه فقط یک فرمان در پرواز (in-flight) دارد، پاسخ‌ها را خط‌به‌خط از LineReader می‌گیرد، نتیجه نهایی را دسته‌بندی می‌کند و از طریق IAtObserver به سرویس فراخوان اطلاع می‌دهد. URCها قبل از رسیدن به observer توسط UrcRouter مصرف می‌شوند.

خط‌لوله پردازش هر بایت

01UART RXبایت‌به‌بایت از IUart خوانده می‌شود
02LineReaderخط کامل، پرامپت «>» یا overflow تولید می‌کند
03echo-killerخط اکو (برابر فرمان ارسالی) دور ریخته می‌شود
04classify_finalاگر پاسخ نهایی است → finish() + observer->on_final()
05UrcRouterاگر URC است → dispatch به Modem::handle_urc
06observer->on_lineدر غیر این صورت خط به سرویس جاری می‌رسد

دسته‌بندی پاسخ‌های نهایی

خط پاسخAtStatusکدمعنی
OKOk-1موفقیت
ERRORError-1خطای عمومی
+CME ERROR: nCmeErrornخطای تجهیز (با متن راهنما لاگ می‌شود)
+CMS ERROR: nCmsErrornخطای پیامک (با متن راهنما لاگ می‌شود)
NO CARRIERNoCarrier-1اتصال حامل قطع شد
BUSYBusy-1طرف مقابل اشغال بود
NO ANSWERNoAnswer-1جواب نداد
NO DIALTONENoDialtone-1تن کنون نداشت

نکات ظریف موتور

  • Guard Time: حداقل ۳۰ms بین دو فرمان پشت‌سرهم از تداخل پاسخ مودم‌های کند جلوگیری می‌کند — یک execute ممکن است به‌ظاهر بی‌دلیل false برگرداند!
  • پرامپت «>»: فقط وقتی بافر خط خالی باشد Prompt تولید می‌شود؛ «>» وسط متن پیامک پرامپت تلقی نمی‌شود.
  • abort ایمن: اگر در انتظار پرامپت باشیم، ابتدا ESC (0x1B) ارسال می‌شود تا مودم از data-entry بیرون بیاید.
  • سرریز خط: خطوط بلندتر از GSM_LINE_MAX (۵۱۲) با Token::Overflow بی‌سروصدا دور ریخته می‌شوند.
SMS SERVICE

سرویس پیامک — فارسی، UCS2، با محافظ صنعتی

ارسال، خواندن، اسکن و حذف پیامک در حالت متنی (CMGF=1) با رمزگذاری خودکار UCS2. سرویس تنظیمات جلسه را کش می‌کند تا فرمان‌های تکراری نفرستد و متن‌های بلند را قبل از هر فرمان AT رد می‌کند.

جریان ارسال پیامک (Send Flow)

گارد طول
count_chars ≤ 60
CMGF=1
text mode
CSCS
="UCS2"
CSMP
از traits
CPMS
مخزن
CMGS
شماره UCS2-hex
prompt >
expect_prompt
payload
hex + Ctrl+Z
wait +CMGS
await_final

گام‌هایی که به لطف کش جلسه (SessionState) قبلاً اعمال شده‌اند، بدون ترافیک AT رد می‌شوند؛ یعنی پیامک دوم فقط AT+CMGS می‌فرستد. هر خطا SessionState را بی‌اعتبار می‌کند تا دفعه بعد کانفیگ از نو اعمال شود.

گارد سخت‌گیرانه طول (v0.4)

  • طول با تعداد نویسه‌های UTF-8 سنجیده می‌شود، نه بایت — «سلام» ۴ نویسه است، نه ۸ بایت.
  • پیامک UCS2 تک‌بخشی حداکثر ۷۰ نویسه است؛ پیش‌فرض کتابخانه ۶۰ نویسه برای حاشیه امن.
  • متن بلندتر اصلاً ارسال نمی‌شود و بلافاصله با kErrSmsTooLong = -100 رد می‌گردد (تلاش مجدد نمی‌شود).
  • دو لایه دفاع: رد قبل از ورود به صف در Modem::send_sms + رد هنگام اجرا در SmsService::start_send.
  • در زمان اجرا قابل تنظیم است: modem.sms().set_max_chars(45) و ۰ یعنی غیرفعال.

چرا این گارد وجود دارد؟ ماژول‌های M08R با payload بزرگ هنگ می‌کنند یا پیامک خالی می‌فرستند. این محافظ از ریشه جلوی این خرابی میدانی را می‌گیرد.

API کامل پیامک

sms api
// ارسال با کال‌بک نتیجه
modem.send_sms("0912...", "سلام دنیا",
[](void*, const Job& j) {
if (j.state == JobState::Succeeded)
printf("ref=%d\n", j.reference);
});
// خواندن / اسکن / حذف
modem.read_sms(index, cb); // AT+CMGR
modem.scan_sms(cb); // AT+CMGL="REC UNREAD"
modem.delete_read_sms(cb); // AT+CMGD=1,1
// شمارش نویسه‌ها
uint16_t n = SmsService::count_chars("سلام"); // 4
// تنظیم سقف در زمان اجرا
modem.sms().set_max_chars(45);

پیام دریافتی با کال‌بک set_on_sms تحویل می‌شود؛ شماره فرستنده و متن به‌صورت خودکار از UCS2-hex به UTF-8 رمزگشایی می‌شوند (با پشتیبانی جفت سورگیت UTF-16).

CALL SUPERVISION

سرویس تماس — سرپرستی مستقل از صف

نسخه 0.4 مدیریت تماس را از پایه بازنویسی کرد: سه مشکل میدانی (مرگ صف با ورود تماس، تشخیص نشدن رد تماس، نبود تایم‌اوت قطع) با یک سرپرستی تماس جدید حل شده‌اند.

قطع خودکار ۲۰ ثانیه‌ای

تماس خروجی که در dial_timeout_ms وصل نشود و زنگ ورودی که درring_timeout_ms جواب داده نشود، خودکار قطع/رد می‌شود (پیش‌فرض ۲۰s، قابل تنظیم در زمان اجرا و کامپایل).

تور ایمنی +CLCC

هر ۲ ثانیه یک AT+CLCC ارسال می‌شود. اگر فهرست تماس‌ها ۲ بار پیاپی خالی باشد و تماس قبلاً دیده شده باشد، «مرده» اعلام و خط فعالانه آزاد می‌شود — حتی اگر مودم هیچ URC پایان تماسی نفرستد (رفتار شناخته‌شده M08R).

قطع دو مرحله‌ای

فرمان اختصاصی هر دستگاه (ATH در SIM800، AT+CHUP در Quectel) اول امتحان می‌شود؛ اگر جواب ندهد، یک بار ATH جایگزین اجرا می‌گردد (HangupFallback) تا حلقه بی‌پایان قطع رخ ندهد.

ماشین حالت تماس

IdleDialingOutgoingActiveReleasing
IncomingRING/+CLIP → از Idle

هر گذار با CallEndReason گزارش می‌شود

نکته کلیدی: monitor() در هر تیک از Modem::tick() صدا زده می‌شود، حتی وقتی هیچ جاب تماسی در صف نیست — ردیابی تماس هرگز به وضعیت صف وابسته نیست.

سیاست تماس (CallPolicy)

runtime config
struct CallPolicy {
uint32_t dial_timeout_ms = 20000; // قطع خودکار MO
uint32_t ring_timeout_ms = 20000; // رد خودکار MT
uint32_t clcc_poll_ms = 2000; // فاصله +CLCC
bool auto_hangup = true; // کلید اصلی
};
// تنظیم زنده:
modem.call_policy().dial_timeout_ms = 30000;
modem.call_policy().ring_timeout_ms = 15000;
// گزارش دلیل پایان:
modem.set_on_call_ended(...);
دلیل پایان تماس (CallEndReason)
مقدارمعنی
LocalHangupخودمان قطع کردیم
RemoteHangupطرف مقابل قطع کرد
Busyاشغال بود
NoAnswerجواب نداد
NoCarrierبدون حامل — اتصال قطع شد
NoDialtoneتن کنون نداشت
Timeoutتایمر قطع خودکار فعال شد
Errorخطای دیگر
USSD SERVICE

سرویس USSD — استعلام شارژ و کدهای دلخواه

سرویس اختصاصی USSD با دستور استاندارد AT+CUSD: تشخیص خودکار اپراتور ایرانی، استعلام شارژ با یک متد، و رمزگشایی خودکار پاسخ (حتی اگر شبکه پاسخ را در UCS2-hex بفرستد).

تشخیص خودکار اپراتور از AT+COPS?

ایرانسل
Irancell · MTN / 43235
*141*1#
کد استعلام شارژ
همراه اول
MCI · TCI / IR-TCI / 43211
*140*11#
کد استعلام شارژ
رایتل
RighTel · 43220
*140#
کد استعلام شارژ
ussd usage
modem.check_balance(); // کد هوشمند بر اساس اپراتور تشخیصی
modem.ussd("*140#"); // یا هر کد دلخواه
modem.set_on_ussd(...); // پاسخ شبکه — خودکار decode شده به UTF-8
modem.ussd_service().send_reply("*1#"); // نشست تعاملی
modem.ussd_service().end_session(); // بستن جلسه با AT+CUSD=2

جریان یک استعلام

تشخیص اپراتور
COPS?
AT+CUSD=1
کد شارژ
انتظار +CUSD
10s
decode DCS
UCS2→UTF-8
کال‌بک پاسخ

جزئیات فنی

  • ✔ پاسخ +CUSD با تحلیل فیلد DCS و در صورت نیاز «بو کردن» محتوا decode می‌شود.
  • ✔ تایم‌اوت شبکه ۱۰ ثانیه؛ نتیجه حتی در صورت Timeout هم گزارش می‌شود.
  • ✔ رمزنگاری UCS2/GSM7 به‌صورت خودکار انتخاب می‌شود؛ اگر GSM7 انتخاب شده باشد ولی متن غیر-ASCII باشد، با هشدار به UCS2 ارتقا می‌یابد.
  • ✔ نشست‌های USSD تعاملی پشتیبانی می‌شوند: reply و بستن جلسه.
STATUS & HOUSEKEEPING

سرویس وضعیت و خانه‌داری — مودم همیشه دیده‌بان

در حالت Ready، ماشین حالت بیکار نمی‌نشیند؛ طبق HousekeepingConfig به‌صورت دوره‌ای سیگنال، رجیستر، سیم‌کارت، مخزن پیامک و اپراتور را استعلام می‌کند و همه چیز را در ModemStatus زنده نگه می‌دارد.

برنامه زمانی خانه‌داری

15000
signal_ms
استعلام قدرت سیگنال (AT+CSQ)
60000
full_sweep_ms
استعلام کامل: CSQ + CREG + CPIN + CPMS + COPS
25000
sms_scan_ms
اسکن پیامک‌های خوانده‌نشده و تحویل آنها
300000
cleanup_ms
پاک‌سازی پیامک‌های خوانده‌شده از حافظه مودم

خانه‌داری فقط در Ready و فقط وقتی گیت تماس باز است اجرا می‌شود؛ هیچ‌وقت با تماس یا Job جاری تداخل پیدا نمی‌کند.

ModemStatus زنده — همه چیز یک‌جا

csq_raw / rssi_dbm
قدرت سیگنال خام و dBm
signal_bars()
خطوط آنتن ۰ تا ۵
registration
وضعیت ثبت شبکه
sim_ready
آمادگی سیم‌کارت
sms_used / sms_total
اشغال مخزن پیامک
operator_name
نام اپراتور تشخیصی
direct access
const ModemStatus& st = modem.status();
printf("rssi=%d dBm bars=%d op=%s\n",
st.rssi_dbm, st.signal_bars(),
st.operator_name.c_str());
MAINTENANCE WATCHDOG

نگه‌بانی صنعتی — دو ریست‌کننده + بررسی سلامت

ماژول سلولار در کار طولانی‌مدت (چند روز تا چند ماه) به مرور کند می‌شود یا هنگ می‌کند. به‌جای انتظار برای خرابی، دو ریست‌کننده مستقل به‌صورت دوره‌ای ماژول را کامل ریست و سپس سلامت آن را بررسی می‌کنند — پایداری ۲۴/۷ بدون دخالت انسان.

ریست‌کننده اول — بودجه زمانی

3600000ms

هر ۱ ساعت (قابل تنظیم) یک ریست کامل ماژول انجام می‌شود. با GSM_PERIODIC_RESET_MS=0 خاموش می‌شود. در زمان اجرا هم با wd.policy().periodic_ms قابل تغییر است.

ریست‌کننده دوم — بودجه عملیات

50 ops

بعد از هر ۵۰ عملیات کامل (هر جاب تمام‌شده: پیامک، تماس، USSD، استعلام) ریست کامل انجام می‌شود. هر کدام زودتر فعال شود، ریست اجرا و هر دو بودجه از نو شروع می‌شوند.

فرصت امن و حفظ صف

  • ریست فقط در فرصت امن اجرا می‌شود: بدون تماس فعال، بدون فرمان AT در پرواز، بدون Job جاری.
  • اگر تا grace_ms (پیش‌فرض ۱۰ ثانیه) فرصت پیدا نشود، ریست اجباری می‌شود.
  • جاب‌های صف‌کشیده در طول ریست نگه داشته می‌شوند و بعد از بالا آمدن مودم ادامه پیدا می‌کنند — full_restart(keep_jobs=true).
  • سیاست سخت‌افزاری: اگر IPowerControl موجود باشد ریست با چرخه PWRKEY انجام می‌شود، وگرنه نرم‌افزاری.

بررسی سلامت چهارمرحله‌ای بعد از هر ریست

AT
پاسخ‌گویی
CPIN?
سیم‌کارت
CREG?
شبکه
CSQ
سیگنال
HealthReport

پاسخ‌های +CPIN و +CREG توسط UrcRouter بلعیده می‌شوند و وضعیت زنده ModemStatus را به‌روز می‌کنند؛ برای همین verdict سلامت از همان وضعیت زنده خوانده می‌شود. نتیجه با کال‌بک set_on_health گزارش می‌شود.

health report
modem.set_on_health(
[](void*, const HealthReport& r) {
if (!r.ok()) { /* اقدام اضطراری */ }
});
const HealthReport& h = modem.maintenance().health();
// h.state: Unknown / Checking / Healthy / Unhealthy
// h.at_ok, h.sim_ok, h.net_ok, h.rssi_dbm, h.resets

تفاوت با واچ‌داگ‌های قدیمی

قابلیتواچ‌داگ صنعتی قبلیریست‌کننده‌های v0.4
تایم‌اوت‌های متوالی AT✔ دارد✔ حفظ شده
گیرکردن در Fault/SimMissing✔ دارد✔ حفظ شده
ریست دوره‌ای زمانی✘✔ پیش‌فرض ۱ ساعت
ریست بعد از N عملیات✘✔ پیش‌فرض ۵۰
بررسی سلامت بعد از ریست✘✔ چهار پروب
حفظ صف در حین ریست✘ صف پاک می‌شد✔ keep_jobs=true
CODECS & UTILITIES

کدک و ابزارها — بلوک‌های سازنده صفر-تخصیص

لایه L1 مجموعه‌ای از ابزارهای خالص و مستقل از سخت‌افزار است: تبدیل‌های رمزگذاری برای فارسی، نرمال‌سازی شماره، رشته ثابت، بافر حلقوی، کال‌بک بدون heap و سامانه لاگ سبک.

codec/ucs2 — رمزگذاری فارسی

تبدیل‌های UTF-8 ↔ hex-UCS2 با پشتیبانی کامل جفت سورگیت UTF-16، تشخیص looks_like_ucs2_hex و شمارش واحدها. پایه رمزنگاری پیامک، USSD و شماره‌ها.

codec/phone_number — E.164

نرمال‌سازی شماره به E.164 با کد کشور پیش‌فرض +98: فرم بین‌المللی، داخلی ۰۹…، و بدون صفر همگی به یک شکل استاندارد می‌رسند.

util/static_string — رشته ثابت

StaticString<N> با ابزارهای پارس AT: parse_int، csv_field، str_starts_with و ... بدون هیچ تخصیصی. هسته پارسینگ خطوط مودم.

util/ring_buffer — بافر حلقوی

بافر حلقوی بایتی ایستا با پرچم overflow؛ مناسب بافر UART در حالت وقفه. RX پیش‌فرض ۱۰۲۴ بایت و قابل کوچک‌سازی.

util/delegate — کال‌بک بدون heap

Delegate سبک‌وزن جایگزین std::function با bind ایمن به متدهای عضو: Delegate<T>(cb_fn, ctx). همه کال‌بک‌های کتابخانه با همین نوع کار می‌کنند.

log — لاگ سبک‌وزن

سطح‌های Off..Trace، کانال‌های تگ‌دار، sinkهای قابل تعویض (stdio / writer / null)، رنگ ANSI قابل خاموشی در runtime، timestamp، bytes() برای دیدن CR/LF/CtrlZ و hexdump. با GSM_LOG_LEVEL_MAX کامپایل حذف می‌شود.

log runtime control
log::Registry::set_color(true); // رنگ‌های ANSI
log::Registry::set_timestamps(true); // برچسب [ms]
log::Registry::set_global_level(log::Level::Debug);
HAL & PORTS

لایه انتزاع سخت‌افزار و پورت‌ها

تنها نقطه وابستگی به پلتفرم، سه اینترفیس L0 است. پورت جدید یعنی پیاده‌سازی همین سه اینترفیس؛ هیچ چیز از هسته تغییر نمی‌کند. شمارنده میلی‌ثانیه با hal::elapsed به‌صورت مقاوم به سرریز مقایسه می‌شود.

پورت STM32

ports/stm32/gsm_port_stm32.hpp
  • ✔UART مبتنی بر DMA با حالت ReceiveToIdle — بدون از دست دادن بایت
  • ✔زمان‌سنج HAL_GetTick با تابع elapsed مقاوم به سرریز
  • ✔Stm32PowerControl برای پالس PWRKEY و RESET مودم
  • ✔همراه با examples/stm32_main.cpp و user_code/cli.cpp

پورت Arduino

ports/arduino/gsm_port_arduino.hpp
  • ✔پیاده‌سازی IUart روی هر Stream سریال (Serial، Serial1، ...)
  • ✔زمان‌سنج millis() آردوینو
  • ✔همان API دقیقاً — فقط دو خط setup فرق می‌کند
  • ✔مثال arduino_basic.ino: دریافت و ارسال پیامک فارسی

پورت Host (میزبان)

ports/host/gsm_port_host.hpp
  • ✔شبیه‌ساز UART/Clock/Power با بافر تزریق/بازرسی
  • ✔پایه تست‌های قطعی host_smoke.cpp بدون سخت‌افزار
  • ✔قابل استفاده در CI و توسعه سریع منطق

اینترفیس‌های HAL

hal.hpp
class IUart {
public:
virtual uint16_t read(uint8_t* buf, uint16_t len) = 0;
virtual uint16_t write(const uint8_t* data, uint16_t len) = 0;
};
class IClock { public: virtual uint32_t millis() = 0; };
class IPowerControl {
public:
virtual void power_key(bool press) = 0; // پالس PWRKEY
};

ماجرای elapsed ضد سرریز

شمارنده millis بعد از ~۴۹ روز سرریز می‌کند؛ مقایسه ساده now - start > timeoutآن لحظه خراب می‌شود. تابع hal::elapsed(now, start, dur) با ریاضیات حسابی بدون علامت همیشه درست کار می‌کند و همه تایمرهای کتابخانه (تایم‌اوت‌های AT، تایمر تماس، خانه‌داری، واچ‌داگ) از همین مسیر عبور می‌کنند.

overflow-safe
bool elapsed(uint32_t now, uint32_t since, uint32_t dur) {
return (uint32_t)(now - since) >= dur; // درست حتی بعد از سرریز
}
INTERACTIVE CONSOLE

کنسول تعاملی gsm::cli::Console

در نسخه 0.4 کنسول از اپلیکیشن به هسته کتابخانه منتقل شد — حالا هر پروژه‌ای (STM32، Arduino، تست میزبان) با چند خط، همان کنسول صنعتی را دارد. بدون heap، بدون STL — مانند بقیه کتابخانه.

راه‌اندازی سریع

3 خط اتصال
static gsm::cli::Console console(modem, clock);
console.set_output(TextSink{&uart_write, nullptr});
console.init(); // ثبت کال‌بک‌ها + رسم منو
// در حلقه اصلی:
modem.tick();
console.tick();
while (uart.readable())
console.feed(uart.read_byte());

پل وب (Web Bridge)

کنسول می‌تواند هم‌زمان رویدادها و لاگ‌ها را به یک پنل وب آینه کند:set_web_output برای خروجی خط‌به‌خط، exec_web برای اجرای دستور از پنل، وstatus_line برای وضعیت فشرده CSV با ۱۵ فیلد (fsm|act|step|ready|sim|net|op|rssi|bars|…).

رویدادهای خودکار

کنسول در init() این کال‌بک‌ها را روی مودم نصب و رنگی چاپ می‌کند:

پیامک دریافتی (با پاسخ خودکار به «ping» با «pong»)
تغییر وضعیت تماس + دلیل پایان تماس
رویداد سیم‌کارت (خروج / آماده)
نتیجه هر جاب (با کد خطا)
گزارش سلامت نگه‌بانی
پل Passthrough: با کلید P تمام بایت‌ها خام بین ترمینال و ماژول جابه‌جا می‌شوند — برای دیباگ مستقیم AT. خروج با کلید e.

کلیدها و دستورات کنسول

mرسم مجدد منوی رنگی راهنمانمایش
iوضعیت زنده مودم (رفرش خودکار هر ۲ ثانیه)نمایش
qجزئیات صف کارها و کارهای در انتظارنمایش
wپنل واچ‌داگ و سلامتنمایش
sاستعلام وضعیت کامل (Sweep)نمایش
rاسکن پیامک‌های خوانده‌نشدهنمایش
dپاک‌سازی پیامک‌های خوانده‌شدهاقدام
bاستعلام شارژ با کد USSD هوشمند اپراتوراقدام
pتوقف / ادامه صفکنترل
xتوقف همه و پاک‌سازی صفکنترل
hقطع فوری تماس (اضطراری)کنترل
Rریست نرم‌افزاری FSMکنترل
Kریست سخت‌افزاری با PWRKEYکنترل
Pپل Passthrough — ارتباط بایت‌به‌بایت (خروج با e)کنترل
Lتغییر سطح لاگسیاست
Tروشن/خاموش تایم‌استمپ لاگسیاست
Cروشن/خاموش رنگ‌های ANSIسیاست
healthاجرای بررسی سلامت چهارپروبهسیاست
wresetدرخواست ریست نگه‌بانیسیاست
smsmax <n>تنظیم سقف طول پیامک (نویسه)سیاست
callto <ms>تنظیم تایم‌اوت قطع خودکار تماسسیاست

دستورات متنی

sms <num> <text>ارسال پیامک فارسی/انگلیسی
dial <num>برقراری تماس صوتی
ussd <code>ارسال کد دلخواه USSD
read <idx>خواندن یک پیامک از حافظه
del <idx>حذف یک پیامک
at <cmd>اجرای مستقیم فرمان AT بدون تداخل با FSM
serial @ 115200
gsm> sms 09121234567 سلام دنیا
[SMS] queued #1 → sending (UCS2, 9 chars)
gsm> b
[USSD] *141*1# → موجودی: 45,000 ریال
DEVICE PROFILES

پروفایل ماژول‌ها (DeviceTraits)

هر مودم با یک ساختار constexpr توصیف می‌شود: فرمان قطع تماس، پارامترهای پیامک، مخزن پیامک، تایم‌اوت‌ها و رفتارهای خاص. اضافه کردن ماژول جدید یعنی فقط تعریف یک پروفایل تازه — هسته دست‌نخورده می‌ماند.

فیلدمعنیkSim800kQuectelM08kQuectelM08R
ucs2_recipient_requiredشماره گیرنده هم باید UCS2-hex باشد✔✔✔
csmp_ucs2پارامتر CSMP برای پیامک UCS2AT+CSMP=17,71,0,25AT+CSMP=17,167,0,8AT+CSMP=17,167,0,8
hangup_cmdفرمان قطع تماسATHAT+CHUPAT+CHUP
storage_cmdمخزن پیامکSM,SM,SMME,ME,MEME,ME,ME
boot_timeout_msمهلت بوت15s20s20s
clcc_authoritativeاتکا به +CLCC به‌جای URC پایان تماس✘✘✔

چرا M08R پروفایل جدا دارد؟

M08R گاهی هیچ‌کدام از URCهای پایان تماس (NO CARRIER / BUSY / NO ANSWER) را نمی‌فرستد و بعد از ارسال پیامکِ نیمه‌کاره هم هنگ می‌کند. فیلد clcc_authoritative = true یعنی پرس‌وجوی +CLCC منبع اصلی حقیقت است و نباید به URCهای پایان تماس تکیه کرد.

انتخاب پروفایل در کد

select device
static Modem modem1(uart, clk, kSim800, &pwr);
static Modem modem2(uart, clk, kQuectelM08, &pwr);
static Modem modem3(uart, clk, kQuectelM08R, &pwr);
CONFIGURATION

پیکربندی کامل — همه ثابت‌ها یک‌جا

همه ثابت‌های کتابخانه با پیش‌فرض منطقی تعریف شده‌اند و با #ifndef قابل بازنویسی در gsm_config.hpp کاربر (GSM_USER_CONFIG) یا پرچم‌های کامپایلر هستند. جستجو کنید تا ماکرو مورد نظر را پیدا کنید.

بافرها و حافظهBuffers
GSM_RX_RING_SIZE
= 1024
اندازه بافر حلقوی دریافت UART (بایت)
GSM_LINE_MAX
= 512
حداکثر طول یک خط AT
GSM_CMD_MAX
= 256
حداکثر طول فرمان ارسالی
GSM_SMS_TEXT_BYTES
= 512
بافر متن پیامک
GSM_SMS_NUMBER_BYTES
= 24
بافر شماره گیرنده/فرستنده
GSM_SMS_INBOX_SLOTS
= 4
تعداد اسلات صندوق ورودی پیامک
GSM_USSD_RESPONSE_BYTES
= 512
بافر پاسخ USSD
صف کارهاQueue
GSM_QUEUE_DEPTH
= 6
ظرفیت صف Job (بدون تخصیص پویا)
GSM_JOB_MAX_AGE_MS
= 45000
حداکثر عمر Job در صف؛ کهنه‌ها لغو می‌شوند
GSM_JOB_START_RETRIES
= 3
تعداد تلاش شروع Job قبل از kErrJobStalled
زمان‌بندی‌های ATAT Timing
GSM_AT_DEFAULT_TIMEOUT
= 1000
تایم‌اوت پیش‌فرض فرمان‌های AT
GSM_AT_PROMPT_TIMEOUT
= 5000
انتظار پرامپت «>» بعد از CMGS
GSM_AT_CMGS_TIMEOUT
= 10000
انتظار نتیجه نهایی ارسال پیامک
GSM_AT_USSD_TIMEOUT
= 10000
انتظار پاسخ شبکه برای USSD
GSM_AT_GUARD_TIME
= 30
حداقل فاصله بین دو فرمان پشت‌سرهم
GSM_REGISTRATION_TIMEOUT
= 45000
مهلت رجیستر شدن در شبکه
GSM_PWRKEY_PULSE_MS
= 500
طول پالس پایه PWRKEY
سرپرستی تماسCall Supervision
GSM_CALL_ANSWER_TIMEOUT_MS
= 20000
تماس خروجی: قطع خودکار اگر وصل نشد
GSM_CALL_RING_TIMEOUT_MS
= 20000
تماس ورودی: رد خودکار اگر جواب داده نشد
GSM_CALL_CLCC_POLL_MS
= 2000
فاصله پرس‌وجوی تور ایمنی +CLCC
GSM_CALL_CLCC_MISSES
= 2
تعداد پاسخ خالی پیاپی برای اعلام «تماس مرده»
نگه‌بانی صنعتیIndustrial Recovery
GSM_SMS_MAX_CHARS
= 60
سقف سخت‌گیرانه طول پیامک (نویسه UTF-8)
GSM_CONSECUTIVE_TIMEOUTS_RESET
= 3
تایم‌اوت‌های متوالی AT قبل از بازیابی
GSM_SIM_RETRY_INTERVAL_MS
= 10000
فاصله پروب CPIN در وضعیت بی‌سیم‌کارت
GSM_CFUN_CYCLE_INTERVAL_MS
= 25000
فاصله چرخه CFUN=0/1 برای بازیابی سیم‌کارت
GSM_HARDWARE_REBOOT_TIMEOUT_MS
= 90000
گیرکردن در Fault/SimMissing → ریست سخت‌افزاری
GSM_PERIODIC_RESET_MS
= 3600000
ریست‌کننده اول: ریست کامل هر ۱ ساعت (0=خاموش)
GSM_OPS_RESET_COUNT
= 50
ریست‌کننده دوم: ریست بعد از هر ۵۰ عملیات (0=خاموش)
GSM_WATCHDOG_GRACE_MS
= 10000
مهلت یافتن فرصت امن برای ریست؛ بعدش اجباری
لاگLogging
GSM_LOG_LEVEL_MAX
= 4
سقف سطح لاگ هنگام کامپایل (0=خاموش تا 5=Trace)
GSM_LOG_BUFFER_SIZE
= 180
اندازه بافر فرمت‌دهی هر خط لاگ
GSM_LOG_COLOR_DEFAULT
= 1
حالت پیش‌فرض رنگ‌های ANSI
user config file (GSM_USER_CONFIG)
// gsm_user_config.hpp — فقط آنچه می‌خواهید عوض کنید:
#define GSM_QUEUE_DEPTH 10 // صف بزرگ‌تر
#define GSM_SMS_MAX_CHARS 70 // سقف کامل UCS2
#define GSM_PERIODIC_RESET_MS (30*60*1000) // ریست هر ۳۰ دقیقه
#define GSM_USER_CONFIG "gsm_user_config.hpp" // در پرچم‌های کامپایلر
FILE REFERENCE

مرجع فایل‌به‌فایل — 50 فایل، بدون حذف جزئیات

هر فایل کتابخانه: نقش، لایه معماری و خلاصه رفتاری. گروه‌بندی بر اساس نوع فایل؛ با جستجو سریع به فایل مورد نظر برسید.

L0gsm/gsm_config.hpp

مرکز پیکربندی کتابخانه: ماکروهای بافر، timeout، نگهبان طول پیامک، نظارت تماس و ریست‌های تعمیراتی.

پیکربندی
L0gsm/hal/hal.hpp

لایه انتزاع سخت‌افزار: اینترفیس‌های IUart، IClock، IPowerControl و تابع مقاوم به سرریز elapsed.

HAL
L2gsm/core/at_types.hpp

انواع پایه AT: AtStatus، AtResult، کدهای خطای kErr* و اینترفیس IAtObserver.

هسته
L2gsm/core/at_engine.hpp

موتور اجرای فرمان‌های AT: ارسال/دریافت، تشخیص prompt و خطوط نهایی، timeout و لغو تراکنش.

هسته
L2gsm/core/line_reader.hpp

تبدیل جریان بایت به خطوط AT و نشان «>» با آشکارسازی overflow.

هسته
L3gsm/core/service.hpp

قرارداد مشترک سرویس‌ها (IService) و وضعیت نشست مشترک (SessionState).

هسته
L2gsm/core/urc_router.hpp

طبقه‌بندی و مسیریابی پیام‌های ناخواسته (URC) مثل RING و +CMTI به handler.

هسته
L3gsm/device/device_traits.hpp

توصیف ماژول‌های SIM800/M08/M08R؛ ویژگی v0.4: clcc_authoritative برای M08R.

پروفایل
L1gsm/log/log.hpp

سامانه لاگ سبک‌وزن: سطح‌ها، کانال‌های تگ‌دار، sinkهای قابل تعویض و ScopedSink.

ابزار
L3gsm/queue/job.hpp

ساختار Job (واحد کاری صف)، JobKind/JobState و فیلد deferred برای وقفه تماس.

صف
L3gsm/queue/job_queue.hpp

صف FIFO ایستا با لغو، تعلیق و purge_stale برای جلوگیری از انباشت صف.

صف
L3gsm/sms/sms_service.hpp

سرویس پیامک: ارسال/خواندن/حذف؛ ویژگی v0.4: نگهبان سخت‌گیرانه طول (max_chars، kErrSmsTooLong).

سرویس
L3gsm/sms/sms_types.hpp

انواع داده پیامک: SmsEncoding، SmsMessage، SmsSendReport و صندوق حلقوی SmsInbox.

سرویس
L3gsm/status/status_service.hpp

سرویس وضعیت مودم: سیگنال، ثبت شبکه، SIM، ذخیره‌سازی و اپراتورهای ایرانی در ModemStatus.

سرویس
L4gsm/tools/serial_bridge.hpp

پل سریال برای اتصال کنسول به مودم: حالت managed (خط‌فرمان) و passthrough (بایت خام).

ابزار
L3gsm/ussd/ussd_service.hpp

سرویس USSD با نشست تعاملی (send_reply/end_session) و پرس‌وجوی خودکار موجودی.

سرویس
L1gsm/util/delegate.hpp

callback سبک‌وزن بدون heap (Delegate) با bind ایمن به متدهای عضو.

ابزار
L1gsm/util/ring_buffer.hpp

بافر حلقوی بایتی ایستا با پرچم overflow؛ مناسب بافر UART در حالت وقفه.

ابزار
L1gsm/util/static_string.hpp

رشته با اندازه ثابت StaticString<N> و ابزارهای پارس AT (parse_int، csv_field).

ابزار
L3gsm/voice/call_service.hpp

سرویس تماس با نظارت v0.4: CallPolicy، CallEndReason، قطع/رد خودکار و polling +CLCC.

سرویس
L3gsm/modem/modem.hpp

کلاس اصلی Modem: چرخه عمر، صف Jobها، housekeeping، بازیابی SIM و گیت تماس.

هسته
L1gsm/codec/phone_number.hpp

نرمال‌سازی شماره تلفن به E.164 با کد کشور پیش‌فرض +98.

کدک
L1gsm/codec/ucs2.hpp

تبدیل‌های UTF-8 ↔ hex-UCS2 برای پیامک فارسی و تشخیص رمزگذاری.

کدک
L4gsm/cli/cli.hpp

کنسول تعاملی cli::Console (منتقل‌شده از برنامه به هسته کتابخانه) با منو، وضعیت زنده و خروجی وب.

کنسول
L3gsm/maintenance/watchdog.hpp

نگهبان تعمیراتی v0.4: دو بازنشان (دوره‌ای + بودجه عملیات)، ResetPolicy و بررسی سلامت.

نگه‌بانی
QUICK START

شروع سریع — از صفر تا اولین پیامک فارسی

کتابخانه با CMake و PlatformIO بیلد می‌شود. سه مسیر رایج: STM32 با CubeHAL، آردوینو و تست میزبان بدون سخت‌افزار.

مسیر STM32 (CMake)

build + link
# بیلد کتابخانه برای میزبان/تست
cmake -B build -DGSM_BUILD_TESTS=ON
cmake --build build
./build/host_smoke # تست دود
# برای STM32: پورت HAL را فعال کنید
cmake -B build-stm32 -DGSM_PORT_STM32=ON \
-DCMAKE_TOOLCHAIN_FILE=arm-gcc.cmake
stm32_main.cpp
static port::Stm32Uart uart(&huart2); // UART مودم
static port::Stm32Clock clk;
static port::Stm32PowerControl pwr(GPIOB, GPIO_PIN_12);
static Modem modem(uart, clk, kQuectelM08R, &pwr);
void AppInit() {
modem.set_on_sms(...); // کال‌بک پیامک دریافتی
modem.begin();
}
void AppRun() { modem.tick(); } // در while(1)

مسیر Arduino

arduino_basic.ino
#include <gsm/modem/modem.hpp>
#include <gsm_port_arduino.hpp>
port::ArduinoUart modem_uart(Serial1);
port::ArduinoClock clk;
Modem modem(modem_uart, clk, kSim800);
void onSms(void*, const SmsMessage& m) {
Serial.println(m.text.c_str()); // متن فارسی UTF-8
}
void setup() {
Serial.begin(115200); Serial1.begin(115200);
modem.set_on_sms(SmsService::Received(&onSms, nullptr));
modem.begin();
}
void loop() {
modem.tick();
if (modem.ready())
modem.send_sms("09037744527", "سلام از آردوینو");
}

PlatformIO

library.json آماده است: پلتفرم‌های ststm32، atmelavr، espressif32 و native با پرچم‌های -std=gnu++17 -fno-exceptions -fno-rtti. کافی است مسیر کتابخانه را به lib_deps اضافه کنید.

تست بدون سخت‌افزار

پورت Host با شبیه‌ساز UART اجازه می‌دهد سناریوها (بوت، USSD، تماس، صف، واچ‌داگ) کاملاً قطعی روی دسکتاپ تست شوند — همان چیزی که host_smoke.cpp انجام می‌دهد.

چه چیزی را تنظیم کنم؟

معمولاً هیچ! پیش‌فرض‌ها برای پیامک فارسی UCS2 و پایداری ۲۴/۷ قفل شده‌اند. فقط اگر روی شبکه/سیم‌کارت متفاوت رفتار عوض شد، رشته CSMP را در DeviceTraits تغییر دهید.

سیم‌کشی سخت‌افزار (پیشنهادی)

پین مودماتصال MCUتوضیح
VCC 5V/4Vمنبع تغذیه 2Aجریان پیکی تماس/انتقال بالاست
TXDRX UART (115200)خط داده از مودم
RXDTX UARTخط داده به مودم
PWRKEYGPIO (مثلاً PB12)اختیاری — برای پالس/ریست سخت‌افزاری
GNDGND مشترکحتماً مشترک با MCU

گام‌های اولیه پیشنهادی

  1. 1 کتابخانه را روی میزبان بیلد و host_smoke را اجرا کنید تا با رفتار آشنا شوید.
  2. 2 مثال STM32/Arduino را با کنسول تعاملی (کلید m برای منو) روی بورد بیندازید.
  3. 3 با کلید i وضعیت زنده را ببینید و با b استعلام شارژ بگیرید.
  4. 4 پیامک فارسی را با دستور sms 09… متن تست کنید.
  5. 5 اگر نیاز به دیباگ AT داشتید، با کلید P به پل Passthrough بروید.
FIELD LESSONS

درس‌های میدانی — تجربه‌های واقعی که در کد قفل شدند

این بخش خلاصه چیزهایی است که از لاگ‌های واقعی مودم‌ها آموخته شد؛ رفتارهایی که مستند نمی‌شوند اما کار شما را خراب می‌کنند. همه این‌ها الان به‌صورت خودکار در کتابخانه هندل می‌شوند.

۱

با CSCS="UCS2" شماره هم باید UCS2 باشد

وقتی charset روی UCS2 قفل شده، اگر شماره را ASCII بفرستید مودم خطای +CMS ERROR: 3518 می‌دهد. شماره باید به هگز UCS2 تبدیل شود — این رفتار در DeviceTraits::ucs2_recipient_required قفل شده و SmsService خودکار تبدیل می‌کند.

log evidence
AT+CSCS="UCS2" → OK
AT+CMGS="+989037744527" → +CMS ERROR: 3518 ← شماره ASCII!
AT+CMGS="002B0039...3237" → > ← همان شماره، UCS2-hex ✔
۲

قبل از Ctrl+Z هیچ‌وقت CRLF نفرست

اگر بعد از payload پیامک CRLF (\r\n) بفرستید و بعد Ctrl+Z، مودم خطای +CMS ERROR: 520 می‌دهد. AtEngine::write_raw هرگز CRLF اضافه نمی‌کند؛ پایان payload فقط با write_ctrl_z() اعلام می‌شود.

log evidence
"0633064406270645\r\n" + 1A → +CMS ERROR: 520
"0633064406270645" + 1A → +CMGS: 3 OK ✔
۳

گیرکردن در پرامپت «>» با ESC حل می‌شود

تا وقتی مودم در حالت prompt/data-entry است، به هیچ فرمان ATای جواب نمی‌دهد و +CME ERROR: 58 می‌دهد. هر لغو یا خطای payload در کتابخانه ابتدا بایت 0x1B (ESC) می‌فرستد تا مودم بیرون بیاید.

log evidence
AT+CMGS="..." → >
"0633..." (بدون 1A)
at → +CME ERROR: 58 ← مودم گیر کرده!
<ESC 0x1B> → آزاد می‌شود ✔
۴

ترتیب امن ارسال پیامک

ترتیب استاندارد: CMGF=1 → CSCS="UCS2" → CSMP → CPMS → CMGS. پارامتر CSMP برای SIM800 روی «17,71,0,25» و برای M08 روی «17,167,0,8» قفل شده؛ هر دو در DeviceTraits قابل تغییرند.

۵

بنر بوت و URCها قابل اعتماد نیستند

پیام‌هایی مثل «DY» (نسخه بریده RDY)، «Call Ready»، «SMS Ready» و «+CPIN: READY» همه در UrcRouter هندل می‌شوند، اما ماشین حالت هرگز منتظرشان نمی‌ماند و خودش با پروب AT مودم را می‌آزماید.

۶

قطع تماس در هر ماژول فرق دارد

فرمان تماس صوتی «ATD09037744527;» (سمی‌کولن = صوتی) است. قطع تماس در SIM800 با ATH و در Quectel M08/M08R با AT+CHUP انجام می‌شود — این تفاوت در DeviceTraits::hangup_cmd ذخیره شده و اگر فرمان اصلی جواب ندهد، ATH به‌عنوان جایگزین یک‌بار امتحان می‌شود.

۷

اکو روشن مودم گیج‌کننده نیست

لاگ‌های میدانی نشان داد اکوی مودم روشن است. ATE0 در بوت خاموشش می‌کند، اما حتی اگر روشن بماند، پارسر AtEngine خط اکو را با فرمان ارسالی مقایسه و دور می‌ریزد.

۸

M08R: پایان تماس بی‌صدا

M08R هنگام رد یا بی‌جوابی تماس، هیچ‌کدام از NO CARRIER / BUSY / NO ANSWER را نمی‌فرستد. راه‌حل: پرس‌وجوی دوره‌ای AT+CLCC به‌عنوان منبع حقیقت (clcc_authoritative=true) به‌علاوه تایمر قطع خودکار ۲۰ ثانیه‌ای.

۹

M08R: هنگ و ارسال پیام خالی

ارسال متن بلند یا ارسال در میانه عملیات دیگر، M08R را به حالت prompt می‌اندازد و گاهی پیامک خالی می‌فرستد. چهار محافظ در برابرش: گارد طول ۶۰ نویسه، گیت تماس، لغو امن با ESC، و ریست‌های دوره‌ای نگه‌بانی.

نکته مشترک همه این درس‌ها: هیچ‌چیز مودم را باور نکن. ماشین حالت همیشه خودش می‌آزماید، تایمر دارد، تور ایمنی دارد و در بدترین حالت خودش را ریست می‌کند. این فلسفه «بی‌اعتمادی نظام‌مند» است که پایداری ۲۴/۷ را ممکن می‌کند.

FOOTPRINT

مصرف حافظه — بودجه‌بندی دقیق بایت‌به‌بایت

با تنظیمات پیش‌فرض، کل کتابخانه حدود ۳ تا ۴ کیلوبایت RAM مصرف می‌کند — همه استاتیک، بدون هیچ تخصیصی در زمان اجرا. همه اجزا با ماکروهای GSM_* قابل کوچک یا بزرگ شدن هستند.

مؤلفهبودجه پیش‌فرضماکرو تنظیم
بافر حلقوی RX1024 بایتGSM_RX_RING_SIZE
DMA/بافر پورت STM32256 بایتدر پورت
صف کارها۶ × ~۲۳۰ بایت ≈ 1.4KBGSM_QUEUE_DEPTH
بافرهای متن پیامک512 + 24 بایتGSM_SMS_TEXT_BYTES
صندوق ورودی۴ اسلات پیامGSM_SMS_INBOX_SLOTS
بافر USSD512 بایتGSM_USSD_RESPONSE_BYTES
بافرهای لاگ/خط180 + 512 بایتGSM_LOG_BUFFER_SIZE

برای MCUهای کوچک، کم کردن GSM_RX_RING_SIZE به ۵۱۲ و GSM_QUEUE_DEPTH به ۴ مصرف را زیر ۲ کیلوبایت می‌برد؛ برای کارهای سنگین (اسپم پیامک، صف تماس) می‌توانید صف را تا ۱۲ بالا ببرید بدون نگرانی از heap.