Skip to content

Add PDU mode for concatenated multi-segment SMS#2

Merged
KerseyFabrications merged 1 commit into
mainfrom
feature/pdu-concatenated-sms
Apr 22, 2026
Merged

Add PDU mode for concatenated multi-segment SMS#2
KerseyFabrications merged 1 commit into
mainfrom
feature/pdu-concatenated-sms

Conversation

@KerseyFabrications

Copy link
Copy Markdown
Contributor

Flip ECHO from text mode (AT+CMGF=1) to PDU mode (AT+CMGF=0) so SMS send/receive works for messages of any length. Outbound is UCS2-only; inbound decodes both UCS2 and GSM7 (most handsets default to GSM7 for plain ASCII). Multi-segment messages concatenate via the UDH reference header on both paths.

New modules:

  • pdu.c/h — encoder, decoder, UDH concat, UCS2+GSM7, hardened bounds checks at every length prefix, UCS2 sanitization (strips NULs, bidi overrides, zero-width, BOM, variation selectors, Unicode tag block)
  • sms_reassembly.c/h — 8-slot inline store, 2 slots/sender, 10-min TTL, LRU eviction weighted by fragment count
  • sms_io.c/h — extracts send + CMTI handling from oasis-echo.c, keeps the main daemon under its size limit

Rate buckets reworked to a leaky-bucket counter; the old 64-entry ring silently never rejected at limits > 64 (fix needed for the 200/hr segment budget). Inter-segment pacing (150ms default) avoids SIM7600 wedging on back-to-back concat sends.

CMTI handling now uses a dedicated deferred queue drained after MQTT commands, and CMGR/CMGD use a 500ms timeout, so a 10-segment inbound burst can no longer stall a concurrent hangup/dial for 40s.

MQTT response schema gains an optional data object for segments_sent / segments_total observability. Existing consumers ignore unknown fields.

Legacy text mode reachable via --legacy-sms for rollback.

Tests: 19 PDU + 8 reassembly. All 6 suites pass.

