Skip to content

Add features: IMEI, RSSI, Versions, Geolocation, Time, Ring Alerts, handle non-responsive modem #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 12, 2021
Merged
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 197 additions & 6 deletions adafruit_rockblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

"""


import time
import struct

Expand All @@ -60,12 +61,14 @@ def __init__(self, uart, baudrate=19200):

def _uart_xfer(self, cmd):
"""Send AT command and return response as tuple of lines read."""

self._uart.reset_input_buffer()
self._uart.write(str.encode("AT" + cmd + "\r"))

resp = []
line = self._uart.readline()
if line is None:
# No response from Modem
return None
resp.append(line)
while not any(EOM in line for EOM in (b"OK\r\n", b"ERROR\r\n")):
line = self._uart.readline()
Expand All @@ -77,8 +80,15 @@ def _uart_xfer(self, cmd):

def reset(self):
"""Perform a software reset."""
self._uart_xfer("&F0") # factory defaults
self._uart_xfer("&K0") # flow control off
if self._uart_xfer("&F0") is None: # factory defaults
return False
if self._uart_xfer("&K0") is None: # flow control off
return False
return True

def _transfer_buffer(self):
"""Copy out buffer to in buffer to simulate receiving a message."""
self._uart_xfer("+SBDTC")

@property
def data_out(self):
Expand Down Expand Up @@ -199,6 +209,187 @@ def model(self):
return resp[1].strip().decode()
return None

def _transfer_buffer(self):
"""Copy out buffer to in buffer to simulate receiving a message."""
self._uart_xfer("+SBDTC")
@property
def imei(self):
"""Return modem imei/serial."""
resp = self._uart_xfer("+CGSN")
if resp[-1].strip().decode() == "OK":
return resp[1].strip().decode()
return None

@property
def rssi(self):
"""Return Received Signal Strength Indicator (RSSI)
values returned are 0 to 5, where 0 is no signal (0 bars) and 5 is strong signal (5 bars).
Important note: signal strength may not be fully accurate, so
waiting for high signal strength prior to sending a message isn't always recommended.
For details see https://docs.rockblock.rock7.com/docs/checking-the-signal-strength
"""
resp = self._uart_xfer("+CSQ")
if resp[-1].strip().decode() == "OK":
return resp[1].strip().decode().split(":")[1]
return None

@property
def version(self):
"""Return the modem components' firmware versions.
For example: Call Processor Version, Modem DSP Version, DBB Version (ASIC),
RFA VersionSRFA2), NVM Version, Hardware Version, BOOT Version
"""
resp = self._uart_xfer("+CGMR")
if resp[-1].strip().decode() == "OK":
lines = []
for x in range(1, len(resp) - 2):
line = resp[x]
if line != b"\r\n":
lines.append(line.decode().strip())
return lines
return None

@property
def ring_alert(self):
"""Retrieve setting for SBD Ring Alerts."""
resp = self._uart_xfer("+SBDMTA?")
if resp[-1].strip().decode() == "OK":
return bool(int(resp[1].strip().decode().split(":")[1]))
return None

@ring_alert.setter
def ring_alert(self, value=1):
"""Enable or disable ring alert feature."""
if value in [True, False]:
resp = self._uart_xfer("+SBDMTA=" + str(int(value)))
if resp[-1].strip().decode() == "OK":
return True
raise RuntimeError("Error setting Ring Alert.")
raise ValueError(
"Use 0 or False to disable Ring Alert or use 0 or True to enable Ring Alert."
)

@property
def ring_indication(self):
"""
Query the ring indication status, returning the reason for the most recent assertion
of the Ring Indicate signal.

The response contains separate indications for telephony and SBD ring indications.
The response is in the form:
[<tel_ri>,<sbd_ri>]

<tel_ri> indicates the telephony ring indication status:
0 No telephony ring alert received.
1 Incoming voice call.
2 Incoming data call.
3 Incoming fax call.

