get cellular setup working

This commit is contained in:
Cyberes 2024-07-01 20:00:06 -06:00
parent a7ff402b77
commit 6507956a83
6 changed files with 173 additions and 326 deletions

View File

@ -7,7 +7,7 @@ from lib.gps.micropyGPS import MicropyGPS
from lib.gps.position import Position
from lib.interval_tracker import interval_tracker
from lib.logging import logger, LogLevel
from lib.runtime import timeout, uTimeoutError
from lib.runtime import runtime_timeout, uTimeoutError
from lib.ttime import initialize_rtc, unix_timestamp
PIN_GPS_POWER = 12
@ -43,7 +43,7 @@ async def get_position():
# TODO: fix altitude always being 0
gps_power_pin.value(1) # Always make sure the GPS is on.
@timeout(TIMEOUT_GNSS_LOCATION)
@runtime_timeout(TIMEOUT_GNSS_LOCATION)
async def get_loc():
timestamp = unix_timestamp()
gnrmc = await read_gps_uart('$GNRMC')

View File

@ -11,5 +11,5 @@ class LogLevel:
def logger(msg: str, level: LogLevel = LogLevel.info, source: str = None):
s = ''
if source:
s = f'[{source}] - '
print(f'{s}{level} -- {time.ticks_ms() / 1000} -- {msg}')
s = f'[{source.upper()}] - '
print(f'{s}{level} -- {round(time.ticks_ms() / 1000, 2)} -- {msg}')

View File

