518 lines
22 KiB
Python
518 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import logging
|
|
import binascii
|
|
import time
|
|
import re
|
|
from typing import Optional, Union, Tuple, List, Dict
|
|
|
|
from m2m.nbiot.module import ModuleBase
|
|
|
|
logger = logging.getLogger('m2m.quectel')
|
|
|
|
class QuectelModule(ModuleBase):
|
|
"""
|
|
Base class for Quectel modules implementing shared AT commands.
|
|
"""
|
|
|
|
def __init__(self, serial_port: str, baudrate: int = 115200, **kwargs):
|
|
super().__init__(serial_port, baudrate, **kwargs)
|
|
self._setup_urcs()
|
|
|
|
def _setup_urcs(self):
|
|
# Socket events
|
|
self.s_port.register_urc(b'+QIURC: "pdpdeact",', True, lambda u: logger.info(f"PDP Deactivated: {u.decode()}"))
|
|
self.s_port.register_urc(b'+QIURC: "recv",', True, lambda u: logger.debug(f"Data Received: {u.decode()}"))
|
|
self.s_port.register_urc(b'+QIOPEN:', True, lambda u: logger.debug(f"Socket Open: {u.decode()}"))
|
|
|
|
# Service URCs
|
|
self.s_port.register_urc(b'+QMTRECV:', True, lambda u: logger.info(f"MQTT Message: {u.decode()}"))
|
|
self.s_port.register_urc(b'+CUSD:', True, lambda u: logger.info(f"USSD Response: {u.decode()}"))
|
|
self.s_port.register_urc(b'+QPING:', True)
|
|
self.s_port.register_urc(b'+QIURC: "dnsgip",', True)
|
|
self.s_port.register_urc(b'+QHTTPGET:', True)
|
|
self.s_port.register_urc(b'+QHTTPPOST:', True)
|
|
self.s_port.register_urc(b'+QHTTPREAD:', True)
|
|
self.s_port.register_urc(b'+QNTP:', True)
|
|
|
|
# Extended URCs
|
|
self.s_port.register_urc(b'+QIND: "nipd",', True, lambda u: logger.info(f"NIDD: {u.decode()}"))
|
|
self.s_port.register_urc(b'+QIND: "GEOFENCE",', True, lambda u: logger.info(f"Geofence: {u.decode()}"))
|
|
|
|
# --- Extended Configuration (AT+QCFG) ---
|
|
|
|
def set_qcfg(self, parameter: str, value: Union[int, str, List[int]], effect_immediately: bool = False) -> bool:
|
|
val_str = ",".join(map(str, value)) if isinstance(value, list) else str(value)
|
|
cmd = f'AT+QCFG="{parameter}",{val_str}'
|
|
if effect_immediately:
|
|
cmd += ",1"
|
|
|
|
with self.transaction:
|
|
self.s_port.send_cmd(cmd.encode())
|
|
return self.at.read_ok(timeout=10)
|
|
|
|
def get_qcfg(self, parameter: str) -> Optional[str]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QCFG="{parameter}"'.encode())
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if b'+QCFG:' in line:
|
|
return line.decode('ascii', errors='ignore')
|
|
return None
|
|
|
|
def set_iot_op_mode(self, mode: str = "NB-IoT") -> bool:
|
|
modes = {"eMTC": 0, "NB-IoT": 1, "Both": 2}
|
|
return self.set_qcfg("iotopmode", modes.get(mode, 1), True)
|
|
|
|
def set_nw_scan_priority(self, first: str = "NB-IoT", second: str = "eMTC", third: str = "GSM") -> bool:
|
|
seq_map = {"GSM": "01", "eMTC": "02", "NB-IoT": "03"}
|
|
try:
|
|
val = seq_map[first] + seq_map[second] + seq_map[third]
|
|
return self.set_qcfg("nwscanseq", val, True)
|
|
except KeyError: return False
|
|
|
|
def lock_bands(self, gsm_bands: List[int] = [], catm_bands: List[int] = [], nb_bands: List[int] = []) -> bool:
|
|
from m2m.utils import bands_to_hex_mask
|
|
val_gsm = bands_to_hex_mask(gsm_bands) if gsm_bands else "0"
|
|
val_catm = bands_to_hex_mask(catm_bands) if catm_bands else "0"
|
|
val_nb = bands_to_hex_mask(nb_bands) if nb_bands else "0"
|
|
return self.set_qcfg("band", [val_gsm, val_catm, val_nb], True)
|
|
|
|
def get_locked_bands(self) -> Dict[str, str]:
|
|
resp = self.get_qcfg("band")
|
|
if resp and '+QCFG: "band",' in resp:
|
|
parts = [p.strip(' "') for p in resp.split(',')]
|
|
if len(parts) >= 4:
|
|
return {'gsm': parts[1], 'catm': parts[2], 'nb': parts[3]}
|
|
return {}
|
|
|
|
def get_nw_scan_mode(self) -> int:
|
|
resp = self.get_qcfg("nwscanmode")
|
|
if resp and '+QCFG: "nwscanmode",' in resp:
|
|
try: return int(resp.split(',')[-1].strip())
|
|
except: pass
|
|
return -1
|
|
|
|
def set_nw_scan_mode(self, mode: int = 0) -> bool:
|
|
"""
|
|
Sets network scan mode (AT+QCFG="nwscanmode").
|
|
0: Auto (GSM + LTE), 1: GSM Only, 3: LTE Only
|
|
"""
|
|
return self.set_qcfg("nwscanmode", mode, True)
|
|
|
|
def set_connectivity_mode(self, mode: str) -> bool:
|
|
"""
|
|
High-level helper to set connectivity preference.
|
|
Modes: 'gsm', 'emtc', 'nb', 'nb+emtc', 'all'
|
|
"""
|
|
mode = mode.lower()
|
|
res = False
|
|
if mode == 'gsm':
|
|
res = self.set_nw_scan_mode(1)
|
|
elif mode == 'emtc':
|
|
# LTE Only (3) + eMTC category (0)
|
|
r1 = self.set_nw_scan_mode(3)
|
|
r2 = self.set_iot_op_mode("eMTC")
|
|
res = r1 and r2
|
|
elif mode == 'nb':
|
|
# LTE Only (3) + NB-IoT category (1)
|
|
r1 = self.set_nw_scan_mode(3)
|
|
r2 = self.set_iot_op_mode("NB-IoT")
|
|
res = r1 and r2
|
|
elif mode == 'nb+emtc':
|
|
# LTE Only (3) + Both categories (2)
|
|
r1 = self.set_nw_scan_mode(3)
|
|
r2 = self.set_iot_op_mode("Both")
|
|
res = r1 and r2
|
|
elif mode == 'all':
|
|
# Auto (0) + Both categories (2) + Priority Sequence
|
|
r1 = self.set_nw_scan_mode(0)
|
|
r2 = self.set_iot_op_mode("Both")
|
|
r3 = self.set_nw_scan_priority("eMTC", "NB-IoT", "GSM")
|
|
res = r1 and r2 and r3
|
|
|
|
return res
|
|
|
|
def get_iot_op_mode_val(self) -> int:
|
|
resp = self.get_qcfg("iotopmode")
|
|
if resp and '+QCFG: "iotopmode",' in resp:
|
|
try: return int(resp.split(',')[-1].strip())
|
|
except: pass
|
|
return -1
|
|
|
|
# --- PDP & Sockets ---
|
|
|
|
def configure_pdp_context(self, context_id: int = 1, context_type: str = "IP", apn: str = "") -> bool:
|
|
type_map = {"IP": 1, "IPV6": 2, "IPV4V6": 3}
|
|
t_val = type_map.get(context_type.upper(), 1)
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QICSGP={context_id},{t_val},"{apn}"'.encode())
|
|
return self.at.read_ok()
|
|
|
|
def activate_pdp_context(self, context_id: int = 1) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QIACT={context_id}'.encode())
|
|
return self.at.read_ok(timeout=150)
|
|
|
|
def deactivate_pdp_context(self, context_id: int = 1) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QIDEACT={context_id}'.encode())
|
|
return self.at.read_ok(timeout=40)
|
|
|
|
def is_pdp_active(self, context_id: int = 1) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QIACT?')
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QIACT:'):
|
|
try:
|
|
parts = line.split(b',')
|
|
cid = int(parts[0].split(b':')[1].strip())
|
|
state = int(parts[1])
|
|
if cid == context_id: return state == 1
|
|
except: pass
|
|
return False
|
|
|
|
def open_socket(self, context_id: int = 1, connect_id: int = 0, service_type: str = "TCP",
|
|
remote_ip: str = "", remote_port: int = 0, access_mode: int = 0, local_port: int = 0) -> int:
|
|
cmd = f'AT+QIOPEN={context_id},{connect_id},"{service_type}","{remote_ip}",{remote_port},{local_port},{access_mode}'
|
|
with self.transaction:
|
|
self.s_port.send_cmd(cmd.encode())
|
|
if not self.at.read_ok(): return -1
|
|
response = self.s_port.read_until_notify(b'+QIOPEN:', timeout=150)
|
|
|
|
if response:
|
|
try:
|
|
parts = response.split(b',')
|
|
return int(parts[1]) if int(parts[1]) == 0 else -int(parts[1])
|
|
except: pass
|
|
return -1
|
|
|
|
def close_socket(self, connect_id: int = 0) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QICLOSE={connect_id}'.encode())
|
|
return self.at.read_ok()
|
|
|
|
def get_socket_state(self, connect_id: Optional[int] = None) -> List[Dict[str, str]]:
|
|
cmd = b'AT+QISTATE'
|
|
if connect_id is not None:
|
|
cmd = f'AT+QISTATE=1,{connect_id}'.encode()
|
|
with self.transaction:
|
|
self.s_port.send_cmd(cmd)
|
|
response = self.s_port.read_until()
|
|
|
|
sockets = []
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QISTATE:'):
|
|
line_str = line.decode('ascii', errors='ignore')
|
|
_, rest = line_str.split(':', 1)
|
|
p = [x.strip(' "') for x in rest.split(',')]
|
|
if len(p) >= 6:
|
|
sockets.append({
|
|
'id': p[0], 'type': p[1], 'remote': f"{p[2]}:{p[3]}",
|
|
'local_port': p[4], 'state': p[5]
|
|
})
|
|
return sockets
|
|
|
|
def send_data_hex(self, connect_id: int, data: bytes) -> bool:
|
|
hex_data = binascii.hexlify(data).decode('ascii')
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QISENDEX={connect_id},"{hex_data}"'.encode())
|
|
response = self.s_port.read_until()
|
|
return b'SEND OK' in response
|
|
|
|
def receive_data(self, connect_id: int, read_length: int = 1500) -> bytes:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QIRD={connect_id},{read_length}'.encode())
|
|
response = self.s_port.read_until()
|
|
|
|
data = b''
|
|
reading = False
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QIRD:'):
|
|
try:
|
|
if int(line.split(b':')[1].strip()) == 0: return b''
|
|
reading = True; continue
|
|
except: pass
|
|
if reading:
|
|
if line.strip() == b'OK': break
|
|
data += line
|
|
return data
|
|
|
|
# --- Network Services ---
|
|
|
|
def ping(self, host: str, context_id: int = 1, timeout: int = 20, num: int = 4) -> List[str]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QPING={context_id},"{host}",{timeout},{num}'.encode())
|
|
if not self.at.read_ok(): return []
|
|
|
|
responses = []
|
|
end_time = time.time() + (timeout * num) + 10
|
|
while time.time() < end_time and len(responses) < num + 1:
|
|
urc = self.s_port.read_until_notify(b'+QPING:', timeout=timeout + 2)
|
|
if not urc: break
|
|
line = urc.decode('ascii', errors='ignore').strip()
|
|
responses.append(line)
|
|
if line.count(',') >= 5: break
|
|
return responses
|
|
|
|
def parse_ping_summary(self, lines: List[str]) -> Dict[str, Union[int, str]]:
|
|
if not lines: return {}
|
|
parts = lines[-1].split(',')
|
|
if len(parts) >= 7 and parts[0] == '0':
|
|
try:
|
|
return {
|
|
'sent': int(parts[1]), 'rcvd': int(parts[2]), 'lost': int(parts[3]),
|
|
'min': int(parts[4]), 'max': int(parts[5]), 'avg': int(parts[6])
|
|
}
|
|
except: pass
|
|
return {}
|
|
|
|
def dns_query(self, host: str, context_id: int = 1, timeout: int = 30) -> List[str]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QIDNSGIP={context_id},"{host}"'.encode())
|
|
if not self.at.read_ok(): return []
|
|
|
|
ips = []
|
|
urc = self.s_port.read_until_notify(b'+QIURC: "dnsgip",', timeout=timeout)
|
|
if urc and urc.startswith(b'0'):
|
|
try:
|
|
count = int(urc.split(b',')[1])
|
|
for _ in range(count):
|
|
ip_urc = self.s_port.read_until_notify(b'+QIURC: "dnsgip",', timeout=5)
|
|
if ip_urc: ips.append(ip_urc.strip(b' "').decode())
|
|
except: pass
|
|
return ips
|
|
|
|
def search_operators(self, timeout: int = 180) -> List[Dict[str, str]]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+COPS=?')
|
|
response = self.s_port.read_until(b'OK', timeout=timeout)
|
|
|
|
operators = []
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+COPS:'):
|
|
matches = re.findall(r'\((\d+),"([^"]*)","([^"]*)","([^"]*)",(\d+)\)', line.decode())
|
|
for m in matches:
|
|
operators.append({'status': m[0], 'long': m[1], 'short': m[2], 'mccmnc': m[3], 'act': m[4]})
|
|
return operators
|
|
|
|
# --- HTTP(S) ---
|
|
|
|
def http_configure(self, context_id: int = 1, response_header: bool = False) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QHTTPCFG="contextid",{context_id}'.encode())
|
|
if not self.at.read_ok(): return False
|
|
self.s_port.send_cmd(f'AT+QHTTPCFG="responseheader",{1 if response_header else 0}'.encode())
|
|
return self.at.read_ok()
|
|
|
|
def http_set_url(self, url: str, timeout: int = 60) -> bool:
|
|
url_bytes = url.encode()
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QHTTPURL={len(url_bytes)},{timeout}'.encode())
|
|
if b'CONNECT' in self.s_port.read_until(b'CONNECT', timeout=5):
|
|
self.s_port._serial.write(url_bytes)
|
|
return self.at.read_ok()
|
|
return False
|
|
|
|
def http_get(self, timeout: int = 60) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QHTTPGET={timeout}'.encode())
|
|
if not self.at.read_ok(timeout=5): return False
|
|
urc = self.s_port.read_until_notify(b'+QHTTPGET:', timeout=timeout + 5)
|
|
return urc and urc.startswith(b'0')
|
|
|
|
def http_post(self, content: bytes, timeout: int = 60) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QHTTPPOST={len(content)},{timeout},{timeout}'.encode())
|
|
if b'CONNECT' not in self.s_port.read_until(b'CONNECT', timeout=5): return False
|
|
self.s_port._serial.write(content)
|
|
if not self.at.read_ok(): return False
|
|
urc = self.s_port.read_until_notify(b'+QHTTPPOST:', timeout=timeout + 5)
|
|
return urc and urc.startswith(b'0')
|
|
|
|
def http_read_response(self, wait_time: int = 60) -> str:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QHTTPREAD={wait_time}'.encode())
|
|
if b'CONNECT' not in self.s_port.read_until(b'CONNECT', timeout=5): return ""
|
|
|
|
buffer = bytearray()
|
|
end_time = time.time() + wait_time
|
|
while time.time() < end_time:
|
|
line = self.s_port._read_line()
|
|
if not line: continue
|
|
if line.strip() == b'OK': break
|
|
buffer.extend(line)
|
|
return buffer.decode('ascii', errors='ignore')
|
|
|
|
# --- Advanced Features ---
|
|
|
|
def configure_psm_optimization(self, enter_immediately: bool = True, enable_urc: bool = True) -> bool:
|
|
r1 = self.set_qcfg("psm/enter", 1 if enter_immediately else 0)
|
|
r2 = self.set_qcfg("psm/urc", 1 if enable_urc else 0)
|
|
return r1 and r2
|
|
|
|
def set_gpio(self, mode: int, pin: int, val: int = 0, save: int = 0) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QCFG="gpio",{mode},{pin},{val},{save}'.encode())
|
|
return self.at.read_ok()
|
|
|
|
def get_gpio(self, pin: int) -> int:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QCFG="gpio",2,{pin}'.encode())
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if b'+QCFG: "gpio"' in line:
|
|
try: return int(line.split(b',')[-1].strip())
|
|
except: pass
|
|
return -1
|
|
|
|
def list_files(self, pattern: str = "*") -> List[Dict[str, Union[str, int]]]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QFLST="{pattern}"'.encode())
|
|
response = self.s_port.read_until()
|
|
files = []
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QFLST:'):
|
|
try:
|
|
p = line.decode('ascii', errors='ignore').split(':', 1)[1].strip().split(',')
|
|
files.append({'name': p[0].strip(' "'), 'size': int(p[1])})
|
|
except: pass
|
|
return files
|
|
|
|
def power_off(self, mode: int = 1) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QPOWD={mode}'.encode())
|
|
return self.at.read_ok(timeout=10)
|
|
|
|
def get_temperature(self) -> float:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QTEMP')
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QTEMP:'):
|
|
try:
|
|
parts = line.split(b':')[1].strip().split(b',')
|
|
return max([float(p) for p in parts]) if parts else 0.0
|
|
except: pass
|
|
return 0.0
|
|
|
|
def upload_file(self, filename: str, content: bytes, storage: str = "RAM") -> bool:
|
|
"""Uploads a file to module storage (AT+QFUPL)."""
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QFUPL="{storage}:{filename}",{len(content)}'.encode())
|
|
resp = self.s_port.read_until(b'CONNECT', timeout=5)
|
|
if b'CONNECT' not in resp: return False
|
|
self.s_port._serial.write(content)
|
|
# After write, modem sends OK
|
|
return self.at.read_ok(timeout=10)
|
|
|
|
def delete_file(self, filename: str, storage: str = "RAM") -> bool:
|
|
"""Deletes a file from module storage (AT+QFDEL)."""
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QFDEL="{storage}:{filename}"'.encode())
|
|
return self.at.read_ok()
|
|
|
|
def ntp_sync(self, server: str = "pool.ntp.org", port: int = 123, context_id: int = 1) -> bool:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QNTP={context_id},"{server}",{port}'.encode())
|
|
if not self.at.read_ok(): return False
|
|
urc = self.s_port.read_until_notify(b'+QNTP:', timeout=75)
|
|
return urc and urc.startswith(b'0')
|
|
|
|
def set_edrx(self, mode: int = 1, act_type: int = 5, value: str = "0010") -> bool:
|
|
"""Sets eDRX parameters (AT+CEDRXS)."""
|
|
cmd = f'AT+CEDRXS={mode},{act_type},"{value}"'.encode()
|
|
with self.transaction:
|
|
self.s_port.send_cmd(cmd)
|
|
return self.at.read_ok()
|
|
|
|
def get_neighbor_cells(self) -> List[Dict[str, Union[str, int]]]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QENG="neighbourcell"')
|
|
response = self.s_port.read_until()
|
|
neighbors = []
|
|
for line in response.splitlines():
|
|
if b'+QENG: "neighbourcell' in line:
|
|
try:
|
|
p = [x.strip(' "') for x in line.decode('ascii', errors='ignore').split(',')]
|
|
if len(p) > 5:
|
|
neighbors.append({'tech': p[1], 'earfcn': p[2], 'pci': p[3], 'rsrq': p[4], 'rsrp': p[5]})
|
|
except: pass
|
|
return neighbors
|
|
|
|
def get_serving_cell_info(self) -> Dict[str, Union[str, int]]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QENG="servingcell"')
|
|
response = self.s_port.read_until()
|
|
res = {}
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QENG:'):
|
|
try:
|
|
p = [x.strip(' "') for x in line.decode('ascii', errors='ignore').split(',')]
|
|
if len(p) > 2:
|
|
res['tech'] = p[2]
|
|
if p[2] in ("LTE", "eMTC"):
|
|
res.update({'mcc-mnc': f"{p[4]}-{p[5]}", 'cellid': p[6], 'rsrp': int(p[13]), 'rsrq': int(p[14])})
|
|
elif p[2] == "NB-IoT":
|
|
res.update({'mcc-mnc': f"{p[4]}-{p[5]}", 'cellid': p[6], 'rsrp': int(p[10]), 'rsrq': int(p[11])})
|
|
except: pass
|
|
return res
|
|
|
|
def get_network_info(self) -> Optional[str]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QNWINFO')
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if line.startswith(b'+QNWINFO:'):
|
|
return line.decode('ascii', errors='ignore').strip().split(':', 1)[1].strip()
|
|
return None
|
|
|
|
# --- Extended Configuration (AT+QCFGEXT) ---
|
|
|
|
def set_qcfg_ext(self, parameter: str, value: str) -> bool:
|
|
cmd = f'AT+QCFGEXT="{parameter}",{value}'
|
|
with self.transaction:
|
|
self.s_port.send_cmd(cmd.encode())
|
|
return self.at.read_ok()
|
|
|
|
def nidd_configure(self, apn: str, account: str = "", pwd: str = "") -> bool:
|
|
return self.set_qcfg_ext("nipdcfg", f'"{apn}","{account}","{pwd}"')
|
|
|
|
def nidd_open(self, enable: bool = True) -> bool:
|
|
return self.set_qcfg_ext("nipd", "1" if enable else "0")
|
|
|
|
def nidd_send(self, data: bytes) -> bool:
|
|
hex_data = binascii.hexlify(data).decode('ascii')
|
|
return self.set_qcfg_ext("nipds", f'"{hex_data}"')
|
|
|
|
def nidd_receive(self) -> bytes:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(b'AT+QCFGEXT="nipdr"')
|
|
response = self.s_port.read_until()
|
|
for line in response.splitlines():
|
|
if b'+QCFGEXT: "nipdr",' in line:
|
|
try:
|
|
return binascii.unhexlify(line.split(b',')[-1].strip(b' "'))
|
|
except: pass
|
|
return b''
|
|
|
|
def add_geofence(self, id: int, shape: int, coords: List[float], radius: int = 0) -> bool:
|
|
val = f"{id},{shape}," + ",".join(map(str, coords))
|
|
if shape == 0: val += f",{radius}"
|
|
return self.set_qcfg_ext("addgeo", val)
|
|
|
|
def delete_geofence(self, id: int) -> bool:
|
|
return self.set_qcfg_ext("deletegeo", str(id))
|
|
|
|
def query_geofence(self, id: int) -> Optional[str]:
|
|
with self.transaction:
|
|
self.s_port.send_cmd(f'AT+QCFGEXT="querygeo",{id}'.encode())
|
|
return self.s_port.read_until().decode('ascii', errors='ignore')
|
|
|
|
def set_usb_power(self, enable: bool = True) -> bool:
|
|
return self.set_qcfg_ext("disusb", "0" if enable else "1")
|
|
|
|
def set_pwm(self, pin: int, freq: int, duty: int) -> bool:
|
|
return self.set_qcfg_ext("pwm", f"{pin},{freq},{duty}")
|