<sbd_ri> indicates the SBD ring indication status:
0 No SBD ring alert received.
1 SBD ring alert received.
"""
resp = self._uart_xfer("+CRIS")
if resp[-1].strip().decode() == "OK":
return resp[1].strip().decode().split(":")[1].split(",")
return None

@property
def geolocation(self):
"""
Return the geolocation of the modem as measured by the Iridium constellation
and the current time based on the Iridium network timestamp.
The response is in the form:
[<x>,<y>,<z>,<timestamp>]

<x>,<y>,<z> is a geolocation grid code from an earth centered Cartesian coordinate system,
using dimensions, x, y, and z, to specify location. The coordinate system is aligned
such that the z-axis is aligned with the north and south poles, leaving the x-axis
and y-axis to lie in the plane containing the equator. The axes are aligned such that
at 0 degrees latitude and 0 degrees longitude, both y and z are zero and
x is positive (x = +6376, representing the nominal earth radius in kilometres).
Each dimension of the geolocation grid code is displayed in decimal form using
units of kilometres. Each dimension of the geolocation grid code has a minimum value
of –6376, a maximum value of +6376, and a resolution of 4.
This geolocation coordinate system is known as ECEF (acronym earth-centered, earth-fixed),
also known as ECR (initialism for earth-centered rotational)

<timestamp> is a time_struct
The timestamp is assigned by the modem when the geolocation grid code received from
the network is stored to the modem's internal memory.
The timestamp used by the modem is Iridium system time, which is a running count of
90 millisecond intervals, since Sunday May 11, 2014, at 14:23:55 UTC.
The timestamp returned by the modem is a 32-bit integer displayed in hexadecimal form.
We convert the modem's timestamp and return it as a time_struct.
"""
resp = self._uart_xfer("-MSGEO")
if resp[-1].strip().decode() == "OK":
temp = resp[1].strip().decode().split(":")[1].split(",")
ticks_since_epoch = int(temp[3], 16)
ms_since_epoch = (
ticks_since_epoch * 90
) # convert iridium ticks to milliseconds

# milliseconds to seconds
# hack to divide by 1000 and avoid using limited floating point math which throws the
# calculations off quite a bit, this should be accurate to 1 second or so
ms_str = str(ms_since_epoch)
substring = ms_str[0 : len(ms_str) - 3]
secs_since_epoch = int(substring)

# iridium epoch
iridium_epoch = time.struct_time(((2014), (5), 11, 14, 23, 55, 6, -1, -1))
iridium_epoch_unix = time.mktime(iridium_epoch)

# add timestamp's seconds to the iridium epoch
time_now_unix = iridium_epoch_unix + int(secs_since_epoch)

# convert to time struct
time_now = time.localtime(time_now_unix)

values = [
int(temp[0]),
int(temp[1]),
int(temp[2]),
time_now,
]
return values
return None

@property
def timestamp(self):
"""
Return the current date and time as given by the Iridium network
The timestamp is assigned by the modem when the geolocation grid code received from
the network is stored to the modem's internal memory.
The timestamp used by the modem is Iridium system time, which is a running count of
90 millisecond intervals, since Sunday May 11, 2014, at 14:23:55 UTC.
We convert the modem's timestamp and return it as a time_struct.
"""
resp = self._uart_xfer("-MSSTM")
if resp[-1].strip().decode() == "OK":
temp = resp[1].strip().decode().split(":")[1]
print(temp)
if temp == " no network service":
return None
ticks_since_epoch = int(temp, 16)
ms_since_epoch = (
ticks_since_epoch * 90
) # convert iridium ticks to milliseconds

# milliseconds to seconds\
# hack to divide by 1000 and avoid using limited floating point math which throws the
# calculations off quite a bit, this should be accurate to 1 second or so
ms_str = str(ms_since_epoch)
substring = ms_str[0 : len(ms_str) - 3]
secs_since_epoch = int(substring)

# iridium epoch
iridium_epoch = time.struct_time(((2014), (5), 11, 14, 23, 55, 6, -1, -1))
iridium_epoch_unix = time.mktime(iridium_epoch)

# add timestamp's seconds to the iridium epoch
time_now_unix = iridium_epoch_unix + int(secs_since_epoch)

# convert to time struct
time_now = time.localtime(time_now_unix)

return time_now
return None