Also: align CLAUDE.md with the OASIS house style (same structure as DAWN's CLAUDE.md — critical rules up top, @imports for ARCHITECTURE / CODING_STYLE_GUIDE, terse gotchas section).

@qodo-code-review

Copy link
Copy Markdown
ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Add PDU mode for concatenated multi-segment SMS with rate limiting and reassembly

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Implements PDU mode (AT+CMGF=0) for SMS send/receive, replacing text mode to support messages of
  any length
• **New modules:**
  - pdu.c/h — 3GPP TS 23.040 encoder/decoder with UCS2/GSM7 support, UDH concatenation, and
  comprehensive Unicode sanitization (strips null bytes, bidi overrides, zero-width chars, BOM,
  variation selectors, tag blocks)
  - sms_reassembly.c/h — 8-slot bounded reassembly store with per-sender cap (2 slots), 10-min TTL,
  LRU eviction weighted by fragment count, and duplicate detection
  - sms_io.c/h — Extracts SMS send/CMTI handling from main daemon, supports both PDU and legacy text
  modes with inter-segment pacing (150ms default)
• **Rate limiting overhaul:** Switched from 64-entry ring buffer to leaky-bucket counter (fixes
  silent rejection at limits > 64); separate segment-level bucket for 200/hr budget
• **Concurrency improvements:** Dual command queues (MQTT commands drain ahead of deferred CMTI
  events) prevent hangup/dial stalls behind multi-segment bursts; CMGR/CMGD use 500ms timeout
• **MQTT schema extension:** Optional data object for segments_sent/segments_total
  observability; existing consumers ignore unknown fields
• **Rollback support:** --legacy-sms flag enables text mode fallback
• **AT command layer:** New at_command_send_pdu() for two-phase PDU transmission with hex
  validation
• **Modem init refactored:** pdu_mode parameter selects AT+CMGF mode; AT+CSMP only sent in text
  mode
• **Documentation:** CLAUDE.md aligned with OASIS house style (critical rules top, @imports for
  standards, terse gotchas)
• **Test coverage:** 19 PDU tests (encode/decode, UDH, sanitization, GSM7, round-trip) + 8
  reassembly tests (eviction, caps, timeouts, duplicates); all 6 suites pass
Diagram
flowchart LR
  A["SMS Input<br/>CMTI Event"] -->|"pdu_decode<br/>UDH concat"| B["sms_reassembly<br/>8-slot store"]
  B -->|"complete"| C["Inbound SMS<br/>reassembled"]
  D["MQTT Command<br/>send SMS"] -->|"sms_io_send<br/>PDU encode"| E["Rate Bucket<br/>leaky-bucket"]
  E -->|"segment pacing<br/>150ms"| F["AT+CMGS<br/>PDU mode"]
  F -->|"SIM7600"| G["Outbound SMS<br/>multi-segment"]
  H["Config"] -->|"pdu_mode flag"| I["modem_init<br/>AT+CMGF=0/1"]
  I -->|"selects"| F
  I -->|"selects"| A
Loading

Grey Divider

File Changes

1. src/pdu.c ✨ Enhancement +1001/-0

PDU encoder/decoder with multi-segment and sanitization support

• New PDU encoder/decoder module implementing 3GPP TS 23.040 SMS wire format with support for UCS2
 and GSM7 character sets
• Handles multi-segment SMS via UDH concatenation headers with 8-bit reference IDs
• Comprehensive input sanitization stripping null bytes, bidi overrides, zero-width characters, and
 Unicode tag blocks (prompt injection vectors)
• Surrogate pair handling for supplementary Unicode characters and hardened bounds checks on all
 length prefixes

src/pdu.c


2. src/sms_io.c ✨ Enhancement +427/-0

SMS I/O abstraction with PDU and legacy text-mode support

• New module extracting SMS send and CMTI handling logic from main daemon to keep binary size under
 limit
• Implements both legacy text-mode (UCS2-hex) and new PDU-mode send paths with inter-segment pacing
 (150ms default)
• Inbound reassembly integration with single-segment fast path and multi-segment buffering
• Segment-level rate limiting separate from message-level budget (200/hr default for segments)

src/sms_io.c


3. src/sms_reassembly.c ✨ Enhancement +254/-0

Bounded reassembly store for multi-segment SMS with attack resistance

• New 8-slot inline reassembly store for multi-segment inbound SMS with per-sender cap (2 slots)
• 10-minute TTL with automatic eviction; LRU weighted by fragment count to resist slot-exhaustion
 attacks
• Duplicate detection via bitmask and per-sender rate limiting to prevent reassembly table poisoning
• Deterministic sweep on every push keeps table self-cleaning without separate scheduler

src/sms_reassembly.c


View more (17)
4. src/oasis-echo.c ✨ Enhancement +117/-178

Rate limiting fix, dual queues, and PDU mode integration

• Rate bucket implementation switched from 64-entry ring buffer to leaky-bucket counter (fixes
 silent rejection at limits > 64)
• Added separate segment-level rate bucket and inter-segment delay configuration
• Dual command queues: MQTT commands drain ahead of deferred CMTI events to prevent hangup/dial
 stalls behind multi-segment bursts
• Modem init now takes pdu_mode flag; --legacy-sms flag enables rollback to text mode
• Integrated sms_io module for send/receive; reassembly sweep runs on main loop

src/oasis-echo.c


5. tests/test_pdu.c 🧪 Tests +334/-0

Comprehensive PDU encoder/decoder unit tests

• 19 unit tests covering PDU encode/decode paths, segment boundary handling, and malformed-input
 rejection
• Tests for UDH concatenation, surrogate pairs, sanitization of dangerous Unicode, and GSM7 decoding
• Validates address encoding/decoding, buffer overflow protection, and round-trip encode-decode
 consistency
• Covers real-world PDU captures (e.g., iPhone "hello" in GSM7)

tests/test_pdu.c


6. tests/test_sms_reassembly.c 🧪 Tests +176/-0

SMS reassembly unit tests with eviction and cap validation

• 8 unit tests for reassembly state machine, slot eviction, timeout, and per-sender limits
• Validates duplicate detection, fragment ordering, and output buffer overflow handling
• Tests LRU eviction bias toward low-fragment-count slots and per-sender cap enforcement

tests/test_sms_reassembly.c


7. tests/test_sms_reassembly.c 🧪 Tests +176/-0

SMS reassembly unit tests with 8 test cases

• New unit test suite with 8 test cases covering SMS fragment reassembly logic
• Tests cover in-order/out-of-order fragment arrival, duplicate rejection, per-sender capacity
 limits, LRU eviction, timeout sweeps, and validation of malformed UDH fields
• Uses Unity framework with setUp/tearDown and assertion macros
• Validates reassembly state machine transitions and statistics counters

tests/test_sms_reassembly.c


8. include/pdu.h ✨ Enhancement +172/-0

PDU encoder/decoder API header for SMS

• New header defining PDU encoder/decoder API for 3GPP TS 23.040 SMS SUBMIT/DELIVER
• Exports pdu_encode_submit() for outbound UCS2 messages and pdu_decode() for inbound with UDH
 concatenation support
• Defines error codes (pdu_err_t), segment structure (pdu_segment_t), and decoded metadata
 (pdu_decoded_t)
• Includes helper functions for segment counting, reference ID generation, and DCS selection

include/pdu.h


9. src/at_command.c ✨ Enhancement +144/-0

PDU mode AT+CMGS send implementation

• Adds at_command_send_pdu() function implementing two-phase AT+CMGS PDU send (prompt wait + hex
 body + Ctrl-Z)
• Includes hex alphabet validation before transmission to prevent modem state corruption
• Uses existing pending-response condvar mechanism with AT_TIMEOUT_SMS timeout
• Handles both prompt timeout and body transmission timeout with ESC abort on failure

src/at_command.c


10. include/sms_reassembly.h ✨ Enhancement +113/-0

SMS reassembly store API with LRU eviction

• New header for inbound multi-segment SMS reassembly with bounded state (8 slots, 2 per sender,
 10-min TTL)
• Exports sms_reassembly_push() for fragment ingestion, sms_reassembly_sweep() for timeout
 eviction, and stats/reset functions
• Defines result codes (reasm_result_t) for incomplete/complete/rejected/error states
• Includes statistics struct tracking completions, timeouts, evictions, duplicates, and per-sender
 cap violations

include/sms_reassembly.h


11. src/modem.c ✨ Enhancement +9/-6

Modem init refactored for PDU/text mode selection

• Adds pdu_mode parameter to modem_init() to select AT+CMGF mode (0=PDU, 1=text)
• Conditionally sends AT+CSMP (SMS parameters) only in text mode; PDU mode carries DCS per-frame
• Updates logging to show selected mode during initialization

src/modem.c


12. src/mqtt_comms.c ✨ Enhancement +18/-3

MQTT response schema extended with optional data object

• Adds optional data_json parameter to mqtt_build_response_json() and mqtt_publish_response()
• Parses caller-provided JSON object and merges it under "data" key in response payload
• Includes error handling for malformed data_json with warning log (bad input dropped, response
 still sent)
• Enables segments_sent/segments_total observability in SMS responses

src/mqtt_comms.c


13. tests/test_mqtt_messages.c 🧪 Tests +3/-3

MQTT test signature updates for data_json parameter

• Updates three test cases to pass NULL for new data_json parameter in
 mqtt_build_response_json() calls
• No logic changes; purely signature adaptation to new optional data field

tests/test_mqtt_messages.c


14. include/echo.h ✨ Enhancement +35/-7

Rate limiter rework and PDU mode configuration

• Adds three new configuration fields: rate_limit_segments_per_hour, inter_segment_delay_ms,
 pdu_mode boolean
• Adds two new constants: ECHO_DEFAULT_RATE_SEGMENTS_H (200) and ECHO_DEFAULT_SEGMENT_DELAY_MS
 (150)
• Replaces ring-buffer rate limiter with leaky-bucket counter design; adds rate_bucket_take_n()
 for atomic multi-token consumption
• Adds AT_TIMEOUT_SMS_STORAGE (500ms) constant for CMGR/CMGD operations to prevent 40s stalls on
 multi-segment bursts

include/echo.h


15. include/sms_io.h ✨ Enhancement +107/-0

SMS I/O orchestration module extracted from daemon

• New header extracting SMS send and CMTI handling from oasis-echo.c into reusable module
• Exports sms_io_send() (low-level) and sms_io_send_and_respond() (with MQTT response) for
 outbound SMS
• Exports sms_io_handle_cmti() for inbound SMS dispatch with reassembly support
• Defines error codes (sms_io_send_err_t) and context struct (sms_io_ctx_t) bundling AT port,
 rate buckets, and config

include/sms_io.h


16. include/modem.h ✨ Enhancement +8/-5

Modem init signature updated for PDU mode parameter

• Updates modem_init() signature to accept pdu_mode boolean parameter
• Documents that AT+CSMP is only sent in text mode; PDU mode carries DCS per-frame
• Clarifies init sequence includes AT+CMGF=0 (PDU) or 1 (text) based on mode

include/modem.h


17. include/mqtt_comms.h ✨ Enhancement +6/-1

MQTT response API extended with optional data field

• Adds data_json parameter to mqtt_publish_response() and mqtt_build_response_json() function
 signatures
• Documents optional pre-serialized JSON object merged under "data" key in response
• Enables segments_sent/segments_total observability in SMS command responses

include/mqtt_comms.h


18. include/at_command.h ✨ Enhancement +22/-0

AT command API extended with PDU send function

• Adds at_command_send_pdu() function declaration for two-phase PDU mode SMS send
• Documents AT+CMGS=<octets> prompt wait followed by hex PDU + Ctrl-Z transmission
• Includes hex alphabet validation and timeout handling details

include/at_command.h


19. CLAUDE.md 📝 Documentation +60/-130

CLAUDE.md aligned with OASIS house style

• Restructured to match DAWN's CLAUDE.md format with critical rules at top, @imports for
 ARCHITECTURE/CODING_STYLE_GUIDE
• Condensed from 182 to 112 lines; moved detailed standards to external references
• Added explicit "Critical Rules" section (no file deletion, no git commands, feedback before
 implementation, format before commit)
• Simplified architecture section to threading model and AT command types; moved detailed file list
 to comments
• Terse gotchas section for SIM7600 hardware and development lifecycle

CLAUDE.md


20. CMakeLists.txt ⚙️ Configuration changes +15/-0

Build configuration for PDU and reassembly modules

• Adds three new source files to SOURCES: src/pdu.c, src/sms_io.c, src/sms_reassembly.c
• Adds two new test executables: test_pdu (PDU encode/decode) and test_sms_reassembly
 (LRU/cap/duplicate handling)
• Links test_pdu against unity, echo_logging, pthread; test_sms_reassembly against unity,
 echo_logging
• Registers both tests with ctest

CMakeLists.txt


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. at_write_raw() return ignored 📘 Rule violation ≡ Correctness
Description
In at_command_send_pdu, the timeout path calls at_write_raw() without checking whether the ESC
abort write succeeded. Ignoring this return can hide serial I/O failures and leave the modem in an
unexpected state after a prompt timeout.
Code

src/at_command.c[R508-510]

+         char esc = 0x1B;
+         at_write_raw(ctx, &esc, 1);
+         ctx->pending.type = AT_PENDING_NONE;
Evidence
PR Compliance ID 343436 requires checking/handling return values; the new timeout handling path
calls at_write_raw() as a standalone statement, discarding its result.

Rule 343436: Check and handle function return values instead of ignoring them
src/at_command.c[508-510]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`at_command_send_pdu()` ignores the return value of `at_write_raw()` when attempting to send an ESC to abort `AT+CMGS` after a prompt timeout.

## Issue Context
This is error-handling code on a serial I/O path; failing to detect/log an abort-write failure makes diagnosing modem state issues much harder.

## Fix Focus Areas
- src/at_command.c[508-516]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. sms_io.c ignores return values 📘 Rule violation ≡ Correctness
Description
New code ignores return values from at_command_send() and build_segment_data_json(), and then
may publish an uninitialized data_buf to MQTT. This can mask CMGD failures and can produce
malformed MQTT responses under buffer/allocation failure paths.
Code

src/sms_io.c[R224-230]

+   char data_buf[128];
+   build_segment_data_json(data_buf, sizeof(data_buf), sent, total);
+
+   switch (rc) {
+      case SMS_IO_SEND_OK:
+         mqtt_publish_response(action, request_id, true, NULL, NULL, NULL, data_buf);
+         break;
Evidence
PR Compliance ID 343436 requires checking/handling return values; cmgd_fire_and_forget() discards
the at_command_send() status and sms_io_send_and_respond() discards build_segment_data_json()
(then uses data_buf regardless).

Rule 343436: Check and handle function return values instead of ignoring them
src/sms_io.c[48-53]
src/sms_io.c[224-230]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sms_io.c` introduces ignored return values:
- `cmgd_fire_and_forget()` discards `at_command_send()` status.
- `sms_io_send_and_respond()` discards `build_segment_data_json()` status and may pass an uninitialized `data_buf` to `mqtt_publish_response()`.

## Issue Context
Even if these failures are rare, ignoring them makes behavior non-deterministic during low-memory conditions or modem/storage errors.

## Fix Focus Areas
- src/sms_io.c[48-53]
- src/sms_io.c[224-230]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. CMGR failure skips delete 🐞 Bug ☼ Reliability
Description
On AT+CMGR failure in both text-mode and PDU-mode inbound handlers, the code returns without issuing
CMGD, leaving the message in modem storage. This contradicts the new short storage timeout policy
and can cause the modem inbox to fill, preventing further SMS reception.
Code

src/sms_io.c[R383-387]

+   at_status_t rc = at_command_send(at, cmd, &resp, AT_TIMEOUT_SMS_STORAGE);
+   if (rc != AT_OK) {
+      OLOG_WARNING("CMGR PDU failed at idx %d: %s", sms_index, at_status_str(rc));
+      return;
+   }
Evidence
The code documents that on storage timeouts it should "fall back to logging + CMGD-fire-forget so
the inbox doesn't fill", and a cmgd_fire_and_forget() helper exists. However, both
handle_cmti_text_mode() and handle_cmti_pdu_mode() return early on CMGR errors without calling
CMGD, leaving the slot occupied indefinitely after the only CMTI notification has already been
consumed.

include/echo.h[56-63]
src/sms_io.c[48-53]
src/sms_io.c[306-314]
src/sms_io.c[379-387]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Inbound CMTI handlers return on `AT+CMGR` failure without deleting the modem storage slot. With the new 500ms storage timeout, transient failures can accumulate undeleted messages until the modem inbox fills.

### Issue Context
`AT_TIMEOUT_SMS_STORAGE` is introduced specifically to avoid long main-thread stalls and the comment states the timeout path should fall back to CMGD fire-and-forget.

### Fix Focus Areas
- src/sms_io.c[306-314]
- src/sms_io.c[379-387]
- include/echo.h[56-63]

### Recommended fix
- In both handlers, when `rc != AT_OK`, log and call `cmgd_fire_and_forget(at, sms_index)` before returning.
- If you still want to preserve messages on *non-timeout* errors, gate the deletion specifically on `rc == AT_TIMEOUT` (and possibly other retryable cases), but ensure the documented timeout policy is enforced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. hex_chars not UPPER_CASE 📘 Rule violation ✧ Quality
Description
The module-level constant hex_chars is defined using lower-case naming. This violates the required
UPPER_CASE naming convention for constants and reduces consistency across the codebase.
Code

src/pdu.c[48]

+static const char hex_chars[] = "0123456789ABCDEF";
Evidence
PR Compliance ID 343440 requires constants use UPPER_CASE naming; hex_chars is a file-scope
static const constant but is named in lower-case.

Rule 343440: Use UPPER_CASE naming for constants
src/pdu.c[48-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Rename the file-scope constant `hex_chars` to follow the required UPPER_CASE naming convention for constants.

## Issue Context
This is a small consistency/style compliance requirement and should be a straightforward rename with no behavioral change.

## Fix Focus Areas
- src/pdu.c[48-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Rate tokens burned on rejects 🐞 Bug ≡ Correctness
Description
sms_io_send() debits the per-message rate bucket before body sanitization and before segment-bucket
checks, so invalid bodies or segment-rate-limited requests still consume message tokens. This can
prematurely rate-limit legitimate sends and makes the limiter dependent on rejected requests, not
successful sends.
Code

src/sms_io.c[R169-182]

+   if (!ctx || !ctx->at || !ctx->msg_bucket || !dest || !utf8_body) {
+      return SMS_IO_SEND_INVALID_NUMBER;
+   }
+   if (!sms_validate_number(dest)) {
+      return SMS_IO_SEND_INVALID_NUMBER;
+   }
+   if (!rate_bucket_allow(ctx->msg_bucket)) {
+      return SMS_IO_SEND_RATE_LIMITED;
+   }
+
+   char clean[SMS_BODY_MAX + 1];
+   if (sms_sanitize_body(utf8_body, clean, sizeof(clean)) < 0) {
+      return SMS_IO_SEND_INVALID_BODY;
+   }
Evidence
The message bucket is consumed immediately after destination validation, but later failures
(sanitize failure, PDU segment limit failure) return errors without refunding. In PDU mode, the
segment limiter is checked only after the message token has already been spent, so SEGMENT_LIMITED
responses also reduce the per-message budget.

src/sms_io.c[153-189]
src/sms_io.c[90-109]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Per-message rate tokens are debited before validating/sanitizing the SMS body and before the per-segment bucket check. Requests that fail validation or segment-rate checks still reduce the message budget.

### Issue Context
This is especially visible for:
- invalid-body requests (Ctrl-Z / ESC) rejected by `sms_sanitize_body()`
- PDU sends rejected by `rate_bucket_take_n(segment_bucket, seg_count)`

### Fix Focus Areas
- src/sms_io.c[153-189]
- src/sms_io.c[90-121]

### Recommended fix
- Validate/sanitize the body first.
- Compute `seg_count` (for PDU mode) before debiting any buckets.
- Then debit buckets in an order that avoids partial charging on failure (e.g., check availability first or debit segment first and message second, with a refund path if the second fails).
- Ensure error paths do not consume tokens unless you explicitly want to rate-limit *attempts* (and if so, reflect that in docs/messages).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Empty body flagged too long 🐞 Bug ≡ Correctness
Description
send_pdu_mode() treats pdu_segment_count()==0 (empty body) as BODY_TOO_LONG, producing an incorrect
error response and interacting poorly with the current rate-debit ordering. This makes empty-body
failures look like concatenation overflow rather than invalid input.
Code

src/sms_io.c[R97-101]

+   bool ucs2 = pdu_needs_ucs2(clean);
+   int seg_count = pdu_segment_count(clean, ucs2);
+   if (seg_count <= 0 || seg_count > PDU_MAX_SEGMENTS) {
+      return SMS_IO_SEND_BODY_TOO_LONG;
+   }
Evidence
pdu_segment_count() returns 0 for empty strings, but send_pdu_mode() maps seg_count<=0 to
SMS_IO_SEND_BODY_TOO_LONG. That response path then maps to a user-facing "SMS body exceeds maximum
concatenated length" message, which is misleading for an empty payload.

src/sms_io.c[90-101]
src/pdu.c[440-443]
src/sms_io.c[243-246]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Empty SMS bodies are currently classified as `SMS_IO_SEND_BODY_TOO_LONG` in PDU mode.

### Issue Context
`pdu_segment_count()` returns 0 for empty inputs, which is distinct from “too long”. This leads to misleading MQTT error codes/messages.

### Fix Focus Areas
- src/sms_io.c[90-101]
- src/pdu.c[440-443]

### Recommended fix
- Add an explicit empty-body check and return `SMS_IO_SEND_INVALID_BODY` (or a new `SMS_IO_SEND_EMPTY_BODY`) before the length/segment checks.
- If you choose to allow empty bodies, decide the correct segment count/encoding and ensure tests cover it.
- Consider moving this validation ahead of rate-bucket debits (ties into the separate rate-debit ordering issue).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

7. Sender key can truncate 🐞 Bug ≡ Correctness
Description
sms_reassembly stores the sender key in PHONE_NUMBER_MAX + 1 bytes and uses strcmp() on that key,
but PDU decode can yield a sender string longer than PHONE_NUMBER_MAX (e.g., '+' plus 20 digits). In
that case snprintf() truncates the stored key, subsequent fragments won't match the existing slot,
and reassembly can fail or waste slots.
Code

src/sms_reassembly.c[R33-57]

+typedef struct {
+   bool in_use;
+   char sender[PHONE_NUMBER_MAX + 1];
+   uint8_t ref_id;
+   uint8_t total;
+   uint16_t received_mask; /* bit (seq-1) set when that fragment has arrived */
+   char fragments[PDU_MAX_SEGMENTS][REASSEMBLY_FRAG_BUF_SIZE];
+   size_t frag_len[PDU_MAX_SEGMENTS];
+   time_t first_seen;
+} reasm_slot_t;
+
+static reasm_slot_t g_slots[REASSEMBLY_SLOTS];
+static sms_reassembly_stats_t g_stats;
+
+/* ── Helpers ─────────────────────────────────────────────────────────── */
+
+static void clear_slot(reasm_slot_t *slot) {
+   memset(slot, 0, sizeof(*slot));
+}
+
+static reasm_slot_t *find_slot(const char *sender, uint8_t ref_id) {
+   for (int i = 0; i < REASSEMBLY_SLOTS; i++) {
+      if (g_slots[i].in_use && g_slots[i].ref_id == ref_id &&
+          strcmp(g_slots[i].sender, sender) == 0) {
+         return &g_slots[i];
Evidence
Slots store sender[PHONE_NUMBER_MAX+1] and are looked up via strcmp(). When creating a slot, the
sender is copied with snprintf() into that fixed buffer, which truncates if longer than
PHONE_NUMBER_MAX. PDU address decoding allows up to 20 digits and prepends '+' for international
numbers, which can exceed PHONE_NUMBER_MAX=20 characters even though it fits in the PDU decode
struct’s sender buffer.

include/echo.h[68-69]
src/sms_reassembly.c[33-57]
src/sms_reassembly.c[190-206]
src/pdu.c[376-405]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`sms_reassembly` uses `PHONE_NUMBER_MAX` for the sender key, but the PDU path can produce sender strings longer than that. If truncation occurs, slot lookup fails and fragments won’t reassemble.

### Issue Context
Even if rare in practice (E.164 is typically <= 15 digits), the code currently accepts up to 20 digits from the PDU and can prepend '+', making the cross-module constraints inconsistent.

### Fix Focus Areas
- src/sms_reassembly.c[33-57]
- src/sms_reassembly.c[190-206]
- src/pdu.c[376-405]
- include/echo.h[68-69]

### Recommended fix
Pick one consistent constraint:
- EITHER increase `PHONE_NUMBER_MAX` (and any dependent buffers) to cover the longest sender you’ll accept in PDU decode,
- OR tighten PDU decode to reject/clip senders beyond `PHONE_NUMBER_MAX` (including '+' when present),
- OR store sender keys in reassembly using `PDU_SENDER_MAX` (or a new shared constant) instead of `PHONE_NUMBER_MAX`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/at_command.c
Comment on lines +508 to +510
char esc = 0x1B;
at_write_raw(ctx, &esc, 1);
ctx->pending.type = AT_PENDING_NONE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. at_write_raw() return ignored 📘 Rule violation ≡ Correctness

In at_command_send_pdu, the timeout path calls at_write_raw() without checking whether the ESC
abort write succeeded. Ignoring this return can hide serial I/O failures and leave the modem in an
unexpected state after a prompt timeout.
Agent Prompt
## Issue description
`at_command_send_pdu()` ignores the return value of `at_write_raw()` when attempting to send an ESC to abort `AT+CMGS` after a prompt timeout.

## Issue Context
This is error-handling code on a serial I/O path; failing to detect/log an abort-write failure makes diagnosing modem state issues much harder.

## Fix Focus Areas
- src/at_command.c[508-516]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/sms_io.c Outdated
Comment on lines +224 to +230
char data_buf[128];
build_segment_data_json(data_buf, sizeof(data_buf), sent, total);

switch (rc) {
case SMS_IO_SEND_OK:
mqtt_publish_response(action, request_id, true, NULL, NULL, NULL, data_buf);
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. sms_io.c ignores return values 📘 Rule violation ≡ Correctness

New code ignores return values from at_command_send() and build_segment_data_json(), and then
may publish an uninitialized data_buf to MQTT. This can mask CMGD failures and can produce
malformed MQTT responses under buffer/allocation failure paths.
Agent Prompt
## Issue description
`sms_io.c` introduces ignored return values:
- `cmgd_fire_and_forget()` discards `at_command_send()` status.
- `sms_io_send_and_respond()` discards `build_segment_data_json()` status and may pass an uninitialized `data_buf` to `mqtt_publish_response()`.

## Issue Context
Even if these failures are rare, ignoring them makes behavior non-deterministic during low-memory conditions or modem/storage errors.

## Fix Focus Areas
- src/sms_io.c[48-53]
- src/sms_io.c[224-230]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/sms_io.c
Comment on lines +383 to +387
at_status_t rc = at_command_send(at, cmd, &resp, AT_TIMEOUT_SMS_STORAGE);
if (rc != AT_OK) {
OLOG_WARNING("CMGR PDU failed at idx %d: %s", sms_index, at_status_str(rc));
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Cmgr failure skips delete 🐞 Bug ☼ Reliability

On AT+CMGR failure in both text-mode and PDU-mode inbound handlers, the code returns without issuing
CMGD, leaving the message in modem storage. This contradicts the new short storage timeout policy
and can cause the modem inbox to fill, preventing further SMS reception.
Agent Prompt
### Issue description
Inbound CMTI handlers return on `AT+CMGR` failure without deleting the modem storage slot. With the new 500ms storage timeout, transient failures can accumulate undeleted messages until the modem inbox fills.

### Issue Context
`AT_TIMEOUT_SMS_STORAGE` is introduced specifically to avoid long main-thread stalls and the comment states the timeout path should fall back to CMGD fire-and-forget.

### Fix Focus Areas
- src/sms_io.c[306-314]
- src/sms_io.c[379-387]
- include/echo.h[56-63]

### Recommended fix
- In both handlers, when `rc != AT_OK`, log and call `cmgd_fire_and_forget(at, sms_index)` before returning.
- If you still want to preserve messages on *non-timeout* errors, gate the deletion specifically on `rc == AT_TIMEOUT` (and possibly other retryable cases), but ensure the documented timeout policy is enforced.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Flip ECHO from text mode (AT+CMGF=1) to PDU mode (AT+CMGF=0) so SMS
send/receive works for messages of any length. Outbound is UCS2-only;
inbound decodes both UCS2 and GSM7 (most handsets default to GSM7 for
plain ASCII). Multi-segment messages concatenate via the UDH reference
header on both paths.

New modules:
- pdu.c/h — encoder, decoder, UDH concat, UCS2+GSM7, hardened bounds
  checks at every length prefix, UCS2 sanitization (strips NULs, bidi
  overrides, zero-width, BOM, variation selectors, Unicode tag block)
- sms_reassembly.c/h — 8-slot inline store, 2 slots/sender, 10-min TTL,
  LRU eviction weighted by fragment count
- sms_io.c/h — extracts send + CMTI handling from oasis-echo.c, keeps
  the main daemon under its size limit

Rate buckets reworked to a leaky-bucket counter; the old 64-entry ring
silently never rejected at limits > 64 (fix needed for the 200/hr
segment budget). Inter-segment pacing (150ms default) avoids SIM7600
wedging on back-to-back concat sends.

CMTI handling now uses a dedicated deferred queue drained after MQTT
commands, and CMGR/CMGD use a 500ms timeout, so a 10-segment inbound
burst can no longer stall a concurrent hangup/dial for 40s.

MQTT response schema gains an optional `data` object for segments_sent /
segments_total observability. Existing consumers ignore unknown fields.

Legacy text mode reachable via --legacy-sms for rollback.

Tests: 19 PDU + 8 reassembly. All 6 suites pass.

Also: align CLAUDE.md with the OASIS house style (same structure as
DAWN's CLAUDE.md — critical rules up top, @imports for ARCHITECTURE /
CODING_STYLE_GUIDE, terse gotchas section).
@KerseyFabrications KerseyFabrications force-pushed the feature/pdu-concatenated-sms branch from 6f07fcc to 6502a4f Compare April 22, 2026 22:33
@KerseyFabrications KerseyFabrications merged commit dbd2d54 into main Apr 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant