feat: Initial commit
This commit is contained in:
commit
2780e3cc5d
4 changed files with 185 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
162
api.py
Normal file
162
api.py
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import binascii
|
||||||
|
import serial
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List
|
||||||
|
from messaging.sms import SmsDeliver, SmsSubmit
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
class ModemException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvalidResponseException(ModemException):
|
||||||
|
def __init__(self, message):
|
||||||
|
super().__init__('Invalid response from modem: ' + message)
|
||||||
|
|
||||||
|
|
||||||
|
def b(value):
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.encode('utf-8')
|
||||||
|
return value
|
||||||
|
|
||||||
|
def encode_utf16(value):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
value = value.decode('utf-8')
|
||||||
|
return binascii.hexlify(value.encode('utf-16-be'))
|
||||||
|
|
||||||
|
def decode_utf16(value):
|
||||||
|
return bytes.fromhex(value).decode('utf-16-be').replace('\x1b\x14', '^')
|
||||||
|
|
||||||
|
def parse_response(line):
|
||||||
|
values = line.split(': ')[1].split(',')
|
||||||
|
# because FUCKING AT HAS COMMAS IN STRINGS
|
||||||
|
for i in range(len(values)):
|
||||||
|
value = values[i]
|
||||||
|
if len(value) > 0 and value[0] == '"' and value[-1] != '"':
|
||||||
|
values[i] = values[i] + ',' + values[i+1]
|
||||||
|
values[i+1] = None
|
||||||
|
values = filter(lambda x: x is not None, values)
|
||||||
|
|
||||||
|
def mapper(value):
|
||||||
|
value = value.strip()
|
||||||
|
if len(value) == 0:
|
||||||
|
return None
|
||||||
|
if value.isnumeric():
|
||||||
|
return int(value)
|
||||||
|
if value[0] == '"' and value[-1] == '"':
|
||||||
|
return value[1:-1]
|
||||||
|
return list(map(mapper, values))
|
||||||
|
|
||||||
|
|
||||||
|
class Modem:
|
||||||
|
def __init__(self, port):
|
||||||
|
self.serial = serial.Serial(port, 460800, timeout=0.1)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def send(self, data):
|
||||||
|
data = b(data) # encode as bytes (if string)
|
||||||
|
if not data.endswith(b'\r'): # add \r to the end of message
|
||||||
|
data += b'\r'
|
||||||
|
print('req', data)
|
||||||
|
self.serial.write(data) # write to the modem
|
||||||
|
res = self.serial.readall() # get modem's response
|
||||||
|
print('res', res)
|
||||||
|
res = res[len(data):] # remove echo
|
||||||
|
res = res.strip() # remove whitespace
|
||||||
|
return res.decode('utf-8') # decode to string
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
res = self.send('ATZ')
|
||||||
|
if res != 'OK':
|
||||||
|
raise InvalidResponseException(res)
|
||||||
|
self.set('CMGF', 0)
|
||||||
|
|
||||||
|
def set(self, property, *params, **kwargs):
|
||||||
|
msg = b'AT+' + b(property) + b'='
|
||||||
|
for param in params:
|
||||||
|
if isinstance(param, str):
|
||||||
|
msg += b'"' + param.encode('utf-8') + b'"'
|
||||||
|
if isinstance(param, int):
|
||||||
|
msg += str(param).encode('utf-8')
|
||||||
|
if isinstance(param, bytes):
|
||||||
|
msg += b'"' + param + b'"'
|
||||||
|
msg += b','
|
||||||
|
# trim the last comma
|
||||||
|
msg = msg[:-1]
|
||||||
|
res = self.send(msg)
|
||||||
|
# accept '>' because of CMGS and '' because it may be already set
|
||||||
|
# also accept when response starts with + because it has data
|
||||||
|
if res not in ('OK', '>', '') and res[0] != '+':
|
||||||
|
raise InvalidResponseException(res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def query(self, property):
|
||||||
|
msg = b'AT+' + b(property) + b'?'
|
||||||
|
res = self.send(msg)
|
||||||
|
return parse_response(res.split('\n')[0])
|
||||||
|
|
||||||
|
def send_sms(self, number, message):
|
||||||
|
submit = SmsSubmit(number, message)
|
||||||
|
csca = self.query('CSCA')[0]
|
||||||
|
if '+' in csca:
|
||||||
|
submit.csca = csca
|
||||||
|
else:
|
||||||
|
submit.csca = decode_utf16(csca)
|
||||||
|
for pdu in submit.to_pdu():
|
||||||
|
self.set('CMGS', int(pdu.length))
|
||||||
|
res = self.send(pdu.pdu.encode('utf-8') + b'\x1a')
|
||||||
|
while '+CMGS: ' not in res:
|
||||||
|
sleep(1)
|
||||||
|
res = self.serial.readall().decode('utf-8')
|
||||||
|
|
||||||
|
def list_sms(self):
|
||||||
|
res = self.set('CMGL', 4)
|
||||||
|
lines = res.split('\r\n')
|
||||||
|
messages = []
|
||||||
|
print(lines)
|
||||||
|
|
||||||
|
for i in range(int(len(lines)/2)):
|
||||||
|
if lines[2*i] == '':
|
||||||
|
continue
|
||||||
|
header = parse_response(lines[2*i])
|
||||||
|
content = lines[2*i+1]
|
||||||
|
|
||||||
|
sms = SMS(content)
|
||||||
|
sms.mid = header[0]
|
||||||
|
sms.parts = []
|
||||||
|
sms.__modem__ = self
|
||||||
|
|
||||||
|
messages.append(sms)
|
||||||
|
|
||||||
|
for i in range(len(messages)):
|
||||||
|
message = messages[i]
|
||||||
|
if message is None:
|
||||||
|
continue
|
||||||
|
if message.udh is None or message.udh.concat is None:
|
||||||
|
continue
|
||||||
|
for j in range(i+1, len(messages)):
|
||||||
|
next_message = messages[j]
|
||||||
|
if next_message.udh is None or next_message.udh.concat is None:
|
||||||
|
continue
|
||||||
|
if message.udh.concat.ref == next_message.udh.concat.ref:
|
||||||
|
message.text += next_message.text
|
||||||
|
message.parts.append(next_message.mid)
|
||||||
|
messages[j] = None
|
||||||
|
|
||||||
|
messages = list(filter(lambda x: x is not None, messages))
|
||||||
|
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def remove_sms(self, sms):
|
||||||
|
self.set('CMGD', sms.mid, 0)
|
||||||
|
for part in sms.parts:
|
||||||
|
self.set('CMGD', part, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class SMS(SmsDeliver):
|
||||||
|
mid: int
|
||||||
|
parts: List[int]
|
||||||
|
__modem__: Modem
|
||||||
|
|
||||||
|
def remove(self):
|
||||||
|
__modem__.remove_sms(self)
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
python-messaging==0.5.13
|
||||||
|
pyserial
|
20
shell.nix
Normal file
20
shell.nix
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.stdenv.mkDerivation {
|
||||||
|
name = "env";
|
||||||
|
buildInputs = [
|
||||||
|
pkgs.usb_modeswitch
|
||||||
|
pkgs.python38
|
||||||
|
pkgs.python38Packages.pyserial
|
||||||
|
(pkgs.python38Packages.buildPythonPackage rec {
|
||||||
|
pname = "python-messaging";
|
||||||
|
version = "0.5.13";
|
||||||
|
src = pkgs.python38Packages.fetchPypi {
|
||||||
|
inherit pname version;
|
||||||
|
sha256 = "183rxrmf8rrm5hw20b4kvzaq18254pmawjbai86nw557gm2x33y0";
|
||||||
|
};
|
||||||
|
doCheck = false;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue