diff options
author | Magnus Feuer <mfeuer@jaguarlandrover.com> | 2015-04-14 08:26:38 -0700 |
---|---|---|
committer | Magnus Feuer <mfeuer@jaguarlandrover.com> | 2015-04-17 14:01:27 -0700 |
commit | b6430f5418653a2c7680217ef692613dde6f600f (patch) | |
tree | 79264d2bfd8e2d08ef62b7dd5b226b51e9574b87 /deps/gsms | |
parent | 682c3666ca5297157ccd76183a2d586ac72ffb69 (diff) | |
download | rvi_core-b6430f5418653a2c7680217ef692613dde6f600f.tar.gz |
Initial commit of deps
Diffstat (limited to 'deps/gsms')
-rw-r--r-- | deps/gsms/LICENSE | 24 | ||||
-rw-r--r-- | deps/gsms/README.md | 32 | ||||
-rw-r--r-- | deps/gsms/include/gsms.hrl | 144 | ||||
-rw-r--r-- | deps/gsms/rebar.config | 9 | ||||
-rw-r--r-- | deps/gsms/src/ERRORCODES | 30 | ||||
-rw-r--r-- | deps/gsms/src/README | 141 | ||||
-rw-r--r-- | deps/gsms/src/gsms.app.src | 9 | ||||
-rw-r--r-- | deps/gsms/src/gsms.erl | 110 | ||||
-rw-r--r-- | deps/gsms/src/gsms_0338.erl | 370 | ||||
-rw-r--r-- | deps/gsms/src/gsms_0705.erl | 972 | ||||
-rw-r--r-- | deps/gsms/src/gsms_app.erl | 83 | ||||
-rw-r--r-- | deps/gsms/src/gsms_codec.erl | 1067 | ||||
-rw-r--r-- | deps/gsms/src/gsms_if_sup.erl | 78 | ||||
-rw-r--r-- | deps/gsms/src/gsms_router.erl | 464 | ||||
-rw-r--r-- | deps/gsms/src/gsms_sup.erl | 68 | ||||
-rw-r--r-- | deps/gsms/src/gsms_uart.erl | 834 | ||||
-rw-r--r-- | deps/gsms/src/log.hrl | 44 | ||||
-rwxr-xr-x | deps/gsms/src/sim900 | 80 | ||||
-rwxr-xr-x | deps/gsms/src/sim900_ft1075k | 75 | ||||
-rw-r--r-- | deps/gsms/sys.config | 47 | ||||
-rw-r--r-- | deps/gsms/tetrapak/config.ini | 13 |
21 files changed, 4694 insertions, 0 deletions
diff --git a/deps/gsms/LICENSE b/deps/gsms/LICENSE new file mode 100644 index 0000000..68ed063 --- /dev/null +++ b/deps/gsms/LICENSE @@ -0,0 +1,24 @@ +Copyright (C) 2007 - 2012, Rogvall Invest AB, <tony@rogvall.se> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Except as contained in this notice, the name(s) of the above copyright holders +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization. + diff --git a/deps/gsms/README.md b/deps/gsms/README.md new file mode 100644 index 0000000..9fb01ab --- /dev/null +++ b/deps/gsms/README.md @@ -0,0 +1,32 @@ + +# Start + +# Send SMS + + gsms_router:send([{addr,"<number>"}], "Hello"). + +# Receive SMS + + {ok,Ref} = gsms_router:subscribe([]). + Pdu = receive {gsms,Ref,Pdu1} -> Pdu1 end. + Text = Pdu#gsms_deliver_pdu.ud. + + +# Raspberry pi setup + +Setup internal serial port /dev/ttyAMA0 for use as +modem or other serial com line then ttyAMA0 must be +disable in /etc/inittab + + # comment out in /etc/inittab + # T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100 + +Also remove rerences to ttyAMA0 in /boot/cmdline.txt +If cmdline looks like + + dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait + +After change it should look like + + dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait + diff --git a/deps/gsms/include/gsms.hrl b/deps/gsms/include/gsms.hrl new file mode 100644 index 0000000..72b0dea --- /dev/null +++ b/deps/gsms/include/gsms.hrl @@ -0,0 +1,144 @@ +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2012, Tony Rogvall +%%% @doc +%%% sms pdu format (ETSI 03.40) +%%% @end +%% ref: http://www.dreamfabric.com/sms/vp.html +%%% Created : 15 Oct 2012 by Tony Rogvall <tony@rogvall.se> + +-ifndef(__GSMS_HRL__). +-define(__GSMS_HRL__, true). + +-record(gsms_addr, + { + type = unknown, + addr = "" + }). + +-type dcs_type() :: message|message_waiting|data. +-type dcs_compression() :: compressed | uncompressed. +-type dcs_alphabet() :: default|ucs2|octet. +-type dcs_class() :: alert|me|sim|te. +-type dcs_store() :: store | discard. +-type dcs_active() :: active | inactive. +-type dcs_wait_type() :: voicemail | fax | email | other. + +-record(gsms_dcs, + { + type = message :: dcs_type(), + compression = uncompressed :: dcs_compression(), + alphabet = default :: dcs_alphabet(), + class = alert :: dcs_class(), + store = store :: dcs_store(), + active = inactive :: dcs_active(), %% type = message_waiting + wait_type = other :: dcs_wait_type() %% type = message_waiting + }). + +-define(IE_CONCAT8, 16#00). +-define(IE_PORT8, 16#04). +-define(IE_PORT16, 16#05). +-define(IE_CONCAT16, 16#08). + +-define(MAX_7BIT_LEN, 160). %% 153 with concat8 header +-define(MAX_8BIT_LEN, 140). %% 134 with concat8 header +-define(MAX_16BIT_LEN, 70). %% 67 with concat8 header + + +-define(VP_RELATIVE, (60*60*24)). %% 1 day +-define(DEFAULT_PID, 0). + +-define(MTI_SMS_DELIVER, 2#00). + +-type uint() :: non_neg_integer(). +-type uint8() :: 0..255. +-type uint16() :: 0..65535. + +-type gsms_ie() :: + {concat,Ref::uint(),N::uint8(),I::uint8()} | + {concat16,Ref::uint16(),N::uint8(),I::uint8()} | + {concat8,Ref::uint8(),N::uint8(),I::uint8()} | + {port8, Dst::uint8(),Src::uint8()} | + {port16,Dst::uint16(),Src::uint16()} + . + +-record(gsms_deliver_pdu, { + smsc, %% :: #gsms_addr{} smsc information + rp=false, %% :1 reply path exists + udhi=false, %% :1 user data header exists + sri=false, %% :1 status report indication + res1=0, %% 0:1 + res2=0, %% 0:1 + mms=false, %% :1 more messages to send + addr, %% :: #gsms_addr{} + pid=?DEFAULT_PID, %% protocol identifier + dcs=#gsms_dcs{}, %% data coding scheme + scts, %% :7/binary + udh=[] :: [gsms_ie()], %% user data header + udl, %% length in septets/octets (depend on dcs) + ud + }). + +-define(MTI_SMS_SUBMIT, 2#01). + +-record(gsms_submit_pdu, { + smsc, %% ::gsms_addr{} smsc information + rp=false, %% :1 reply path exists + udhi=false, %% :1 user data header exists + srr=false, %% :1 status report request + vpf=relative, %% :2 validity periad format 0..3 + rd=true, %% :1 reject duplicates + mref=0, %% :8 + addr, %% ::gsms_addr{} + pid=?DEFAULT_PID, %% protocol identifier + dcs=#gsms_dcs{}, %% data coding scheme + vp=?VP_RELATIVE, %% vary depend on vpf + udh=[] :: [gsms_ie()], %% user data header + udl, + ud + }). + +-type gsms_addr() :: #gsms_addr{} | string(). +-type gsms_pid() :: uint8(). +-type gsms_port() :: uint16(). +-type gsms_vpf() :: none|relative|enhanced|absolute. + +-type gsms_pdu_option() :: + {smsc, gsms_addr()} | + {rp, boolean()} | + {udhi, boolean()} | + {udh, [gsms_ie()]} | + {srr, boolean()} | + {mref, uint8()} | + {vpf, gsms_vpf()} | + {vp, term()} | %% fixme + {addr, gsms_addr()} | + {pid, gsms_pid()} | + {type, dcs_type()} | + {class, dcs_class()} | + {alphabet, dcs_alphabet() } | + {compression, dcs_compression() } | + {store, dcs_store()} | + {active, dcs_active()} | + {wait_type, dcs_wait_type()} | + {dcs, #gsms_dcs{}} + . + +-type filter() :: + [filter()] | + {type, dcs_type()} | + {class, dcs_class()} | + {alphabet, dcs_alphabet()} | + {pid, gsms_pid()} | + {src, gsms_port()} | + {dst, gsms_port()} | + {anumber, gsms_addr()} | + {bnumber, gsms_addr()} | + {smsc, gsms_addr()} | + {reg_exp, re:mp() | iodata()} | + {'not', filter()} | + {'and', filter(), filter()} | + {'or', filter(), filter()}. + +-endif. + + diff --git a/deps/gsms/rebar.config b/deps/gsms/rebar.config new file mode 100644 index 0000000..5f6d3b4 --- /dev/null +++ b/deps/gsms/rebar.config @@ -0,0 +1,9 @@ +%% -*- erlang -*- +%% Config file for gsms application +{deps, [ {uart, ".*", {git, "git@github.com:tonyrog/uart.git"}}, + {lager, ".*", {git, "git://github.com/Feuerlabs/lager.git"}}]}. + + +%% {erl_opts, [debug_info, fail_on_warning, {d,debug}]}. +{erl_opts, [debug_info, fail_on_warning]}. +{sub_dirs, ["src"]}. diff --git a/deps/gsms/src/ERRORCODES b/deps/gsms/src/ERRORCODES new file mode 100644 index 0000000..0f8d184 --- /dev/null +++ b/deps/gsms/src/ERRORCODES @@ -0,0 +1,30 @@ + + ++CMS ERROR +Message service error +Description +These are the error codes for +CMS ERROR. + +Error code Meaning +0-127 GSM 04.11 Annex E-2 values +128-255 GSM 03.40 section 9.2.3.22 values +300 Phone failure +301 SMS service of phone reserved +302 Operation not allowed +303 Operation not supported +304 Invalid PDU mode parameter +305 Invalid text mode parameter +310 SIM not inserted +311 SIM PIN necessary +312 PH-SIM PIN necessary +313 SIM failure +314 SIM busy +315 SIM wrong +320 Memory failure +321 Invalid memory index +322 Memory full +330 SMSC (message service center) address unknown +331 No network service +332 Network timeout +500 Unknown error +512 Manufacturer specific diff --git a/deps/gsms/src/README b/deps/gsms/src/README new file mode 100644 index 0000000..7331707 --- /dev/null +++ b/deps/gsms/src/README @@ -0,0 +1,141 @@ +Turn off echo +============= + ATE + | + | OK + +Check if pin code is needed +=========================== + AT+CPIN? + | + | +CPIN: READY + | + | OK + +Set pin code if not ready +========================= + AT+CPIN="1234" + | ERROR + +Disable pin code when ready +=========================== + AT+CLCK="SC",0,"1234" + | + +Register in network +=================== + AT+CREG=1 + | OK + +Check SMS capability +==================== + AT+CSMS=0 + | +CSMS: 1,1,1 + | ERROR + +Write message to storage +======================== + AT+CMGW="+46702575687" + > Hej <ctl-z> + | +CMGW: 1 + +Send message from storage (1) +============================= + AT+CMSS=1 + | +CMS ERROR: 331 (network not available) + | +CMS ERROR: 303 (....) + +Service center +============== + AT+CSCA? + | +CSCA: "+46705008999",145 + +Text/Pdu mode +============= + AT+CMGF=1 text mode + AT+CMGF=0 pdu mode + +Receive SMS notification +======================== + AT+CNMI=1,1 - New incoming SMS is displayed like + + | +CMTI: "SM",<x> (x = counter) + + AT+CNMI=1,2 - Display incoming messages on arrival (unsolicited) + +List SMS +======== + AT+CMGL + | +CMGL:<x>,"REC UNREAD","+number" + | message + + AT+CMGL="ALL" + + AT+CMGL=1 (pdu mode) + | +CMGL: <x>,<y>,<str>,<len> + | <PDU> + | + +Read SMS +======== + AT+CMGR=<x> + +Delete SMS +========== + AT+CMGD=<x>(,<x>)* + + +Send SMS in PDU mode +==================== + + + AT+CMGS=23 //Send message, 23 octets (excluding the two initial zeros) +%% >0011000B916407281553F80000AA0AE8329BFD4697D9EC37<ctrl-z> +%%) +%% There are 23 octets in this message (46 'characters'). The first octet ("00") +%% doesn't count, it is only an indicator of the length of the SMSC information +%% supplied (0). +%% +%% + +Sigal quality + +AT+CSQ? ++CSQ:18,99 + +AT+CSQ + ++CSQ: 4,0 + +OK + +Value RSSI dBm Condition +2 -109 Marginal +3 -107 Marginal +4 -105 Marginal +5 -103 Marginal +6 -101 Marginal +7 -99 Marginal +8 -97 Marginal +9 -95 Marginal +10 -93 OK +11 -91 OK +12 -89 OK +13 -87 OK +14 -85 OK +15 -83 Good +16 -81 Good +17 -79 Good +18 -77 Good +19 -75 Good +20 -73 Excellent +21 -71 Excellent +22 -69 Excellent +23 -67 Excellent +24 -65 Excellent +25 -63 Excellent +26 -61 Excellent +27 -59 Excellent +28 -57 Excellent +29 -55 Excellent +30 -53 Excellent
\ No newline at end of file diff --git a/deps/gsms/src/gsms.app.src b/deps/gsms/src/gsms.app.src new file mode 100644 index 0000000..357ec77 --- /dev/null +++ b/deps/gsms/src/gsms.app.src @@ -0,0 +1,9 @@ +{application, gsms, + [ + {description, "ETSI 03.40 Sender & Reciver"}, + {vsn, git}, + {registered, [gsms_router]}, + {mod, {gsms_app,[]}}, + {env, []}, + {applications, [kernel,stdlib,uart]} + ]}. diff --git a/deps/gsms/src/gsms.erl b/deps/gsms/src/gsms.erl new file mode 100644 index 0000000..e682672 --- /dev/null +++ b/deps/gsms/src/gsms.erl @@ -0,0 +1,110 @@ +%%%---- BEGIN COPYRIGHT -------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2012, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT ---------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2013, Tony Rogvall +%%% @doc +%%% +%%% Created : 18 Apr 2013 by Tony Rogvall +%%% @end + +-module(gsms). +-include("gsms.hrl"). + +%% Interface +-export([send/2, + subscribe/1, + unsubscribe/1]). + +%% For testing +%% @private +-export([start/0, + stop/0]). + +%%%=================================================================== +%%% API +%%%=================================================================== +%%-------------------------------------------------------------------- +%% @doc +%% Send an SMS. +%% @end +%%-------------------------------------------------------------------- +-spec send(Options::list({Key::atom(), Value::term()}), Body::string()) -> + {ok, Ref::reference()} | + {error, Reason::term()}. + +send(Opts, Body) -> + gsms_router:send(Opts, Body). + +%%-------------------------------------------------------------------- +%% @doc +%% Subscribe to incoming SMS:s filtered by Filter. +%% @end +%%-------------------------------------------------------------------- +-spec subscribe(Filter::list(filter())) -> + {ok,Ref::reference()} | + {error,Reason::term()}. + +subscribe(Filter) -> + case lists:keytake(reg_exp, 1, Filter) of + {value, RegExp, Rest} when is_list(RegExp) -> + %% Convert to mp-format (and verify format) + case re:compile(RegExp, [unicode]) of + {ok, MP} -> + gsms_router:subscribe([{reg_exp, MP} | Rest]); + {error, _ErrSpec} = E-> + E + end; + {value, _RegExp, _Rest} -> + %% Assuming mp-format + gsms_router:subscribe(Filter); + false -> + gsms_router:subscribe(Filter) + end. + +%%-------------------------------------------------------------------- +%% @doc +%% Remove previous subscription refered to by Ref. +%% @end +%%-------------------------------------------------------------------- +-spec unsubscribe(Ref::reference()) -> ok. + +unsubscribe(Ref) -> + gsms_router:unsubscribe(Ref). + +%%%=================================================================== +start() -> + call([lager,ale,uart,gsms], start). + +stop() -> + call([gsms,uart,ale,lager], stop). + +call([App|Apps], F) -> + error_logger:info_msg("~p: ~p\n", [F,App]), + case application:F(App) of + {error,{not_started,App1}} -> + call([App1,App|Apps], F); + {error,{already_started,App}} -> + call(Apps, F); + ok -> + call(Apps, F); + Error -> + Error + end; +call([],_F) -> + ok. + + diff --git a/deps/gsms/src/gsms_0338.erl b/deps/gsms/src/gsms_0338.erl new file mode 100644 index 0000000..6dadd5b --- /dev/null +++ b/deps/gsms/src/gsms_0338.erl @@ -0,0 +1,370 @@ +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2012, Tony Rogvall +%%% @doc +%%% GSM 03.38 codec +%%% @end +%%% Created : 16 Oct 2012 by Tony Rogvall <tony@rogvall.se> + +-module(gsms_0338). + +-export([decode/1, decode_char/1]). +-export([encode/1, encode/2, encode_char/1]). + +decode(Bin) when is_binary(Bin) -> + decode_list(binary_to_list(Bin)); +decode(Cs) when is_list(Cs) -> + decode_list(Cs). + +%% +decode_list([16#1B,C|Cs]) -> + case C of + 16#0A -> [16#000C|decode_list(Cs)]; %% FORM FEED + 16#14 -> [16#005E|decode_list(Cs)]; %% CIRCUMFLEX ACCENT + 16#28 -> [16#007B|decode_list(Cs)]; %% LEFT CURLY BRACKET + 16#29 -> [16#007D|decode_list(Cs)]; %% RIGHT CURLY BRACKET + 16#2F -> [16#005C|decode_list(Cs)]; %% REVERSE SOLIDUS + 16#3C -> [16#005B|decode_list(Cs)]; %% LEFT SQUARE BRACKET + 16#3D -> [16#007E|decode_list(Cs)]; %% TILDE + 16#3E -> [16#005D|decode_list(Cs)]; %% RIGHT SQUARE BRACKET + 16#40 -> [16#007C|decode_list(Cs)]; %% VERTICAL LINE + 16#65 -> [16#20AC|decode_list(Cs)]; %% EURO SIGN + _ -> [16#A0|decode_list(Cs)] + end; +decode_list([C|Cs]) -> + [decode_char(C)|decode_list(Cs)]; +decode_list([]) -> + []. + +decode_char(C) -> + case C band 16#7f of + 16#00 -> 16#0040; %% COMMERCIAL AT + %%16#00 16#0000 %% NULL + 16#01 -> 16#00A3; %% POUND SIGN + 16#02 -> 16#0024; %% DOLLAR SIGN + 16#03 -> 16#00A5; %% YEN SIGN + 16#04 -> 16#00E8; %% LATIN SMALL LETTER E WITH GRAVE + 16#05 -> 16#00E9; %% LATIN SMALL LETTER E WITH ACUTE + 16#06 -> 16#00F9; %% LATIN SMALL LETTER U WITH GRAVE + 16#07 -> 16#00EC; %% LATIN SMALL LETTER I WITH GRAVE + 16#08 -> 16#00F2; %% LATIN SMALL LETTER O WITH GRAVE + 16#09 -> 16#00E7; %% LATIN SMALL LETTER C WITH CEDILLA + %%16#09 16#00C7 %% LATIN CAPITAL LETTER C WITH CEDILLA + 16#0A -> 16#000A; %% LINE FEED + 16#0B -> 16#00D8; %% LATIN CAPITAL LETTER O WITH STROKE + 16#0C -> 16#00F8; %% LATIN SMALL LETTER O WITH STROKE + 16#0D -> 16#000D; %% CARRIAGE RETURN + 16#0E -> 16#00C5; %% LATIN CAPITAL LETTER A WITH RING ABOVE + 16#0F -> 16#00E5; %% LATIN SMALL LETTER A WITH RING ABOVE + 16#10 -> 16#0394; %% GREEK CAPITAL LETTER DELTA + 16#11 -> 16#005F; %% LOW LINE + 16#12 -> 16#03A6; %% GREEK CAPITAL LETTER PHI + 16#13 -> 16#0393; %% GREEK CAPITAL LETTER GAMMA + 16#14 -> 16#039B; %% GREEK CAPITAL LETTER LAMDA + 16#15 -> 16#03A9; %% GREEK CAPITAL LETTER OMEGA + 16#16 -> 16#03A0; %% GREEK CAPITAL LETTER PI + 16#17 -> 16#03A8; %% GREEK CAPITAL LETTER PSI + 16#18 -> 16#03A3; %% GREEK CAPITAL LETTER SIGMA + 16#19 -> 16#0398; %% GREEK CAPITAL LETTER THETA + 16#1A -> 16#039E; %% GREEK CAPITAL LETTER XI + 16#1C -> 16#00C6; %% LATIN CAPITAL LETTER AE + 16#1D -> 16#00E6; %% LATIN SMALL LETTER AE + 16#1E -> 16#00DF; %% LATIN SMALL LETTER SHARP S (German) + 16#1F -> 16#00C9; %% LATIN CAPITAL LETTER E WITH ACUTE + 16#20 -> 16#0020; %% SPACE + 16#21 -> 16#0021; %% EXCLAMATION MARK + 16#22 -> 16#0022; %% QUOTATION MARK + 16#23 -> 16#0023; %% NUMBER SIGN + 16#24 -> 16#00A4; %% CURRENCY SIGN + 16#25 -> 16#0025; %% PERCENT SIGN + 16#26 -> 16#0026; %% AMPERSAND + 16#27 -> 16#0027; %% APOSTROPHE + 16#28 -> 16#0028; %% LEFT PARENTHESIS + 16#29 -> 16#0029; %% RIGHT PARENTHESIS + 16#2A -> 16#002A; %% ASTERISK + 16#2B -> 16#002B; %% PLUS SIGN + 16#2C -> 16#002C; %% COMMA + 16#2D -> 16#002D; %% HYPHEN-MINUS + 16#2E -> 16#002E; %% FULL STOP + 16#2F -> 16#002F; %% SOLIDUS + 16#30 -> 16#0030; %% DIGIT ZERO + 16#31 -> 16#0031; %% DIGIT ONE + 16#32 -> 16#0032; %% DIGIT TWO + 16#33 -> 16#0033; %% DIGIT THREE + 16#34 -> 16#0034; %% DIGIT FOUR + 16#35 -> 16#0035; %% DIGIT FIVE + 16#36 -> 16#0036; %% DIGIT SIX + 16#37 -> 16#0037; %% DIGIT SEVEN + 16#38 -> 16#0038; %% DIGIT EIGHT + 16#39 -> 16#0039; %% DIGIT NINE + 16#3A -> 16#003A; %% COLON + 16#3B -> 16#003B; %% SEMICOLON + 16#3C -> 16#003C; %% LESS-THAN SIGN + 16#3D -> 16#003D; %% EQUALS SIGN + 16#3E -> 16#003E; %% GREATER-THAN SIGN + 16#3F -> 16#003F; %% QUESTION MARK + 16#40 -> 16#00A1; %% INVERTED EXCLAMATION MARK + 16#41 -> 16#0041; %% LATIN CAPITAL LETTER A + %%16#41 16#0391 %% GREEK CAPITAL LETTER ALPHA + 16#42 -> 16#0042; %% LATIN CAPITAL LETTER B + %%16#42 16#0392 %% GREEK CAPITAL LETTER BETA + 16#43 -> 16#0043; %% LATIN CAPITAL LETTER C + 16#44 -> 16#0044; %% LATIN CAPITAL LETTER D + 16#45 -> 16#0045; %% LATIN CAPITAL LETTER E + %%16#45 16#0395 %% GREEK CAPITAL LETTER EPSILON + 16#46 -> 16#0046; %% LATIN CAPITAL LETTER F + 16#47 -> 16#0047; %% LATIN CAPITAL LETTER G + 16#48 -> 16#0048; %% LATIN CAPITAL LETTER H + %%16#48 16#0397 %% GREEK CAPITAL LETTER ETA + 16#49 -> 16#0049; %% LATIN CAPITAL LETTER I + %%16#49 16#0399 %% GREEK CAPITAL LETTER IOTA + 16#4A -> 16#004A; %% LATIN CAPITAL LETTER J + 16#4B -> 16#004B; %% LATIN CAPITAL LETTER K + %%16#4B 16#039A %% GREEK CAPITAL LETTER KAPPA + 16#4C -> 16#004C; %% LATIN CAPITAL LETTER L + 16#4D -> 16#004D; %% LATIN CAPITAL LETTER M + %%16#4D 16#039C %% GREEK CAPITAL LETTER MU + 16#4E -> 16#004E; %% LATIN CAPITAL LETTER N + %%16#4E 16#039D %% GREEK CAPITAL LETTER NU + 16#4F -> 16#004F; %% LATIN CAPITAL LETTER O + %%16#4F 16#039F %% GREEK CAPITAL LETTER OMICRON + 16#50 -> 16#0050; %% LATIN CAPITAL LETTER P + %%16#50 16#03A1 %% GREEK CAPITAL LETTER RHO + 16#51 -> 16#0051; %% LATIN CAPITAL LETTER Q + 16#52 -> 16#0052; %% LATIN CAPITAL LETTER R + 16#53 -> 16#0053; %% LATIN CAPITAL LETTER S + 16#54 -> 16#0054; %% LATIN CAPITAL LETTER T + %%16#54 16#03A4 %% GREEK CAPITAL LETTER TAU + 16#55 -> 16#0055; %% LATIN CAPITAL LETTER U + 16#56 -> 16#0056; %% LATIN CAPITAL LETTER V + 16#57 -> 16#0057; %% LATIN CAPITAL LETTER W + 16#58 -> 16#0058; %% LATIN CAPITAL LETTER X + %%16#58 16#03A7 %% GREEK CAPITAL LETTER CHI + 16#59 -> 16#0059; %% LATIN CAPITAL LETTER Y + %%16#59 16#03A5 %% GREEK CAPITAL LETTER UPSILON + 16#5A -> 16#005A; %% LATIN CAPITAL LETTER Z + %%16#5A 16#0396 %% GREEK CAPITAL LETTER ZETA + 16#5B -> 16#00C4; %% LATIN CAPITAL LETTER A WITH DIAERESIS + 16#5C -> 16#00D6; %% LATIN CAPITAL LETTER O WITH DIAERESIS + 16#5D -> 16#00D1; %% LATIN CAPITAL LETTER N WITH TILDE + 16#5E -> 16#00DC; %% LATIN CAPITAL LETTER U WITH DIAERESIS + 16#5F -> 16#00A7; %% SECTION SIGN + 16#60 -> 16#00BF; %% INVERTED QUESTION MARK + 16#61 -> 16#0061; %% LATIN SMALL LETTER A + 16#62 -> 16#0062; %% LATIN SMALL LETTER B + 16#63 -> 16#0063; %% LATIN SMALL LETTER C + 16#64 -> 16#0064; %% LATIN SMALL LETTER D + 16#65 -> 16#0065; %% LATIN SMALL LETTER E + 16#66 -> 16#0066; %% LATIN SMALL LETTER F + 16#67 -> 16#0067; %% LATIN SMALL LETTER G + 16#68 -> 16#0068; %% LATIN SMALL LETTER H + 16#69 -> 16#0069; %% LATIN SMALL LETTER I + 16#6A -> 16#006A; %% LATIN SMALL LETTER J + 16#6B -> 16#006B; %% LATIN SMALL LETTER K + 16#6C -> 16#006C; %% LATIN SMALL LETTER L + 16#6D -> 16#006D; %% LATIN SMALL LETTER M + 16#6E -> 16#006E; %% LATIN SMALL LETTER N + 16#6F -> 16#006F; %% LATIN SMALL LETTER O + 16#70 -> 16#0070; %% LATIN SMALL LETTER P + 16#71 -> 16#0071; %% LATIN SMALL LETTER Q + 16#72 -> 16#0072; %% LATIN SMALL LETTER R + 16#73 -> 16#0073; %% LATIN SMALL LETTER S + 16#74 -> 16#0074; %% LATIN SMALL LETTER T + 16#75 -> 16#0075; %% LATIN SMALL LETTER U + 16#76 -> 16#0076; %% LATIN SMALL LETTER V + 16#77 -> 16#0077; %% LATIN SMALL LETTER W + 16#78 -> 16#0078; %% LATIN SMALL LETTER X + 16#79 -> 16#0079; %% LATIN SMALL LETTER Y + 16#7A -> 16#007A; %% LATIN SMALL LETTER Z + 16#7B -> 16#00E4; %% LATIN SMALL LETTER A WITH DIAERESIS + 16#7C -> 16#00F6; %% LATIN SMALL LETTER O WITH DIAERESIS + 16#7D -> 16#00F1; %% LATIN SMALL LETTER N WITH TILDE + 16#7E -> 16#00FC; %% LATIN SMALL LETTER U WITH DIAERESIS + 16#7F -> 16#00E0 %% LATIN SMALL LETTER A WITH GRAVE + end. + +encode(Data) -> + encode(Data, -1). + +encode(Bin,Max) when is_binary(Bin), is_integer(Max) -> + encode_list(binary_to_list(Bin), Max); +encode(Cs,Max) when is_list(Cs), is_integer(Max) -> + encode_list(Cs,Max). + + +encode_list(Cs,Max) -> + encode_list_(Cs,Max,[]). + +encode_list_(Cs,0,Acc) -> + {iolist_to_binary(lists:reverse(Acc)),Cs}; +encode_list_(Cs1=[C|Cs],I,Acc) -> + try encode_char(C) of + [Esc,Y] -> + if I >= 2 -> encode_list_(Cs,I-2,[Y,Esc|Acc]); + true -> {iolist_to_binary(lists:reverse(Acc)),Cs1} + end; + Y -> encode_list_(Cs,I-1,[Y|Acc]) + catch + error:_ -> + encode_list_(Cs,I-2,[16#A0,16#1B|Acc]) + end; +encode_list_([],_I,Acc) -> + {iolist_to_binary(lists:reverse(Acc)),[]}. + + +encode_char(C) -> + case C of + 16#0040 -> 16#00; %% COMMERCIAL AT + 16#0000 -> 16#00; %% NULL + 16#00A3 -> 16#01; %% POUND SIGN + 16#0024 -> 16#02; %% DOLLAR SIGN + 16#00A5 -> 16#03; %% YEN SIGN + 16#00E8 -> 16#04; %% LATIN SMALL LETTER E WITH GRAVE + 16#00E9 -> 16#05; %% LATIN SMALL LETTER E WITH ACUTE + 16#00F9 -> 16#06; %% LATIN SMALL LETTER U WITH GRAVE + 16#00EC -> 16#07; %% LATIN SMALL LETTER I WITH GRAVE + 16#00F2 -> 16#08; %% LATIN SMALL LETTER O WITH GRAVE + 16#00E7 -> 16#09; %% LATIN SMALL LETTER C WITH CEDILLA + 16#00C7 -> 16#09; %% LATIN CAPITAL LETTER C WITH CEDILLA + 16#000A -> 16#0A; %% LINE FEED + 16#00D8 -> 16#0B; %% LATIN CAPITAL LETTER O WITH STROKE + 16#00F8 -> 16#0C; %% LATIN SMALL LETTER O WITH STROKE + 16#000D -> 16#0D; %% CARRIAGE RETURN + 16#00C5 -> 16#0E; %% LATIN CAPITAL LETTER A WITH RING ABOVE + 16#00E5 -> 16#0F; %% LATIN SMALL LETTER A WITH RING ABOVE + 16#0394 -> 16#10; %% GREEK CAPITAL LETTER DELTA + 16#005F -> 16#11; %% LOW LINE + 16#03A6 -> 16#12; %% GREEK CAPITAL LETTER PHI + 16#0393 -> 16#13; %% GREEK CAPITAL LETTER GAMMA + 16#039B -> 16#14; %% GREEK CAPITAL LETTER LAMDA + 16#03A9 -> 16#15; %% GREEK CAPITAL LETTER OMEGA + 16#03A0 -> 16#16; %% GREEK CAPITAL LETTER PI + 16#03A8 -> 16#17; %% GREEK CAPITAL LETTER PSI + 16#03A3 -> 16#18; %% GREEK CAPITAL LETTER SIGMA + 16#0398 -> 16#19; %% GREEK CAPITAL LETTER THETA + 16#039E -> 16#1A; %% GREEK CAPITAL LETTER XI + 16#00C6 -> 16#1C; %% LATIN CAPITAL LETTER AE + 16#00E6 -> 16#1D; %% LATIN SMALL LETTER AE + 16#00DF -> 16#1E; %% LATIN SMALL LETTER SHARP S (German) + 16#00C9 -> 16#1F; %% LATIN CAPITAL LETTER E WITH ACUTE + 16#0020 -> 16#20; %% SPACE + 16#0021 -> 16#21; %% EXCLAMATION MARK + 16#0022 -> 16#22; %% QUOTATION MARK + 16#0023 -> 16#23; %% NUMBER SIGN + 16#00A4 -> 16#24; %% CURRENCY SIGN + 16#0025 -> 16#25; %% PERCENT SIGN + 16#0026 -> 16#26; %% AMPERSAND + 16#0027 -> 16#27; %% APOSTROPHE + 16#0028 -> 16#28; %% LEFT PARENTHESIS + 16#0029 -> 16#29; %% RIGHT PARENTHESIS + 16#002A -> 16#2A; %% ASTERISK + 16#002B -> 16#2B; %% PLUS SIGN + 16#002C -> 16#2C; %% COMMA + 16#002D -> 16#2D; %% HYPHEN-MINUS + 16#002E -> 16#2E; %% FULL STOP + 16#002F -> 16#2F; %% SOLIDUS + 16#0030 -> 16#30; %% DIGIT ZERO + 16#0031 -> 16#31; %% DIGIT ONE + 16#0032 -> 16#32; %% DIGIT TWO + 16#0033 -> 16#33; %% DIGIT THREE + 16#0034 -> 16#34; %% DIGIT FOUR + 16#0035 -> 16#35; %% DIGIT FIVE + 16#0036 -> 16#36; %% DIGIT SIX + 16#0037 -> 16#37; %% DIGIT SEVEN + 16#0038 -> 16#38; %% DIGIT EIGHT + 16#0039 -> 16#39; %% DIGIT NINE + 16#003A -> 16#3A; %% COLON + 16#003B -> 16#3B; %% SEMICOLON + 16#003C -> 16#3C; %% LESS-THAN SIGN + 16#003D -> 16#3D; %% EQUALS SIGN + 16#003E -> 16#3E; %% GREATER-THAN SIGN + 16#003F -> 16#3F; %% QUESTION MARK + 16#00A1 -> 16#40; %% INVERTED EXCLAMATION MARK + 16#0041 -> 16#41; %% LATIN CAPITAL LETTER A + 16#0391 -> 16#41; %% GREEK CAPITAL LETTER ALPHA + 16#0042 -> 16#42; %% LATIN CAPITAL LETTER B + 16#0392 -> 16#42; %% GREEK CAPITAL LETTER BETA + 16#0043 -> 16#43; %% LATIN CAPITAL LETTER C + 16#0044 -> 16#44; %% LATIN CAPITAL LETTER D + 16#0045 -> 16#45; %% LATIN CAPITAL LETTER E + 16#0395 -> 16#45; %% GREEK CAPITAL LETTER EPSILON + 16#0046 -> 16#46; %% LATIN CAPITAL LETTER F + 16#0047 -> 16#47; %% LATIN CAPITAL LETTER G + 16#0048 -> 16#48; %% LATIN CAPITAL LETTER H + 16#0397 -> 16#48; %% GREEK CAPITAL LETTER ETA + 16#0049 -> 16#49; %% LATIN CAPITAL LETTER I + 16#0399 -> 16#49; %% GREEK CAPITAL LETTER IOTA + 16#004A -> 16#4A; %% LATIN CAPITAL LETTER J + 16#004B -> 16#4B; %% LATIN CAPITAL LETTER K + 16#039A -> 16#4B; %% GREEK CAPITAL LETTER KAPPA + 16#004C -> 16#4C; %% LATIN CAPITAL LETTER L + 16#004D -> 16#4D; %% LATIN CAPITAL LETTER M + 16#039C -> 16#4D; %% GREEK CAPITAL LETTER MU + 16#004E -> 16#4E; %% LATIN CAPITAL LETTER N + 16#039D -> 16#4E; %% GREEK CAPITAL LETTER NU + 16#004F -> 16#4F; %% LATIN CAPITAL LETTER O + 16#039F -> 16#4F; %% GREEK CAPITAL LETTER OMICRON + 16#0050 -> 16#50; %% LATIN CAPITAL LETTER P + 16#03A1 -> 16#50; %% GREEK CAPITAL LETTER RHO + 16#0051 -> 16#51; %% LATIN CAPITAL LETTER Q + 16#0052 -> 16#52; %% LATIN CAPITAL LETTER R + 16#0053 -> 16#53; %% LATIN CAPITAL LETTER S + 16#0054 -> 16#54; %% LATIN CAPITAL LETTER T + 16#03A4 -> 16#54; %% GREEK CAPITAL LETTER TAU + 16#0055 -> 16#55; %% LATIN CAPITAL LETTER U + 16#0056 -> 16#56; %% LATIN CAPITAL LETTER V + 16#0057 -> 16#57; %% LATIN CAPITAL LETTER W + 16#0058 -> 16#58; %% LATIN CAPITAL LETTER X + 16#03A7 -> 16#58; %% GREEK CAPITAL LETTER CHI + 16#0059 -> 16#59; %% LATIN CAPITAL LETTER Y + 16#03A5 -> 16#59; %% GREEK CAPITAL LETTER UPSILON + 16#005A -> 16#5A; %% LATIN CAPITAL LETTER Z + 16#0396 -> 16#5A; %% GREEK CAPITAL LETTER ZETA + 16#00C4 -> 16#5B; %% LATIN CAPITAL LETTER A WITH DIAERESIS + 16#00D6 -> 16#5C; %% LATIN CAPITAL LETTER O WITH DIAERESIS + 16#00D1 -> 16#5D; %% LATIN CAPITAL LETTER N WITH TILDE + 16#00DC -> 16#5E; %% LATIN CAPITAL LETTER U WITH DIAERESIS + 16#00A7 -> 16#5F; %% SECTION SIGN + 16#00BF -> 16#60; %% INVERTED QUESTION MARK + 16#0061 -> 16#61; %% LATIN SMALL LETTER A + 16#0062 -> 16#62; %% LATIN SMALL LETTER B + 16#0063 -> 16#63; %% LATIN SMALL LETTER C + 16#0064 -> 16#64; %% LATIN SMALL LETTER D + 16#0065 -> 16#65; %% LATIN SMALL LETTER E + 16#0066 -> 16#66; %% LATIN SMALL LETTER F + 16#0067 -> 16#67; %% LATIN SMALL LETTER G + 16#0068 -> 16#68; %% LATIN SMALL LETTER H + 16#0069 -> 16#69; %% LATIN SMALL LETTER I + 16#006A -> 16#6A; %% LATIN SMALL LETTER J + 16#006B -> 16#6B; %% LATIN SMALL LETTER K + 16#006C -> 16#6C; %% LATIN SMALL LETTER L + 16#006D -> 16#6D; %% LATIN SMALL LETTER M + 16#006E -> 16#6E; %% LATIN SMALL LETTER N + 16#006F -> 16#6F; %% LATIN SMALL LETTER O + 16#0070 -> 16#70; %% LATIN SMALL LETTER P + 16#0071 -> 16#71; %% LATIN SMALL LETTER Q + 16#0072 -> 16#72; %% LATIN SMALL LETTER R + 16#0073 -> 16#73; %% LATIN SMALL LETTER S + 16#0074 -> 16#74; %% LATIN SMALL LETTER T + 16#0075 -> 16#75; %% LATIN SMALL LETTER U + 16#0076 -> 16#76; %% LATIN SMALL LETTER V + 16#0077 -> 16#77; %% LATIN SMALL LETTER W + 16#0078 -> 16#78; %% LATIN SMALL LETTER X + 16#0079 -> 16#79; %% LATIN SMALL LETTER Y + 16#007A -> 16#7A; %% LATIN SMALL LETTER Z + 16#00E4 -> 16#7B; %% LATIN SMALL LETTER A WITH DIAERESIS + 16#00F6 -> 16#7C; %% LATIN SMALL LETTER O WITH DIAERESIS + 16#00F1 -> 16#7D; %% LATIN SMALL LETTER N WITH TILDE + 16#00FC -> 16#7E; %% LATIN SMALL LETTER U WITH DIAERESIS + 16#00E0 -> 16#7F; %% LATIN SMALL LETTER A WITH GRAVE + %% extensions + 16#000C -> [16#1B,16#0A]; %% FORM FEED + 16#005E -> [16#1B,16#14]; %% CIRCUMFLEX ACCENT + 16#007B -> [16#1B,16#28]; %% LEFT CURLY BRACKET + 16#007D -> [16#1B,16#29]; %% RIGHT CURLY BRACKET + 16#005C -> [16#1B,16#2F]; %% REVERSE SOLIDUS + 16#005B -> [16#1B,16#3C]; %% LEFT SQUARE BRACKET + 16#007E -> [16#1B,16#3D]; %% TILDE + 16#005D -> [16#1B,16#3E]; %% RIGHT SQUARE BRACKET + 16#007C -> [16#1B,16#40]; %% VERTICAL LINE + 16#20AC -> [16#1B,16#65] %% EURO SIGN + end. diff --git a/deps/gsms/src/gsms_0705.erl b/deps/gsms/src/gsms_0705.erl new file mode 100644 index 0000000..8756d3c --- /dev/null +++ b/deps/gsms/src/gsms_0705.erl @@ -0,0 +1,972 @@ +%%%------------------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2012, Tony Rogvall +%%% @doc +%%% SMS service using gsms_uart to access 07.05 commands +%%% @end +%%% Created : 24 Oct 2012 by Tony Rogvall <tony@rogvall.se> +%%%------------------------------------------------------------------- +-module(gsms_0705). + +-behaviour(gen_server). + +-export([start/2]). +-export([start_link/2]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +%% api from gsms_router +-export([send/3]). %% send straight +-export([write/3]). %% write to sim but do not send +-export([scan_input/1]). + + +%% access 07.05 commands +-export([get_version/1]). +-export([get_manufacturer/1, get_model/1]). +-export([get_imei/1, get_msisdn/1, get_imsi/1]). +-export([get_activity_status/1]). +-export([get_network_registration_status/1]). +-export([get_signal_strength/1]). +-export([get_battery_status/1]). +-export([get_smsc/1]). + +-export([list_unread_messages/1]). +-export([list_read_messages/1]). +-export([list_unsent_messages/1]). +-export([list_sent_messages/1]). +-export([list_all_messages/1]). +-export([list_indices/1]). + +-export([delete_message/2]). +-export([delete_read_messages/1]). +-export([delete_sent_messages/1]). +-export([delete_unsent_messages/1]). +-export([delete_all_messages/1]). + +-export([read_message/2]). + +%% access 07.05 commands given the driver pid +-export([drv_reset/1]). +-export([drv_init_csms_service/1]). +-export([drv_check_csms_capability/1]). +-export([drv_set_csms_notification/1]). +-export([drv_set_csms_pdu_mode/1]). +-export([drv_get_version/1]). +-export([drv_get_manufacturer/1, drv_get_model/1]). +-export([drv_get_imei/1, drv_get_msisdn/1, drv_get_imsi/1]). +-export([drv_get_activity_status/1]). +-export([drv_get_network_registration_status/1]). +-export([drv_get_signal_strength/1]). +-export([drv_get_battery_status/1]). +-export([drv_get_smsc/1]). + +-export([drv_list_unread_messages/1]). +-export([drv_list_read_messages/1]). +-export([drv_list_unsent_messages/1]). +-export([drv_list_sent_messages/1]). +-export([drv_list_all_messages/1]). +-export([drv_list_indices/1]). + +-export([drv_delete_message/2]). +-export([drv_delete_message/3]). +-export([drv_delete_all_message/1]). +-export([drv_read_message/2]). +-export([drv_write_message/3]). +-export([drv_send_message/3]). + +-include("../include/gsms.hrl"). + +-type qkey_t() :: {#gsms_addr{},MRef::integer}. +-type isegment_t() :: {I::integer(),Ix::integer(),Pdu::#gsms_deliver_pdu{}}. +-type qelem_t() :: {qkey_t(),TRef::reference(),N::integer,[isegment_t()]}. + +-type osegment_t() :: {send | write, + I::integer, + N::integer, + SRef::reference(), + Notify::boolean(), + Sender::pid(), + Pdu::#gsms_submit_pdu{}}. + +-type uart_driver() :: pid() | atom(). + +-define(DEFAULT_SEND_DELAY, 0). +-define(DEFAULT_SEGMENT_TIMEOUT, 60000). +-define(DEFAULT_CONCAT_8BIT, true). +-define(DEFAULT_CONCAT_SEQUENCE, true). +-define(DEFAULT_CONCAT_REF, 0). +-define(DEFAULT_SIMPIN, ""). + +-type gsms_option() :: + {simpin, Pin::string()} | + {bnumber, Number::string()} | + {attributes, [{Key::atom(),Value::term()}]} | + {segment_timeout, Timeout::timeout()} | + {send_delay, Delay::timeout()} | + {concat_8bit, EightBit :: boolean()} | + {concat_seq, Sequential :: boolean()} + . + +-type gsms_send_option() :: + {ref, ConCatRef::uint16()} | + {notify, boolean()}. + +-record(state, + { + id :: integer(), %% id in gsms_router + drv :: pid(), %% pid of the gsms_uart AT driver + ref :: reference(), %% notification reference + %% state + drv_up = false, %% drv ready to process + sending = false :: boolean(), %% sending message outstanding? + inq = [] :: [qelem_t()], + outq = [] :: [osegment_t()], + concat_ref = ?DEFAULT_CONCAT_REF :: integer(), + %% config + bnumber = "" :: string(), %% modem phone number + simpin = "" :: string(), %% SIM pin when needed + segment_timeout :: timeout(),%% max wait for segment + send_delay :: timeout(), %% delay between sending segments + concat_seq :: boolean(), %% concat ref is sequence or random + concat_8bit :: boolean(), %% 8bit or 16bit + attributes = [] :: [{atom(),term()}] + }). + +%%%=================================================================== +%%% API +%%%=================================================================== + +-spec send(Pid::pid(), Opts::[gsms_pdu_option()|gsms_send_option()], + Message::list()) -> + {ok, Ref::reference()} | {error, Reason::term()}. + +%% send options: pdu_options ++ [{notify,boolean()},{ref,ConCatRef}] + +send(Pid, Opts, Message) -> + gen_server:call(Pid, {send, self(), Opts, Message}). + +-spec write(Pid::pid(), + Opts::[gsms_pdu_option()|gsms_send_option()], + Message::list()) -> + {ok, Ref::reference()} | {error, Reason::term()}. + +write(Pid, Opts, Message) -> + gen_server:call(Pid, {write, self(), Opts, Message}). + +%% +%% Check if we have messages to process in the inbox +%% +scan_input(Pid) -> + Pid ! scan_input. + +get_version(Pid) -> + gen_server:call(Pid, get_version). +get_manufacturer(Pid) -> + gen_server:call(Pid, get_manufacturer). +get_model(Pid) -> + gen_server:call(Pid, get_model). +get_imei(Pid) -> + gen_server:call(Pid, get_imei). +get_msisdn(Pid) -> + gen_server:call(Pid, get_msisdn). +get_imsi(Pid) -> + gen_server:call(Pid, get_imsi). +get_activity_status(Pid) -> + gen_server:call(Pid, get_activity_status). +get_network_registration_status(Pid) -> + gen_server:call(Pid, get_network_registration_status). +get_signal_strength(Pid) -> + gen_server:call(Pid, get_signal_strength). +get_battery_status(Pid) -> + gen_server:call(Pid, get_battery_status). +get_smsc(Pid) -> + gen_server:call(Pid, get_smsc). + +list_unread_messages(Pid) -> + gen_server:call(Pid, list_unread_messages). +list_read_messages(Pid) -> + gen_server:call(Pid, list_read_messages). +list_unsent_messages(Pid) -> + gen_server:call(Pid, list_unsent_messages). +list_sent_messages(Pid) -> + gen_server:call(Pid, list_sent_messages). +list_all_messages(Pid) -> + gen_server:call(Pid, list_all_messages). +list_indices(Pid) -> + gen_server:call(Pid, list_indices). + +delete_message(Pid, I) when is_integer(I), I>=0 -> + gen_server:call(Pid, {delete_message, I}). +delete_read_messages(Pid) -> + gen_server:call(Pid, {delete_messages,1}). +delete_sent_messages(Pid) -> %% and read messages + gen_server:call(Pid, {delete_messages,2}). +delete_unsent_messages(Pid) -> %% and read,sent messages + gen_server:call(Pid, {delete_messages,3}). +delete_all_messages(Pid) -> + gen_server:call(Pid, {delete_messages,4}). + +read_message(Pid,I) when is_integer(I), I>=0 -> + gen_server:call(Pid, {read_messages,I}). + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% +%% @spec start(Options) -> {ok, Pid} | ignore | {error, Error} +%% @end +%%-------------------------------------------------------------------- + +-spec start(Id::integer(),Opts::[gsms_option()]) -> + {ok,pid()} | {error,Reason::term()}. + +start(Id, Opts) when is_integer(Id), is_list(Opts) -> + gsms:start(), + ChildSpec= {{?MODULE,Id}, {?MODULE, start_link, [Id,Opts]}, + permanent, 5000, worker, [?MODULE]}, + supervisor:start_child(gsms_if_sup, ChildSpec). + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% +%% @spec start_link() -> {ok, Pid} | ignore | {error, Error} +%% @end +%%-------------------------------------------------------------------- +start_link(Id, Opts) -> + gen_server:start_link(?MODULE, [Id,Opts], []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% +%% @spec init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @end +%%-------------------------------------------------------------------- +init([Id,Opts]) -> + State0 = #state { simpin = ?DEFAULT_SIMPIN, + segment_timeout = ?DEFAULT_SEGMENT_TIMEOUT, + send_delay = ?DEFAULT_SEND_DELAY, + concat_seq = ?DEFAULT_CONCAT_SEQUENCE, + concat_8bit = ?DEFAULT_CONCAT_8BIT }, + {Opts1,State1} = setopts(Opts, State0), + {ok,Pid} = gsms_uart:start_link(Opts1), + {ok,Ref} = gsms_uart:subscribe(Pid), %% subscribe to all events + {ok, State1#state { id = Id, + drv = Pid, + ref = Ref }}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @spec handle_call(Request, From, State) -> +%% {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_call({send,Sender,Opts,Body}, _From, State) -> + case gsms_codec:make_sms_submit(Opts, Body) of + {ok,PduList} -> + {CRef,Opts1} = next_concat_ref(Opts,State), + Notify = proplists:get_bool(notify, Opts1), + N = length(PduList), + SRef = make_ref(), + OutQ = State#state.outq ++ + [{send,I,N,SRef,Notify,Sender,Pdu} || + {Pdu,I} <- lists:zip(PduList, lists:seq(1,N))], + State1 = sending(OutQ,State), + {reply, {ok,SRef}, State1#state { concat_ref = CRef}}; + Error -> + {reply, Error, State} + end; +handle_call({write,Sender,Opts,Body}, _From, State) -> + case gsms_codec:make_sms_submit(Opts, Body) of + {ok,PduList} -> + {CRef,Opts1} = next_concat_ref(Opts,State), + Notify = proplists:get_bool(notify, Opts1), + N = length(PduList), + WRef = make_ref(), + OutQ = State#state.outq ++ + [{write,I,N,WRef,Notify,Sender,Pdu} || + {Pdu,I} <- lists:zip(PduList, lists:seq(1,N))], + State1 = sending(OutQ,State), + {reply, {ok,WRef}, State1#state { concat_ref = CRef}}; + Error -> + {reply, Error, State} + end; +handle_call({cancel,Ref}, _From, State) -> + %% Remove and segments not sent for SRef + %% Possibly sent a cancel command ? if supported + OutQ = + lists:foldr( + fun(E={_Operation,_I,_N,Ref1,_Notify,_Sender,_Pdu}, Acc) -> + if Ref1 =:= Ref -> Acc; + true -> [E|Acc] + end + end, [], State#state.outq), + {reply, ok, State#state { outq = OutQ }}; + +handle_call(get_version, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_version(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_manufacturer, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_manufacturer(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_model, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_model(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_imei, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_imei(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_msisdn, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_msisdn(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_imsi, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_imsi(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_network_registration_status, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_network_registration_status(State#state.drv), + State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_signal_strength, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_signal_strength(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_battery_status, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_battery_status(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call(get_smsc, _From, State) -> + if State#state.drv_up -> + {reply, drv_get_smsc(State#state.drv), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call({list_messages,N},_From,State) -> + if State#state.drv_up -> + {reply, drv_list_messages(State#state.drv, N), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call({delete_message,I},_From,State) -> + if State#state.drv_up -> + {reply, drv_delete_message(State#state.drv,I), State}; + true -> + {reply, {error, not_up}, State} + end; +handle_call({delete_messages,F},_From,State) when F>0 -> + if State#state.drv_up -> + {reply, drv_delete_message(State#state.drv,0,F), State}; + true -> + {reply, {error, not_up}, State} + end; + + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @spec handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @spec handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_info({gsms_event,Ref,Event}, State) when State#state.ref =:= Ref -> + case Event of + {cmti,[{"store",_Name},{"index", Ix}]} -> + %% read a stored message + case drv_read_message(State#state.drv, Ix) of + {ok, Sms} -> + lager:debug("read_message: ~p", [Sms]), + State1 = handle_sms(Sms, Ix, State), + {noreply, State1}; + Error -> + lager:info("read_message failed: ~p\n", [Error]), + {noreply, State} + end; + {data,"+CREG:"++Params} -> + case erl_scan:string(Params) of + {ok,[{integer,_,Status}|_],_} -> + %% we do not use the the access value + gsms_router:input_from(State#state.bnumber,{creg,Status}), + {noreply, State}; + _ -> + lager:info("event ignored ~p\n", [Event]), + {noreply, State} + end; + _ -> + lager:info("event ignored ~p\n", [Event]), + {noreply, State} + end; +handle_info({gsms_uart, Pid, up}, State) when State#state.drv =:= Pid -> + %% gsms_uart is up and running + %% FIXME: check if sim is locked & unlock if possible + gsms_uart:at(Pid,"E0"), %% disable echo (again) + timer:sleep(100), %% help? + ok = drv_check_csms_capability(Pid), + ok = drv_set_csms_pdu_mode(Pid), + ok = drv_set_csms_notification(Pid), + BNumber = if State#state.bnumber =:= "" -> + case drv_get_msisdn(Pid) of + ok -> ""; + {ok,B} -> B + end; + true -> + State#state.bnumber + end, + State1 = State#state { bnumber = BNumber, drv_up = true }, + ok = gsms_router:join(BNumber, State1#state.attributes), + %% make sure we scan messages that arrived while we where gone, + scan_input(self()), + lager:debug("running state = ~p", [State1]), + {noreply, State1}; + +handle_info({timeout,TRef,{cancel,Key}}, State) -> + %% reject incoming message because of timeout + Q = State#state.inq, + case lists:keytake(Key, 1, Q) of + false -> + lager:warning("message from ~p not present in timeout", [Key]), + {noreply, State}; + {value,{_,TRef,_N,Segments},Q0} -> + lager:warning("message from ~p dropped, timeout", [Key]), + lists:foreach( + fun({_I,Ix,_}) -> + drv_delete_message(State#state.drv, Ix) + end, Segments), + {noreply, State#state {inq = Q0}} + end; +handle_info(scan_input, State) -> + %% scan_input should be run after startup to get buffered + %% messages stored in SIM card while application was down + case drv_list_indices(State#state.drv) of + {ok,[Ixs | _]} -> + Pid = self(), + Ref = State#state.ref, + lists:foreach( + fun(Ix) -> + Event = {cmti,[{"store","SM"},{"index",Ix}]}, + Pid ! {gsms_event,Ref,Event} + end, expand_index_list(Ixs)), + {noreply, State}; + Reply -> + lager:debug("list_indices reply ~p", [Reply]), + {noreply, State} + end; +handle_info(send, State) -> + %% send next PDU + case State#state.outq of + [{send,I,N,SRef,Notify,Sender,Pdu}|OutQ] -> + gsms_codec:dump_yang(Pdu), + Bin = gsms_codec:encode_sms(Pdu), + Hex = gsms_codec:binary_to_hex(Bin), + Len = (length(Hex)-2) div 2, + Reply = + gsms_uart:atd(State#state.drv, + "+CMGS="++integer_to_list(Len),Hex), + lager:debug("send status segment ~w of ~w response=~p\n", + [I,N,Reply]), + %% Fixme handle Reply=error!!! cancel rest of segments etc + if I =:= N, Notify =:= true -> + Sender ! {gsms_notify, SRef, ok}; + true -> + ok + end, + {noreply, sending(OutQ, State)}; + [{write,I,N,SRef,Notify,Sender,Pdu}|OutQ] -> + gsms_codec:dump_yang(Pdu), + Bin = gsms_codec:encode_sms(Pdu), + Hex = gsms_codec:binary_to_hex(Bin), + Len = (length(Hex)-2) div 2, + Reply = gsms_uart:atd(State#state.drv, + "+CMGW="++integer_to_list(Len),Hex), + lager:debug("wrote status segment ~w of ~w response=~p\n", + [I,N,Reply]), + %% Fixme handle Reply=error!!! cancel rest of segments etc + if I =:= N, Notify -> %% assume ok + Sender ! {gsms_notify, SRef, ok}; + true -> + ok + end, + {noreply, sending(OutQ, State)}; + [] -> + {noreply, State#state { sending = false }} + end; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + + +-spec drv_reset(Drv::uart_driver()) -> ok. + +drv_reset(Drv) -> + gsms_uart:at(Drv,"Z"), + gsms_uart:at(Drv,"E0"), + drv_set_csms_pdu_mode(Drv), + drv_set_csms_notification(Drv). + + +-spec drv_init_csms_service(Drv::uart_driver()) -> ok. +drv_init_csms_service(Drv) -> + ok = drv_check_csms_capability(Drv), + ok = drv_set_csms_pdu_mode(Drv), + ok = drv_set_csms_notification(Drv), + %% how do we read the stored pdu like they where receive and when? + %% trigger automatic read of stored SMS when a subsrciber is + %% registered? subscriber need to ack in order to delete the + %% stored SMS + ok. + + +-spec drv_check_csms_capability(Drv::uart_driver()) -> ok. +drv_check_csms_capability(Drv) -> + case gsms_uart:at(Drv,"+CSMS=0") of + {ok, "+CSMS:"++Storage} -> + lager:debug("sms_capability: +CSMS: ~s", [Storage]); + Error -> + Error + end. + +-spec drv_set_csms_pdu_mode(Drv::uart_driver()) -> ok. + +drv_set_csms_pdu_mode(Drv) -> + gsms_uart:at(Drv,"+CMGF=0"). + +%% AT+CNMI=1,1,0,0,0 Set the new message indicators. +%% +%% AT+CNMI=<mode>,<mt>,<bm>,<ds>,<bfr> +%% +%% <mode>=1 discard unsolicited result codes indication when TA – +%% TE link is reserved. +%% <mt>=1 SMS-DELIVERs are delivered to the SIM and routed using +%% unsolicited code. +%% <bm>=0 no CBM indications are routed to the TE. +%% <ds>=0 no SMS-STATUS-REPORTs are routed. +%% <bfr>=0 TA buffer of unsolicited result codes defined within this +%% command is flushed to the TE. +%% OK Modem Response. +drv_set_csms_notification(Drv) -> + gsms_uart:at(Drv,"+CNMI=1,1,0,0,0"). + +%% pick up information about various things +drv_get_version(Drv) -> + gsms_uart:at(Drv,"+CGMR"). + +drv_get_manufacturer(Drv) -> + gsms_uart:at(Drv,"+CGMI"). + +drv_get_model(Drv) -> + gsms_uart:at(Drv,"+CGMM"). + +drv_get_imei(Drv) -> + gsms_uart:at(Drv,"+CGSN"). + +drv_get_msisdn(Drv) -> + gsms_uart:at(Drv,"+CNUM"). + +drv_get_imsi(Drv) -> + gsms_uart:at(Drv,"+CIMI"). + +drv_get_activity_status(Drv) -> + gsms_uart:eat(Drv,"+CPAS"). + +drv_get_network_registration_status(Drv) -> + gsms_uart:at(Drv,"+CREG?"). + +drv_get_signal_strength(Drv) -> + gsms_uart:at(Drv,"+CSQ"). + +drv_get_battery_status(Drv) -> + gsms_uart:at(Drv,"+CBC"). + +drv_get_smsc(Drv) -> + gsms_uart:at(Drv, "+CSCA?"). + +%% SMS commands + +drv_list_unread_messages(Drv) -> + drv_list_messages(Drv, 0). +drv_list_read_messages(Drv) -> + drv_list_messages(Drv, 1). +drv_list_unsent_messages(Drv) -> + drv_list_messages(Drv, 2). +drv_list_sent_messages(Drv) -> + drv_list_messages(Drv, 3). +drv_list_all_messages(Drv) -> + drv_list_messages(Drv, 4). + +drv_list_messages(Drv, N) when is_integer(N), N>=0, N=<4 -> + gsms_uart:at(Drv, "+CMGL="++integer_to_list(N)). + +%% message list +drv_list_indices(Drv) -> + case gsms_uart:at(Drv,"+CMGD=?") of + {ok,"+CMGD:"++Params} -> + case erl_scan:string(Params) of + {ok,Ts,_} -> + parse_index_lists(Ts); + Error -> + Error + end; + Error -> + Error + end. + + +%% +CMGD=I == +CMDG=I,0 only delete message I +%% +CMGD=I,1 == +CMGD=0,1 delete ALL "read" messages +%% +CMGD=I,2 == +CMGD=0,2 delete ALL "read","sent" messages +%% +CMGD=I,3 == +CMGD=0,3 delete ALL "read","sent", "unsent" messages +%% +CMFD=I,4 == +CMGD=0,4 delete ALL messages + +drv_delete_message(Drv,I) when is_integer(I), I>=0 -> + drv_delete_message(Drv,I,0). + +drv_delete_message(Drv,I,F) when is_integer(I), I>=0, F>=0,F=<4 -> + gsms_uart:at(Drv,"+CMGD="++integer_to_list(I)++","++integer_to_list(F)). + +drv_delete_all_message(Drv) -> + drv_delete_message(Drv,1,4). + +drv_read_message(Drv,I) when is_integer(I), I>=0 -> + case gsms_uart:at(Drv,"+CMGR="++integer_to_list(I)) of + ok -> + {error, no_such_index}; + {ok,["+CMGR: "++_StatStoreLen,HexPdu]} -> + gsms_codec:decode_in_hex(HexPdu); + {error, Error} -> + {error, cms_error(Error)} + end. + + +%% test send message +drv_send_message(Drv,Opts,Body) -> + case gsms_codec:make_sms_submit(Opts, Body) of + {ok,PduList} -> + lists:foreach( + fun(Pdu) -> + gsms_codec:dump_yang(Pdu), + Bin = gsms_codec:encode_sms(Pdu), + Hex = gsms_codec:binary_to_hex(Bin), + Len = (length(Hex)-2) div 2, + gsms_uart:atd(Drv,"+CMGS="++integer_to_list(Len),Hex) + end, PduList), + ok; + Error -> + Error + end. + +%% test write message +drv_write_message(Drv,Opts,Body) -> + case gsms_codec:make_sms_submit(Opts, Body) of + {ok,PduList} -> + lists:foreach( + fun(Pdu) -> + gsms_codec:dump_yang(Pdu), + Bin = gsms_codec:encode_sms(Pdu), + Hex = gsms_codec:binary_to_hex(Bin), + Len = (length(Hex)-2) div 2, + gsms_uart:atd(Drv,"+CMGW="++integer_to_list(Len),Hex) + end, PduList), + ok; + Error -> + Error + end. + +parse_index_lists(Ts) -> + parse_index_lists(Ts, []). + +parse_index_lists([{'(',_}|Ts], Acc) -> parse_index_list(Ts, [], Acc); +parse_index_lists([], Acc) -> {ok,lists:reverse(Acc)}; +parse_index_lists(_Ts, _Acc) -> {error, {syntax_error, _Ts}}. + +parse_index_lists1([{',',_}|Ts], Acc) -> parse_index_lists(Ts, Acc); +parse_index_lists1([], Acc) -> {ok,lists:reverse(Acc)}; +parse_index_lists1(_Ts, _Acc) -> {error, {syntax_error, _Ts}}. + +%% +%% ival = <i> '-' <j> +%% ival = <i> +%% ival-list = '(' (ival (',' ival)*) ? ')' +%% +parse_index_list([{integer,_,I},{'-',_},{integer,_,J}|Ts],Iv,Acc) -> + parse_index_list1(Ts,[{I,J}|Iv],Acc); +parse_index_list([{integer,_,I}|Ts],Iv,Acc) -> + parse_index_list1(Ts,[I|Iv],Acc); +parse_index_list([{')',_}|Ts],Iv,Acc) -> + parse_index_lists1(Ts,[lists:reverse(Iv)|Acc]); +parse_index_list(_Ts, _Iv, _Acc) -> + {error, {syntax_error,_Ts}}. + +parse_index_list1([{',',_},{integer,_,I},{'-',_},{integer,_,J}|Ts],Iv,Acc) -> + parse_index_list1(Ts,[{I,J}|Iv],Acc); +parse_index_list1([{',',_},{integer,_,I}|Ts],Iv,Acc) -> + parse_index_list1(Ts,[I|Iv],Acc); +parse_index_list1([{')',_}|Ts],Iv,Acc) -> + parse_index_lists1(Ts,[lists:reverse(Iv)|Acc]); +parse_index_list1(_Ts,_Iv,_Acc) -> + {error, {syntax_error,_Ts}}. + +expand_index_list([{I,J}|Is]) when is_integer(I),is_integer(J),I>=0,I=<J -> + lists:seq(I,J) ++ expand_index_list(Is); +expand_index_list([I|Is]) when is_integer(I), I>=0 -> + [I|expand_index_list(Is)]; +expand_index_list([]) -> + []. + +cms_error(Code) when is_list(Code) -> + Code ++ ": " ++ cms_error_string(list_to_integer(Code)). + +cms_error_string(300) -> "Phone failure"; +cms_error_string(301) -> "SMS service of phone reserved"; +cms_error_string(302) -> "Operation not allowed"; +cms_error_string(303) -> "Operation not supported"; +cms_error_string(304) -> "Invalid PDU mode parameter"; +cms_error_string(305) -> "Invalid text mode parameter"; +cms_error_string(310) -> "SIM not inserted"; +cms_error_string(311) -> "SIM PIN necessary"; +cms_error_string(312) -> "PH-SIM PIN necessary"; +cms_error_string(313) -> "SIM failure"; +cms_error_string(314) -> "SIM busy"; +cms_error_string(315) -> "SIM wrong"; +cms_error_string(320) -> "Memory failure"; +cms_error_string(321) -> "Invalid memory index"; +cms_error_string(322) -> "Memory full"; +cms_error_string(330) -> "SMSC (message service center) address unknown"; +cms_error_string(331) -> "No network service"; +cms_error_string(332) -> "Network timeout"; +cms_error_string(500) -> "Unknown error"; +cms_error_string(512) -> "Manufacturer specific"; +cms_error_string(_) -> "Unknown". + + +setopts(Opts, State) -> + setopts(Opts, State, []). + +setopts([Opt|Opts], State, Opts1) -> + case Opt of + {simpin,Pin} when is_list(Pin) -> + setopts(Opts, State#state { simpin = Pin }, Opts1); + {bnumber,BNumber} when is_list(BNumber) -> + setopts(Opts, State#state { bnumber = BNumber }, Opts1); + {attributes,As} when is_list(As) -> + setopts(Opts, State#state { attributes = As }, Opts1); + {segment_timeout,T} when is_integer(T), T >= 0 -> + setopts(Opts, State#state { segment_timeout = T }, Opts1); + {send_delay, T} when is_integer(T), T >= 0 -> + setopts(Opts, State#state { send_delay = T }, Opts1); + {concat_8bit, B} when is_boolean(B) -> + setopts(Opts, State#state { concat_8bit = B }, Opts1); + {concat_seq, B} when is_boolean(B) -> + setopts(Opts, State#state { concat_seq = B }, Opts1); + _ -> + setopts(Opts, State, [Opt|Opts1]) + end; +setopts([], State, Opts1) -> + {lists:reverse(Opts1), State}. + + +next_concat_ref(Opts, State) -> + case lists:keymember(ref,1,Opts) of + true -> %% ref is givent in the pdu + {State#state.concat_ref, Opts}; + false -> + CRef0 = if State#state.concat_seq -> + State#state.concat_ref + 1; + true -> + random:unifrom(16#10000)-1 + end, + CRef1 = if State#state.concat_8bit -> + CRef0 band 16#ff; + true -> + CRef0 band 16#ffff + end, + {CRef1, [{ref,CRef1} | Opts]} + end. + +%% +%% Continue send segments or we are done ? +%% +sending([], State) -> + State#state { sending=false, outq = []}; +sending(OutQ, State) -> + erlang:send_after(State#state.send_delay, self(), send), + State#state { sending=true, outq = OutQ }. + +handle_sms(Sms, Ix, State) -> + %% check if this is Sms is part of an concatenated message + case lists:keyfind(concat, 1, Sms#gsms_deliver_pdu.udh) of + false -> %% singleton message, forward + forward_sms(Sms, [Ix], State); + {concat,_MRef,1,1} -> %% singleton message, forward ! + forward_sms(Sms, [Ix], State); + {concat,MRef,N,I} when I > 0, I =< N -> + %% check if we already have segments stored + Key = {Sms#gsms_deliver_pdu.addr,MRef}, + Q = State#state.inq, + Tmo = State#state.segment_timeout, + case lists:keytake(Key, 1, Q) of + false -> %% new enqueue + TRef = start_timer(Tmo,{cancel,Key}), + Segments = [{I,Ix,Sms}], + Q1 = [{Key,TRef,N,Segments} | Q], + State#state { inq = Q1 }; + {value,{_,TRef,N,Segments},Q0} -> + case lists:keymember(I,1,Segments) of + false -> + stop_timer(TRef), + Segments1 = [{I,Ix,Sms}|Segments], + case length(Segments1) of + N -> + {Sms1,Ixs} = assemble_sms(Segments1), + State1 = State#state { inq=Q0}, + forward_sms(Sms1,Ixs,State1); + _ -> + TRef1 = start_timer(Tmo,{cancel,Key}), + Q1 = [{Key,TRef1,N,Segments1} | Q0], + State#state { inq=Q1 } + end; + true -> + lager:warning("segment ~w already received!", [I]), + State + end + end; + Concat -> + lager:warning("bad concat udh element ~p", [Concat]), + State + end. + +%% +%% Send sms to gsms_router & delete stored segments +%% +forward_sms(Sms, Ixs, State) -> + gsms_router:input_from(State#state.bnumber, Sms), + lists:foreach( + fun(Ix) -> + drv_delete_message(State#state.drv, Ix) + end, Ixs), + State. + +%% assemble sms segments into one message, +%% assume all I 1..N are present (by pigeon hole principle) +-spec assemble_sms([isegment_t()]) -> + {#gsms_deliver_pdu{}, [Ix::integer()]}. + +assemble_sms(Segments) -> + [{1,Ix,Sms} | Segments1] = lists:keysort(1, Segments), + Udh0 = lists:keydelete(concat, 1, Sms#gsms_deliver_pdu.udh), + Sms0 = Sms#gsms_deliver_pdu { udh=Udh0, udl=0, ud=[] }, + assemble_(Segments1, + [Sms#gsms_deliver_pdu.ud], Sms#gsms_deliver_pdu.udl, + [Ix], Sms0). + +assemble_([{_,Ix,Sms}|Segments], Uds, Udl, Ixs, Sms0) -> + assemble_(Segments, + [Sms#gsms_deliver_pdu.ud|Uds], Sms#gsms_deliver_pdu.udl + Udl, + [Ix|Ixs], Sms0); +assemble_([], Uds, Udl, Ixs, Sms0) -> + Ud = lists:append(lists:reverse(Uds)), + {Sms0#gsms_deliver_pdu { udl = Udl, ud = Ud }, lists:reverse(Ixs)}. + + +start_timer(Time, Message) -> + erlang:start_timer(Time, self(), Message). + +stop_timer(undefined) -> + ok; +stop_timer(Ref) -> + erlang:cancel_timer(Ref), + receive + {timeout, Ref, _} -> + ok + after 0 -> + ok + end. diff --git a/deps/gsms/src/gsms_app.erl b/deps/gsms/src/gsms_app.erl new file mode 100644 index 0000000..700561b --- /dev/null +++ b/deps/gsms/src/gsms_app.erl @@ -0,0 +1,83 @@ +%%%---- BEGIN COPYRIGHT ------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2012, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT --------------------------------------------------------- +%%%------------------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2010, Tony Rogvall +%%% @doc +%%% +%%% @end +%%% Created : 24 May 2010 by Tony Rogvall <tony@rogvall.se> +%%%------------------------------------------------------------------- +-module(gsms_app). + +-behaviour(application). + +-include("log.hrl"). + +%% Application callbacks +-export([start/2, + start/0, + stop/1, + stop/0]). + +%%%=================================================================== +%%% Application callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @spec start(StartType, StartArgs) -> {ok, Pid} | +%% {ok, Pid, State} | +%% {error, Reason} +%% StartType = normal | {takeover, Node} | {failover, Node} +%% StartArgs = term() +%% @doc +%% This function is called whenever an application is started using +%% application:start/[1,2], and should start the processes of the +%% application. If the application is structured according to the OTP +%% design principles as a supervision tree, this means starting the +%% top supervisor of the tree. +%% +%% @end +%%-------------------------------------------------------------------- +start(_StartType, _StartArgs) -> + Args = case application:get_env(gsms, arguments) of + undefined -> []; + {ok,As} -> As + end, + gsms_sup:start_link(Args). + +%% @private +start() -> + application:start(uart), + application:start(gsms). + +%%-------------------------------------------------------------------- +%% @doc +%% This function is called whenever an application has stopped. It +%% is intended to be the opposite of Module:start/2 and should do +%% any necessary cleaning up. The return value is ignored. +%% +%% @spec stop(State) -> void() +%% @end +%%-------------------------------------------------------------------- +stop(_State) -> + ?debug("gsms_app: stop/1.", []), + ok. + +%% @private +stop() -> + application:stop(gsms). diff --git a/deps/gsms/src/gsms_codec.erl b/deps/gsms/src/gsms_codec.erl new file mode 100644 index 0000000..c904f77 --- /dev/null +++ b/deps/gsms/src/gsms_codec.erl @@ -0,0 +1,1067 @@ +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2012, Tony Rogvall +%%% @doc +%%% Send/Recive SMS with GSM modem +%%% @end +%%% Created : 15 Oct 2012 by Tony Rogvall <tony@rogvall.se> + +-module(gsms_codec). + +-compile(export_all). + +-export([decode_in/1, decode_in_hex/1]). +-export([decode_out/1, decode_out_hex/1]). +-export([hex_to_binary/1]). +-export([is_valid_scts/1]). +-export([decode_dcs/1]). + +-import(lists, [reverse/1]). + +-include("log.hrl"). +-include("../include/gsms.hrl"). + +-define(Q, $"). + +-define(decode_bool(X), ((X) =/= 0)). +-define(encode_bool(X), if (X) -> 1; true -> 0 end). +-define(is_byte(X), (((X) band (bnot 16#ff)) =:= 0)). +-define(is_short(X), (((X) band (bnot 16#ffff)) =:= 0)). + +decode_in_hex(RawData) -> + decode_in(hex_to_binary(RawData)). + +decode_out_hex(RawData) -> + decode_out(hex_to_binary(RawData)). + +-spec decode_in(Message::binary()) -> + {ok, #gsms_deliver_pdu{}} | + {error,Reason::atom()}. + +decode_in(<<L1,SmscAddr:L1/binary, + TP_RP:1,TP_UDHI:1,TP_SRI:1,R1:1,R2:1,TP_MMS:1,?MTI_SMS_DELIVER:2, + AddrLen,Data0/binary>>) -> + ALen = (AddrLen + 1) bsr 1, %% number of bytes + <<AType,Addr:ALen/binary,TP_PID,TP_DCS, + TP_SCTS:7/binary,TP_UDL,TP_UD/binary>> = Data0, + AddrType = decode_addr_type(AType), + Dcs = decode_dcs(TP_DCS), + {UDH,UD} = decode_ud(Dcs,TP_UDHI,TP_UDL,TP_UD), + {ok, #gsms_deliver_pdu { + smsc = decode_smsc_addr(SmscAddr), + rp = ?decode_bool(TP_RP), + udhi = ?decode_bool(TP_UDHI), + sri = ?decode_bool(TP_SRI), + res1 = R1, + res2 = R2, + mms = ?decode_bool(TP_MMS), + addr = decode_addr(AddrType,Addr), + pid = TP_PID, + dcs = Dcs, + scts = decode_scts(TP_SCTS), + udl = TP_UDL, + udh = UDH, + ud = UD + }}; +decode_in(Binary) when is_binary(Binary) -> + {error, einval}. + + +-spec decode_out(Message::binary()) -> + {ok, #gsms_submit_pdu{}} | + {error,Reason::atom()}. + +decode_out(<<L1,SmscAddr:L1/binary, + TP_RP:1,TP_UDHI:1,TP_SRR:1,TP_VPF:2,TP_RD:1,?MTI_SMS_SUBMIT:2, + TP_MREF, + AddrLen, %% in semi octets + Data0/binary>>) -> + ALen = (AddrLen + 1) bsr 1, %% number of bytes + <<AType,Addr:ALen/binary,TP_PID,TP_DCS,Data1/binary>> = Data0, + AddrType = decode_addr_type(AType), + VPF = decode_vpf(TP_VPF), + VPLen = case VPF of + none -> 0; + relative -> 1; + enhanced -> 7; + absolute -> 7 + end, + <<VP:VPLen/binary, TP_UDL:8, TP_UD/binary>> = Data1, + Dcs = decode_dcs(TP_DCS), + {UDH,UD} = decode_ud(Dcs,TP_UDHI,TP_UDL,TP_UD), + {ok, #gsms_submit_pdu { + smsc = decode_smsc_addr(SmscAddr), + rp = ?decode_bool(TP_RP), + udhi = ?decode_bool(TP_UDHI), + srr = ?decode_bool(TP_SRR), + vpf = VPF, + rd = ?decode_bool(TP_RD), + mref = TP_MREF, + addr = decode_addr(AddrType,Addr), + pid = TP_PID, + dcs = Dcs, + vp = decode_vp(VPF,VP), + udl = TP_UDL, + udh = UDH, + ud = UD + }}; +decode_out(Binary) when is_binary(Binary) -> + {error, einval}. + + + +%% return a list of pdu's +-spec make_sms_submit(Opts::[gsms_pdu_option()], + Message::[integer()]) -> + {ok,[#gsms_submit_pdu{}]} | + {error, term()}. + +make_sms_submit(Opts,Message) -> + case set_pdu_opts(Opts,#gsms_submit_pdu{}) of + {ok,Pdu} -> + Ref = proplists:get_value(ref, Opts, 1), + {Dcs,UDList} = encode_ud(Pdu#gsms_submit_pdu.dcs, Message, + Pdu#gsms_submit_pdu.udh, Ref), + {ok,lists:map( + fun({Udl,Ud,Udhi0}) -> + Udhi = Udhi0 or Pdu#gsms_submit_pdu.udhi, + Pdu#gsms_submit_pdu { dcs=Dcs, udl=Udl, + ud=Ud, udhi=Udhi } + end, UDList)}; + Error -> + Error + end. + + +-spec set_pdu_opts(Opts::[gsms_pdu_option()], Pdu::#gsms_submit_pdu{}) -> + {ok,Pdu::#gsms_submit_pdu{}} | + {error, badarg}. + + +set_pdu_opts([{Key,Value}|Kvs], R=#gsms_submit_pdu{}) -> + case Key of + smsc when is_record(Value,gsms_addr) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { smsc=Value }); + smsc when is_list(Value),hd(Value)=:=$+ -> + Addr = #gsms_addr { type=international, addr=Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { smsc=Addr }); + smsc when is_list(Value) -> + Addr = #gsms_addr { type=unknown, addr=Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { smsc=Addr}); + rp when is_boolean(Value) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { rp=Value }); + udhi when is_boolean(Value) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { udhi=Value }); + udh when is_list(Value), Value =/= [] -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { udhi=true, udh=Value }); + udh when Value =:= [] -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { udhi=false, udh=Value }); + srr when is_boolean(Value) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { srr=Value }); + mref when ?is_byte(Value) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { mref=Value }); + vpf when Value =:= none; + Value =:= relative; + Value =:= enhanced; + Value =:= absolute -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { vpf=Value }); + vp -> + case Value of + none -> + set_pdu_opts(Kvs, R#gsms_submit_pdu + { vpf = none, vp=undefined }); + {relative, Seconds} when is_integer(Seconds), Seconds>=0 -> + set_pdu_opts(Kvs, R#gsms_submit_pdu + { vpf = relative, vp=Seconds }); + {absolute,DateTimeTz} -> + case is_valid_scts(DateTimeTz) of + true -> + set_pdu_opts(Kvs, R#gsms_submit_pdu + { vpf = absolute, + vp=DateTimeTz }); + false -> + {error, badarg} + end; + {enhanced,_} -> + {error, not_supported} %% yet + end; + addr when is_record(Value,gsms_addr) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { addr=Value }); + addr when is_list(Value),hd(Value)=:=$+ -> + Addr = #gsms_addr { type=international, addr=Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { addr=Addr}); + addr when is_list(Value) -> + Addr = #gsms_addr { type=unknown, addr=Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { addr=Addr}); + pid when ?is_byte(Value) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { pid=Value }); + dcs when is_record(Value,gsms_dcs) -> + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Value }); + dcs when is_integer(Value) -> + Dcs = decode_dcs(Value), + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs }); + type -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { type = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + class -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { class = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + alphabet -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { alphabet = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + compression -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { compression = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + store -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { store = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + wait_type -> %% fixme test + Dcs = R#gsms_submit_pdu.dcs, + Dcs1 = Dcs#gsms_dcs { wait_type = Value }, + set_pdu_opts(Kvs, R#gsms_submit_pdu { dcs=Dcs1 }); + %% recognized options, but not for pdu + notify -> %% fixme test? + set_pdu_opts(Kvs, R); + ref -> %% fixme test? + set_pdu_opts(Kvs, R); + _ -> + lager:debug("set_pdu_opts: unknown pdu option ~p", + [{Key,Value}]), + {error, badarg} + end; +set_pdu_opts([], R) -> + {ok,R}. + +is_valid_scts({{Date,{H,M,S}},Tz}) -> + try calendar:valid_date(Date) of + true -> + if is_integer(H), H >= 0, H =< 23, + is_integer(M), M >= 0, M =< 59, + is_integer(S), S >= 0, S =< 59, + is_float(Tz) -> + true; + true -> + false + end; + false -> false + catch + error:_ -> false + end; +is_valid_scts(_) -> + false. + + + + +encode_sms(R=#gsms_submit_pdu{}) -> + SmscAddr = encode_smsc_addr(R#gsms_submit_pdu.smsc), + TP_RP = ?encode_bool(R#gsms_submit_pdu.rp), + TP_UDHI = ?encode_bool(R#gsms_submit_pdu.udhi), + TP_SRR = ?encode_bool(R#gsms_submit_pdu.srr), + TP_VPF = encode_vpf(R#gsms_submit_pdu.vpf), + TP_RD = ?encode_bool(R#gsms_submit_pdu.rd), + TP_MREF = R#gsms_submit_pdu.mref, + {AType,Addr,AddrLen} = encode_addr(R#gsms_submit_pdu.addr), + TP_PID = R#gsms_submit_pdu.pid, + TP_DCS = encode_dcs(R#gsms_submit_pdu.dcs), + TP_VP = encode_vp(R#gsms_submit_pdu.vpf,R#gsms_submit_pdu.vp), + TP_UDL = R#gsms_submit_pdu.udl, + TP_UD = R#gsms_submit_pdu.ud, + ?debug("SmscAddr=~p,Addr=~p,TP_VP=~p,TP_UD=~p\n", + [SmscAddr,Addr,TP_VP,TP_UD]), + L1 = byte_size(SmscAddr), + <<L1,SmscAddr:L1/binary, + TP_RP:1,TP_UDHI:1,TP_SRR:1,TP_VPF:2,TP_RD:1,?MTI_SMS_SUBMIT:2, + TP_MREF, + AddrLen, %% in semi octets + AType, + Addr/binary, + TP_PID, + TP_DCS, + TP_VP/binary, + TP_UDL, + TP_UD/binary>>. + + +binary_to_hex(Bin) -> + [ element(I+1,{$0,$1,$2,$3,$4,$5,$6,$7,$8,$9, + $A,$B,$C,$D,$E,$F}) || <<I:4>> <= Bin ]. + +hex_to_binary(RawData) -> + << << H:4 >> || H <- hex_norm_nibbles(RawData) >>. + +hex_norm_nibbles([$\s|Cs]) -> hex_norm_nibbles(Cs); +hex_norm_nibbles([$\t|Cs]) -> hex_norm_nibbles(Cs); +hex_norm_nibbles([$\r|Cs]) -> hex_norm_nibbles(Cs); +hex_norm_nibbles([$\n|Cs]) -> hex_norm_nibbles(Cs); +hex_norm_nibbles([C|Cs]) -> + if C >= $0, C =< $9 -> [C-$0 | hex_norm_nibbles(Cs)]; + C >= $A, C =< $F -> [(C-$A)+10 | hex_norm_nibbles(Cs)]; + C >= $a, C =< $f -> [(C-$A)+10 | hex_norm_nibbles(Cs)] + end; +hex_norm_nibbles([]) -> + []. + +%% +%% addresses +%% + +decode_smsc_addr_type(<<AType,_/binary>>) -> + decode_addr_type(AType); +decode_smsc_addr_type(<<>>) -> + undefined. + +decode_smsc_addr(<<AType,Ds/binary>>) -> + T = decode_addr_type(AType), + A = decode_addr_semi(T, Ds), + #gsms_addr { type=T, addr=A }; +decode_smsc_addr(<<>>) -> + undefined. + +decode_vpf(2#00) -> none; +decode_vpf(2#10) -> relative; +decode_vpf(2#01) -> enhanced; +decode_vpf(2#11) -> absolute. + +encode_vpf(none) -> 2#00; +encode_vpf(relative) -> 2#10; +encode_vpf(enhanced) -> 2#01; +encode_vpf(absolute) -> 2#11. + +%% validity period in seconds +decode_vp(none,<<>>) -> 0; +decode_vp(relative,<<VP>>) -> + if VP =< 143 -> (VP+1)*5*60; %% 1..12 hours (+ 5min) + VP =< 167 -> (12*60 + (VP-143)*30)*60; %% 12..24 hours (+ 30min) + VP =< 196 -> (VP-166)*24*60*60; %% 2 .. 30 days + true -> ((VP-192)*7*24)*60*60 %% 5 .. 63 weeks + end; +decode_vp(absolute,V) -> + decode_scts(V). + +encode_vp(none, _) -> <<>>; +encode_vp(relative, V) -> + Min_0 = V div 60, %% number of minutes + Hour_1 = Min_0 div 60, %% number of hours + _Min_1 = Min_0 rem 60, %% mintes with in the hour + Day_2 = Hour_1 div 24, %% days + _Hour_2 = Hour_1 rem 24, %% hour with in day + Week_3 = Day_2 div 7, %% weeks + _Day_3 = Day_2 rem 7, %% day within week + if Week_3 >= 5, Week_3 =< 64 -> + <<((Week_3-5)+197)>>; + Day_2 >= 2, Day_2 =< 30 -> + <<((Day_2-2)+168)>>; + Min_0 >= 12*60, Min_0 =< 24*60 -> + <<(((Min_0 - 12*60) div 30) + 143)>>; + Min_0 >= 5 -> + <<((Min_0 div 5)-1)>>; + true -> + <<0>> + end; +encode_vp(absolute,V) -> + encode_scts(V). + +encode_smsc_addr(undefined) -> + <<>>; +encode_smsc_addr(#gsms_addr {type=T,addr=A}) -> + AType = encode_addr_type(T), + Ds = encode_addr_semi(T, A), + <<AType,Ds/binary>>. + +%% Handle the ones in use only! (FIXME) +decode_addr_type(2#10000001) -> unknown; +decode_addr_type(2#10010001) -> international; +decode_addr_type(2#10101000) -> national; +decode_addr_type(T) -> T. + +encode_addr_type(unknown) -> 16#81; %% 129 +encode_addr_type(international) -> 16#91; %% 145 +encode_addr_type(national) -> 16#A8; +encode_addr_type(T) -> T. + +decode_addr(T, <<>>) -> + #gsms_addr { type=T, addr=[] }; +decode_addr(T, Data) -> + A = decode_addr_semi(T, Data), + #gsms_addr { type=T, addr=A }. + +encode_addr(#gsms_addr { type=T, addr=A }) -> + Addr = encode_addr_semi(T, A), + AddrLen = length(A), %% number semi digits + {encode_addr_type(T),Addr,AddrLen}. + +decode_addr_semi(international, SemiOctets) -> + [$+|decode_semi_octets(SemiOctets)]; +decode_addr_semi(_, SemiOctets) -> + decode_semi_octets(SemiOctets). + +encode_addr_semi(international, [$+|Ds]) -> + encode_semi_octets(Ds); +encode_addr_semi(_, Ds) -> + encode_semi_octets(Ds). + + +decode_bcd(<<H:4,L:4, T/binary>>) -> + [H*10+L | decode_bcd(T)]; +decode_bcd(<<>>) -> + []. + +decode_swapped_bcd(Bin) -> + [(H*10+L) || <<L:4,H:4>> <= Bin]. + +encode_swapped_bcd(Ds) -> + << <<(D rem 10):4, (D div 10):4>> || D <- Ds >>. + +decode_semi_octets(<<>>) -> []; +decode_semi_octets(<<16#F:4,D:4>>) -> [D+$0]; +decode_semi_octets(<<L:4,H:4,Ds/binary>>) -> [H+$0,L+$0|decode_semi_octets(Ds)]. + +encode_semi_octets([]) -> <<>>; +encode_semi_octets([D]) -> <<16#F:4,(D-$0):4>>; +encode_semi_octets([H,L|Ds]) -> + <<(L-$0):4,(H-$0):4, (encode_semi_octets(Ds))/binary>>. + +decode_scts(Bin) -> + [YY,Mon,Day,Hour,Min,Sec,Tz] = decode_swapped_bcd(Bin), + {Year0,_,_} = date(), %% Year0 is current year + YY0 = Year0 rem 100, %% YY0 curent year two digits + Century = Year0 - YY0, %% Current Century, + Year = if YY > YY0+1 -> + YY + (Century - 100); + true -> + YY + Century + end, + Date = {Year,Mon,Day}, + Time = {Hour,Min,Sec}, + Tz0 = (15*(Tz band 16#7f))/60, + TzH = if (Tz band 16#80) =/= 0 -> -Tz0; true -> Tz0 end, + {{Date,Time},TzH}. + +encode_scts({{{Year0,Mon,Day},{Hour,Min,Sec}},TzH}) -> + Year = Year0 - 2000, + Tz = if TzH < 0 -> + 16#80 bor (trunc((TzH * 60)/15) band 16#7f); + true -> + trunc((TzH * 60)/15) band 16#7f + end, + encode_swapped_bcd([Year,Mon,Day,Hour,Min,Sec,Tz]). + + +decode_dcs(V) -> + if (V bsr 6) =:= 2#00 -> %% 00xxxxxx + #gsms_dcs { type=message, + compression = if V band 2#00100000 =:= 0 -> + uncompressed; + true -> compressed + end, + alphabet = case V band 2#1100 of + 2#0000 -> default; + 2#0100 -> octet; + 2#1000 -> ucs2; + 2#1100 -> reserved + end, + class = case V band 2#11 of + 2#00 -> alert; + 2#01 -> me; + 2#10 -> sim; + 2#11 -> te + end }; + (V bsr 4) =:= 2#1100 -> %% message waiting, discard + dcs_message_waiting(V,discard,default); + (V bsr 4) =:= 2#1101 -> %% message waiting, store + dcs_message_waiting(V,store,default); + (V bsr 4) =:= 2#1110 -> %% message waiting, store + dcs_message_waiting(V,store,ucs2); + (V bsr 4) =:= 2#1111 -> + #gsms_dcs { type=data,compression=uncompressed, + alphabet=if V band 2#0100 =:= 0 -> octet; + true -> default + end, + class = case V band 2#11 of + 2#00 -> alert; + 2#01 -> me; + 2#10 -> sim; + 2#11 -> te + end + }; + true -> + lager:error("unknown dcs value ~p", [V]), + #gsms_dcs {} + end. + +dcs_message_waiting(V,Store,Alphabet) -> + #gsms_dcs {type=message_waiting, + compression=uncompressed, + alphabet=Alphabet, + store=Store, + active=if V band 2#0100 =:= 0 -> + inactive; + true -> active + end, + wait_type = case V band 2#11 of + 2#00 -> voicemail; + 2#01 -> fax; + 2#10 -> email; + 2#11 -> other + end + }. + + +encode_dcs(#gsms_dcs{type=message,compression=Comp, + alphabet=Alpha,class=Class}) -> + 2#00000000 + + if Comp =:= compressed -> 2#00100000; + true -> 2#00000000 + end + + case Alpha of + default -> 2#0000; + octet -> 2#0100; + ucs2 -> 2#1000; + reserved -> 2#1100 + end + + case Class of + alert -> 2#00; + me -> 2#01; + sim -> 2#10; + te -> 2#11 + end; +encode_dcs(#gsms_dcs {type=message_waiting,compression=uncompressed, + alphabet=Alphabet,store=Store,active=Active, + wait_type=WaitType}) -> + if Alphabet =:= ucs2, Store =:= store -> + 2#11110000; + Alphabet =:= default, Store =:= store -> + 2#11100000; + Alphabet =:= default, Store =:= discard -> + 2#11000000 + end + + if Active =:= active -> 2#0100; + true -> 2#0000 + end + + case WaitType of + voicemail -> 2#00; + fax -> 2#01; + email -> 2#10; + other -> 2#11 + end; +encode_dcs(#gsms_dcs{type=data,compression=uncompressed, + alphabet=Alphabet,class=Class}) -> + 2#11110000 + + if Alphabet =:= octet -> 2#100; + true -> 2#000 + end + + case Class of + alert -> 2#00; + me -> 2#01; + sim -> 2#10; + te -> 2#11 + end. + + +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=default}, + 0, UDL, Data) -> + L7 = UDL, %% number of septets + <<Gsm8:L7/binary,_/binary>> = decode_gsm7(Data), + {[],gsms_0338:decode(Gsm8)}; +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=default}, + 1, UDL, Data = <<UDHL,_/binary>>) -> + %% UDHL as number of septets and including it self + UDHL7 = ((UDHL+1)*8 + 6) div 7, + L7 = UDL-UDHL7, %% number of septets in message + <<_:UDHL7/binary,Gsm8:L7/binary,_/binary>> = decode_gsm7(Data), + <<_,UDHBin:UDHL/binary,_/binary>> = Data, + UDH = decode_udh(UDHBin), + {UDH,gsms_0338:decode(Gsm8)}; +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=ucs2}, + 0,UDL,Data1) -> + case Data1 of + <<Data2:UDL/binary, _/binary>> -> + Ucs = [ U || <<U:16/big>> <= Data2 ], + {[],Ucs} + end; +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=ucs2}, + 1, UDL, Data1 = <<UDHL,UDHBin:UDHL/binary,_/binary>>) -> + Pad = (UDHL+1) band 1, + UDL8 = UDL - (UDHL+1+Pad), + %% io:format("UDL=~w, Pad=~w, UDL8=~w\n", [UDL,Pad,UDL8]), + case Data1 of + <<_,_:UDHL/binary,_:Pad/unit:8,Data2:UDL8/binary>> -> + UDH = decode_udh(UDHBin), + {UDH, [ U || <<U:16/big>> <= Data2 ]} + end; +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=octet}, + 0,UDL, Data1) -> + case Data1 of + <<Data2:UDL/binary>> -> + {[], binary_to_list(Data2)} + end; +decode_ud(#gsms_dcs{compression=uncompressed,alphabet=octet}, + 1, UDL, <<UDHL,UDHBin:UDHL/binary,Data1/binary>>) -> + UDL8 = UDL - (UDHL+1), + case Data1 of + <<_,_:UDHL/binary,Data2:UDL8/binary>> -> + UDH = decode_udh(UDHBin), + {UDH, binary_to_list(Data2)} + end. + + + +encode_ud(Dcs0=#gsms_dcs{alphabet=Alpha0}, + Message, UDH, Ref) -> + %% fixme: check that there are no concat header ? + %% calculate total number of letters in message and the encoding + {Alpha,_TotLen} = message_len(Message,Alpha0), + Dcs = Dcs0#gsms_dcs{alphabet=Alpha}, + case encode_ud_(Dcs, Message, UDH) of + {UDL,UD,[],UDHI} -> + {Dcs, [{UDL,iolist_to_binary(UD),UDHI}]}; + {_UDL,_UD,_Message1,_} -> + {Dcs,encode_ud_loop(Dcs, Message, UDH, Ref, 1, [])} + end. + +encode_ud_loop(Dcs, Message, UDH, Ref, I, Acc) -> + case encode_ud_(Dcs, Message, [{concat,Ref,0,I}|UDH]) of + {UDL,UD,[],UDHI} -> + ud_patch([{UDL,UD,UDHI}|Acc], I, []); + {UDL,UD,Message1,UDHI} -> + UDH1 = [], %% kill header for segments + encode_ud_loop(Dcs,Message1,UDH1,Ref,I+1,[{UDL,UD,UDHI} | Acc]) + end. + +%% patch concat and reverse list +ud_patch([{UDL,UD,UDHI}|Tail], N, Acc) -> + case UD of + [UDHL,[<<?IE_CONCAT8,3,Ref:8,_N,I>>|UDH],Data] -> + UD1 = iolist_to_binary([UDHL,[<<?IE_CONCAT8,3,Ref:8,N,I>>|UDH], + Data]), + ud_patch(Tail, N, [{UDL,UD1,UDHI}|Acc]); + [UDHL,[<<?IE_CONCAT16,4,Ref:16,_N,I>>|UDH],Data] -> + UD1 = iolist_to_binary([UDHL,[<<?IE_CONCAT16,4,Ref:16,N,I>>|UDH], + Data]), + ud_patch(Tail, N, [{UDL,UD1,UDHI}|Acc]) + end; +ud_patch([], _N, Acc) -> + Acc. + + +encode_ud_(#gsms_dcs{compression=uncompressed,alphabet=default}, + Message, UDH) -> + case len_udh(UDH) of + 0 -> + {Gsm8,Message1} = gsms_0338:encode(Message,?MAX_7BIT_LEN), + L7 = byte_size(Gsm8), %% number of septets + L8 = (L7*7 + 7) div 8, %% number of octets needed + <<Gsm7:L8/binary,_/binary>> = encode_gsm7(Gsm8), + {L7,Gsm7,Message1,false}; + UDHL -> + %% UDHL as number of septets and including it self + UDHL8 = (UDHL+1)*8, %% number of bits including self + UDHL7 = (UDHL8 + 6) div 7, %% number of septets + UDHData = encode_udh(UDH), + Message0 = prepend(UDHL7,0,Message), + {Gsm8,Message1} = gsms_0338:encode(Message0,?MAX_7BIT_LEN-UDHL7), + Pad = UDHL7*7 - UDHL8, + L7 = byte_size(Gsm8)-UDHL7, %% number of septets in message + L8 = (L7*7+Pad+7) div 8, %% number of octets needed + <<_,_:UDHL/binary,Gsm7:L8/binary,_/binary>> = encode_gsm7(Gsm8), + {UDHL7+L7,[UDHL,UDHData,Gsm7],Message1,true} + end; +encode_ud_(#gsms_dcs{compression=uncompressed,alphabet=ucs2}, Message, UDH) -> + case len_udh(UDH) of + 0 -> + {Ucs2,Message1} = encode_ucs2(Message,?MAX_16BIT_LEN,[]), + N = byte_size(Ucs2), + {N, Ucs2, Message1, false}; + UDHL -> + UDHData = encode_udh(UDH), + Pad = (UDHL+1) band 1, + UDHL16 = (UDHL+1+Pad) div 2, %% check this + {Ucs2,Message1} = encode_ucs2(Message,?MAX_16BIT_LEN-UDHL16,[]), + N = byte_size(Ucs2), + Ucs21 = prepend(Pad,0,Ucs2), + UDL = N + UDHL + 1 + Pad, + {UDL,[UDHL,UDHData,Ucs21],Message1,true} + end; +encode_ud_(#gsms_dcs{compression=uncompressed,alphabet=octet}, Message, UDH) -> + case len_udh(UDH) of + 0 -> + {Octets,Message1} = encode_octets(Message,?MAX_8BIT_LEN,[]), + N = byte_size(Octets), + {N, Octets, Message1, false}; + UDHL -> + UDHData = encode_udh(UDH), + {Octets,Message1} = encode_octets(Message,?MAX_8BIT_LEN-UDHL,[]), + N = byte_size(Octets), + {UDHL+N,[UDHL,UDHData,Octets],Message1,true} + end. + + + +encode_octets(Cs, 0, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), Cs}; +encode_octets([C|Cs], I, Acc) -> + encode_octets(Cs, I-1, [C|Acc]); +encode_octets([], _I, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), []}. + +%% +%% Fixme: add BOM (byte order mark) character +%% BOM = 0xFEFF, if BOM is received as 0xFEFF then +%% it is coded in default big endian, else it +%% will be read as 0xFFFE and is coded in little endian. +%% +encode_ucs2(Cs, 0, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), Cs}; +encode_ucs2([C|Cs], I, Acc) -> + encode_ucs2(Cs, I-1, [<<C:16/big>>|Acc]); +encode_ucs2([], _I, Acc) -> + {iolist_to_binary(lists:reverse(Acc)), []}. + + + +message_len(Cs) -> + message_len_auto_(Cs,0). + +message_len(Cs,auto) -> + message_len_auto_(Cs,0); +message_len(Cs,default) -> + {default,message_len_default_(Cs,0)}; +message_len(Cs,octet) -> + {octet,message_len_octet_(Cs,0)}; +message_len(Cs,ucs2) -> + {ucs2,message_len_ucs2_(Cs,0)}. + + +message_len_auto_(Cs,N) -> + try message_len_default_(Cs,N) of + Len7 -> {default,Len7} + catch + error:_ -> + {ucs2,message_len_ucs2_(Cs,N)} + end. + +message_len_default_([C|Cs], Len) -> + case gsms_0338:encode_char(C) of + [_Esc,_Y] -> message_len_default_(Cs, Len+2); + _Y -> message_len_default_(Cs, Len+1) + end; +message_len_default_([], Len) -> + Len. + +message_len_octet_([C|Cs], Len) when ?is_byte(C) -> + message_len_octet_(Cs, Len+1); +message_len_octet_([], Len) -> + Len. + +message_len_ucs2_([C|Cs], Len) when ?is_short(C) -> + message_len_ucs2_(Cs, Len+1); +message_len_ucs2_([], Len) -> + Len. + + +%% +%% Calculate number of bytes in UDH (not incuding the length byte) +%% +-spec len_udh(IEs::[gsms_ie()]) -> integer(). + +len_udh([]) -> + 0; +len_udh(IEs) -> + len_udh(IEs, 0). + +len_udh([{concat,Ref,_N,_I}|IEs],Len) -> + if Ref >= 0, Ref =< 16#ff -> len_udh(IEs, Len+5); + Ref >= 0, Ref =< 16#ffff -> len_udh(IEs, Len+6) + end; +len_udh([{concat8,_Ref,_N,_I}|IEs], Len) -> len_udh(IEs,Len+5); +len_udh([{concat16,_Ref,_N,_I}|IEs], Len) -> len_udh(IEs,Len+6); +len_udh([{port,Dst,Src}|IEs], Len) -> + if Dst >= 0, Dst =< 16#ff, + Src >= 0, Src =< 16#ff -> + len_udh(IEs,Len+4); + Dst >= 0, Dst =< 16#ffff, + Src >= 0, Src =< 16#ffff -> + len_udh(IEs,Len+6) + end; +len_udh([{port8,_Dst,_Src}|IEs], Len) -> len_udh(IEs,Len+4); +len_udh([{port16,_Dst,_Src}|IEs],Len) -> len_udh(IEs,Len+6); +len_udh([], Len) -> Len. + +%% +%% Encode user data header IEs +%% +-spec encode_udh(IEs::[gsms_ie()]) -> [binary()]. + +encode_udh([]) -> + []; +encode_udh(IEs) -> + encode_udh(IEs, []). + +encode_udh([{concat,Ref,N,I}|IEs], Acc) -> + if Ref >= 0, Ref =< 16#ff -> + encode_udh(IEs,[ <<?IE_CONCAT8,3,Ref,N,I>>|Acc]); + Ref >= 0, Ref =< 16#ffff -> + encode_udh(IEs,[ <<?IE_CONCAT16,4,Ref:16,N,I>> | Acc ]) + end; +encode_udh([{concat8,Ref,N,I}|IEs], Acc) -> + encode_udh(IEs,[ <<?IE_CONCAT8,3,Ref,N,I>>|Acc]); +encode_udh([{concat16,Ref,N,I}|IEs], Acc) -> + encode_udh(IEs,[ <<?IE_CONCAT16,4,Ref:16,N,I>> | Acc ]); +encode_udh([{port,Dst,Src}|IEs], Acc) -> + if Dst >= 0, Dst =< 16#ff, + Src >= 0, Src =< 16#ff -> + encode_udh(IEs,[ <<?IE_PORT8,2,Dst,Src>> | Acc]); + Dst >= 0, Dst =< 16#ffff, + Src >= 0, Src =< 16#ffff -> + encode_udh(IEs,[<<?IE_PORT16,4,Dst:16,Src:16>> | Acc]) + end; +encode_udh([{port8,DstPort,SrcPort}|IEs], Acc) -> + encode_udh(IEs,[ <<?IE_PORT8,2,DstPort,SrcPort>> | Acc]); +encode_udh([{port16,DstPort,SrcPort}|IEs],Acc) -> + encode_udh(IEs,[<<?IE_PORT16,4,DstPort:16,SrcPort:16>> | Acc]); +encode_udh([{ie,IE,Args}|IEs], Acc) when is_binary(Args) -> + encode_udh(IEs,[<<IE,(byte_size(Args)),Args/binary>> | Acc]); +encode_udh([], Acc) -> + reverse(Acc). + + +decode_udh(UDHBin) when is_binary(UDHBin) -> + decode_udh(UDHBin, []). + +decode_udh(<<?IE_CONCAT8,3,Ref,N,I,IEs/binary>>, Acc) -> + decode_udh(IEs,[{concat,Ref,N,I}|Acc]); +decode_udh(<<?IE_CONCAT16,4,Ref:16,N,I,IEs/binary>>,Acc) -> + decode_udh(IEs,[{concat,Ref,N,I}|Acc ]); +decode_udh(<<?IE_PORT8,2,DstPort,SrcPort,IEs/binary>>, Acc) -> + decode_udh(IEs,[{port,DstPort,SrcPort}| Acc]); +decode_udh(<<?IE_PORT16,4,DstPort:16,SrcPort:16,IEs/binary>>,Acc) -> + decode_udh(IEs,[{port,DstPort,SrcPort} | Acc]); +decode_udh(<<IE,N,Args:N/binary,IEs/binary>>,Acc) -> + decode_udh(IEs,[{ie,IE,Args}|Acc]); +decode_udh(<<>>, Acc) -> + reverse(Acc). + + +decode_gsm7(<<X1:1,Y7:7, X2:2,Y6:6, X3:3,Y5:5, X4:4,Y4:4, + X5:5,Y3:3, X6:6,Y2:2, X7:7,Y1:1, More/binary>>) -> + <<0:1,Y7:7, + 0:1,Y6:6,X1:1, + 0:1,Y5:5,X2:2, + 0:1,Y4:4,X3:3, + 0:1,Y3:3,X4:4, + 0:1,Y2:2,X5:5, + 0:1,Y1:1,X6:6, + 0:1,X7:7, + (decode_gsm7(More))/binary>>; +decode_gsm7(<<>>) -> + <<>>; +decode_gsm7(More) when byte_size(More) > 0, + byte_size(More) < 7 -> + Pad = 7 - byte_size(More), %% 6..1 + decode_gsm7(<<More/binary, 0:Pad/unit:8>>). + + + +%% encode gsm7 bit encoding +encode_gsm7(<<0:1,Y7:7, 0:1,Y6:6,X1:1, 0:1,Y5:5,X2:2, 0:1,Y4:4,X3:3, + 0:1,Y3:3,X4:4, 0:1,Y2:2,X5:5, 0:1,Y1:1,X6:6, 0:1, X7:7, + More/binary >>) -> + <<X1:1,Y7:7, X2:2,Y6:6, X3:3,Y5:5, X4:4,Y4:4, + X5:5,Y3:3, X6:6,Y2:2, X7:7,Y1:1, + (encode_gsm7(More))/binary >>; +encode_gsm7(<<>>) -> + <<0>>; %% extra bits to compensate for UDH padding. +encode_gsm7(More) when byte_size(More) > 0, + byte_size(More) < 8 -> + Pad = 8 - byte_size(More), %% 7..1 + encode_gsm7(<<More/binary, 0:Pad/unit:8>>). + +%% prepend N number of E's to a list +prepend(N,E,List) when is_list(List) -> + prepend_list(N,E,List); +prepend(N,E,Binary) when is_binary(Binary) -> + <<E:N/unit:8, Binary/binary>>. + +prepend_list(0,_,Acc) -> Acc; +prepend_list(I,E,Acc) -> prepend_list(I-1,E,[E|Acc]). + + + +is_gsms_record(P) -> + case P of + #gsms_addr{} -> true; + #gsms_submit_pdu{} -> true; + #gsms_deliver_pdu{} -> true; + _ -> false + end. + +separator(D,S) -> + case is_gsms_record(D) of + true -> ""; + false -> S + end. + +indent(I) -> + lists:duplicate(I, $\s). + +dump(P) -> + dump_json(P). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Format values and records in JSON format +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +dump_json(P) -> + io:format("~s\n", [fmt_json(P)]). + +fmt_json(P) -> + case is_gsms_record(P) of + true -> fmt_json_record(0,P,"\n"); + false -> fmt_json_value(0,P,"\n") + end. + +fmt_json_record(I,P=#gsms_addr{},N) -> + fmt_json_record(I,P,record_info(fields,gsms_addr),N); +fmt_json_record(I,P=#gsms_submit_pdu{},N) -> + fmt_json_record(I,P,record_info(fields,gsms_submit_pdu),N); +fmt_json_record(I,P=#gsms_deliver_pdu{},N) -> + fmt_json_record(I,P,record_info(fields,gsms_deliver_pdu),N). + +fmt_json_record(I,P,Fs,N) -> + [R|Ds] = tuple_to_list(P), + [ "{",N, + fmt_json_fields(I+2, R, [struct|Fs], [R|Ds], N), + N, indent(I), "}" ]. + +fmt_json_fields(I,R,[F],[D],N) -> + [ [indent(I),fmt_json_field(I,R,F,D,N) ] ]; +fmt_json_fields(I,R,[F|Fs],[D|Ds],N) -> + [ [indent(I),fmt_json_field(I,R,F,D,N),",",N] | + fmt_json_fields(I,R,Fs,Ds,N)]; +fmt_json_fields(_I,_R,[],[],_N) -> + []. + +fmt_json_field(I,R,F,D,N) -> + Fk = atom_to_list(F), + Dk = fmt_json_value(I,R,F,D,N), + [Fk,": ",Dk]. + +fmt_json_value(I,D,N) -> + fmt_json_value(I,undefined,undefined,D,N). + +fmt_json_value(I,R,F,D,N) -> + if + is_boolean(D) -> [atom_to_list(D)]; + is_integer(D) -> [integer_to_list(D)]; + is_atom(D) -> [?Q,atom_to_list(D),?Q]; + is_binary(D), R =:= ipv4, (F =:= src orelse F =:= dst) -> + <<X1,X2,X3,X4>> = D, + [?Q,io_lib:format("~w.~w.~w.~w", [X1,X2,X3,X4]),?Q]; + is_binary(D) -> + io_lib:format("~p", [D]); + is_tuple(D) -> + case is_gsms_record(D) of + true -> + fmt_json_record(I+2,D,N); + false -> + io_lib:format("~p", [D]) + end; + is_list(D) -> + try iolist_size(D) of + _Sz -> [?Q,D,?Q] + catch + error:_ -> %% formt as JSON array? + fmt_json_array(D) + end + end. + +fmt_json_array(Ds) -> + ["[", fmt_json_elems(Ds), "]"]. + +fmt_json_elems([D]) -> + fmt_json_value(0,D,""); +fmt_json_elems([D|Ds]) -> + [fmt_json_value(0,D,""),"," | fmt_json_elems(Ds)]; +fmt_json_elems([]) -> []. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% YANG FORMAT +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +dump_yang(P) -> + io:format("~s\n", [fmt_yang(P)]). + +fmt_yang(P) -> + case is_gsms_record(P) of + true -> fmt_yang_record(0,P,"\n"); + false -> fmt_yang_value(0,P,"\n") + end. + +fmt_yang_record(I,P=#gsms_addr{},N) -> + fmt_yang_record(I,P,record_info(fields,gsms_addr),N); +fmt_yang_record(I,P=#gsms_submit_pdu{},N) -> + fmt_yang_record(I,P,record_info(fields,gsms_submit_pdu),N); +fmt_yang_record(I,P=#gsms_deliver_pdu{},N) -> + fmt_yang_record(I,P,record_info(fields,gsms_deliver_pdu),N). + +fmt_yang_record(I,P,Fs,N) -> + [R|Ds] = tuple_to_list(P), + [ atom_to_list(R), " {", N, + fmt_yang_fields(I+2, R, Fs, Ds, N), + indent(I), "}" ]. + +fmt_yang_fields(I,R,[F|Fs],[D|Ds],N) -> + [ [indent(I),fmt_yang_field(I,R,F,D,N),separator(D,";"),N] | + fmt_yang_fields(I,R,Fs,Ds,N)]; +fmt_yang_fields(_I,_R,[],[],_N) -> + []. + +fmt_yang_field(I,R,F,D,N) -> + Fk = atom_to_list(F), + Dk = fmt_yang_value(I,R,F,D,N), + [Fk," ",Dk]. + +fmt_yang_value(I,D,N) -> + fmt_yang_value(I,undefined,undefined,D,N). + +fmt_yang_value(I,R,F,D,N) -> + if + is_boolean(D) -> [atom_to_list(D)]; + is_integer(D) -> [integer_to_list(D)]; + is_atom(D) -> [atom_to_list(D)]; + is_binary(D), R =:= ipv4, (F =:= src orelse F =:= dst) -> + <<X1,X2,X3,X4>> = D, + [?Q,io_lib:format("~w.~w.~w.~w", [X1,X2,X3,X4]),?Q]; + is_binary(D) -> %% fixme! + [?Q,io_lib:format("~p", [D]),?Q]; + is_tuple(D) -> + case is_gsms_record(D) of + true -> + fmt_yang_record(I+2,D,N); + false -> + io_lib:format("~p", [D]) + end; + is_list(D) -> + try iolist_size(D) of + _Sz -> [?Q,D,?Q] + catch + error:_ -> %% formt as YANG array? + fmt_yang_array(D) + end + end. + +fmt_yang_array(Ds) -> + fmt_yang_elems(Ds). + +fmt_yang_elems([D]) -> + fmt_yang_value(0,D,""); +fmt_yang_elems([D|Ds]) -> + [fmt_yang_value(0,D,"")," " | fmt_yang_elems(Ds)]; +fmt_yang_elems([]) -> []. diff --git a/deps/gsms/src/gsms_if_sup.erl b/deps/gsms/src/gsms_if_sup.erl new file mode 100644 index 0000000..3916a96 --- /dev/null +++ b/deps/gsms/src/gsms_if_sup.erl @@ -0,0 +1,78 @@ +%%%---- BEGIN COPYRIGHT ------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2013, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT --------------------------------------------------------- +%%% @author Malotte W Lönne <malotte@malotte.net> +%%% @copyright (C) 2013, Tony Rogvall +%%% @doc +%%% sms interface supervisor +%%% +%%% Created: 2013 by Malotte W Lönne +%%% @end + +-module(gsms_if_sup). + +-behaviour(supervisor). + +-include("log.hrl"). +%% external exports +-export([start_link/0, + start_link/1, + stop/1]). + +%% supervisor callbacks +-export([init/1]). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start_link(Args) -> + ?info("~p: start_link: args = ~p\n", [?MODULE, Args]), + case supervisor:start_link({local, ?MODULE}, ?MODULE, Args) of + {ok, Pid} -> + {ok, Pid, {normal, Args}}; + Error -> + Error + end. + +start_link() -> + supervisor:start_link({local,?MODULE}, ?MODULE, []). + +stop(_StartArgs) -> + ok. + +%%%---------------------------------------------------------------------- +%%% Callback functions from supervisor +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%%---------------------------------------------------------------------- +init(Args) -> + ?info("~p: init: args = ~p,\n pid = ~p\n", [?MODULE, Args, self()]), + Interfaces = + lists:foldr( + fun({Mod,I,Opts},Acc) -> + Spec={{Mod,I}, + {Mod, start_link, [I,Opts]}, + permanent, 5000, worker, [Mod]}, + [Spec | Acc] + end, [], + case application:get_env(gsms, interfaces) of + undefined -> []; + {ok,IfList} when is_list(IfList) -> + IfList + end), + ?info("~p: init: starting interfaces ~p", [?MODULE, Interfaces]), + {ok,{{one_for_one,3,5}, Interfaces}}. diff --git a/deps/gsms/src/gsms_router.erl b/deps/gsms/src/gsms_router.erl new file mode 100644 index 0000000..bb027f8 --- /dev/null +++ b/deps/gsms/src/gsms_router.erl @@ -0,0 +1,464 @@ +%%%---- BEGIN COPYRIGHT -------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2012, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT ---------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @doc +%%% Message router for SMS +%%% +%%% Created : 17 Apr 2013 by Tony Rogvall +%%% @end +%%%------------------------------------------------------------------- +-module(gsms_router). + +-behaviour(gen_server). + +-include("log.hrl"). +-include("../include/gsms.hrl"). + +%% API +-export([start_link/1, + send/2, + subscribe/1, + unsubscribe/1, + join/2, + input_from/2]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +%% testing +-export([dump/0]). + +-define(SERVER, ?MODULE). + +-record(subscription, + { + pid :: pid(), %% subscriber process + ref :: reference(), %% reference / monitor + filter = [] :: filter() + }). + +-record(interface, + { + pid :: pid(), %% interface pid + mon :: reference(), %% monitor reference + bnumber :: gsms_addr(), %% modem msisdn + rssi = 99 :: integer(), %% last known rssi value + attributes = [] :: [{atom(),term()}] %% general match keys + }). + +-record(state, + { + csq_ival = 0, + csq_tmr, + subs = [] :: [#subscription{}], + ifs = [] :: [#interface{}] + }). + +%%%=================================================================== +%%% API +%%%=================================================================== +-spec send(Options::list({Key::atom(), Value::term()}), Body::string()) -> + {ok, Ref::reference()} | + {error, Reason::term()}. + +send(Opts, Body) -> + gen_server:call(?SERVER, {send, Opts, Body}). + +-spec subscribe(Filter::[filter()]) -> {ok,Ref::reference()} | + {error,Reason::term()}. + +subscribe(Filter) -> + gen_server:call(?SERVER, {subscribe, self(), Filter}). + +-spec unsubscribe(Ref::reference()) -> ok. + +unsubscribe(Ref) -> + gen_server:call(?SERVER, {unsubscribe, Ref}). + +join(BNumber,Attributes) -> + gen_server:call(?SERVER, {join,self(),BNumber,Attributes}). + +%% +%% Called from gsms_0705 backend to enter incoming message +%% +input_from(BNumber, Sms) -> + lager:debug("message input modem:~s, message = ~p\n", + [BNumber, Sms]), + ?SERVER ! {input_from, BNumber, Sms}, + ok. + +%% Test functions +%% @private +dump() -> + gen_server:call(?SERVER, dump). + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server +%% +%% @spec start_link() -> {ok, Pid} | ignore | {error, Error} +%% @end +%%-------------------------------------------------------------------- +start_link(Args) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, Args, []). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Initializes the server +%% +%% @spec init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @end +%%-------------------------------------------------------------------- +init(Args) -> + Csq_ival = proplists:get_value(csq_ival, Args, 0), + Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 -> + erlang:start_timer(Csq_ival, self(), csq); + true -> undefined + end, + process_flag(trap_exit, true), + {ok, #state{ csq_ival=Csq_ival, csq_tmr=Csq_tmr}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @spec handle_call(Request, From, State) -> +%% {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_call({send,Opts,Body}, _From, State) -> + %% forward to the correct interface handler + %% FIXME: add code to match attributes! + case proplists:get_value(bnumber, Opts) of + undefined -> + case State#state.ifs of + [I|_] -> + Reply = gsms_0705:send(I#interface.pid, Opts, Body), + {reply, Reply, State}; + [] -> + {reply, {error,enoent}, State} + end; + BNumber -> + case lists:keyfind(BNumber,#interface.bnumber,State#state.ifs) of + false -> + {reply, {error,enoent}, State}; + I -> + Reply = gsms_0705:send(I#interface.pid, Opts, Body), + {reply, Reply, State} + end + end; +handle_call({subscribe,Pid,Filter}, _From, State) -> + Ref = erlang:monitor(process, Pid), + Subs = [#subscription { pid = Pid, + ref = Ref, + filter = Filter } | State#state.subs], + {reply, {ok,Ref}, State#state { subs = Subs} }; +handle_call({unsubscribe,Ref}, _From, State) -> + case lists:keytake(Ref, #subscription.ref, State#state.subs) of + false -> + {reply, ok, State}; + {value,_S,Subs} -> + erlang:demonitor(Ref, [flush]), + {reply, ok, State#state { subs = Subs} } + end; +handle_call({join,Pid,BNumber,Attributes}, _From, State) -> + case lists:keytake(BNumber, #interface.bnumber, State#state.ifs) of + false -> + ?debug("gsms_router: process ~p, bnumber ~p joined.", + [Pid, BNumber]), + State1 = add_interface(Pid,BNumber,Attributes,State), + {reply, ok, State1}; + {value,I,IFs} -> + receive + {'EXIT', OldPid, _Reason} when I#interface.pid =:= OldPid -> + ?debug("join: restart detected", []), + State1 = add_interface(Pid,BNumber,Attributes, + State#state { ifs=IFs} ), + {reply, ok, State1} + after 0 -> + {reply, {error,ealready}, State} + end + end; +handle_call(dump, _From, State=#state {subs = Subs, ifs= Ifs}) -> + io:format("LoopData:\n", []), + io:format("Subscriptions:\n", []), + lists:foreach(fun(_Sub=#subscription {pid = Pid, ref = Ref, filter = F}) -> + io:format("pid ~p, ref ~p, filter ~p~n", + [Pid, Ref, F]) + end, + Subs), + io:format("Interfaces:\n", []), + lists:foreach(fun(_If=#interface {pid = Pid, mon = Ref, + bnumber = B, attributes = A}) -> + io:format("pid ~p, ref ~p, bnumber ~p, attributes ~p~n", + [Pid, Ref, B, A]) + end, + Ifs), + {reply, ok, State}; + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @spec handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @spec handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% @end +%%-------------------------------------------------------------------- +handle_info({'DOWN',Ref,process,Pid,_Reason}, State) -> + case lists:keytake(Ref, #subscription.ref, State#state.subs) of + false -> + case lists:keytake(Pid, #interface.pid, State#state.ifs) of + false -> + {noreply, State}; + {value,_If,Ifs} -> + ?debug("gsms_router: interface ~p died, reason ~p\n", + [_If, _Reason]), + %% Restart done by gsms_if_sup + {noreply,State#state { ifs = Ifs }} + end; + {value,_S,Subs} -> + {noreply, State#state { subs = Subs} } + end; + +handle_info({input_from, BNumber, Pdu}, State) -> + lager:debug("input bnumber: ~p, pdu=~p\n", [BNumber,Pdu]), + lists:foreach(fun(S) -> match_filter(S, BNumber, Pdu) end, + State#state.subs), + {noreply, State}; +handle_info({timeout, Tmr, csq}, State) when State#state.csq_tmr =:= Tmr -> + Is = + lists:map( + fun(I) -> + R = gsms_0705:get_signal_strength(I#interface.pid), + lager:debug("csq result: ~p\n", [R]), + case R of + {ok,"+CSQ:"++Params} -> + case erl_scan:string(Params) of + {ok,[{integer,_,Rssi}|_],_} -> + Pdu = {rssi, Rssi}, + BNumber = I#interface.bnumber, + if I#interface.rssi =/= Rssi -> + lists:foreach( + fun(S) -> + match_filter(S,BNumber,Pdu) + end, State#state.subs), + I#interface { rssi = Rssi }; + true -> + I + end; + _Error -> + lager:error("unable to read rssi: ~p", + [_Error]), + I + end; + _Error -> + lager:error("unable to read rssi: ~p",[_Error]), + I + end + end, State#state.ifs), + Csq_ival = State#state.csq_ival, + Csq_tmr = if is_integer(Csq_ival), Csq_ival > 0 -> + erlang:start_timer(Csq_ival, self(), csq); + true -> undefined + end, + {noreply, State#state { ifs = Is, csq_tmr=Csq_tmr }}; +handle_info({'EXIT', Pid, Reason}, State) -> + case lists:keytake(Pid, #interface.pid, State#state.ifs) of + {value,_If,Ifs} -> + %% One of our interfaces died, log and ignore + ?debug("gsms_router: interface ~p died, reason ~p\n", + [_If, Reason]), + {noreply,State#state { ifs = Ifs }}; + false -> + %% Someone else died, log and terminate + ?debug("gsms_router: linked process ~p died, reason ~p, terminating\n", + [Pid, Reason]), + {stop, Reason, State} + end; +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @spec terminate(Reason, State) -> void() +%% @end +%%-------------------------------------------------------------------- +terminate(_Reason, _State) -> + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process state when code is changed +%% +%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} +%% @end +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +add_interface(Pid,BNumber,Attributes,State) -> + Mon = erlang:monitor(process, Pid), + I = #interface { pid=Pid, mon=Mon, + bnumber=BNumber, attributes=Attributes }, + link(Pid), + State#state { ifs = [I | State#state.ifs ] }. + +match_filter(_S=#subscription {filter = Filter, + pid = Pid, + ref = Ref}, + BNumber, Pdu) -> + lager:debug("match filter: ~p", [Filter]), + case match(Filter, BNumber, Pdu) of + true -> + lager:debug("filter match success.", []), + lager:debug("send to ~p", [Pid]), + Pid ! {gsms, Ref, Pdu}; + false -> + lager:debug("filter match fail"), + ok + end. + +match(As, BNum, Sms) when is_list(As) -> + match_clause(As, BNum, Sms); +match({'not',Filter}, BNum, Sms) -> + not match(Filter, BNum, Sms); +match({'and',A,B}, BNum, Sms) -> + match(A,BNum,Sms) andalso match(B,BNum,Sms); +match({'or',A,B}, BNum, Sms) -> + match(A,BNum,Sms) orelse match(B,BNum,Sms); +match({bnumber,Addr}, BNum, _Sms) -> + %% receiving modem + match_addr(Addr, BNum); +match(Match, _BNum, Sms) when is_record(Sms,gsms_deliver_pdu) -> + match_sms(Match, Sms); +match({rssi,true}, _BNum, {rssi,_}) -> + true; +match({creg,true}, _BNum, {creg,_}) -> + true; +match(_, _BNum, _)-> + false. + +match_clause([A|As], BNum, Sms) -> + case match(A, BNum, Sms) of + true -> match_clause(As, BNum, Sms); + false -> false + end; +match_clause([], _BNum, _Sms) -> + true. + +match_sms({type,Type}, Sms) -> + case Sms#gsms_deliver_pdu.dcs of + [Type|_] -> true; + _ -> false + end; +match_sms({alphabet,Alphabet}, Sms) -> + case Sms#gsms_deliver_pdu.dcs of + [_Type,_Compress,Alphabet|_] -> true; + _ -> false + end; +match_sms({class,Class}, Sms) -> + case Sms#gsms_deliver_pdu.dcs of + [_Type,_Compress,_Alphabet,Class|_] -> true; + _ -> false + end; +match_sms({pid,Pid}, Sms) -> + Sms#gsms_deliver_pdu.pid =:= Pid; +match_sms({dst,Port}, Sms) -> + case lists:keyfind(1, port, Sms#gsms_deliver_pdu.udh) of + {port,Port,_} -> true; + _ -> false + end; +match_sms({src,Port}, Sms) -> + case lists:keyfind(1, port, Sms#gsms_deliver_pdu.udh) of + {port,_,Port} -> true; + _ -> false + end; +match_sms({anumber,Addr}, Sms) -> + match_addr(Addr, Sms#gsms_deliver_pdu.addr); +match_sms({smsc,Addr}, Sms) -> + match_addr(Addr, Sms#gsms_deliver_pdu.smsc); +match_sms({reg_exp, RegExp}, Sms) -> + match_body(RegExp, Sms#gsms_deliver_pdu.ud); +match_sms({rssi,_}, _Sms) -> + false; +match_sms({creg,_}, _Sms) -> + false. + + +%% Add some more smart matching here to select international / national +%% country suffix etc. +match_addr(Addr, Addr) -> true; +match_addr(Addr, #gsms_addr { addr = Addr }) when is_list(Addr) -> true; +match_addr(#gsms_addr { type=unknown, addr=Addr}, + #gsms_addr { addr=Addr}) -> true; +match_addr(_, _) -> false. + +match_body(RegExp, UserData) -> + lager:debug("match regular expression: ~p", [RegExp]), + case re:run(UserData, RegExp, [{capture, none}]) of + match -> true; + nomatch -> false + end. diff --git a/deps/gsms/src/gsms_sup.erl b/deps/gsms/src/gsms_sup.erl new file mode 100644 index 0000000..7687476 --- /dev/null +++ b/deps/gsms/src/gsms_sup.erl @@ -0,0 +1,68 @@ +%%%---- BEGIN COPYRIGHT ------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2013, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT --------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @copyright (C) 2013, Tony Rogvall +%%% @doc +%%% Gsms supervisor +%%% +%%% Created: 28 Aug 2006 by Tony Rogvall +%%% @end + +-module(gsms_sup). + +-behaviour(supervisor). + +-include("log.hrl"). +%% external exports +-export([start_link/0, + start_link/1, + stop/0]). + +%% supervisor callbacks +-export([init/1]). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start_link(Args) -> + ?info("~p: start_link: args = ~p\n", [?MODULE, Args]), + case supervisor:start_link({local, ?MODULE}, ?MODULE, Args) of + {ok, Pid} -> + {ok, Pid, {normal, Args}}; + Error -> + Error + end. + +start_link() -> + supervisor:start_link({local,?MODULE}, ?MODULE, []). + +stop() -> + exit(normal). + +%%%---------------------------------------------------------------------- +%%% Callback functions from supervisor +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%%---------------------------------------------------------------------- +init(Args) -> + ?info("~p: init: args = ~p,\n pid = ~p\n", [?MODULE, Args, self()]), + GsmsRouter = {gsms_router, {gsms_router, start_link, [Args]}, + permanent, 5000, worker, [gsms_router]}, + GsmsIfSup = {gsms_if_sup, {gsms_if_sup, start_link, []}, + permanent, 5000, worker, [gsms_if_sup]}, + {ok,{{one_for_all,3,5}, [GsmsRouter, GsmsIfSup]}}. diff --git a/deps/gsms/src/gsms_uart.erl b/deps/gsms/src/gsms_uart.erl new file mode 100644 index 0000000..4c5d0ee --- /dev/null +++ b/deps/gsms/src/gsms_uart.erl @@ -0,0 +1,834 @@ +%%%---- BEGIN COPYRIGHT -------------------------------------------------------- +%%% +%%% Copyright (C) 2007 - 2012, Rogvall Invest AB, <tony@rogvall.se> +%%% +%%% This software is licensed as described in the file COPYRIGHT, which +%%% you should have received as part of this distribution. The terms +%%% are also available at http://www.rogvall.se/docs/copyright.txt. +%%% +%%% You may opt to use, copy, modify, merge, publish, distribute and/or sell +%%% copies of the Software, and permit persons to whom the Software is +%%% furnished to do so, under the terms of the COPYRIGHT file. +%%% +%%% This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY +%%% KIND, either express or implied. +%%% +%%%---- END COPYRIGHT ---------------------------------------------------------- +%%%------------------------------------------------------------------- +%%% @author Tony Rogvall <tony@rogvall.se> +%%% @author Malotte Westman Lönne <malotte@malotte.net> +%%% @copyright (C) 2012, Tony Rogvall +%%% @doc +%%% SMS command driver. +%%% +%%% Created : 1 Jul 2010 by Tony Rogvall +%%% @end +%%%------------------------------------------------------------------- +-module(gsms_uart). + +-behaviour(gen_server). + +-include("log.hrl"). + +%% API +-export([start_link/1, + stop/1, + subscribe/1, + subscribe/2, + unsubscribe/2, + setopts/2, + at/2, atd/3, send/2]). + +%% Option processing +-export([options/0, + split_opts/2, + normalise_opts/1, + validate_opts/1, + validate_opt/2]). + +-export([trimhd/1,trimtl/1,trim/1]). + +%% gen_server callbacks +-export([init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3]). + +-define(SERVER, ?MODULE). +-define(UART_DEFAULT_OPTS, + [{baud,115200},{mode,list},{active,true}, + {packet,line},{csize,8},{parity,none},{stopb,1}]). +-define(CTRL_Z, 16#1A). +-define(ESC, 16#1B). + +%% For dialyzer +-type gsms_uart_option() :: device | reopen_timeout | reply_timeout. + +-type gsms_uart_config() :: + {device, string()} | + {reopen_timeout, timeout()} | + {reply_timeout, timeout()} | + {smsc, string()}. + +-record(subscription, + { + pid, + mon, + pattern + }). + +-record(qent, + { + from, %% caller tag + mon, %% caller/pid monitor + command %% call or cast + }). + +-record(ctx, + { + uart, %% serial port descriptor + device, %% device string + caller, %% parent pid + uopts=[], %% uart options + opts=[], %% sms options + command="", %% last command + reply=[], %% list of reply line data + client, %% last client + client_data, %% ATD hex data if or undefined + queue = [], %% request queue [#qent{}] + reply_timer, %% timeout waiting for reply + reopen_timer, %% timer ref + subs = [] %% #subscription{} + }). + +%%%=================================================================== +%%% API +%%%=================================================================== + +-spec options() -> [uart:uart_option()|gsms_uart_option()]. + +options() -> + uart:options() ++ + [ + smsc, + reopen_timeout, + reply_timeout + ]. + +%%-------------------------------------------------------------------- +%% @doc +%% Starts the server. +%% +%% Device contains the path to the Device. <br/> +%% reopen_timeout =/= 0 means that if the driver fails to open the device it +%% will try again in Timeout milliseconds.<br/> +%% +%% @end +%%-------------------------------------------------------------------- +-spec start_link([gsms_uart_config()]) -> + {ok, Pid::pid()} | + ignore | + {error, Error::term()}. +%% +%% HUAWEI: uses device /dev/tty.HUAWEIMobile-Pcui +%% for SMS services to be able to get notifications which are +%% NOT available on /dev/tty.HUAWEIMobile-Modem +%% +start_link(Opts) -> + lager:info("~p: start_link: args = ~p\n", [?MODULE, Opts]), + gen_server:start_link(?MODULE, [self(),Opts], []). + +%%-------------------------------------------------------------------- +%% @doc +%% Stops the server. +%% +%% @end +%%-------------------------------------------------------------------- +-spec stop(Drv::pid()) -> ok | {error, Error::term()}. + +stop(Drv) -> + gen_server:call(Drv, stop). + + +%%-------------------------------------------------------------------- +%% @doc +%% Subscribe to sms events. +%% +%% @end +%%-------------------------------------------------------------------- +-spec subscribe(Drv::pid()) -> {ok,reference()} | {error, Error::term()}. + +subscribe(Drv) -> + subscribe(Drv, []). + +%%-------------------------------------------------------------------- +%% @doc +%% Subscribe to sms events. +%% +%% @end +%%-------------------------------------------------------------------- +-spec subscribe(Drv::pid(),Pattern::[{atom(),string()}]) -> + {ok,reference()} | {error, Error::term()}. +subscribe(Drv,Pattern) -> + gen_server:call(Drv, {subscribe,self(),Pattern}). + +%%-------------------------------------------------------------------- +%% @doc +%% Unsubscribe from sms events. +%% +%% @end +%%-------------------------------------------------------------------- +-spec unsubscribe(Drv::pid(),Ref::reference()) -> ok | {error, Error::term()}. +unsubscribe(Drv,Ref) -> + gen_server:call(Drv, {unsubscribe,Ref}). + +%%-------------------------------------------------------------------- +%% @doc +%% Set various options from uart options to smsc options +%% +%% @end +%%-------------------------------------------------------------------- + +setopts(Drv,Opts) when is_list(Opts) -> + gen_server:call(Drv, {setopts, Opts}). + +%%-------------------------------------------------------------------- +%% @doc +%% Run a raw command +%% +%% @end +%%-------------------------------------------------------------------- + +%% normal at command +at(Drv,Command) -> + gen_server:call(Drv, {at,Command}). + +%% send command and data in data-enter state +atd(Drv, Command, Hex) -> + gen_server:call(Drv, {atd,Command,Hex}, 20000). + +send(Drv, Data) -> + gen_server:call(Drv, {send,Data}). + +%%%=================================================================== +%%% gen_server callbacks +%%%=================================================================== + +%% @private +%%-------------------------------------------------------------------- +%% @doc +%% Initializes the server +%% +%% @end +%%-------------------------------------------------------------------- +-spec init([Caller::pid()|gsms_uart_config()]) -> + {ok, Ctx::#ctx{}} | + {ok, Ctx::#ctx{}, Timeout::timeout()} | + ignore | + {stop, Reason::term()}. + +init([Caller,Opts]) -> + lager:info("~p: init: args = ~p,\n pid = ~p\n", + [?MODULE, Opts, self()]), + Opts1 = normalise_opts(?UART_DEFAULT_OPTS ++ Opts), + {Uopts0,Opts2} = split_opts(Opts1, uart:options()), + {Gopts0,Opts3} = split_opts(Opts2, options()), + case check_options(Uopts0,Gopts0,Opts3) of + ok -> + Uopts1 = proplists:delete(device, Uopts0), + Device = case proplists:get_value(device, Uopts0) of + undefined -> + case os:getenv("GSMS_DEVICE") of + false -> ""; + Name -> Name + end; + Name -> Name + end, + S = #ctx { device = Device, + uopts = Uopts1, + caller = Caller, + opts = Gopts0, + queue = [] + }, + case open(S) of + {ok, S1} -> {ok, S1}; + Error -> {stop, Error} + end; + Error -> + {stop, Error} + end. + + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling call messages +%% +%% @end +%%-------------------------------------------------------------------- + +handle_call({setopts, Opts},_From, Ctx=#ctx { uart = U}) -> + lager:debug("setopts ~p", [Opts]), + Opts1 = normalise_opts(Opts), + {Uopts0,Opts2} = split_opts(Opts1, uart:options()), + {Gopts0,Opts3} = split_opts(Opts2, options()), + case check_options(Uopts0,Gopts0,Opts3) of + ok -> + Uopts1 = proplists:delete(device, Uopts0), + case proplists:get_value(device, Uopts0) of + Device when is_list(Device), Device =/= Ctx#ctx.device -> + Uopts2 = normalise_opts(Ctx#ctx.uopts ++ Uopts1), + Ctx1 = close(Ctx), + Ctx2 = Ctx1#ctx { uart=undefined, + reopen_timer = undefined, + device=Device, + uopts=Uopts2, + opts=Gopts0 }, + case open(Ctx2) of + {ok, Ctx3} -> {reply,ok,Ctx3}; + Error -> {stop, Error, Ctx2} + end; + _ -> + case U of + undefined -> + {reply, {error,no_port}, Ctx}; + simulated -> + {reply, ok, Ctx#ctx { uopts=Uopts1, opts=Gopts0} }; + _ -> + lager:debug("uart:setopts ~p", [Uopts1]), + case uart:setopts(U, Uopts1) of + ok -> + {reply, ok, Ctx#ctx { uopts=Uopts1, + opts=Gopts0} }; + Error -> + {reply, Error, Ctx} + end + end + end; + Error -> + {reply, Error, Ctx} + end; + +handle_call({subscribe,Pid,Pattern},_From,Ctx=#ctx { subs=Subs}) -> + Mon = erlang:monitor(process, Pid), + Subs1 = [#subscription { pid = Pid, mon = Mon, pattern = Pattern}|Subs], + {reply, {ok,Mon}, Ctx#ctx { subs = Subs1}}; + +handle_call({unsubscribe,Ref},_From,Ctx) -> + erlang:demonitor(Ref), + Ctx1 = remove_subscription(Ref,Ctx), + {reply, ok, Ctx1}; +handle_call(stop, _From, Ctx) -> + {stop, normal, ok, Ctx}; + + +%% other commands we queue if gsms_uart is busy processing command +handle_call(Call,From,Ctx=#ctx {client = Client}) + when Client =/= undefined andalso Call =/= stop -> + %% Driver is busy .. + lager:debug("handle_call: Driver busy, store call ~p", [Call]), + %% set timer already here? probably! + {Pid,_Tag} = From, + Mon = erlang:monitor(process, Pid), + Timeout=proplists:get_value(reply_timeout,Ctx#ctx.opts,5000), + QE = #qent { from=From, mon=Mon, command={call,Call}}, + Q = Ctx#ctx.queue ++ [QE], + {noreply, Ctx#ctx { queue = Q }, Timeout}; + +handle_call({at,Command},From,Ctx) -> + lager:debug("handle_call: command ~p", [Command]), + case Ctx#ctx.uart of + simulated -> + lager:info("simulated output ~p\n", [Command]), + {reply, ok, Ctx}; + undefined -> + lager:info("~p: No port defined yet.\n", [?MODULE]), + {reply, {error,no_port}, Ctx}; + U -> + case uart:send(U, ["AT",Command,"\r\n"]) of + ok -> + lager:debug("command: sent"), + %% Wait for confirmation + Tm=proplists:get_value(reply_timeout,Ctx#ctx.opts,5000), + TRef = erlang:start_timer(Tm, self(), reply), + {noreply,Ctx#ctx {command = Command, + client=From, + client_data = undefined, + reply = [], + reply_timer = TRef}}; + Other -> + lager:debug("command: send failed, reason ~p", [Other]), + {reply, Other, Ctx} + end + end; + +handle_call({atd,Command,Hex},From,Ctx) -> + lager:debug("handle_call: command ~p", [Command]), + case Ctx#ctx.uart of + simulated -> + lager:info("simulated output ~p\n", [Command]), + {reply, ok, Ctx}; + undefined -> + lager:info("~p: No port defined yet.\n", [?MODULE]), + {reply, {error,no_port}, Ctx}; + U -> + uart:setopts(U, [{active,false}]), + case uart:send(U, ["AT",Command,"\r\n"]) of + ok -> + lager:debug("command: sent\n", []), + %% wait for exacly ">\r\n" + uart:setopts(U, [{active,once},{packet,{size,3}}]), + %% Wait for confirmation + Tm=proplists:get_value(reply_timeout,Ctx#ctx.opts,5000), + TRef = erlang:start_timer(Tm, self(), reply), + {noreply,Ctx#ctx {command = Command, + client=From, + client_data=Hex, + reply = [], + reply_timer = TRef}}; + Other -> + uart:setopts(U, [{active,true}]), + lager:debug("command: send failed, reason ~p", [Other]), + {reply, Other, Ctx} + end + end; + +handle_call({send,Data},_From,Ctx) -> + lager:debug("handle_call: send ~p", [Data]), + case Ctx#ctx.uart of + simulated -> + lager:info("simulated output ~p\n", [Data]), + {reply, ok, Ctx}; + undefined -> + lager:info("~p: No port defined yet.\n", [?MODULE]), + {reply, {error,no_port}, Ctx}; + U -> + Reply = uart:send(U, Data), + {reply, Reply, Ctx} + end; +handle_call(_Request, _From, Ctx) -> + {reply, {error,bad_call}, Ctx}. + + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling cast messages +%% +%% @end +%%-------------------------------------------------------------------- +-spec handle_cast(Msg::term(), Ctx::#ctx{}) -> + {noreply, Ctx::#ctx{}} | + {stop, Reason::term(), Ctx::#ctx{}}. + + +handle_cast(Cast, Ctx=#ctx {uart = U, client=Client}) + when U =/= undefined, Client =/= undefined -> + lager:debug("handle_cast: Driver busy, store cast ~p", [Cast]), + %% FIXME: add timer when/if we start using this + QE = #qent { from=undefined, mon=undefined, command = {cast,Cast}}, + Q = Ctx#ctx.queue ++ [QE], + {noreply, Ctx#ctx { queue = Q }}; + +handle_cast(_Msg, Ctx) -> + lager:debug("handle_cast: Unknown message ~p", [_Msg]), + {noreply, Ctx}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Handling all non call/cast messages +%% +%% @end +%%-------------------------------------------------------------------- +-type info():: + {uart, U::port(), Data::binary()} | + {uart_error, U::port(), Reason::term()} | + {uart_closed, U::port()} | + {timeout, reference(), reply} | + {timeout, reference(), reopen} | + {'DOWN',Ref::reference(),process,pid(),Reason::term()}. + +-spec handle_info(Info::info(), Ctx::#ctx{}) -> + {noreply, Ctx::#ctx{}} | + {stop, Reason::term(), Ctx::#ctx{}}. + +handle_info({timeout,TRef,reply}, + Ctx=#ctx {client=Client, reply_timer=TRef}) -> + lager:debug("handle_info: timeout waiting for port", []), + gen_server:reply(Client, {error, port_timeout}), + Ctx1 = Ctx#ctx { reply_timer=undefined, reply=[], client = undefined}, + next_command(Ctx1); + +handle_info({uart,U,Data}, Ctx) when U =:= Ctx#ctx.uart, is_binary(Data) -> + lager:debug("handle_info: noreply ~p", [Data]), + send_event(Ctx#ctx.subs, {data,Data}), + {noreply,Ctx}; +handle_info({uart,U,Data}, Ctx) when U =:= Ctx#ctx.uart -> + lager:debug("got uart data: ~p\n", [Data]), + case trim(Data) of + "" -> %% empty line (may add this later?) + {noreply, Ctx}; + ">" when Ctx#ctx.client_data =/= undefined -> + uart:setopts(U, [{active,true},{packet,line}]), + case uart:send(U, [Ctx#ctx.client_data,?CTRL_Z]) of + ok -> + lager:debug("data sent ~p\n", [Ctx#ctx.client_data]), + stop_timer(Ctx#ctx.reply_timer), + Tm=proplists:get_value(reply_timeout,Ctx#ctx.opts,10000), + TRef = start_timer(Tm, reply), + {noreply,Ctx#ctx { reply_timer=TRef }}; + Error -> + lager:debug("command: send failed, reason ~p", [Error]), + reply(Error, Ctx) + end; + "OK" -> + reply(ok, Ctx); + "ERROR" -> + reply(error, Ctx); + "+CMS ERROR:"++Code -> + reply(error, Ctx#ctx { reply=[trimhd(Code)|Ctx#ctx.reply]}); + "+CMTI:"++EventData -> %% new SMS message (stored) arrived + Ctx1 = event_notify(cmti,trimhd(EventData), Ctx), + {noreply, Ctx1}; + "+CMT:"++EventData -> %% new SMS message arrived + Ctx1 = event_notify(cmt,trimhd(EventData), Ctx), + {noreply, Ctx1}; + "+CMGS:"++EventData -> %% Send SMS response code + Ctx1 = event_notify(cmgs,trimhd(EventData), Ctx), + {noreply, Ctx1}; + "+CDSI:"++EventData -> %% SMS status report (stored) arrived + Ctx1 = event_notify(cdsi,trimhd(EventData), Ctx), + {noreply, Ctx1}; + "+CDS:"++EventData -> %% SMS status report + Ctx1 = event_notify(cds,trimhd(EventData), Ctx), + {noreply, Ctx1}; + "^"++EventData -> %% Periodic information + Ctx1 = event_notify(periodic,trimhd(EventData), Ctx), + {noreply,Ctx1}; + Reply -> + if Ctx#ctx.client =/= undefined -> + lager:debug("handle_info: data ~p", [Reply]), + {noreply,Ctx#ctx { reply=[Reply|Ctx#ctx.reply]}}; + true -> + lager:debug("handle_info: noreply ~p", [Reply]), + send_event(Ctx#ctx.subs, {data,Data}), + {noreply,Ctx} + end + end; + +handle_info({uart_error,U,Reason}, Ctx) when U =:= Ctx#ctx.uart -> + if Reason =:= enxio -> + lager:error("uart error ~p device ~s unplugged?", + [Reason,Ctx#ctx.device]); + true -> + lager:error("uart error ~p for device ~s", + [Reason,Ctx#ctx.device]) + end, + {noreply, Ctx}; + +handle_info({uart_closed,U}, Ctx) when U =:= Ctx#ctx.uart -> + uart:close(U), + lager:error("uart close device ~s will retry", [Ctx#ctx.device]), + case open(Ctx#ctx { uart=undefined}) of + {ok, Ctx1} -> {noreply, Ctx1}; + Error -> {stop, Error, Ctx} + end; + +handle_info({timeout,Ref,reopen}, Ctx) when Ctx#ctx.reopen_timer =:= Ref -> + case open(Ctx#ctx { uart=undefined, reopen_timer=undefined}) of + {ok, Ctx1} -> {noreply, Ctx1}; + Error -> {stop, Error, Ctx} + end; + +handle_info({'DOWN',Ref,process,_Pid,_Reason},Ctx) -> + lager:debug("handle_info: subscriber ~p terminated: ~p", + [_Pid, _Reason]), + case lists:keytake(Ref,#qent.mon, Ctx#ctx.queue) of + false -> remove_subscription(Ref,Ctx); + {value,_QE,Q} -> + {noreply,Ctx#ctx { queue=Q}} + end; +handle_info(_Info, Ctx) -> + lager:debug("handle_info: Unknown info ~p", [_Info]), + {noreply, Ctx}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates +%% with Reason. The return value is ignored. +%% +%% @end +%%-------------------------------------------------------------------- +-spec terminate(Reason::term(), Ctx::#ctx{}) -> + ok. + +terminate(_Reason, Ctx) -> + close(Ctx), + ok. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Convert process ctx when code is changed +%% +%% @end +%%-------------------------------------------------------------------- +-spec code_change(OldVsn::term(), Ctx::#ctx{}, Extra::term()) -> + {ok, NewCtx::#ctx{}}. + +code_change(_OldVsn, Ctx, _Extra) -> + {ok, Ctx}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +check_options(Uopts,Gopts,[]) -> + case uart:validate_opts(Uopts) of + ok -> validate_opts(Gopts); + Error -> Error + end; +check_options(_Uopt,_Gopts,Opts) -> + {error, {unknown_opts,Opts}}. + + +open(Ctx=#ctx {device = ""}) -> + lager:debug("open: simulated\n", []), + {ok, Ctx#ctx { uart=simulated }}; + +open(Ctx=#ctx {device = Name, uopts=UOpts }) -> + case uart:open(Name,UOpts) of + {ok,U} -> + lager:debug("open: ~s [~w] [~w]: ~p", + [Name,UOpts,uart:getopts(U,uart:options()),U]), + flush_uart(U), + lager:debug("sync start"), + %% waky, waky ? do not echo + uart:send(U, [?ESC|"AT\r\n"]), %% escape if stuck in message send + flush_uart(U, 1000, 0), + uart:send(U, "ATZ\r\n"), %% reset + flush_uart(U, 2000, 0), + uart:send(U, "ATE0\r\n"), %% echo off + flush_uart(U, 1000, 0), + uart:send(U, "AT\r\n"), %% empty + flush_uart(U, 100, 0), + uart:send(U, "AT\r\n"), %% empty + flush_uart(U, 100, 0), + lager:debug("sync stop"), + %% signal that the the uart device is up running + Ctx#ctx.caller ! {gsms_uart, self(), up}, + {ok, Ctx#ctx { uart=U }}; + {error, E} when E == eaccess; + E == enoent -> + case proplists:get_value(reopen_timeout,Ctx#ctx.opts,infinity) of + infinity -> + lager:debug("open: Driver not started, reason = ~p.\n",[E]), + {error, E}; + Ival -> + lager:debug("open: uart could not be opened, will try again" + " in ~p millisecs.\n", [Ival]), + Reopen_timer = start_timer(Ival, reopen), + {ok, Ctx#ctx { reopen_timer = Reopen_timer }} + end; + + Error -> + lager:debug("open: Driver not started, reason = ~p.\n", + [Error]), + Error + end. + +close(Ctx=#ctx {uart = U}) when is_port(U) -> + lager:debug("close: ~p", [U]), + uart:close(U), + {ok, Ctx#ctx { uart=undefined }}; +close(Ctx) -> + {ok, Ctx}. + + +%% flush uart data messages +flush_uart(U) -> + flush_uart(U, 1000, 250). + +flush_uart(U,T0,T1) -> + receive + {uart,U,_Data} -> + lager:debug("flush: uart ~p\n", [_Data]), + flush_uart(U,T1) + after T0 -> + ok + end. + +flush_uart(U,T) -> + receive + {uart,U,_Data} -> + lager:debug("flush: uart ~p\n", [_Data]), + flush_uart(U,T) + after T -> + ok + end. + +start_timer(0, _Message) -> undefined; +start_timer(infinity, _Message) -> undefined; +start_timer(Time, Message) -> erlang:start_timer(Time, self(), Message). + +stop_timer(undefined) -> ok; +stop_timer(Ref) when is_reference(Ref) -> + erlang:cancel_timer(Ref), + receive + {timeout,Ref,_} -> ok + after 0 -> + ok + end. + + +reply(Tag, Ctx) -> + if Ctx#ctx.client =/= undefined -> + stop_timer(Ctx#ctx.reply_timer), + case lists:reverse(Ctx#ctx.reply) of + [] -> + gen_server:reply(Ctx#ctx.client, Tag); + [Response] -> + gen_server:reply(Ctx#ctx.client, {Tag,Response}); + MultiResponse -> + gen_server:reply(Ctx#ctx.client, {Tag,MultiResponse}) + end, + Ctx1 = Ctx#ctx { client=undefined, + client_data = undefined, + reply_timer=undefined, + command = "", reply=[] }, + next_command(Ctx1); + true -> + {noreply, Ctx} + end. + +next_command(Ctx) -> + case Ctx#ctx.queue of + [#qent{from=From,mon=Mon,command={call,Call}} | Q1] -> + case handle_call(Call, From, Ctx#ctx { queue=Q1}) of + {reply,Reply,Ctx1} -> + gen_server:reply(From,Reply), + erlang:demonitor(Mon,[flush]), + next_command(Ctx1); + CallResult -> + CallResult + end; + [#qent {command={cast,Cast}} | Q1] -> + handle_cast(Cast, Ctx#ctx { queue=Q1}); + [] -> + uart:setopts(Ctx#ctx.uart, [{active,true}]), + {noreply, Ctx#ctx { queue=[]}} + end. + +trimhd([$\s|Cs]) -> trimhd(Cs); +trimhd([$\t|Cs]) -> trimhd(Cs); +trimhd([$\r|Cs]) -> trimhd(Cs); +trimhd([$\n|Cs]) -> trimhd(Cs); +trimhd([0|Cs]) -> trimhd(Cs); +trimhd(Cs) -> Cs. + +trimtl(Cs) -> lists:reverse(trimhd(lists:reverse(Cs))). + +trim(Cs) -> trimtl(trimhd(Cs)). + +unquote([$"|Cs]) -> + case lists:reverse(Cs) of + [$"|Cs1] -> lists:reverse(Cs1); + Cs1 -> Cs1 + end; +unquote(Cs1) -> Cs1. + +to_integer(Cs) -> + try erlang:list_to_integer(Cs, 10) of + Value -> Value + catch + error:_ -> Cs + end. + +remove_subscription(Ref, Ctx=#ctx { subs=Subs}) -> + Subs1 = lists:keydelete(Ref, #subscription.mon, Subs), + Ctx#ctx { subs = Subs1 }. + + +event_notify(Name,String, Ctx) -> + Args = + case string:tokens(String, ",") of + [Store,Index] -> + [{"store",unquote(Store)},{"index",to_integer(Index)}]; + Items -> + [{"items", Items}] + end, + Event = {Name,Args}, + lager:debug("Event: ~p", [Event]), + send_event(Ctx#ctx.subs, Event), + Ctx. + +send_event([#subscription{pid=Pid,mon=Ref,pattern=Pattern}|Tail], Event) -> + case match_event(Pattern, Event) of + true -> Pid ! {gsms_event,Ref,Event}; + false -> false + end, + send_event(Tail,Event); +send_event([],_Event) -> + ok. + +match_event([], _) -> true; +match_event([{Key,ValuePat}|Kvs],Event) -> + case lists:keyfind(Key, 1, Event) of + {Key,ValuePat} -> match_event(Kvs, Event); + _ -> false + end. + +%% split options in two groups {A,B} +%% where A is the group with all keys in Keys B are the rest of the options +split_opts(List, Keys) -> + {Lists,Rest} = proplists:split(List, Keys), + List1 = %% get last element from each list, simulate seq setting + lists:foldr(fun([],Acc) -> Acc; + (L,Acc) -> [lists:last(L)|Acc] + end, [], Lists), + {List1,Rest}. + +%% remove duplicate options keep later than earlier options +%% normalise boolean options +normalise_opts([Opt|Opts]) -> + case Opt of + Kv={Key,_} -> + case proplists:is_defined(Key, Opts) of + true -> normalise_opts(Opts); + false -> [Kv|normalise_opts(Opts)] + end; + Key -> + case proplists:is_defined(Key, Opts) of + true -> normalise_opts(Opts); + false -> [{Key,true}|normalise_opts(Opts)] + end + end; +normalise_opts([]) -> + []. + +validate_opts([{K,V}|Kvs]) -> + case validate_opt(K,V) of + true -> validate_opts(Kvs); + false -> {error,{type_error,K,V}}; + undefined -> {error,{unknown_opt,K}}; + Error -> Error + end; +validate_opts([]) -> + ok. + +validate_opt(device, Arg) -> is_list(Arg); +validate_opt(reopen_timeout, Arg) -> is_timeout(Arg); +validate_opt(reply_timeout, Arg) -> is_timeout(Arg); +validate_opt(smsc, Arg) -> is_list(Arg); +validate_opt(_,_Arg) -> undefined. + +is_timeout(T) -> + (T =:= infinity) orelse + (is_integer(T) andalso (T>=0)). + diff --git a/deps/gsms/src/log.hrl b/deps/gsms/src/log.hrl new file mode 100644 index 0000000..270f06c --- /dev/null +++ b/deps/gsms/src/log.hrl @@ -0,0 +1,44 @@ +%% +%% Log macros +%% +-ifndef(__LOG_HRL__). +-define(__LOG_HRL__, true). + +-compile({parse_transform, lager_transform}). + +%% Lager logging levels +%% debug, info, notice, warning, error, critical, alert, emergency, none. + +-define(debug(Fmt), lager:debug(Fmt)). +-define(debug(Fmt, Args), lager:debug(Fmt, Args)). +-define(debug(Attrs, Fmt, Args), lager:debug(Attrs, Fmt, Args)). + +-define(info(Fmt), lager:info(Fmt)). +-define(info(Fmt, Args), lager:info(Fmt, Args)). +-define(info(Attrs, Fmt, Args), lager:info(Attrs, Fmt, Args)). + +-define(notice(Fmt), lager:notice(Fmt)). +-define(notice(Fmt, Args), lager:notice(Fmt, Args)). +-define(notice(Attrs, Fmt, Args), lager:notice(Attrs, Fmt, Args)). + +-define(warning(Fmt), lager:warning(Fmt)). +-define(warning(Fmt, Args), lager:warning(Fmt, Args)). +-define(warning(Attrs, Fmt, Args), lager:warning(Attrs, Fmt, Args)). + +-define(error(Fmt), lager:error(Fmt)). +-define(error(Fmt, Args), lager:error(Fmt, Args)). +-define(error(Attrs, Fmt, Args), lager:error(Attrs, Fmt, Args)). + +-define(critical(Fmt), lager:critical(Fmt)). +-define(critical(Fmt, Args), lager:critical(Fmt, Args)). +-define(critical(Attrs, Fmt, Args), lager:critical(Attrs, Fmt, Args)). + +-define(alert(Fmt), lager:alert(Fmt)). +-define(alert(Fmt, Args), lager:alert(Fmt, Args)). +-define(alert(Attrs, Fmt, Args), lager:alert(Attrs, Fmt, Args)). + +-define(emergency(Fmt), lager:emergency(Fmt)). +-define(emergency(Fmt, Args), lager:emergency(Fmt, Args)). +-define(emergency(Attrs, Fmt, Args), lager:emergency(Attrs, Fmt, Args)). + +-endif. diff --git a/deps/gsms/src/sim900 b/deps/gsms/src/sim900 new file mode 100755 index 0000000..40f646f --- /dev/null +++ b/deps/gsms/src/sim900 @@ -0,0 +1,80 @@ +#!/usr/bin/env escript + +%% GPIO map +-define(NRST, 23). %% output pin +-define(PWRKEY, 24). %% output pin +-define(RI, 17). %% input +-define(DTR, 22). %% output pin + +main([]) -> + usage(0); +main(["help"|_Options]) -> + usage(0); +main(["-h"|_Options]) -> + usage(0); +main(Options) -> + case string:to_integer(os:cmd("id -u")) of + {0, _} -> + %% require root access + application:start(gpio), + command(Options), + halt(0); + {_,_} -> + io:fwrite(escript:script_name() ++ " need root privileges\n"), + halt(1) + end. + +command(["init"|Cmds]) -> %% init only + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,low), + gpio:init(?NRST), + gpio:set_direction(?NRST,low), + gpio:init(?RI), + gpio:init(?DTR), + gpio:set_direction(?DTR,low), + command(Cmds); +command(["pwron"|Cmds]) -> + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,out), + gpio:set(?PWRKEY), + timer:sleep(1000+10), %% >1s + gpio:clr(?PWRKEY), + timer:sleep(2200+10), + command(Cmds); +command(["pwroff"|Cmds]) -> + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,out), + gpio:set(?PWRKEY), + timer:sleep(1000+10), %% >1s + gpio:clr(?PWRKEY), + timer:sleep(1700+10), + command(Cmds); +command(["pwrrst"|Cmds]) -> %% combined pwr + command(["pwroff"]), + timer:sleep(500+10), %% >500ms + command(["pwron"]), + command(Cmds); +command(["rst"|Cmds]) -> + gpio:init(?NRST), + gpio:set_direction(?NRST, out), + gpio:set(?NRST), + timer:sleep(1000+10), %% >1s + gpio:clr(?NRST), + timer:sleep(1700+10), + command(Cmds); +command(["help"|_Cmds]) -> + usage(0); +command(["-h"|_Cmds]) -> + usage(0); +command([_|_Cmds]) -> + usage(1); +command([]) -> + ok. + +help() -> + io:fwrite("usage: "++ escript:script_name() ++ + " [-h] help|init|pwron|pwroff|pwrrst|rst\n"). + +usage(N) -> + help(), + halt(N). diff --git a/deps/gsms/src/sim900_ft1075k b/deps/gsms/src/sim900_ft1075k new file mode 100755 index 0000000..98a7c51 --- /dev/null +++ b/deps/gsms/src/sim900_ft1075k @@ -0,0 +1,75 @@ +#!/usr/bin/env escript + +%% GPIO map +-define(NRST, 22). %% output pin 15 +-define(PWRKEY, 27). %% output pin 13 + +main([]) -> + usage(0); +main(["help"|_Options]) -> + usage(0); +main(["-h"|_Options]) -> + usage(0); +main(Options) -> + case string:to_integer(os:cmd("id -u")) of + {0, _} -> + %% require root access + application:start(gpio), + command(Options), + halt(0); + {_,_} -> + io:fwrite(escript:script_name() ++ " need root privileges\n"), + halt(1) + end. + +command(["init"|Cmds]) -> %% init only + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,low), + gpio:init(?NRST), + gpio:set_direction(?NRST,low), + command(Cmds); +command(["pwron"|Cmds]) -> + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,out), + gpio:set(?PWRKEY), + timer:sleep(1000+10), %% >1s + gpio:clr(?PWRKEY), + timer:sleep(2200+10), + command(Cmds); +command(["pwroff"|Cmds]) -> + gpio:init(?PWRKEY), + gpio:set_direction(?PWRKEY,out), + gpio:set(?PWRKEY), + timer:sleep(1000+10), %% >1s + gpio:clr(?PWRKEY), + timer:sleep(1700+10), + command(Cmds); +command(["pwrrst"|Cmds]) -> %% combined pwr + command(["pwroff"]), + timer:sleep(500+10), %% >500ms + command(["pwron"]), + command(Cmds); +command(["rst"|Cmds]) -> + gpio:init(?NRST), + gpio:set_direction(?NRST, out), + gpio:set(?NRST), + timer:sleep(1000+10), %% >1s + gpio:clr(?NRST), + timer:sleep(1700+10), + command(Cmds); +command(["help"|_Cmds]) -> + usage(0); +command(["-h"|_Cmds]) -> + usage(0); +command([_|_Cmds]) -> + usage(1); +command([]) -> + ok. + +help() -> + io:fwrite("usage: "++ escript:script_name() ++ + " [-h] help|init|pwron|pwroff|pwrrst|rst\n"). + +usage(N) -> + help(), + halt(N). diff --git a/deps/gsms/sys.config b/deps/gsms/sys.config new file mode 100644 index 0000000..3db5e31 --- /dev/null +++ b/deps/gsms/sys.config @@ -0,0 +1,47 @@ +%% -*- erlang -*- +%% +%% Configuration file for included erlang applications. +%% +[ + %% SASL config + {sasl, [ + {sasl_error_logger, {file, "log/sasl-error.log"}}, + {errlog_type, error}, + {error_logger_mf_dir, "log/sasl"}, % Log directory + {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size + {error_logger_mf_maxfiles, 5} % 5 files max + ]}, + {lager, [ + {handlers, [ + {lager_console_backend, info}, + {lager_file_backend, + [ + {"log/lager/error.log", error, 10485760, "$D0", 5}, + {"log/lager/console.log", info, 10485760, "$D0", 5} + ]} + ]} + ]}, +{ale, + [{init_traces, [ +%% {[{module, gsms_uart}], debug}, +%% {[{module, gsms_0705}], debug}, +%% {[{module, gsms_router}], debug} + ]}]}, + %% + %% GSMS interfaces (example!) + %% + {gsms, [{interfaces, + [ + {gsms_0705, 1, [{device,"/dev/tty.usbserial-FTF5DP2J"}, + {bnumber, "<phone-number>"},{baud,19200}, + {reopen_timeout, 5000}]} + {gsms_0705, 2, [{device,"/dev/tty.HUAWEIMobile-Pcui"}, + {bnumber, "<phone-number>"}, + {reopen_timeout, 5000}]} + {gsms_0705, 3, [{device, "/dev/tty.usbserial"}, + {bnumber, "<phone-number>"}, + {baud, 9600}]} + ]} + ]} + +]. diff --git a/deps/gsms/tetrapak/config.ini b/deps/gsms/tetrapak/config.ini new file mode 100644 index 0000000..1574279 --- /dev/null +++ b/deps/gsms/tetrapak/config.ini @@ -0,0 +1,13 @@ +[build] +version = "~t.~o~~~c" + +[package] +maintainer = "Magnus Feuer <magnus@feuerlabs.com>" +exclude = "\\.gitignore|README.md" +architecture = host + +[xref] +ignore_undef = [ + { gsms_uart,eat, 2 }, + { random, unifrom, 1 } + ] |