comettextel 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ """CometTextel — thin Python FFI for the C ABI (PDU + modem).
2
+
3
+ Requires the shared library from the C SDK (`comettextel.dll` / `libcomettextel.so`).
4
+ Does not reimplement PDU codecs.
5
+
6
+ Copyright (c) Ji-Feng Tsai. All rights reserved.
7
+ Code released under the MIT license.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .errors import CometTextelError, Status
13
+ from .modem import GsmModem
14
+ from .pdu import (
15
+ DCS_8BIT,
16
+ DCS_GSM7,
17
+ DCS_UCS2,
18
+ Message,
19
+ decode,
20
+ encode_submit,
21
+ encode_submit_segments,
22
+ status_string,
23
+ )
24
+
25
+ __all__ = [
26
+ "CometTextelError",
27
+ "Status",
28
+ "DCS_GSM7",
29
+ "DCS_8BIT",
30
+ "DCS_UCS2",
31
+ "Message",
32
+ "GsmModem",
33
+ "decode",
34
+ "encode_submit",
35
+ "encode_submit_segments",
36
+ "status_string",
37
+ ]
38
+
39
+ __version__ = "1.3.0"
comettextel/_lib.py ADDED
@@ -0,0 +1,207 @@
1
+ """Locate and load ``comettextel`` shared library; bind C ABI exports.
2
+
3
+ Copyright (c) Ji-Feng Tsai. All rights reserved.
4
+ Code released under the MIT license.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import os
11
+ import sys
12
+ from ctypes import (
13
+ POINTER,
14
+ c_char,
15
+ c_char_p,
16
+ c_int,
17
+ c_int32,
18
+ c_size_t,
19
+ c_uint32,
20
+ c_void_p,
21
+ )
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ # Layout must match struct ct_message in include/comettextel/c_api.h.
26
+ class CtMessage(ctypes.Structure):
27
+ """Structure representing a CT message."""
28
+
29
+ _fields_ = [
30
+ ("index", c_int32),
31
+ ("dcs", c_int32),
32
+ ("has_udh", c_int32),
33
+ ("service_center", c_char * 32),
34
+ ("peer_address", c_char * 32),
35
+ ("service_timestamp", c_char * 32),
36
+ ("user_data", c_char * 512),
37
+ ("is_concatenated", c_int32),
38
+ ("concat_ref", c_int32),
39
+ ("concat_total", c_int32),
40
+ ("concat_seq", c_int32),
41
+ ]
42
+
43
+
44
+ _lib: Optional[ctypes.CDLL] = None
45
+
46
+
47
+ def _candidate_names() -> list[str]:
48
+ """Return a list of candidate library names."""
49
+
50
+ if sys.platform == "win32":
51
+ return ["comettextel.dll"]
52
+ if sys.platform == "darwin":
53
+ return ["libcomettextel.dylib", "comettextel.dylib"]
54
+ return ["libcomettextel.so", "libcomettextel.so.1", "comettextel.so"]
55
+
56
+
57
+ def _search_dirs(explicit: Optional[Path]) -> list[Path]:
58
+ """Search for the shared library in the given directories."""
59
+
60
+ dirs: list[Path] = []
61
+ if explicit is not None:
62
+ dirs.append(explicit if explicit.is_dir() else explicit.parent)
63
+
64
+ env = os.environ.get("COMETTEXTEL_LIB")
65
+ if env:
66
+ p = Path(env)
67
+ dirs.append(p if p.is_dir() else p.parent)
68
+
69
+ # sdk/python/comettextel/_lib.py → parents[1]=sdk/python, [2]=sdk, [3]=repo root
70
+ here = Path(__file__).resolve()
71
+ sdk_python = here.parents[1]
72
+ repo = here.parents[3] if len(here.parents) > 3 else here.parents[-1]
73
+
74
+ dirs.extend(
75
+ [
76
+ Path.cwd(),
77
+ sdk_python,
78
+ sdk_python / "examples",
79
+ repo / "artifact" / "comettextel-c-sdk-windows-x64" / "bin",
80
+ repo / "artifact" / "comettextel-c-sdk-linux-x64" / "lib",
81
+ repo / "build-c-sdk" / "Release",
82
+ repo / "build-c-sdk" / "Debug",
83
+ repo / "build" / "Release",
84
+ repo / "build" / "Debug",
85
+ ]
86
+ )
87
+
88
+ # Deduplicate while preserving order.
89
+ seen: set[str] = set()
90
+ out: list[Path] = []
91
+ for d in dirs:
92
+ key = str(d.resolve()) if d.exists() else str(d)
93
+ if key not in seen:
94
+ seen.add(key)
95
+ out.append(d)
96
+ return out
97
+
98
+
99
+ def find_library(path: Optional[str | Path] = None) -> Path:
100
+ """Resolve the shared library path.
101
+
102
+ Search order: explicit path / ``COMETTEXTEL_LIB`` / cwd / common build &
103
+ artifact folders next to this package.
104
+ """
105
+
106
+ explicit: Optional[Path] = Path(path) if path else None
107
+ if explicit is not None and explicit.is_file():
108
+ return explicit.resolve()
109
+
110
+ names = _candidate_names()
111
+ for directory in _search_dirs(explicit):
112
+ if not directory.is_dir():
113
+ continue
114
+ for name in names:
115
+ candidate = directory / name
116
+ if candidate.is_file():
117
+ return candidate.resolve()
118
+
119
+ raise FileNotFoundError(
120
+ "comettextel shared library not found. Set COMETTEXTEL_LIB to the DLL/SO "
121
+ "path (or its directory), or place it next to the example / on PATH. "
122
+ f"Looked for: {', '.join(names)}"
123
+ )
124
+
125
+
126
+ def load(path: Optional[str | Path] = None) -> ctypes.CDLL:
127
+ """Load (or return cached) shared library and configure C ABI prototypes."""
128
+
129
+ global _lib
130
+ if _lib is not None and path is None:
131
+ return _lib
132
+
133
+ lib_path = find_library(path)
134
+ if sys.platform == "win32":
135
+ # Ensure dependent MSVC runtime resolution from the DLL folder.
136
+ os.add_dll_directory(str(lib_path.parent))
137
+ loaded = ctypes.WinDLL(str(lib_path))
138
+ else:
139
+ loaded = ctypes.CDLL(str(lib_path))
140
+
141
+ loaded.ct_status_string.argtypes = [c_int]
142
+ loaded.ct_status_string.restype = c_char_p
143
+
144
+ loaded.ct_modem_create.argtypes = []
145
+ loaded.ct_modem_create.restype = c_void_p
146
+
147
+ loaded.ct_modem_destroy.argtypes = [c_void_p]
148
+ loaded.ct_modem_destroy.restype = None
149
+
150
+ loaded.ct_modem_open.argtypes = [c_void_p, c_char_p, c_uint32]
151
+ loaded.ct_modem_open.restype = c_int
152
+
153
+ loaded.ct_modem_send.argtypes = [
154
+ c_void_p,
155
+ c_char_p,
156
+ c_char_p,
157
+ c_char_p,
158
+ c_int,
159
+ c_int,
160
+ ]
161
+ loaded.ct_modem_send.restype = c_int
162
+
163
+ loaded.ct_modem_list.argtypes = [
164
+ c_void_p,
165
+ POINTER(CtMessage),
166
+ c_int,
167
+ POINTER(c_int),
168
+ c_int,
169
+ ]
170
+ loaded.ct_modem_list.restype = c_int
171
+
172
+ loaded.ct_modem_delete.argtypes = [c_void_p, c_int, c_int]
173
+ loaded.ct_modem_delete.restype = c_int
174
+
175
+ loaded.ct_pdu_encode_submit.argtypes = [
176
+ c_char_p,
177
+ c_char_p,
178
+ c_char_p,
179
+ c_int,
180
+ c_void_p,
181
+ c_size_t,
182
+ ]
183
+ loaded.ct_pdu_encode_submit.restype = c_int
184
+
185
+ loaded.ct_pdu_encode_submit_segments.argtypes = [
186
+ c_char_p,
187
+ c_char_p,
188
+ c_char_p,
189
+ c_int,
190
+ c_void_p,
191
+ c_size_t,
192
+ POINTER(c_int),
193
+ ]
194
+ loaded.ct_pdu_encode_submit_segments.restype = c_int
195
+
196
+ loaded.ct_pdu_decode.argtypes = [c_char_p, POINTER(CtMessage)]
197
+ loaded.ct_pdu_decode.restype = c_int
198
+
199
+ _lib = loaded
200
+ return _lib
201
+
202
+
203
+ def reset() -> None:
204
+ """Drop the cached CDLL (mainly for tests)."""
205
+
206
+ global _lib
207
+ _lib = None
comettextel/errors.py ADDED
@@ -0,0 +1,35 @@
1
+ """Status codes mirroring ``enum ct_status`` in ``c_api.h``.
2
+
3
+ Copyright (c) Ji-Feng Tsai. All rights reserved.
4
+ Code released under the MIT license.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import IntEnum
10
+
11
+
12
+ class Status(IntEnum):
13
+ OK = 0
14
+ INVALID_ARGUMENT = 1
15
+ NOT_OPEN = 2
16
+ ALREADY_OPEN = 3
17
+ IO = 4
18
+ TIMEOUT = 5
19
+ MODEM_REJECTED = 6
20
+ ENCODE = 7
21
+ DECODE = 8
22
+ UNSUPPORTED = 9
23
+ UNKNOWN = 100
24
+
25
+
26
+ class CometTextelError(RuntimeError):
27
+ """Raised when a ``ct_*`` call returns a non-OK status."""
28
+
29
+ def __init__(self, status: int, what: str, detail: str = "") -> None:
30
+ self.status = int(status)
31
+ self.what = what
32
+ message = f"{what} failed ({self.status})"
33
+ if detail:
34
+ message = f"{message}: {detail}"
35
+ super().__init__(message)
comettextel/modem.py ADDED
@@ -0,0 +1,127 @@
1
+ """GSM modem helpers over ``ct_modem_*``.
2
+
3
+ Copyright (c) Ji-Feng Tsai. All rights reserved.
4
+ Code released under the MIT license.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ from types import TracebackType
11
+ from typing import Optional, Type
12
+
13
+ from . import _lib
14
+ from .errors import CometTextelError
15
+ from .pdu import DCS_UCS2, Message, _as_utf8, _check, message_from_ct
16
+
17
+
18
+ class GsmModem:
19
+ """Context-managed wrapper around an opaque ``ct_modem`` handle."""
20
+
21
+ def __init__(self) -> None:
22
+ """Create a new modem object."""
23
+
24
+ lib = _lib.load()
25
+ handle = lib.ct_modem_create()
26
+ if not handle:
27
+ raise CometTextelError(100, "ct_modem_create", "null handle")
28
+ self._handle = handle
29
+ self._closed = False
30
+
31
+ def close(self) -> None:
32
+ """Destroy the native modem (idempotent)."""
33
+
34
+ if self._closed:
35
+ return
36
+ lib = _lib.load()
37
+ lib.ct_modem_destroy(self._handle)
38
+ self._handle = None
39
+ self._closed = True
40
+
41
+ def __enter__(self) -> GsmModem:
42
+ """Return the modem when the context is entered."""
43
+
44
+ return self
45
+
46
+ def __exit__(
47
+ self,
48
+ exc_type: Optional[Type[BaseException]],
49
+ exc: Optional[BaseException],
50
+ tb: Optional[TracebackType],
51
+ ) -> None:
52
+ """Close the modem when the context is exited."""
53
+
54
+ self.close()
55
+
56
+ def __del__(self) -> None:
57
+ """Destroy the modem when the object is garbage collected."""
58
+
59
+ try:
60
+ self.close()
61
+ except Exception:
62
+ pass
63
+
64
+ def _ensure_open(self) -> ctypes.CDLL:
65
+ """Ensure the modem is open."""
66
+
67
+ if self._closed or not self._handle:
68
+ raise CometTextelError(2, "GsmModem", "modem is closed")
69
+ return _lib.load()
70
+
71
+ def open(self, port: str, baud_rate: int = 115200) -> None:
72
+ """Open the serial port and initialize PDU mode."""
73
+
74
+ if not port:
75
+ raise ValueError("port must be non-empty")
76
+ lib = self._ensure_open()
77
+ status = lib.ct_modem_open(self._handle, _as_utf8(port), int(baud_rate))
78
+ _check(status, "ct_modem_open")
79
+
80
+ def send(
81
+ self,
82
+ destination: str,
83
+ text: str,
84
+ smsc: str = "",
85
+ dcs: int = DCS_UCS2,
86
+ timeout_ms: int = 10000,
87
+ ) -> None:
88
+ """Send an SMS (long text auto-splits with concat UDH)."""
89
+
90
+ if not destination:
91
+ raise ValueError("destination must be non-empty")
92
+ lib = self._ensure_open()
93
+ status = lib.ct_modem_send(
94
+ self._handle,
95
+ _as_utf8(smsc),
96
+ _as_utf8(destination),
97
+ _as_utf8(text),
98
+ int(dcs),
99
+ int(timeout_ms),
100
+ )
101
+ _check(status, "ct_modem_send")
102
+
103
+ def list(self, max_count: int = 64, timeout_ms: int = 8000) -> list[Message]:
104
+ """List stored messages (complete concat sets are rejoined)."""
105
+
106
+ if max_count <= 0:
107
+ raise ValueError("max_count must be positive")
108
+ lib = self._ensure_open()
109
+ buffer = (_lib.CtMessage * max_count)()
110
+ count = ctypes.c_int(0)
111
+ status = lib.ct_modem_list(
112
+ self._handle,
113
+ buffer,
114
+ int(max_count),
115
+ ctypes.byref(count),
116
+ int(timeout_ms),
117
+ )
118
+ _check(status, "ct_modem_list")
119
+ n = int(count.value)
120
+ return [message_from_ct(buffer[i]) for i in range(n)]
121
+
122
+ def delete(self, index: int, timeout_ms: int = 5000) -> None:
123
+ """Delete one stored message by modem storage index."""
124
+
125
+ lib = self._ensure_open()
126
+ status = lib.ct_modem_delete(self._handle, int(index), int(timeout_ms))
127
+ _check(status, "ct_modem_delete")
comettextel/pdu.py ADDED
@@ -0,0 +1,167 @@
1
+ """PDU encode / decode helpers over ``ct_pdu_*``.
2
+
3
+ Copyright (c) Ji-Feng Tsai. All rights reserved.
4
+ Code released under the MIT license.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ from dataclasses import dataclass
11
+ from typing import Optional
12
+
13
+ from . import _lib
14
+ from .errors import CometTextelError, Status
15
+
16
+ DCS_GSM7 = 0
17
+ DCS_8BIT = 4
18
+ DCS_UCS2 = 8
19
+
20
+ _DEFAULT_HEX_CAP = 65536
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Message:
25
+ """Decoded message fields (UTF-8 text)."""
26
+
27
+ peer_address: str
28
+ user_data: str
29
+ dcs: int
30
+ has_udh: bool = False
31
+ is_concatenated: bool = False
32
+ concat_ref: int = 0
33
+ concat_total: int = 0
34
+ concat_seq: int = 0
35
+ service_center: str = ""
36
+ service_timestamp: str = ""
37
+ index: int = -1
38
+
39
+ @property
40
+ def is_reassembled_concat(self) -> bool:
41
+ """True when list/reassembly joined a complete multi-part set."""
42
+ return self.is_concatenated and self.concat_seq == 0 and self.concat_total > 0
43
+
44
+
45
+ def _as_utf8(text: str) -> bytes:
46
+ """Convert a Python string to a UTF-8 encoded bytes object."""
47
+
48
+ return text.encode("utf-8")
49
+
50
+
51
+ def _c_z(buf: ctypes.Array) -> str:
52
+ """Convert a C string buffer to a Python string."""
53
+
54
+ raw = bytes(buf)
55
+ nul = raw.find(b"\x00")
56
+ if nul >= 0:
57
+ raw = raw[:nul]
58
+ return raw.decode("utf-8", errors="replace")
59
+
60
+
61
+ def message_from_ct(out: _lib.CtMessage) -> Message:
62
+ """Build a :class:`Message` from a native ``ct_message``."""
63
+
64
+ return Message(
65
+ index=int(out.index),
66
+ dcs=int(out.dcs),
67
+ has_udh=bool(out.has_udh),
68
+ service_center=_c_z(out.service_center),
69
+ peer_address=_c_z(out.peer_address),
70
+ service_timestamp=_c_z(out.service_timestamp),
71
+ user_data=_c_z(out.user_data),
72
+ is_concatenated=bool(out.is_concatenated),
73
+ concat_ref=int(out.concat_ref),
74
+ concat_total=int(out.concat_total),
75
+ concat_seq=int(out.concat_seq),
76
+ )
77
+
78
+
79
+ def status_string(status: int) -> str:
80
+ """Convert a status code to a string."""
81
+
82
+ lib = _lib.load()
83
+ ptr = lib.ct_status_string(int(status))
84
+ if not ptr:
85
+ return ""
86
+ return ptr.decode("utf-8", errors="replace")
87
+
88
+
89
+ def _check(status: int, what: str) -> None:
90
+ """Check if a status code is OK."""
91
+
92
+ if status != Status.OK:
93
+ raise CometTextelError(status, what, status_string(status))
94
+
95
+
96
+ def encode_submit(
97
+ destination: str,
98
+ text: str,
99
+ smsc: str = "",
100
+ dcs: int = DCS_UCS2,
101
+ *,
102
+ out_hex_cap: int = _DEFAULT_HEX_CAP,
103
+ ) -> str:
104
+ """Encode a single-segment submit PDU (hex). Raises if the text needs split."""
105
+
106
+ lib = _lib.load()
107
+ buf = ctypes.create_string_buffer(out_hex_cap)
108
+ status = lib.ct_pdu_encode_submit(
109
+ _as_utf8(smsc),
110
+ _as_utf8(destination),
111
+ _as_utf8(text),
112
+ int(dcs),
113
+ buf,
114
+ out_hex_cap,
115
+ )
116
+ _check(status, "ct_pdu_encode_submit")
117
+ return buf.value.decode("ascii")
118
+
119
+
120
+ def encode_submit_segments(
121
+ destination: str,
122
+ text: str,
123
+ smsc: str = "",
124
+ dcs: int = DCS_UCS2,
125
+ *,
126
+ out_hex_cap: int = _DEFAULT_HEX_CAP,
127
+ ) -> list[str]:
128
+ """Encode one or more submit PDUs; auto-splits with concat UDH when needed."""
129
+
130
+ lib = _lib.load()
131
+ buf = ctypes.create_string_buffer(out_hex_cap)
132
+ count = ctypes.c_int(0)
133
+ status = lib.ct_pdu_encode_submit_segments(
134
+ _as_utf8(smsc),
135
+ _as_utf8(destination),
136
+ _as_utf8(text),
137
+ int(dcs),
138
+ buf,
139
+ out_hex_cap,
140
+ ctypes.byref(count),
141
+ )
142
+ _check(status, "ct_pdu_encode_submit_segments")
143
+ joined = buf.value.decode("ascii")
144
+ parts = [p for p in joined.split("\n") if p]
145
+ if count.value and len(parts) != int(count.value):
146
+ raise CometTextelError(
147
+ Status.ENCODE,
148
+ "ct_pdu_encode_submit_segments",
149
+ f"segment count mismatch (api={count.value}, parsed={len(parts)})",
150
+ )
151
+ return parts
152
+
153
+
154
+ def decode(pdu_hex: str) -> Message:
155
+ """Decode a PDU hex string into a :class:`Message`."""
156
+
157
+ lib = _lib.load()
158
+ out = _lib.CtMessage()
159
+ status = lib.ct_pdu_decode(_as_utf8(pdu_hex.strip()), ctypes.byref(out))
160
+ _check(status, "ct_pdu_decode")
161
+ return message_from_ct(out)
162
+
163
+
164
+ def load_library(path: Optional[str] = None) -> None:
165
+ """Eagerly load the native library (optional; otherwise lazy on first call)."""
166
+
167
+ _lib.load(path)
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: comettextel
3
+ Version: 1.3.0
4
+ Summary: Thin ctypes FFI for the CometTextel C ABI (PDU + GSM modem).
5
+ Author-email: Ji-Feng Tsai <jiowcl@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://inwazy.com
8
+ Project-URL: Source, https://github.com/jiowcl/CometTextel
9
+ Project-URL: Issues, https://github.com/jiowcl/CometTextel/issues
10
+ Keywords: sms,pdu,gsm,comettextel
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7; extra == "dev"
15
+
16
+ # CometTextel — Python (thin FFI)
17
+
18
+ Thin **ctypes** binding to the same C ABI as the C SDK and CometTextel.NET (`c_api.h`).
19
+ This is **not** a second native library: it runtime-loads `comettextel.dll` / `libcomettextel.so`.
20
+
21
+ Covers **PDU** helpers and **GsmModem** (`open` / `send` / `list` / `delete`).
22
+ Hardware modem tests are optional; smoke tests run without a device.
23
+
24
+ ![Python](https://img.shields.io/badge/language-python-blue.svg)
25
+
26
+ ## Layout
27
+
28
+ ```text
29
+ sdk/python/
30
+ ├── comettextel/ # package (ctypes + PDU + modem)
31
+ ├── examples/
32
+ │ ├── pdu_example.py # encode / decode + self-check
33
+ │ └── modem_example.py # list / send / delete
34
+ ├── tests/
35
+ │ ├── test_pdu.py
36
+ │ └── test_modem.py
37
+ └── README.md
38
+ ```
39
+
40
+ ## Prerequisites
41
+
42
+ | Item | Detail |
43
+ |------|--------|
44
+ | Python | 3.10+ (3.13 verified) |
45
+ | Native | C SDK shared library (`comettextel.dll` or `libcomettextel.so`) |
46
+ | Modem (optional) | AT modem in **PDU mode** (e.g. `COM3`, `/dev/ttyUSB0`) |
47
+
48
+ Download `comettextel-c-sdk-*` from CI Artifacts or a GitHub Release, or use a local CMake build.
49
+
50
+ ### Finding the library
51
+
52
+ Search order:
53
+
54
+ 1. Explicit path passed to `comettextel.pdu.load_library(...)`
55
+ 2. Environment variable **`COMETTEXTEL_LIB`** (file or directory)
56
+ 3. Current working directory
57
+ 4. Nearby build / artifact folders (`build-c-sdk/Release`, `artifact/comettextel-c-sdk-*/…`)
58
+
59
+ ```powershell
60
+ # Windows
61
+ $env:COMETTEXTEL_LIB = "D:\path\to\comettextel.dll"
62
+ ```
63
+
64
+ ```bash
65
+ # Linux
66
+ export COMETTEXTEL_LIB=/path/to/libcomettextel.so
67
+ ```
68
+
69
+ ## PDU example
70
+
71
+ ```powershell
72
+ cd sdk\python
73
+ python examples\pdu_example.py
74
+ python examples\pdu_example.py 886912345678 "Hello" 886932000000
75
+ python examples\pdu_example.py 886912345678 "測試中文簡訊" 886932000000
76
+ ```
77
+
78
+ ## Modem example
79
+
80
+ ```powershell
81
+ python examples\modem_example.py list COM3
82
+ python examples\modem_example.py send COM3 886932000000 886912345678 "Hello"
83
+ python examples\modem_example.py delete COM3 1
84
+ ```
85
+
86
+ `list` rejoins **complete** concatenated SMS sets (`concat_seq == 0` / `Message.is_reassembled_concat`).
87
+
88
+ ## Tests
89
+
90
+ ```powershell
91
+ cd sdk\python
92
+ pip install pytest
93
+ pytest -q
94
+ ```
95
+
96
+ ## API sketch
97
+
98
+ ```python
99
+ from comettextel import DCS_UCS2, GsmModem, decode, encode_submit_segments
100
+
101
+ parts = encode_submit_segments("886912345678", "Hello", "886932000000", DCS_UCS2)
102
+ msg = decode(parts[0])
103
+
104
+ with GsmModem() as modem:
105
+ modem.open("COM3", 115200)
106
+ modem.send("886912345678", "Hello from Python", smsc="886932000000")
107
+ for m in modem.list():
108
+ print(m.index, m.peer_address, m.user_data, m.is_reassembled_concat)
109
+ ```
110
+
111
+ All text at the native boundary is **UTF-8**. Do not reimplement PDU codecs in Python; if the C ABI changes, update this package only.
112
+
113
+ ## See also
114
+
115
+ - [`include/comettextel/c_api.h`](../../include/comettextel/c_api.h)
116
+ - [`sdk/c/README.md`](../c/README.md)
117
+ - [`sdk/freebasic/README.md`](../freebasic/README.md)
118
+ - [`sdk/purebasic/README.md`](../purebasic/README.md)
119
+ - [`examples/c_api_example.c`](../../examples/c_api_example.c)
@@ -0,0 +1,9 @@
1
+ comettextel/__init__.py,sha256=unJUyDfVdsDSB-oRsei7zgqidbdwznsRz7laUzjMtUo,819
2
+ comettextel/_lib.py,sha256=4MMqL7sGB8avNaMCdYNQC4Sz1yWUwXLj0pzzeiP2wS8,5854
3
+ comettextel/errors.py,sha256=jejEfEaMPfkaEQhUnzeG0oowCYqWQ7tYTjliJmZzF6A,852
4
+ comettextel/modem.py,sha256=bOVoJkYcYzMyMOomsc1VAmFVnlDLJ-ftuqfDTYlrU9c,3867
5
+ comettextel/pdu.py,sha256=NICvns0dBgI1zsl4bt5Y7MynvRwPsIeFo7WY5sNtXGw,4535
6
+ comettextel-1.3.0.dist-info/METADATA,sha256=_qmtch8u1nEnUYsv3kfJfIT69FXvPSDE44CW15DBoIQ,3692
7
+ comettextel-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ comettextel-1.3.0.dist-info/top_level.txt,sha256=OKfoCtq8MdBy3M_koAWo-tnaqCZJQrsVUvsypYWYn0U,12
9
+ comettextel-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ comettextel