@ -1,86 +1,97 @@
import asyncio
import gc
import time
import machine
from machine import Pin
from machine import UART, Pin
from config import CELLULAR_APN, CELLULAR_STARTUP_TIMEOUT, TRACCAR_HOST
from config import CELLULAR_STARTUP_TIMEOUT, CELLULAR_APN
from lib.logging import logger, LogLevel
from lib.runtime import timeout, uTimeoutError, run_with_timeout
from lib.runtime import runtime_timeout, uTimeoutError, run_with_timeout
PIN_BEE_POWER = 27
PIN_BEE_UART_RXD = 35
PIN_BEE_UART_TXD = 2
VERBOSE_XBEE_COMM = False
DEBUG_XBEE = True
bee_power_pin = Pin(PIN_BEE_POWER, Pin.OUT)
bee_power_pin.value(0)
bee_power_pin.value(1)
class ModemErrorResponse(Exception):
def __init__(self, message="Modem returned an error", code: tuple = None):
self.command = code[0]
self.lines = code[1]
super().__init__(f'{message}: {code}')
class ModemError(Exception):
def __init__(self, message: str, sent_command: bytes, response: list):
super().__init__(f'{message}: {sent_command} -> {response}')
async def xb_toggle_power():
bee_power_pin.value(0)
await asyncio.sleep(0.2)
bee_power_pin.value(1)
class CellularModem:
_lock = asyncio.Lock()
def __init__(self):
self.uart = machine.UART(2, baudrate=115200, timeout=4, rx=PIN_BEE_UART_RXD, tx=PIN_BEE_UART_TXD)
self.swriter = asyncio.StreamWriter(self.uart, {})
self.sreader = asyncio.StreamReader(self.uart)
asyncio.create_task(self.keep_alive())
self.uart = UART(2, baudrate=115200, timeout=4, rx=PIN_BEE_UART_RXD, tx=PIN_BEE_UART_TXD)
async def keep_alive(self):
while True:
await asyncio.sleep(40)
async with self._lock:
res = await self.send_command("AT")
if not res:
raise OSError("Modem not responding")
async def xb_read(self, timeout):
@runtime_timeout(timeout)
async def inner():
if self.uart.any():
return self.uart.read()
return None
async def send_command(self, command, require_ok: bool = False, expecing_plus: bool = False, read_until: bytes = None, clean_lines: bool = True) -> tuple:
lines = []
async with self._lock:
while self.uart.any():
# Clear any initial garbage
self.uart.read(1)
await self.swriter.awrite(f'{command}\r\n')
for i in range(10):
raw_line = await self.sreader.readline()
print(raw_line)
if len(raw_line.decode().strip('\r\n')) > 0 and raw_line != f'{command}\r\r\n'.encode():
lines.append(raw_line)
if raw_line == b'ERROR\r\n':
raise ModemErrorResponse(code=(command, lines))
if len(lines):
if expecing_plus:
if lines[-1].decode().startswith('+'):
break
elif read_until:
if lines[-1] == read_until:
break
elif lines[-1] == b'OK\r\n':
try:
return await inner()
except uTimeoutError:
return None
def xb_purge(self):
while self.uart.any():
self.uart.read()
async def send_and_receive(self, data_to_send: bytes, read_until: any = b'OK\r\n', expected: bytes = None, timeout: int = 1000) -> list:
if not isinstance(data_to_send, bytes):
raise Exception
if read_until is not None and not isinstance(read_until, bytes):
raise Exception
if data_to_send.endswith(b'\r\n'):
raise Exception
if read_until is not None and expected is not None:
raise Exception
data_to_send += b'\r\n'
if VERBOSE_XBEE_COMM:
print(f'@====> {data_to_send}')
self.uart.write(data_to_send)
# Wait and read the response
t = time.ticks_ms()
data = b''
expected_not_found = 0
while time.ticks_diff(time.ticks_ms(), t) < timeout:
read = await self.xb_read(50)
if read and read != data_to_send:
if VERBOSE_XBEE_COMM:
print(f'<====@ {read}')
data += read
if read_until is not None and data.endswith(read_until):
break
elif expected is not None:
if data.endswith(expected):
break
if clean_lines:
response = tuple(x for x in [x.decode().strip('\r\n') for x in lines] if len(x))
else:
response = lines
if require_ok:
if response[-1] != 'OK':
raise ModemErrorResponse(code=(command, lines))
return response
else:
expected_not_found += 1
if expected_not_found == 3:
raise ModemError(f'Did not recieve expected {expected}', sent_command=data_to_send, response=data)
return [x.decode() for x in data.replace(b'\r\r\n', b'\r\n').split(b'\r\n') if len(x)] if data else []
cellular = CellularModem()
cell_modem = CellularModem()
def _parse_csq(csq_value: int):
def parse_csq(csq_value: int):
if csq_value == 99:
return 'unknown'
return 'unknown (likely disconnected)'
elif csq_value == 31:
return '51 dBm or greater'
elif csq_value == 0:
@ -93,58 +104,13 @@ def _parse_csq(csq_value: int):
return 'Invalid CSQ value'
async def cellular_wake_modem():
i = 2
while i > 0:
resp = await cellular.send_command('AT')
if len(resp) == 1 and resp[0] == 'OK':
i -= 1
else:
logger('\n'.join(resp), source='CELL')
i += 1
await asyncio.sleep(0.5)
async def cellular_wait_cpsi():
while True:
cpsi_resp = await cellular.send_command('AT+CPSI?')
try:
parts = cpsi_resp[0].lstrip('+CPSI: ').split(',')
except:
print(cpsi_resp)
raise
if parts[1] == 'Online':
return True, parts
elif parts[1] == 'Low Power Mode':
return False, parts
await asyncio.sleep(0.5)
async def cellular_wait_creg():
# https://community.hologram.io/t/sim7600-in-ovms-board-t-mobile-registration-denied/4616/12
async def check():
resp = await cellular.send_command('AT+CREG?')
if len(resp) == 2 and resp[0].startswith('+CREG: '):
return resp[0] == '+CREG: 0,1' or resp[0] == '+CREG: 0,5', resp
else:
raise Exception(resp)
i = 0
while True:
ready, code = await check()
if ready:
return ready, code
i += 1
await asyncio.sleep(0.5)
async def cellular_signal_strength():
@timeout(10)
@runtime_timeout(10)
async def inner():
resp = await cellular.send_command('AT+CSQ')
resp = await cell_modem.send_and_receive(b'AT+CSQ')
if len(resp) == 2 and resp[0].startswith('+CSQ: '):
s = int(resp[0].lstrip('+CSQ: ').split(',')[0])
return _parse_csq(s)
return parse_csq(s)
else:
raise Exception(resp)
@ -157,235 +123,118 @@ async def cellular_signal_strength():
async def cellular_ip() -> str:
try:
ip_resp = await cellular.send_command('AT+IPADDR')
ip_resp = await cell_modem.send_and_receive(b'AT+IPADDR')
if ip_resp[0].startswith('+IPADDR: '):
return ip_resp[0].strip('+IPADDR: ')
except ModemErrorResponse:
except ModemError:
return '0.0.0.0'
async def cellular_check_service_ready():
resp = await cellular.send_command('AT+CGATT?')
if len(resp) == 2 and resp[0].startswith('+CGATT: '):
return resp[0] == '+CGATT: 1', resp[0]
else:
raise Exception(resp)
async def restart_modem():
await asyncio.sleep(0.5)
bee_power_pin.value(0)
time.sleep(0.5)
bee_power_pin.value(1)
await asyncio.sleep(0.5)
async def cellular_check_connected():
cpsi = await run_with_timeout(func=cellular_wait_cpsi, timeout_sec=1)
if cpsi.failure:
return False
if cpsi.result[1][0] != 'NO SERVICE':
return False
if (await cellular_ip()) == '0.0.0.0':
return False
if (await cellular_signal_strength()) == 'disconnected':
return False
return True
@timeout(CELLULAR_STARTUP_TIMEOUT)
@runtime_timeout(CELLULAR_STARTUP_TIMEOUT)
async def start_modem():
await xb_toggle_power()
while True:
failures = -1
try:
if (await run_with_timeout(func=cellular_wake_modem, timeout_sec=5)).failure:
logger('Modem wake-up timed out', source='CELL', level=LogLevel.error)
logger('Setting up modem', source='CELL')
failures = 0
while True:
at = await cell_modem.send_and_receive(b'AT')
if len(at) and at[-1] == 'OK':
logger('Modem woken up', source='CELL')
break
else:
if DEBUG_XBEE:
logger(f'Modem start failed: {at}', source='CELL', level=LogLevel.debug)
failures += 1
if failures == 11:
machine.reset()
await restart_modem()
if failures == 5:
return False
await asyncio.sleep(0.5)
continue
except ModemErrorResponse:
# Sporadic ERROR responses
await restart_modem()
continue
sim_resp = await cellular.send_command('AT+CPIN?')
if not sim_resp[0].endswith(' READY'):
msg = '\n'.join(sim_resp)
logger(f'SIM card not ready: {msg}', source='CELL', level=LogLevel.error)
await restart_modem()
continue
cmds = ['ATE0', 'ATI']
for cmd in cmds:
while True:
init_resp = await cellular.send_command(cmd)
logger('\n'.join((x for x in init_resp if x != 'OK')), source='CELL', level=LogLevel.info)
if init_resp[-1] != 'OK':
await asyncio.sleep(0.1)
else:
break
simcom_model_resp = await cellular.send_command('AT+SIMCOMATI')
model = simcom_model_resp[1]
if not model.startswith('Model:'):
raise Exception
if 'SIM7600' not in model:
raise Exception('Not implemented')
cpsi = await run_with_timeout(func=cellular_wait_cpsi, timeout_sec=1)
if cpsi.failure:
logger('AT+CPSI timeout', source='CELL', level=LogLevel.error)
await restart_modem()
continue
if not cpsi.result[0]:
logger('Bad AT+CPSI status', source='CELL', level=LogLevel.error)
await restart_modem()
continue
creg = await run_with_timeout(func=cellular_wait_creg, timeout_sec=1)
if creg.failure:
logger('AT+CREG timeout, no signal?', source='CELL', level=LogLevel.warning)
elif not creg.result[0]:
logger(f'Bad AT+CREG response: {creg.result[1]}', source='CELL', level=LogLevel.error)
await restart_modem()
continue
try:
await cellular.send_command(f'AT+CGDCONT=1,"IP","{CELLULAR_APN}"', require_ok=True)
await cellular.send_command('AT+CSOCKSETPN=1', require_ok=True)
await cellular.send_command('AT+CIPMODE=0', require_ok=True)
await cellular.send_command('AT+CNMP=38', require_ok=True)
await cellular.send_command('AT+NETOPEN', require_ok=True)
break
except ModemErrorResponse as e:
print(e)
await restart_modem()
break
await cell_modem.send_and_receive(b'ATE0', expected=b'OK\r\n', read_until=None)
except ModemError as e:
logger(f'Modem start failed: {e}', source='CELL', level=LogLevel.error)
return False
ip_get = await run_with_timeout(func=cellular_ip, timeout_sec=3)
if not ip_get.failure:
logger(f'IP: {ip_get.result}', source='CELL')
else:
logger(f'IP: 0.0.0.0', source='CELL')
ati = await cell_modem.send_and_receive(b'ATI')
if not isinstance(ati, list) or ati[-1] != 'OK':
logger(f'Modem start failed: {b"ATI"} -> {ati}', source='CELL', level=LogLevel.error)
return False
for line in ati:
if line != 'OK':
logger(line, source='CELL')
# Assuming model is SIM7600
logger(f'Signal strength: {await cellular_signal_strength()}', source='CELL')
logger('Modem initialized', source='CELL')
cpin = await cell_modem.send_and_receive(b'AT+CPIN?')
if not cpin[0].endswith(': READY'):
logger('NO SIM CARD', source='CELL', level=LogLevel.error)
await asyncio.sleep(30)
return False
signal_strength = await cellular_signal_strength()
logger(f'Signal strength: {signal_strength}', source='CELL')
cpsi = await cell_modem.send_and_receive(b'AT+CPSI?')
if 'OK' not in cpsi:
logger(f'Modem start failed: {b"AT+CPSI?"} -> {cpsi}', source='CELL', level=LogLevel.error)
return False
cpsi_parts = cpsi[0].lstrip('+CPSI: ').split(',')
if cpsi_parts[0] == 'NO SERVICE':
logger('No service', source='CELL', level=LogLevel.warning)
await asyncio.sleep(10)
return False
elif cpsi_parts[1] != 'Online':
logger(f'Modem not online: {",".join(cpsi_parts)}', source='CELL', level=LogLevel.error)
return False
else:
logger(f'Connected to {cpsi_parts[0]}', source='CELL')
for cmd in [b'AT+CREG?', b'AT+CGREG?']:
r = await cell_modem.send_and_receive(cmd)
if len(r):
# Page 85, 185
if ' 0,2' in r[0] or ' 0,0' in r[0]:
logger(f'Modem not attached to network: {r}', source='CELL', level=LogLevel.warning)
await asyncio.sleep(30)
return False
elif ' 0,1' not in r[0] and ' 0,5' not in r[0]:
logger(f'Modem start failed: {cmd} -> {r}', source='CELL', level=LogLevel.error)
return False
cgact = await cell_modem.send_and_receive(b'AT+CGACT?')
success = False
for line in cgact:
if '+CGACT: 1,' in line:
success = True
break
if not success:
logger(f'Modem start failed: {b"AT+CGACT?"} -> {cgact}', source='CELL', level=LogLevel.error)
return False
await cell_modem.send_and_receive(f'AT+CGSOCKCONT=1,"IP","{CELLULAR_APN}"'.encode())
await cell_modem.send_and_receive(b'AT+CSOCKSETPN=1', read_until=None, expected=b'OK\r\n')
await cell_modem.send_and_receive(b'AT+CIPMODE=0', read_until=None, expected=b'OK\r\n')
await cell_modem.send_and_receive(b'AT+NETOPEN', read_until=None, expected=b'OK\r\n')
ip_get = await run_with_timeout(func=cellular_ip, timeout_sec=3)
if not ip_get.failure:
logger(f'IP: {ip_get.result}', source='CELL')
else:
logger(f'IP: 0.0.0.0', source='CELL')
async def cellular_start_http():
if TRACCAR_HOST.startswith('http://'):
code = 'AT+HTTPINIT'
elif TRACCAR_HOST.startswith('https://'):
code = 'AT+CCHSTART'
else:
raise Exception
try:
if TRACCAR_HOST.startswith('https://'):
await cellular.send_command('AT+CSSLCFG="sslversion",0,4')
await cellular.send_command('AT+CSSLCFG="authmode",0,0')
await cellular.send_command('AT+CSSLCFG="ignorelocaltime",0,1')
await cellular.send_command('AT+CSSLCFG="enableSNI",0,1')
await cellular.send_command('AT+CCHSET=1')
await cellular.send_command('AT+CCHSSLCFG=0,0')
await cellular.send_command(code)
return True
except ModemErrorResponse:
return False
async def start_modem_task():
logger('Initalizing modem', source='CELL')
async def inner():
while True:
try:
gc.collect()
await start_modem()
except uTimeoutError:
await asyncio.sleep(10)
continue
while True:
if await start_modem():
break
if not (await cell_modem.send_and_receive(b'AT+CPOF')):
await xb_toggle_power()
await asyncio.sleep(2)
while True:
x = await run_with_timeout(func=inner, timeout_sec=300)
if not x.failure:
break
gc.collect()
cellular.uart.write('AT+CIPOPEN=0,"UDP","64.227.105.164",10001,8000')
data = 'Hello!!'
cellular.uart.write(f'AT+CIPSEND=0,{len(data.encode())},"64.227.105.164",10001')
await asyncio.sleep(0.1)
cellular.uart.write(data)
await cellular.send_command('AT+CIPCLOSE=0')
# TODO: loop to reconnect modem
@timeout(30)
async def cellular_send_http(url: str, method: str, data: str = None):
m = method.upper()
if m == 'GET':
mode = 0
elif m == 'POST':
if not data:
raise Exception
mode = 1
else:
raise Exception
# for i in range(30):
# httpinit = await run_with_timeout(func=cellular_start_http, timeout_sec=5)
# if httpinit.failure:
# # logger('AT+HTTPINIT timeout', source='CELL', level=LogLevel.error)
# await restart_modem()
# continue
# if not httpinit.result:
# # logger('AT+HTTPINIT failure', source='CELL', level=LogLevel.error)
# await cellular.send_command('AT+CRESET')
# # await restart_modem()
# # break
await cellular_start_http()
if TRACCAR_HOST.startswith('http://'):
await cellular.send_command(f'AT+HTTPPARA="URL","{url}"')
_, resp = await cellular.send_command(f'AT+HTTPACTION={mode}', expecing_plus=True)
_, status_code, data_len = resp.strip('+HTTPACTION: ').split(',')
if status_code != '200':
logger(f'HTTP {m} failed: {status_code}', source='CELL', level=LogLevel.error)
return
data = await cellular.send_command(f'AT+HTTPREAD={data_len}', read_until=b'+HTTPREAD:0\r\n', clean_lines=False)
elif TRACCAR_HOST.startswith('https://'):
# HTTPS does not work.
# ModemErrorResponse: Modem returned an error: ('AT+CCHOPEN=0,"postman-echo.com",443,2', [b'ERROR\r\n'])
parts = url.replace('https://', '', 1).split('/')
host = parts.pop(0)
path = '/' + '/'.join(parts)
await cellular.send_command(f'AT+CCHOPEN=0,"{host}",443,2')
content = f"""GET {path} HTTP/1.1
Host: {host}
User-Agent: Freematics
Proxy-Connection: keep-alive
Content-Length: 0"""
await cellular.send_command(f'AT+CCHSEND=0,{len(content.encode())}')
resp = await cellular.send_command('AT+CCHRECV=0', read_until=b'+CCHRECV: 0,0\r\n')
print(resp)
else:
raise Exception
if data[0] != b'OK\r\n':
raise Exception
data = list(data)
del data[0]
del data[0]
del data[-1]
data_clean = ''.join([x.decode() for x in data]).strip('\r\n')
return data_clean
asyncio.run(start_modem_task())
await asyncio.sleep(100)
# TODO: handle reconnection here

