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.gps.position import Position
from lib.interval_tracker import interval_tracker from lib.interval_tracker import interval_tracker
from lib.logging import logger, LogLevel 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 from lib.ttime import initialize_rtc, unix_timestamp
PIN_GPS_POWER = 12 PIN_GPS_POWER = 12
@ -43,7 +43,7 @@ async def get_position():
# TODO: fix altitude always being 0 # TODO: fix altitude always being 0
gps_power_pin.value(1) # Always make sure the GPS is on. 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(): async def get_loc():
timestamp = unix_timestamp() timestamp = unix_timestamp()
gnrmc = await read_gps_uart('$GNRMC') 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): def logger(msg: str, level: LogLevel = LogLevel.info, source: str = None):
s = '' s = ''
if source: if source:
s = f'[{source}] - ' s = f'[{source.upper()}] - '
print(f'{s}{level} -- {time.ticks_ms() / 1000} -- {msg}') print(f'{s}{level} -- {round(time.ticks_ms() / 1000, 2)} -- {msg}')

View File

@ -1,86 +1,97 @@
import asyncio import asyncio
import gc
import time import time
import machine from machine import UART, Pin
from machine import 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.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_POWER = 27
PIN_BEE_UART_RXD = 35 PIN_BEE_UART_RXD = 35
PIN_BEE_UART_TXD = 2 PIN_BEE_UART_TXD = 2
VERBOSE_XBEE_COMM = False
DEBUG_XBEE = True
bee_power_pin = Pin(PIN_BEE_POWER, Pin.OUT) bee_power_pin = Pin(PIN_BEE_POWER, Pin.OUT)
bee_power_pin.value(0) bee_power_pin.value(0)
bee_power_pin.value(1)
class ModemErrorResponse(Exception): class ModemError(Exception):
def __init__(self, message="Modem returned an error", code: tuple = None): def __init__(self, message: str, sent_command: bytes, response: list):
self.command = code[0] super().__init__(f'{message}: {sent_command} -> {response}')
self.lines = code[1]
super().__init__(f'{message}: {code}')
async def xb_toggle_power():
bee_power_pin.value(0)
await asyncio.sleep(0.2)
bee_power_pin.value(1)
class CellularModem: class CellularModem:
_lock = asyncio.Lock()
def __init__(self): def __init__(self):
self.uart = machine.UART(2, baudrate=115200, timeout=4, rx=PIN_BEE_UART_RXD, tx=PIN_BEE_UART_TXD) self.uart = 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())
async def keep_alive(self): async def xb_read(self, timeout):
while True: @runtime_timeout(timeout)
await asyncio.sleep(40) async def inner():
async with self._lock: if self.uart.any():
res = await self.send_command("AT") return self.uart.read()
if not res: return None
raise OSError("Modem not responding")
async def send_command(self, command, require_ok: bool = False, expecing_plus: bool = False, read_until: bytes = None, clean_lines: bool = True) -> tuple: try:
lines = [] return await inner()
async with self._lock: except uTimeoutError:
return None
def xb_purge(self):
while self.uart.any(): while self.uart.any():
# Clear any initial garbage self.uart.read()
self.uart.read(1)
await self.swriter.awrite(f'{command}\r\n') async def send_and_receive(self, data_to_send: bytes, read_until: any = b'OK\r\n', expected: bytes = None, timeout: int = 1000) -> list:
for i in range(10): if not isinstance(data_to_send, bytes):
raw_line = await self.sreader.readline() raise Exception
print(raw_line) if read_until is not None and not isinstance(read_until, bytes):
if len(raw_line.decode().strip('\r\n')) > 0 and raw_line != f'{command}\r\r\n'.encode(): raise Exception
lines.append(raw_line) if data_to_send.endswith(b'\r\n'):
if raw_line == b'ERROR\r\n': raise Exception
raise ModemErrorResponse(code=(command, lines)) if read_until is not None and expected is not None:
if len(lines): raise Exception
if expecing_plus:
if lines[-1].decode().startswith('+'): 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 break
elif read_until: elif expected is not None:
if lines[-1] == read_until: if data.endswith(expected):
break break
elif lines[-1] == b'OK\r\n':
break
if clean_lines:
response = tuple(x for x in [x.decode().strip('\r\n') for x in lines] if len(x))
else: else:
response = lines expected_not_found += 1
if require_ok: if expected_not_found == 3:
if response[-1] != 'OK': raise ModemError(f'Did not recieve expected {expected}', sent_command=data_to_send, response=data)
raise ModemErrorResponse(code=(command, lines)) 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 []
return response
cellular = CellularModem() cell_modem = CellularModem()
def _parse_csq(csq_value: int): def parse_csq(csq_value: int):
if csq_value == 99: if csq_value == 99:
return 'unknown' return 'unknown (likely disconnected)'
elif csq_value == 31: elif csq_value == 31:
return '51 dBm or greater' return '51 dBm or greater'
elif csq_value == 0: elif csq_value == 0:
@ -93,58 +104,13 @@ def _parse_csq(csq_value: int):
return 'Invalid CSQ value' 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(): async def cellular_signal_strength():
@timeout(10) @runtime_timeout(10)
async def inner(): 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: '): if len(resp) == 2 and resp[0].startswith('+CSQ: '):
s = int(resp[0].lstrip('+CSQ: ').split(',')[0]) s = int(resp[0].lstrip('+CSQ: ').split(',')[0])
return _parse_csq(s) return parse_csq(s)
else: else:
raise Exception(resp) raise Exception(resp)
@ -157,112 +123,98 @@ async def cellular_signal_strength():
async def cellular_ip() -> str: async def cellular_ip() -> str:
try: 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: '): if ip_resp[0].startswith('+IPADDR: '):
return ip_resp[0].strip('+IPADDR: ') return ip_resp[0].strip('+IPADDR: ')
except ModemErrorResponse: except ModemError:
return '0.0.0.0' return '0.0.0.0'
async def cellular_check_service_ready(): @runtime_timeout(CELLULAR_STARTUP_TIMEOUT)
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)
async def start_modem(): async def start_modem():
await xb_toggle_power()
while True: while True:
failures = -1 logger('Setting up modem', source='CELL')
try: failures = 0
if (await run_with_timeout(func=cellular_wake_modem, timeout_sec=5)).failure:
logger('Modem wake-up timed out', source='CELL', level=LogLevel.error)
failures += 1
if failures == 11:
machine.reset()
await restart_modem()
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: while True:
init_resp = await cellular.send_command(cmd) at = await cell_modem.send_and_receive(b'AT')
logger('\n'.join((x for x in init_resp if x != 'OK')), source='CELL', level=LogLevel.info) if len(at) and at[-1] == 'OK':
if init_resp[-1] != 'OK': logger('Modem woken up', source='CELL')
await asyncio.sleep(0.1) break
else: else:
break if DEBUG_XBEE:
logger(f'Modem start failed: {at}', source='CELL', level=LogLevel.debug)
simcom_model_resp = await cellular.send_command('AT+SIMCOMATI') failures += 1
model = simcom_model_resp[1] if failures == 5:
if not model.startswith('Model:'): return False
raise Exception await asyncio.sleep(0.5)
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 continue
try: try:
await cellular.send_command(f'AT+CGDCONT=1,"IP","{CELLULAR_APN}"', require_ok=True) await cell_modem.send_and_receive(b'ATE0', expected=b'OK\r\n', read_until=None)
await cellular.send_command('AT+CSOCKSETPN=1', require_ok=True) except ModemError as e:
await cellular.send_command('AT+CIPMODE=0', require_ok=True) logger(f'Modem start failed: {e}', source='CELL', level=LogLevel.error)
await cellular.send_command('AT+CNMP=38', require_ok=True) return False
await cellular.send_command('AT+NETOPEN', require_ok=True)
break ati = await cell_modem.send_and_receive(b'ATI')
except ModemErrorResponse as e: if not isinstance(ati, list) or ati[-1] != 'OK':
print(e) logger(f'Modem start failed: {b"ATI"} -> {ati}', source='CELL', level=LogLevel.error)
await restart_modem() return False
for line in ati:
if line != 'OK':
logger(line, source='CELL')
# Assuming model is SIM7600
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 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) ip_get = await run_with_timeout(func=cellular_ip, timeout_sec=3)
if not ip_get.failure: if not ip_get.failure:
@ -270,122 +222,19 @@ async def start_modem():
else: else:
logger(f'IP: 0.0.0.0', source='CELL') logger(f'IP: 0.0.0.0', source='CELL')
logger(f'Signal strength: {await cellular_signal_strength()}', source='CELL')
logger('Modem initialized', 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 return True
except ModemErrorResponse:
return False
async def start_modem_task(): async def start_modem_task():
logger('Initalizing modem', source='CELL')
async def inner():
while True: while True:
try: if await start_modem():
gc.collect()
await start_modem()
except uTimeoutError:
await asyncio.sleep(10)
continue
break break
if not (await cell_modem.send_and_receive(b'AT+CPOF')):
await xb_toggle_power()
await asyncio.sleep(2)
while True: while True:
x = await run_with_timeout(func=inner, timeout_sec=300) await asyncio.sleep(100)
if not x.failure: # TODO: handle reconnection here
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())

View File

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

View File

@ -6,7 +6,7 @@ class uTimeoutError(Exception):
super().__init__(message) 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. 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: async def run_with_timeout(func, timeout_sec: int, *args, **kwargs) -> RunWithtimeoutResult:
@timeout(timeout_sec) @runtime_timeout(timeout_sec)
async def inner(): async def inner():
return await func(*args, **kwargs) return await func(*args, **kwargs)

View File

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