From 6507956a8320b5c5be69aef8fea72572fb4e3cfe Mon Sep 17 00:00:00 2001 From: Cyberes Date: Mon, 1 Jul 2024 20:00:06 -0600 Subject: [PATCH] get cellular setup working --- src/lib/gps/read.py | 4 +- src/lib/logging.py | 4 +- src/lib/networking/cell_modem.py | 479 +++++++++++-------------------- src/lib/networking/wifi.py | 4 +- src/lib/runtime.py | 4 +- src/reset.py | 4 +- 6 files changed, 173 insertions(+), 326 deletions(-) diff --git a/src/lib/gps/read.py b/src/lib/gps/read.py index 0351604..a73715a 100644 --- a/src/lib/gps/read.py +++ b/src/lib/gps/read.py @@ -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') diff --git a/src/lib/logging.py b/src/lib/logging.py index 090f20c..49f3013 100644 --- a/src/lib/logging.py +++ b/src/lib/logging.py @@ -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}') diff --git a/src/lib/networking/cell_modem.py b/src/lib/networking/cell_modem.py index c724476..2367ca6 100644 --- a/src/lib/networking/cell_modem.py +++ b/src/lib/networking/cell_modem.py @@ -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 diff --git a/src/lib/networking/wifi.py b/src/lib/networking/wifi.py index d8191e2..1cf2b1f 100644 --- a/src/lib/networking/wifi.py +++ b/src/lib/networking/wifi.py @@ -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") diff --git a/src/lib/runtime.py b/src/lib/runtime.py index b67972d..cf384fa 100644 --- a/src/lib/runtime.py +++ b/src/lib/runtime.py @@ -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) diff --git a/src/reset.py b/src/reset.py index 0451d7b..ea14a26 100644 --- a/src/reset.py +++ b/src/reset.py @@ -1,3 +1 @@ -from lib.runtime import reboot - -reboot() +from lib.runtime import reboot;reboot()