What is ISBD?
ISBD stands for Iridium Short Burst Data. It is a system used by iridium.com to transfer data to and especially from its Iridium Satellite Constellation. This communication satellite system is able to send and receive data messages to isolated ground units which can be arbitrarily located anywhere on the surface of the earth.
How does it work?
Devices using the Iridium system can send data from remote locations (M.O. - Mobile Originate). These messages are moved through the satellite communications network in space and then to a ground station which is able to attempt delivery to the final application user. One way it can do this is to simply email the message. While this may be fine for isolated high-priority problem reports it is not robust for large sensor arrays. Eventually at scale, email will be indistinguishable from spam to intermediate networks. The Iridium relay service has a more efficient option which is to make direct TCP/IP socket connections to specially prepared servers waiting to receive such messages. The example program shown below is exactly this kind of server. The point of this module is to make this kind of server extremely simple to create.
Who would use this? Let’s say you threw some drifting sensors (like this) off the back of a ship in the middle of the ocean. The sensors measure ocean and air temperature, water salinity, maybe some other interesting properties. The unit has a transmitter that can send this data to the Iridium network. How does the data get from the Iridium network to the researchers who deployed the unit? The researchers can use this module to write custom parsers for Iridium SBD messages received by email or they can use it to create a custom server, as shown in the example below, which can receive and process the messages directly.
What about unpacking the payload data?
What makes these ISBD messages a bit tricky is that they’re packed very efficiently. The whole point of the ISBD system is to deliver to the end user some application specific data. This application data itself is also, usually, packed very efficiently per the end user’s specifications. Of course this module can’t anticipate what end users are going to require, but it does serve as a reasonable template for making a separate custom module that can decode compact application specific binary data messages into useful application data.
Copy this module, rename it to reflect your application, and change the unpacking scheme. Adjust all the other things that are specific to Iridium SBD, but generally, the structure and types of methods required will be quite similar.
Taming ISBD With Python
Why is Python a good way to handle this?
-
Excellent native network socket handling tools.
-
Excellent native threading tools.
-
Good at complicated objects with many properties and method requirements.
-
Good at working with databases and file systems.
-
Writing your own custom scripts is relatively easy and the resulting code is especially comprehensible and maintainable.
What kinds of tools can be created with this module?
-
A complete message receiving server. See isbdd below.
-
Message validators to check for errors.
-
Filters to find specific data in archived messages.
-
Utilities to build SQL databases from archived messages.
-
Client simulations to test servers using archived or synthesized messages.
Dependencies
The isbd module requires no exotic dependencies. It depends on only standard Python modules which should be universally available.
-
os
-
datetime
-
struct
-
binascii
-
socket *
-
MySQLdb *
*Optional
License
The module’s license is LGPL V.3.
Download
Here is the complete module: isbd.py (20kB)
It’s short. You can just read through it.
Usage
Simply copy the module (it’s a single file, isbd.py
) in the
directory with the program that will use it (or do exotic things with
PYTHONPATH as you like) and import isbd
. With this successfully
imported into your Python program, you have access to all of the
features of the Isbdmsg
class.
Isbdmsg attributes
The Isbdmsg class represents the data of the received message. Class methods allow the original data to be unpacked, queried, checked, output, archived, and anything else that may relate to the ISBD message handling. Class attributes allow access to any component of the message in isolation.
|
Entire binary data blob as received. |
|
Should always equal 1. |
|
Number of bytes sent by ISBD. |
|
Should always equal 1. |
|
Should always equal 28. |
|
Call Detail Record Reference. An automatic ID number. |
|
IMEI Unit Identification. |
|
Session status - 0=success; 1&2 also mostly ok.; 10,12-15=problem |
|
Mobile Originated Message Sequence Number. |
|
Mobile Terminated Message Sequence Number. Should equal 0. |
|
Time Iridium sees msg (not arrival or unit generated). Seconds since epoch. |
|
Should always equal 3. |
|
Should always be 11. |
|
Location orientation code (0=N,E; 1=N,W; 2=S,E; 3=S,W). |
|
Latitude - degree part. |
|
Latitude - minute part. |
|
Longitude - degree part. |
|
Longitude - minute part. |
|
Circular Error Probable (CEP) Radius. |
|
Start of Payload IEI type. Should always equal 2. |
|
Length of this payload |
|
The actual message sent from the unit, bit for bit. |
|
Printable hex string of payload. |
Isbdmsg methods
The following functions can be called from Isbdmsg objects.
-
Isbdmsg.load(data)
- A separate function so that users can create empty objects and load them (or reload them) with different data by calling only this function. With an argument of None this should also clear the data from a full object. -
Isbdmsg.unpack()
- Extract all possible ISBD message header data fields from the binary ISBD message blob received. If the data blob is not present (inself.entire_isbd_msg
), then all of the fields will get a value of None. -
Isbdmsg.pack()
- In theory, this should be similar to unpack using thestruct.pack()
function. In practice, each field will have to be checked to make sure it is ready and many fields will have to be converted to the proper type. This would be useful for synthesizing new binary messages suitable for testing as well as for editing existing ones. (Not yet implemented.) -
Isbdmsg.__repr__()
- Normal Python representation string. Allows objects to be printed in a reasonably sensible way. -
Isbdmsg.html()
- HTMLized string representation of message object. Useful for showing the data on web pages and indicative of the kind of custom output features possible. -
Isbdmsg.parts_for_output()
- Returns a list of (label,value) tuples of suitable output components. Applicable to various output modes. -
Isbdmsg.errors_check_msg()
- Checks issues with the entire message itself. Returns errors found. Or None, i.e. no errors, if good. -
Isbdmsg.errors_check_parts()
- Some things should reliably be constant values in all messages. It may not be catastrophic if not, but it might be worth a log entry. Returns errors found. Or None, i.e. no errors, if good. -
Isbdmsg.log_entry()
- Return a string suitable for putting in a log file for a server. The server should add the timestamp. -
Isbdmsg.execute_mysql(con,sql)
- Connect to a MySQL/MariaDB database using the supplied connection parameters and execute the SQL.con
should be a dictionary similar to this:{'host':'sql.xed.ch', 'user':'xedtester', 'passwd':pw, 'db':'isbd_msg'}
-
Isbdmsg.insert_in_mysql(con)
- Do an SQL "INSERT" to add this ISBD message as a record. -
Isbdmsg.insert_in_pgsql(con)
- Connect to a PostgreSQL database using the supplied connection parameters and "INSERT" this message as a record. (Not yet implemented.) -
Isbdmsg.read_sbd_file(filename)
- Load data a message blob from a file system file and unpack so that the object is ready to use with the file’s contents. This could be useful to load archived received message files for replay testing or later analysis. -
Isbdmsg.write_sbd_file(filename)
- Write this binary message blob to a file. This could be useful for writing messages received by a socket server or some other kind of acquisition mechanism or it could be used to create message files of synthesized data. -
Isbdmsg.dated_filename(basedir,bonus="")
- This will create a filename sensible for storing received messages. It will require a top level base directory, something like "/home/isbd/data/received". It will figure out a date-based subdirectory from the message metadata, not actual arrival time, allowing same day messages to be grouped properly. "/home/isbd/data/received/20161106". Note this is just a name; the directory is checked for existence on write_sbd_file(). The file name will start with the IMEI and full date. An optional bonus string (not integer) can be supplied to further identify the file; this is ideal for sending'-%06d'%mno
where mno is an integer of the server’s message number. Example call and returned value.M.dated_filename('/home/isbd/newmessages','-%06d'%369) /home/isbd/newmessages/20161105/20161105180202-300234063250980-000371.sbd
-
Isbdmsg.timestamp_fmt(style="log")
- Useful for using the time stamp found inself.msg_timestamp
in a more practical way. The format ofmsg_timestamp
is in seconds since the Unix epoch. This function returns a string that is more human readable. Style options are log, justdate, iso8601, mysql. -
Isbdmsg.location_fmt(style="human")
- Return a formatted string of the message location. Useful for using geographic coordinates in different applications. Styles can cover all different types of fashionable lat/long formats. See ISO-6709 for example. This function returns a string that is more human readable and/or more useful with other software. Style options are svg, lat, lon, iso6709, google, log, human. -
Isbdmsg.send_as_socket_client(con)
- Useful for testing servers. A testing program could be written that goes through a set of archived files and simulates the Iridium network using them as sample messages. Or a program could be written that creates synthetic test messages and sends them at programmed intervals. Needs connection information taking the form of a tuple of host and port.con= ('isbdserver.example.edu',10800)
Typical module use
Let’s examine the parts of the server example that specifically use
the module. Start by creating an Isbdmsg
object. In this single
line, the object is filled with data
and then unpacked into its
parts.
M= isbd.Isbdmsg(data).unpack()
The next line uses a utility function to generate a sensible name for a file that will contain this particular message and then the message is written to that file.
M.write_sbd_file( M.dated_filename(OUTDIR,'-%06d'%mno) )
In addition to writing the message to a file, it’s parts are also inserted into a MySQL database. Other database’s engines are possible instead or additionally.
M.insert_in_mysql(my_DB_connection_info)
Finally compose a log entry for this particular message and send it to the logging function which you can write however you like (adding time stamps, etc).
log( 'RECV:%s/%d,%s'%(ip[0],ip[1],M.log_entry()) )
A Complete Iridium SBD Message Server
The most obvious use of the isbd
module is to create a server that
can receive messages sent by the Iridium network’s ground station
relay clients. This server is a perfect example of how to use the
module and how effective it can be. To demonstrate how simple this can
be I will list the entire source code, less than 75 lines. The server,
called isbdd for ISBD Daemon, is complete and ready to receive
messages. It is multi-threaded and can handle thousands of connections
a minute, perhaps a lot more.
#!/usr/bin/python # isbdd - Iridium Short Burst Data Server # Chris X Edwards <isbd@xed.ch> - 2016-11-06 # Usage: # isbdd | tee ${SOMELOGFILE} # What PID is listening? # lsof -i :10800 import isbd import socket from thread import * import datetime OUTDIR= '/home/isbd/data/received/' # Obviously set this to make sense. PORT= 10800 # 10800 is the normal port, but can be changed for dev testing. HOST= '' # Server interface to bind to. Blank is `INADDR_ANY`. BACKLOG= 20 # Max connections on accept queue. See notes. LOGQ= list() # Need a global queue so fast log posts don't garble each other. def log(m): '''Log uses real "now" (arrival) time, not ISBD message or payload time.''' ts= datetime.datetime.now().strftime('%Y%m%dT%H%M%SZ') # ISO8601 LOGQ.append( ts+':'+m ) # Append is a thread-safe operation. # == Connection Handling For Each Thread == def servicethread(connection,mno,ip): READ_BYTES= 2048 # Optimize per application. data= '' while True: rdata= connection.recv(READ_BYTES) data+= rdata # This may not be necessary, but allows for huge messages. #print 'Connection said: %s' % data if not rdata: break M= isbd.Isbdmsg(data).unpack() M.write_sbd_file( M.dated_filename(OUTDIR,'-%06d'%mno) ) M.insert_in_mysql({'host':'sql.xed.ch','user':'xedtester','passwd':'xxxxxxxxx','db':'isbd_msg'}) #M.insert_in_pgsql(con): log( 'RECV:%s/%d,%s'%(ip[0],ip[1],M.log_entry()) ) connection.close() # == Create Socket == s= socket.socket(socket.AF_INET, socket.SOCK_STREAM) # I.E. (IPv4,UDP) log( 'Socket Creation OK' ) # == Binding == try: # This line is to prevent "Bind failed! Address already in use (error #98)" # Which occurs if you restart the server too quickly. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST,PORT)) except socket.error as msg: log( 'ERROR: Bind failed! %s (error #%s)' % (msg[1],str(msg[0])) ) s.close() raise SystemExit log('Socket Binding OK') # == Listening == s.listen(BACKLOG) log('Socket Listening on %d OK' % PORT) # == Client Transaction == msg_num= int(0) while True: while len(LOGQ): # Clear out queue log, i.e. don't write log concurrently. print LOGQ.pop(0) try: msg_num= (msg_num+1)%1e6 # Reset to 0 after a million. conn,addr= s.accept() #log( 'Connected to %s:%d' % addr ) start_new_thread(servicethread, (conn,msg_num,addr) ) except KeyboardInterrupt: break s.close() log('Socket Closed OK')
SBD Message Details
Byte | Function | Byte Within Segment |
---|---|---|
0 |
version (should always = 1) |
|
1 |
total message length |
|
2 |
MO Header IEI (should always = 1) |
|
3 |
MO DirectIP head length (should always = 28) |
|
4 |
CDR Reference (an automatic ID number) |
(1,2,3,4) |
5 |
IMEI First byte of a 15 byte sequence. |
(5) |
… |
||
19 |
15th and last byte of the IMEI sequence. |
(19) |
20 |
Session status - 0=success, 1&2 also mostly ok. 10,12-15= problem |
(20) |
21 |
MOMSN - Message Originate Message Sequence Number |
(21,22) |
22 |
MTMSN - Not message terminated so… (should always = 0) |
(23,24) |
23 |
Time of session in epoch time (4 bytes) |
(25,26,27,28) |
24 |
Start of Payload IEI type - MO Location Information IEI (should always = 3) |
|
25 |
Length of this payload in 2 byte unsigned short (should always = 11) |
|
26 |
Location orientation code (0=N,E; 1=N,W; 2=S,E; 3=S,W) |
(1) |
27 |
Latitude - degree part |
(2) |
28 |
Lat. minute part (2 bytes) |
(3,4) |
29 |
Longitude - degree part |
(5) |
30 |
Lon. minutes part (2 bytes) |
(6,7) |
31 |
CEP Radius (4 bytes) |
(8,9,10,11) |
32 |
Start of Payload IEI type - MO payload IEI (actual) (should always = 2) |
|
33 |
Length of this payload (2 bytes) |
|
34 |
DATA First byte |
|
… |
||
N |
DATA Last byte, where N is 33 plus the value of part 33 (i.e. payload length) |
$ xxd my_msg.sbd
00000000: 0100 4501 001c 8317 5a9e 3330 3032 3339 ..E.....Z.300234
00000010: 3939 3939 3939 3939 3900 2d85 0000 56cf 062959960.-...V.
00000020: aacc 0300 0b01 20c2 6375 3265 0000 0000 ...... .cu2e....
00000030: 0200 1500 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 ........
Byte | Value | Parts | Unpack Code | Explanation |
---|---|---|---|---|
1 |
01 |
c |
0 |
Obligatory "1" - version |
2 |
00 |
H |
1 |
2nd+3rd = unsigned short value |
3 |
45 |
. |
1 |
In this case it looks like x45 = 69d (total message length) |
4 |
01 |
c |
2 |
MO Header Information Element Identifier (IEI) |
5 |
00 |
H |
3 |
with next byte is unsigned short for length |
6 |
1c |
. |
3 |
0x1c is 28 dec, always 28 more bytes in the MO DirectIP header |
7 |
83 |
I |
4 |
CDR Reference (Auto ID number) 0-4294967295 |
8 |
17 |
. |
4 |
|
9 |
5a |
. |
4 |
|
10 |
9e |
. |
4 |
10000011 00010111 01011010 10011110 = 2 199 345 822 |
11 |
33 |
c |
5 |
"3" Start 15 bytes of IMEI char data |
12 |
30 |
c |
6 |
"0" |
13 |
30 |
c |
7 |
"0" |
14 |
32 |
c |
8 |
"2" |
15 |
33 |
c |
9 |
"3" |
16 |
39 |
c |
10 |
"9" |
17 |
39 |
c |
11 |
"9" |
18 |
39 |
c |
12 |
"9" |
19 |
39 |
c |
13 |
"9" |
20 |
39 |
c |
14 |
"9" |
21 |
39 |
c |
15 |
"9" |
22 |
39 |
c |
16 |
"9" |
23 |
39 |
c |
17 |
"9" |
24 |
39 |
c |
18 |
"9" |
25 |
39 |
c |
19 |
"9" -→ This example is "300239999999999". |
26 |
00 |
c |
20 |
Session status - 0=success, 1&2 also mostly ok. 10,12-15= problem |
27 |
2d |
H |
21 |
MOMSN - Message Sequence Number 00101101 = 45 dec |
28 |
85 |
. |
21 |
Total=11653 dec 10000101 = 133 dec |
29 |
00 |
H |
22 |
MTMSN - This is not an MT, so no MT Message Sequence Number |
30 |
00 |
. |
22 |
MTMSN - This is not an MT, so no MT Message Sequence Number |
31 |
56 |
I |
23 |
01010110 Time of session 4 bytes of unsigned int epoch time |
32 |
cf |
. |
23 |
11001111 Decimal total = 1456450252 |
33 |
aa |
. |
23 |
10101010 $(date --date="1456450252") = |
34 |
cc |
. |
23 |
11001100 "Thu Feb 25 17:30:52 PST 2016" Correct! |
35 |
03 |
c |
24 |
Start of Payload IEI type - MO Location Information IEI is code 3 |
36 |
00 |
H |
25 |
Length of this payload 2 byte unsigned short |
37 |
0b |
. |
25 |
0x0b = 11 bytes of this location data to follow. |
38 |
01 |
c |
26 |
Start 7 byte Location (0=N,E; 1=N,W; 2=S,E; 3=S,W) |
39 |
20 |
c |
27 |
Latitude in degrees (0x00=0, 0x5a=90) This is 32 degrees |
40 |
c2 |
H |
28 |
Lat. Minutes MSB \ Range is 0 - 59999 0x0000 - 0xea5f |
41 |
63 |
. |
28 |
Lat. Minutes LSB / Where 59999 is 59.999 min. 0xc263= 49.763 |
42 |
75 |
c |
29 |
Longitude in degrees (0x00=0, 0xb4=180) This is 117 degrees |
43 |
32 |
H |
30 |
Lon. Min. MSB \ Range 0 - 59.999 -→ N32d49.763', W117d12.901' |
44 |
65 |
. |
30 |
Lon. Min. LSB / 0x3265= 12.901 -→ Clairemont Mesa & Regents Rd |
45 |
00 |
I |
31 |
Start 4 byte unsigned int of CEP radius |
46 |
00 |
. |
31 |
radius in km around the "center point" |
47 |
00 |
. |
31 |
I think this is just a way to say 8 of 10 times it’ll be |
48 |
00 |
. |
31 |
no farther from the distance specified. Not used apparently. |
49 |
02 |
c |
32 |
Start of Payload IEI type - MO Payload IEI (actual) is code 2 |
50 |
00 |
H |
33 |
Length of this payload 2 byte unsigned short |
51 |
15 |
. |
33 |
0x15 = 21 bytes of this payload data to follow. |
52 |
00 |
c |
34 |
data 0- |
53 |
00 |
c |
35 |
data 8- |
54 |
00 |
c |
36 |
data 16- |
55 |
00 |
c |
37 |
data 24- |
56 |
00 |
c |
38 |
data 32- |
57 |
00 |
c |
39 |
data 40- |
58 |
00 |
c |
40 |
data 48- |
59 |
00 |
c |
41 |
data 56- |
60 |
00 |
c |
42 |
data 64- |
61 |
00 |
c |
43 |
data 72- |
62 |
00 |
c |
44 |
data 80- |
63 |
00 |
c |
45 |
data 88- |
64 |
00 |
c |
46 |
data 96- |
65 |
00 |
c |
47 |
data 104- |
66 |
00 |
c |
48 |
data 112- |
67 |
00 |
c |
49 |
data 120- |
68 |
00 |
c |
50 |
data 128- |
69 |
00 |
c |
51 |
data 136- |
70 |
00 |
c |
52 |
data 144- |
71 |
00 |
c |
53 |
data 152- |
72 |
00 |
c |
54 |
data 160- |
-
> = Byte order (big endian I think)
-
c = char 1 byte 0-255 (256 values)
-
H = unsigned short 2 byte 0-65535 (65,536 values)
-
I = unsigned int 4 bytes 0-4294967295 (4,294,967,296 values)
-
. = In the table above this shows another byte being used for the previous field.
These need to match:
packformat= '>cHcHIccccccccccccccccHHIcHccHcHIcH' + 'c'*(l-51)
l=list(packformat); print l.count('H')*2 + l.count('I')*4 + l.count('c')
print len(self.entire_isbd_msg)
Acronyms
CEP |
Circular Error Probable |
DR |
Call Detail Record |
CRC |
Cyclical Redundancy Check |
CDR |
Call Detail Record |
DB |
Database |
DSC |
Delivery Short Code |
DSD |
Data Set Download (aka DSDR) |
DSDR |
Data Set Download Response (aka DR) |
DSS |
Diagnostic System Services |
DTE |
Data Terminal Equipment |
ECS |
ETC Communications Sub-system |
ETC |
Earth Terminal Controller (ETC consists of ECS, ETS & ESS) |
ETS |
ETC Transmission Subsystem |
FA |
Field Application |
GBS |
Gateway Billing Subsystem |
GEO |
Geographical (as used in ‘geographical location’) |
GIE |
Gateway Infrastructure Equipment |
GSM |
Global System for Mobile Communication |
GSS |
Gateway SBD Subsystem |
GW |
Gateway |
IE |
Information Element |
IEI |
Information Element Identifier |
IMEI |
International Mobile Equipment Identifier |
IP |
Internet Protocol |
ISU |
Iridium Subscriber Unit (NAL Research’s modems and trackers) |
LBT |
L-Band Transceiver |
MO |
Mobile Originated - I think this is us, sensors to base |
MOM |
Mobile Originated Message |
MOMSN |
Mobile Originated Message Sequence Number |
MT |
Mobile Terminated |
MTM |
Mobile Terminated Message |
MTMSN |
Mobile Terminated Message Sequence Number |
SBD |
Short Burst Data |
SEP |
Short Burst Data ETC Processor |
SPNet |
Iridium Service Provider Network Provisioning Tool |
SPP |
Short Burst Data Post Processor |
VA |
Vendor Application |
Alternatives
Don’t like my way of doing things? As I discover them, I’ll list other projects that provide similar functionality.
-
https://github.com/gadomski/sbd - Pete Gadomski’s
sbd
system -
https://gadomski.github.io/sbd-rs/sbd/index.html - Similar in Rust
Known Errors
I found this error which needs to be solved. I suspect unpacking returning none causes this.
Unhandled exception in thread started by <function servicethread at 0x7ffcd30be9b0>
Traceback (most recent call last):
File "./isbdd", line 55, in servicethread
M= isbd.Isbdmsg(data).unpack()
File "/export/home/isbd/isbd_server/isbd.py", line 23, in __init__
self.load(data)
File "/export/home/isbd/isbd_server/isbd.py", line 31, in load
self.unpack()
File "/export/home/isbd/isbd_server/isbd.py", line 43, in unpack
m= list(struct.unpack(packformat,self.entire_isbd_msg)) # m is message component list.
struct.error: unpack requires a string argument of length 51
Copyright © 2016 - Chris X Edwards