View File

@ -4,7 +4,7 @@ import network
from config import WIFI_SSID, WIFI_PASSWORD, TIMEOUT_WIFI_CONNECT
from lib.logging import logger, LogLevel
from lib.runtime import timeout
from lib.runtime import runtime_timeout
class WifiStatus:
@ -38,7 +38,7 @@ class WifiMananger:
def disconnect(self):
self._wlan.disconnect()
@timeout(TIMEOUT_WIFI_CONNECT)
@runtime_timeout(TIMEOUT_WIFI_CONNECT)
async def connect(self, ignore_missing: bool = False):
if self._wlan.isconnected() or self.status() == WifiStatus.CONNECTED:
raise Exception("Already connected")

View File

@ -6,7 +6,7 @@ class uTimeoutError(Exception):
super().__init__(message)
def timeout(seconds):
def runtime_timeout(seconds):
"""
This will not work if your decorated function uses `time.sleep()`!!! Use `asyncio.sleep()` instead.
"""
@ -35,7 +35,7 @@ class RunWithtimeoutResult:
async def run_with_timeout(func, timeout_sec: int, *args, **kwargs) -> RunWithtimeoutResult:
@timeout(timeout_sec)
@runtime_timeout(timeout_sec)
async def inner():
return await func(*args, **kwargs)

View File

@ -1,3 +1 @@
from lib.runtime import reboot
reboot()
from lib.runtime import reboot;reboot()