From ad37cb3b19f2db57a0439e96e4ec6738a5b5e514 Mon Sep 17 00:00:00 2001 From: "kurtis.heimerl" Date: Wed, 14 Aug 2013 00:52:14 +0000 Subject: [PATCH] sync of openbts git-svn-id: http://wush.net/svn/range/software/public/openbts/trunk@6168 19bc5d8c-e614-43d4-8b26-e1612bc8e597 --- AUTHORS | 2 + AsteriskConfig/README.AsteriskConf | 4 - AsteriskConfig/cdr.conf | 148 -- AsteriskConfig/extensions.conf | 57 - AsteriskConfig/indications.conf | 733 ------ AsteriskConfig/logger.conf | 69 - AsteriskConfig/modules.conf | 36 - AsteriskConfig/sip.conf | 91 - CLI/CLI.cpp | 762 ++++++- CLI/CLI.h | 30 +- COPYING | 10 +- Control/CallControl.cpp | 185 +- Control/CallControl.h | 29 +- Control/ControlCommon.cpp | 85 +- Control/ControlCommon.h | 34 +- Control/DCCHDispatch.cpp | 63 +- Control/Makefile.am | 3 + Control/MobilityManagement.cpp | 250 +- Control/MobilityManagement.h | 18 +- Control/RRLPServer.cpp | 15 +- Control/RRLP_PDU_Test.cpp | 23 + Control/RadioResource.cpp | 384 +++- Control/RadioResource.h | 44 +- Control/SMSCB.cpp | 199 ++ Control/SMSControl.cpp | 34 +- Control/SMSControl.h | 24 +- Control/TMSITable.cpp | 137 +- Control/TMSITable.h | 38 +- Control/TransactionTable.cpp | 422 +++- Control/TransactionTable.h | 159 +- GPRS/BSSG.cpp | 370 +++ GPRS/BSSG.h | 116 + GPRS/BSSGMessages.cpp | 618 +++++ GPRS/BSSGMessages.h | 523 +++++ GPRS/ByteVector.cpp | 669 ++++++ GPRS/ByteVector.h | 382 ++++ GPRS/CS4.txt | 59 + GPRS/FEC.cpp | 918 ++++++++ GPRS/FEC.h | 284 +++ GPRS/GPRSCLI.cpp | 696 ++++++ GPRS/GPRSExport.h | 103 + GPRS/GPRSInternal.h | 129 ++ GPRS/GPRSRLC.h | 161 ++ GPRS/GPRSTDMA.h | 69 + GPRS/MAC.cpp | 2570 +++++++++++++++++++++ GPRS/MAC.h | 522 +++++ GPRS/MSInfo.cpp | 1127 +++++++++ GPRS/MSInfo.h | 601 +++++ GPRS/Makefile.am | 63 + GPRS/MsgBase.cpp | 108 + GPRS/MsgBase.h | 216 ++ GPRS/RLC.cpp | 83 + GPRS/RLCEngine.cpp | 1330 +++++++++++ GPRS/RLCEngine.h | 260 +++ GPRS/RLCHdr.h | 481 ++++ GPRS/RLCMessages.cpp | 197 ++ GPRS/RLCMessages.h | 1547 +++++++++++++ GPRS/RList.h | 214 ++ GPRS/ScalarTypes.h | 136 ++ GPRS/TBF.cpp | 1445 ++++++++++++ GPRS/TBF.h | 522 +++++ GPRS/makefile.pat | 138 ++ GPRS/makefile.tests | 60 + GPRS/notes.txt | 151 ++ GPRS/pat.txt | 788 +++++++ GPRS/pinghttp.c | 926 ++++++++ GPRS/todo.txt | 356 +++ GSM/AppInfTest.cpp | 22 + GSM/GSM610Tables.cpp | 18 +- GSM/GSM610Tables.h | 17 +- GSM/GSMCommon.cpp | 65 +- GSM/GSMCommon.h | 56 +- GSM/GSMConfig.cpp | 437 +++- GSM/GSMConfig.h | 107 +- GSM/GSML1FEC.cpp | 745 ++++-- GSM/GSML1FEC.h | 465 +++- GSM/GSML2LAPDm.cpp | 69 +- GSM/GSML2LAPDm.h | 48 +- GSM/GSML3CCElements.cpp | 47 +- GSM/GSML3CCElements.h | 23 +- GSM/GSML3CCMessages.cpp | 27 +- GSM/GSML3CCMessages.h | 45 +- GSM/GSML3CommonElements.cpp | 26 +- GSM/GSML3CommonElements.h | 20 +- GSM/GSML3GPRSElements.cpp | 375 +++ GSM/GSML3GPRSElements.h | 180 ++ GSM/GSML3MMElements.cpp | 24 +- GSM/GSML3MMElements.h | 25 +- GSM/GSML3MMMessages.cpp | 26 +- GSM/GSML3MMMessages.h | 21 +- GSM/GSML3Message.cpp | 30 +- GSM/GSML3Message.h | 35 +- GSM/GSML3RRElements.cpp | 248 +- GSM/GSML3RRElements.h | 353 ++- GSM/GSML3RRMessages.cpp | 283 ++- GSM/GSML3RRMessages.h | 417 +++- GSM/GSMLogicalChannel.cpp | 222 +- GSM/GSMLogicalChannel.h | 175 +- GSM/GSMSAPMux.cpp | 18 +- GSM/GSMSAPMux.h | 19 +- GSM/GSMSMSCBL3Messages.cpp | 115 + GSM/GSMSMSCBL3Messages.h | 182 ++ GSM/GSMTAPDump.cpp | 43 +- GSM/GSMTAPDump.h | 24 +- GSM/GSMTDMA.cpp | 35 +- GSM/GSMTDMA.h | 18 +- GSM/GSMTransfer.cpp | 82 +- GSM/GSMTransfer.h | 58 +- GSM/Makefile.am | 8 +- GSM/PhysicalStatus.cpp | 137 +- GSM/PhysicalStatus.h | 20 +- GSM/PowerManager.cpp | 18 +- GSM/PowerManager.h | 18 +- GSM/gsmtap.h | 6 +- Globals/Defines.h | 44 + Globals/Globals.cpp | 68 +- Globals/Globals.h | 34 +- Globals/Makefile.am | 4 +- LEGAL | 2 +- Makefile.am | 11 +- Makefile.common | 18 +- {AsteriskConfig => Peering}/Makefile.am | 23 +- Peering/NeighborTable.cpp | 358 +++ Peering/NeighborTable.h | 106 + Peering/Peering.cpp | 451 ++++ Peering/Peering.h | 159 ++ README | 18 +- SGSNGGSN/GPRSL3Messages.cpp | 1441 ++++++++++++ SGSNGGSN/GPRSL3Messages.h | 1080 +++++++++ SGSNGGSN/Ggsn.cpp | 628 ++++++ SGSNGGSN/Ggsn.h | 252 +++ SGSNGGSN/LLC.cpp | 751 ++++++ SGSNGGSN/LLC.h | 622 +++++ SGSNGGSN/Makefile.am | 46 + SGSNGGSN/Sgsn.cpp | 1677 ++++++++++++++ SGSNGGSN/Sgsn.h | 332 +++ SGSNGGSN/SgsnBase.h | 144 ++ SGSNGGSN/SgsnCli.cpp | 167 ++ SGSNGGSN/SgsnExport.h | 194 ++ SGSNGGSN/iputils.cpp | 504 +++++ SGSNGGSN/miniggsn.cpp | 661 ++++++ SGSNGGSN/miniggsn.h | 106 + SIP/SIPEngine.cpp | 396 ++-- SIP/SIPEngine.h | 101 +- SIP/SIPInterface.cpp | 26 +- SIP/SIPMessage.cpp | 153 +- SIP/SIPMessage.h | 26 +- SIP/SIPUtility.cpp | 20 +- SIP/SIPUtility.h | 18 +- SMS/SMSMessages.cpp | 22 +- SMS/SMSMessages.h | 26 +- SMS/SMSTransfer.cpp | 22 +- SMS/SMSTransfer.h | 21 +- TRXManager/TRXManager.cpp | 186 +- TRXManager/TRXManager.h | 76 +- Transceiver52M/Transceiver.cpp | 165 +- Transceiver52M/Transceiver.h | 22 +- TransceiverRAD1/Complex.h | 18 +- TransceiverRAD1/DummyLoad.cpp | 4 +- TransceiverRAD1/FactoryCalibration.cpp | 256 +++ TransceiverRAD1/FactoryCalibration.h | 45 + TransceiverRAD1/Makefile.am | 22 +- TransceiverRAD1/RAD1Cmd.cpp | 5 +- TransceiverRAD1/RAD1Device.cpp | 26 +- TransceiverRAD1/RAD1Device.h | 17 +- TransceiverRAD1/RAD1RxRawPower.cpp | 59 +- TransceiverRAD1/RAD1RxRawPowerSweep.cpp | 108 + TransceiverRAD1/RAD1SN.cpp | 3 +- TransceiverRAD1/RAD1ping.cpp | 11 +- TransceiverRAD1/README.DFEsymbolspaced | 14 + TransceiverRAD1/Transceiver.cpp | 446 ++-- TransceiverRAD1/Transceiver.h | 71 +- TransceiverRAD1/burn-rnrad1-eeprom.sh | 79 + TransceiverRAD1/fpga_regs.h | 25 - TransceiverRAD1/fusb.cpp | 25 - TransceiverRAD1/fusb.h | 25 - TransceiverRAD1/i2c.h | 25 - TransceiverRAD1/ids.h | 25 - TransceiverRAD1/inband-signaling-usb | 314 +++ TransceiverRAD1/interfaces.h | 25 - TransceiverRAD1/pulseApproximate.m | 15 + TransceiverRAD1/radioDevice.h | 26 +- TransceiverRAD1/radioInterface.cpp | 25 +- TransceiverRAD1/radioInterface.h | 30 +- TransceiverRAD1/rnrad1.h | 25 - TransceiverRAD1/rnrad1Core.cpp | 31 +- TransceiverRAD1/rnrad1Core.h | 25 - TransceiverRAD1/rnrad1Rx.cpp | 25 - TransceiverRAD1/rnrad1Tx.cpp | 25 - TransceiverRAD1/runTransceiver.cpp | 61 +- TransceiverRAD1/sigProcLib.cpp | 28 +- TransceiverRAD1/sigProcLib.h | 22 +- TransceiverRAD1/sigProcLibTest.cpp | 22 +- TransceiverRAD1/spi.h | 25 - apps/GetConfigurationKeys.cpp | 2761 +++++++++++++++++++++++ apps/Makefile.am | 37 +- apps/OpenBTS.cpp | 345 ++- apps/OpenBTS.example.sql | 266 ++- apps/OpenBTS.logrotate | 6 + apps/OpenBTSCLI.cpp | 39 +- apps/OpenBTSDo.cpp | 15 +- apps/exportConfigTable.sh | 3 + apps/generateConfigTable.sh | 20 + apps/generateTeX.sh | 3 + apps/importConfigTable.sh | 19 + apps/iptables.rules | 13 + apps/openbtsconfig | 7 + apps/rsyslogd.OpenBTS.conf | 4 + apps/runloop.OpenBTS.sh | 4 - configure.ac | 25 +- ctags.sh | 14 + debian/changelog | 14 +- debian/control | 11 +- debian/postinst | 2 +- debian/preinst | 18 + debian/rules | 1 + doc/Makefile.am | 3 +- doc/SoftwareP2.8Manual.pdf | Bin 465576 -> 0 bytes doxconfig | 1078 +++++++++ 219 files changed, 42300 insertions(+), 4100 deletions(-) delete mode 100644 AsteriskConfig/README.AsteriskConf delete mode 100644 AsteriskConfig/cdr.conf delete mode 100644 AsteriskConfig/extensions.conf delete mode 100644 AsteriskConfig/indications.conf delete mode 100644 AsteriskConfig/logger.conf delete mode 100644 AsteriskConfig/modules.conf delete mode 100644 AsteriskConfig/sip.conf create mode 100644 Control/RRLP_PDU_Test.cpp create mode 100644 Control/SMSCB.cpp create mode 100644 GPRS/BSSG.cpp create mode 100644 GPRS/BSSG.h create mode 100644 GPRS/BSSGMessages.cpp create mode 100644 GPRS/BSSGMessages.h create mode 100644 GPRS/ByteVector.cpp create mode 100644 GPRS/ByteVector.h create mode 100644 GPRS/CS4.txt create mode 100644 GPRS/FEC.cpp create mode 100644 GPRS/FEC.h create mode 100644 GPRS/GPRSCLI.cpp create mode 100644 GPRS/GPRSExport.h create mode 100644 GPRS/GPRSInternal.h create mode 100644 GPRS/GPRSRLC.h create mode 100644 GPRS/GPRSTDMA.h create mode 100644 GPRS/MAC.cpp create mode 100644 GPRS/MAC.h create mode 100644 GPRS/MSInfo.cpp create mode 100644 GPRS/MSInfo.h create mode 100644 GPRS/Makefile.am create mode 100644 GPRS/MsgBase.cpp create mode 100644 GPRS/MsgBase.h create mode 100644 GPRS/RLC.cpp create mode 100644 GPRS/RLCEngine.cpp create mode 100644 GPRS/RLCEngine.h create mode 100644 GPRS/RLCHdr.h create mode 100644 GPRS/RLCMessages.cpp create mode 100644 GPRS/RLCMessages.h create mode 100644 GPRS/RList.h create mode 100644 GPRS/ScalarTypes.h create mode 100644 GPRS/TBF.cpp create mode 100644 GPRS/TBF.h create mode 100644 GPRS/makefile.pat create mode 100644 GPRS/makefile.tests create mode 100644 GPRS/notes.txt create mode 100644 GPRS/pat.txt create mode 100644 GPRS/pinghttp.c create mode 100644 GPRS/todo.txt create mode 100644 GSM/AppInfTest.cpp create mode 100644 GSM/GSML3GPRSElements.cpp create mode 100644 GSM/GSML3GPRSElements.h create mode 100644 GSM/GSMSMSCBL3Messages.cpp create mode 100644 GSM/GSMSMSCBL3Messages.h create mode 100644 Globals/Defines.h rename {AsteriskConfig => Peering}/Makefile.am (74%) create mode 100644 Peering/NeighborTable.cpp create mode 100644 Peering/NeighborTable.h create mode 100644 Peering/Peering.cpp create mode 100644 Peering/Peering.h create mode 100644 SGSNGGSN/GPRSL3Messages.cpp create mode 100644 SGSNGGSN/GPRSL3Messages.h create mode 100644 SGSNGGSN/Ggsn.cpp create mode 100644 SGSNGGSN/Ggsn.h create mode 100644 SGSNGGSN/LLC.cpp create mode 100644 SGSNGGSN/LLC.h create mode 100644 SGSNGGSN/Makefile.am create mode 100644 SGSNGGSN/Sgsn.cpp create mode 100644 SGSNGGSN/Sgsn.h create mode 100644 SGSNGGSN/SgsnBase.h create mode 100644 SGSNGGSN/SgsnCli.cpp create mode 100644 SGSNGGSN/SgsnExport.h create mode 100644 SGSNGGSN/iputils.cpp create mode 100644 SGSNGGSN/miniggsn.cpp create mode 100644 SGSNGGSN/miniggsn.h create mode 100644 TransceiverRAD1/FactoryCalibration.cpp create mode 100644 TransceiverRAD1/FactoryCalibration.h create mode 100644 TransceiverRAD1/RAD1RxRawPowerSweep.cpp create mode 100644 TransceiverRAD1/README.DFEsymbolspaced create mode 100755 TransceiverRAD1/burn-rnrad1-eeprom.sh create mode 100644 TransceiverRAD1/inband-signaling-usb create mode 100644 TransceiverRAD1/pulseApproximate.m create mode 100644 apps/GetConfigurationKeys.cpp create mode 100755 apps/OpenBTS.logrotate create mode 100755 apps/exportConfigTable.sh create mode 100755 apps/generateConfigTable.sh create mode 100755 apps/generateTeX.sh create mode 100755 apps/importConfigTable.sh create mode 100644 apps/iptables.rules create mode 100755 apps/openbtsconfig create mode 100644 apps/rsyslogd.OpenBTS.conf delete mode 100755 apps/runloop.OpenBTS.sh create mode 100644 ctags.sh delete mode 100644 doc/SoftwareP2.8Manual.pdf create mode 100644 doxconfig diff --git a/AUTHORS b/AUTHORS index 598b606c..e3d7ae4d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -190,3 +190,5 @@ Alon Levy, alonlevy1@gmail.com RRLPMessages.h RRLPTest.cpp +Pat Thompson + GPRS/* diff --git a/AsteriskConfig/README.AsteriskConf b/AsteriskConfig/README.AsteriskConf deleted file mode 100644 index 36ee3a08..00000000 --- a/AsteriskConfig/README.AsteriskConf +++ /dev/null @@ -1,4 +0,0 @@ -This file contains example Asterisk configuration files for the OpenBTS Asterisk server. - -This file does not contain REAL configuration files, since those would include real IMSIs. - diff --git a/AsteriskConfig/cdr.conf b/AsteriskConfig/cdr.conf deleted file mode 100644 index c2882c1f..00000000 --- a/AsteriskConfig/cdr.conf +++ /dev/null @@ -1,148 +0,0 @@ -; -; Asterisk Call Detail Record engine configuration -; -; CDR is Call Detail Record, which provides logging services via a variety of -; pluggable backend modules. Detailed call information can be recorded to -; databases, files, etc. Useful for billing, fraud prevention, compliance with -; Sarbanes-Oxley aka The Enron Act, QOS evaluations, and more. -; - -[general] - -; Define whether or not to use CDR logging. Setting this to "no" will override -; any loading of backend CDR modules. Default is "yes". -;enable=yes - -; Define whether or not to log unanswered calls. Setting this to "yes" will -; report every attempt to ring a phone in dialing attempts, when it was not -; answered. For example, if you try to dial 3 extensions, and this option is "yes", -; you will get 3 CDR's, one for each phone that was rung. Default is "no". Some -; find this information horribly useless. Others find it very valuable. Note, in "yes" -; mode, you will see one CDR, with one of the call targets on one side, and the originating -; channel on the other, and then one CDR for each channel attempted. This may seem -; redundant, but cannot be helped. -;unanswered = no - -; Define the CDR batch mode, where instead of posting the CDR at the end of -; every call, the data will be stored in a buffer to help alleviate load on the -; asterisk server. Default is "no". -; -; WARNING WARNING WARNING -; Use of batch mode may result in data loss after unsafe asterisk termination -; ie. software crash, power failure, kill -9, etc. -; WARNING WARNING WARNING -; -;batch=no - -; Define the maximum number of CDRs to accumulate in the buffer before posting -; them to the backend engines. 'batch' must be set to 'yes'. Default is 100. -;size=100 - -; Define the maximum time to accumulate CDRs in the buffer before posting them -; to the backend engines. If this time limit is reached, then it will post the -; records, regardless of the value defined for 'size'. 'batch' must be set to -; 'yes'. Note that time is in seconds. Default is 300 (5 minutes). -;time=300 - -; The CDR engine uses the internal asterisk scheduler to determine when to post -; records. Posting can either occur inside the scheduler thread, or a new -; thread can be spawned for the submission of every batch. For small batches, -; it might be acceptable to just use the scheduler thread, so set this to "yes". -; For large batches, say anything over size=10, a new thread is recommended, so -; set this to "no". Default is "no". -;scheduleronly=no - -; When shutting down asterisk, you can block until the CDRs are submitted. If -; you don't, then data will likely be lost. You can always check the size of -; the CDR batch buffer with the CLI "cdr status" command. To enable blocking on -; submission of CDR data during asterisk shutdown, set this to "yes". Default -; is "yes". -;safeshutdown=yes - -; Normally, CDR's are not closed out until after all extensions are finished -; executing. By enabling this option, the CDR will be ended before executing -; the "h" extension so that CDR values such as "end" and "billsec" may be -; retrieved inside of of this extension. -;endbeforehexten=no - -; -; -; CHOOSING A CDR "BACKEND" (what kind of output to generate) -; -; To choose a backend, you have to make sure either the right category is -; defined in this file, or that the appropriate config file exists, and has the -; proper definitions in it. If there are any problems, usually, the entry will -; silently ignored, and you get no output. -; -; Also, please note that you can generate CDR records in as many formats as you -; wish. If you configure 5 different CDR formats, then each event will be logged -; in 5 different places! In the example config files, all formats are commented -; out except for the cdr-csv format. -; -; Here are all the possible back ends: -; -; csv, custom, manager, odbc, pgsql, radius, sqlite, tds -; (also, mysql is available via the asterisk-addons, due to licensing -; requirements) -; (please note, also, that other backends can be created, by creating -; a new backend module in the source cdr/ directory!) -; -; Some of the modules required to provide these backends will not build or install -; unless some dependency requirements are met. Examples of this are pgsql, odbc, -; etc. If you are not getting output as you would expect, the first thing to do -; is to run the command "make menuselect", and check what modules are available, -; by looking in the "2. Call Detail Recording" option in the main menu. If your -; backend is marked with XXX, you know that the "configure" command could not find -; the required libraries for that option. -; -; To get CDRs to be logged to the plain-jane /var/log/asterisk/cdr-csv/Master.csv -; file, define the [csv] category in this file. No database necessary. The example -; config files are set up to provide this kind of output by default. -; -; To get custom csv CDR records, make sure the cdr_custom.conf file -; is present, and contains the proper [mappings] section. The advantage to -; using this backend, is that you can define which fields to output, and in -; what order. By default, the example configs are set up to mimic the cdr-csv -; output. If you don't make any changes to the mappings, you are basically generating -; the same thing as cdr-csv, but expending more CPU cycles to do so! -; -; To get manager events generated, make sure the cdr_manager.conf file exists, -; and the [general] section is defined, with the single variable 'enabled = yes'. -; -; For odbc, make sure all the proper libs are installed, that "make menuselect" -; shows that the modules are available, and the cdr_odbc.conf file exists, and -; has a [global] section with the proper variables defined. -; -; For pgsql, make sure all the proper libs are installed, that "make menuselect" -; shows that the modules are available, and the cdr_pgsql.conf file exists, and -; has a [global] section with the proper variables defined. -; -; For logging to radius databases, make sure all the proper libs are installed, that -; "make menuselect" shows that the modules are available, and the [radius] -; category is defined in this file, and in that section, make sure the 'radiuscfg' -; variable is properly pointing to an existing radiusclient.conf file. -; -; For logging to sqlite databases, make sure the 'cdr.db' file exists in the log directory, -; which is usually /var/log/asterisk. Of course, the proper libraries should be available -; during the 'configure' operation. -; -; For tds logging, make sure the proper libraries are available during the 'configure' -; phase, and that cdr_tds.conf exists and is properly set up with a [global] category. -; -; Also, remember, that if you wish to log CDR info to a database, you will have to define -; a specific table in that databse to make things work! See the doc directory for more details -; on how to create this table in each database. -; - -[csv] -usegmtime=yes ; log date/time in GMT. Default is "no" -loguniqueid=yes ; log uniqueid. Default is "no -loguserfield=yes ; log user field. Default is "no - -;[radius] -;usegmtime=yes ; log date/time in GMT -;loguniqueid=yes ; log uniqueid -;loguserfield=yes ; log user field -; Set this to the location of the radiusclient-ng configuration file -; The default is /etc/radiusclient-ng/radiusclient.conf -;radiuscfg => /usr/local/etc/radiusclient-ng/radiusclient.conf diff --git a/AsteriskConfig/extensions.conf b/AsteriskConfig/extensions.conf deleted file mode 100644 index 7fca0eea..00000000 --- a/AsteriskConfig/extensions.conf +++ /dev/null @@ -1,57 +0,0 @@ -[globals] - - -[default] -; This is the context for handsets that are allowed to attached via open registration. -; Normally, this context is only used for testing. - -; These are test extensions that you might want to disable after installation. - -; Create an extension, 2600, for evaluating echo latency. -exten => 2600,1,Answer() ; Do the echo test -exten => 2600,n,Echo ; Do the echo test -exten => 2600,n,Hangup - -; The 2101 extension is used for factory testing with zoiper. -exten => 2101,1,Dial(SIP/zoiper) - -; The 2100 extension is for factory testing with the test SIM. -exten => 2100,1,Dial(SIP/IMSI001010000000000) - - - -[outbound-trunk] -; If you had an external trunk, you would dial it here. -exten => _N.,1,Answer() - - - - - -[phones] -; This is the context for handsets provisioned through the realtime database. -; This assumes that OpenBTS units all are running their SIP interfaces on port 5062. -exten => _N.,1,Set(Name=${ODBC_SQL(select dial from dialdata_table where exten = \"${EXTEN}\")}) -exten => _N.,n,GotoIf($["${Name}" = ""] ?outbound-trunk,${EXTEN},1) -exten => _N.,n,Set(IPAddr=${ODBC_SQL(select ipaddr from sip_buddies where name = \"${Name}\")}) -exten => _N.,n,GotoIf($["${IPAddr}" = ""] ?outbound-trunk,${EXTEN},1) -exten => _N.,n,Dial(SIP/${Name}@${IPAddr}:5062) - - - -[sip-local] -; This context is the union of all of the in-network contexts. -include => default -include => phones - - - - -[sip-external] -; This is the top-level context that gives access to out-of-network calling. -; also includes the in-network calling. -include => sip-local -include => outbound-trunk - - - diff --git a/AsteriskConfig/indications.conf b/AsteriskConfig/indications.conf deleted file mode 100644 index 03a3fadb..00000000 --- a/AsteriskConfig/indications.conf +++ /dev/null @@ -1,733 +0,0 @@ -; indications.conf -; Configuration file for location specific tone indications -; used by the pbx_indications module. -; -; NOTE: -; When adding countries to this file, please keep them in alphabetical -; order according to the 2-character country codes! -; -; The [general] category is for certain global variables. -; All other categories are interpreted as location specific indications -; -; -[general] -country=us ; default location - - -; [example] -; description = string -; The full name of your country, in English. -; alias = iso[,iso]* -; List of other countries 2-letter iso codes, which have the same -; tone indications. -; ringcadence = num[,num]* -; List of durations the physical bell rings. -; dial = tonelist -; Set of tones to be played when one picks up the hook. -; busy = tonelist -; Set of tones played when the receiving end is busy. -; congestion = tonelist -; Set of tones played when there is some congestion (on the network?) -; callwaiting = tonelist -; Set of tones played when there is a call waiting in the background. -; dialrecall = tonelist -; Not well defined; many phone systems play a recall dial tone after hook -; flash. -; record = tonelist -; Set of tones played when call recording is in progress. -; info = tonelist -; Set of tones played with special information messages (e.g., "number is -; out of service") -; 'name' = tonelist -; Every other variable will be available as a shortcut for the "PlayList" command -; but will not be used automatically by Asterisk. -; -; -; The tonelist itself is defined by a comma-separated sequence of elements. -; Each element consist of a frequency (f) with an optional duration (in ms) -; attached to it (f/duration). The frequency component may be a mixture of two -; frequencies (f1+f2) or a frequency modulated by another frequency (f1*f2). -; The implicit modulation depth is fixed at 90%, though. -; If the list element starts with a !, that element is NOT repeated, -; therefore, only if all elements start with !, the tonelist is time-limited, -; all others will repeat indefinitely. -; -; concisely: -; element = [!]freq[+|*freq2][/duration] -; tonelist = element[,element]* -; -; Please note that SPACES ARE NOT ALLOWED in tone lists! -; - -[at] -description = Austria -ringcadence = 1000,5000 -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -dial = 420 -busy = 420/400,0/400 -ring = 420/1000,0/5000 -congestion = 420/200,0/200 -callwaiting = 420/40,0/1960 -dialrecall = 420 -; RECORDTONE - not specified -record = 1400/80,0/14920 -info = 950/330,1450/330,1850/330,0/1000 -stutter = 380+420 - -[au] -description = Australia -; Reference http://www.acif.org.au/__data/page/3303/S002_2001.pdf -; Normal Ring -ringcadence = 400,200,400,2000 -; Distinctive Ring 1 - Forwarded Calls -; 400,400,200,200,400,1400 -; Distinctive Ring 2 - Selective Ring 2 + Operator + Recall -; 400,400,200,2000 -; Distinctive Ring 3 - Multiple Subscriber Number 1 -; 200,200,400,2200 -; Distinctive Ring 4 - Selective Ring 1 + Centrex -; 400,2600 -; Distinctive Ring 5 - Selective Ring 3 -; 400,400,200,400,200,1400 -; Distinctive Ring 6 - Multiple Subscriber Number 2 -; 200,400,200,200,400,1600 -; Distinctive Ring 7 - Multiple Subscriber Number 3 + Data Privacy -; 200,400,200,400,200,1600 -; Tones -dial = 413+438 -busy = 425/375,0/375 -ring = 413+438/400,0/200,413+438/400,0/2000 -; XXX Congestion: Should reduce by 10 db every other cadence XXX -congestion = 425/375,0/375,420/375,0/375 -callwaiting = 425/200,0/200,425/200,0/4400 -dialrecall = 413+438 -; Record tone used for Call Intrusion/Recording or Conference -record = !425/1000,!0/15000,425/360,0/15000 -info = 425/2500,0/500 -; Other Australian Tones -; The STD "pips" indicate the call is not an untimed local call -std = !525/100,!0/100,!525/100,!0/100,!525/100,!0/100,!525/100,!0/100,!525/100 -; Facility confirmation tone (eg. Call Forward Activated) -facility = 425 -; Message Waiting "stutter" dialtone -stutter = 413+438/100,0/40 -; Ringtone for calls to Telstra mobiles -ringmobile = 400+450/400,0/200,400+450/400,0/2000 - -[bg] -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -description = Bulgaria -ringdance = 1000,4000 -; -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/250,0/250 -callwaiting = 425/150,0/150,425/150,0/4000 -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -record = 1400/425,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -stutter = 425/1500,0/100 - -[br] -description = Brazil -ringcadence = 1000,4000 -dial = 425 -busy = 425/250,0/250 -ring = 425/1000,0/4000 -congestion = 425/250,0/250,425/750,0/250 -callwaiting = 425/50,0/1000 -; Dialrecall not used in Brazil standard (using UK standard) -dialrecall = 350+440 -; Record tone is not used in Brazil, use busy tone -record = 425/250,0/250 -; Info not used in Brazil standard (using UK standard) -info = 950/330,1400/330,1800/330 -stutter = 350+440 - -[be] -description = Belgium -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,3000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/3000 -congestion = 425/167,0/167 -callwaiting = 1400/175,0/175,1400/175,0/3500 -; DIALRECALL - not specified -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440" -; RECORDTONE - not specified -record = 1400/500,0/15000 -info = 900/330,1400/330,1800/330,0/1000 -stutter = 425/1000,0/250 - -[ch] -description = Switzerland -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/200,0/200,425/200,0/4000 -; DIALRECALL - not specified -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -; RECORDTONE - not specified -record = 1400/80,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -stutter = 425+340/1100,0/1100 - -[cl] -description = Chile -; According to specs from Telefonica CTC Chile -ringcadence = 1000,3000 -dial = 400 -busy = 400/500,0/500 -ring = 400/1000,0/3000 -congestion = 400/200,0/200 -callwaiting = 400/250,0/8750 -dialrecall = !400/100,!0/100,!400/100,!0/100,!400/100,!0/100,400 -record = 1400/500,0/15000 -info = 950/333,1400/333,1800/333,0/1000 -stutter = !400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,400 - -[cn] -description = China -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 450 -busy = 450/350,0/350 -ring = 450/1000,0/4000 -congestion = 450/700,0/700 -callwaiting = 450/400,0/4000 -dialrecall = 450 -record = 950/400,0/10000 -info = 450/100,0/100,450/100,0/100,450/100,0/100,450/400,0/400 -; STUTTER - not specified -stutter = 450+425 - -[cz] -description = Czech Republic -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425/330,0/330,425/660,0/660 -busy = 425/330,0/330 -ring = 425/1000,0/4000 -congestion = 425/165,0/165 -callwaiting = 425/330,0/9000 -; DIALRECALL - not specified -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425/330,0/330,425/660,0/660 -; RECORDTONE - not specified -record = 1400/500,0/14000 -info = 950/330,0/30,1400/330,0/30,1800/330,0/1000 -; STUTTER - not specified -stutter = 425/450,0/50 - -[de] -description = Germany -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425 -busy = 425/480,0/480 -ring = 425/1000,0/4000 -congestion = 425/240,0/240 -callwaiting = !425/200,!0/200,!425/200,!0/5000,!425/200,!0/200,!425/200,!0/5000,!425/200,!0/200,!425/200,!0/5000,!425/200,!0/200,!425/200,!0/5000,!425/200,!0/200,!425/200,0 -; DIALRECALL - not specified -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -; RECORDTONE - not specified -record = 1400/80,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -stutter = 425+400 - -[dk] -description = Denmark -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = !425/200,!0/600,!425/200,!0/3000,!425/200,!0/200,!425/200,0 -; DIALRECALL - not specified -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -; RECORDTONE - not specified -record = 1400/80,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -; STUTTER - not specified -stutter = 425/450,0/50 - -[ee] -description = Estonia -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425 -busy = 425/300,0/300 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -; CALLWAIT not in accordance to ITU -callwaiting = 950/650,0/325,950/325,0/30,1400/1300,0/2600 -; DIALRECALL - not specified -dialrecall = 425/650,0/25 -; RECORDTONE - not specified -record = 1400/500,0/15000 -; INFO not in accordance to ITU -info = 950/650,0/325,950/325,0/30,1400/1300,0/2600 -; STUTTER not specified -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 - -[es] -description = Spain -ringcadence = 1500,3000 -dial = 425 -busy = 425/200,0/200 -ring = 425/1500,0/3000 -congestion = 425/200,0/200,425/200,0/200,425/200,0/600 -callwaiting = 425/175,0/175,425/175,0/3500 -dialrecall = !425/200,!0/200,!425/200,!0/200,!425/200,!0/200,425 -record = 1400/500,0/15000 -info = 950/330,0/1000 -dialout = 500 - - -[fi] -description = Finland -ringcadence = 1000,4000 -dial = 425 -busy = 425/300,0/300 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/150,0/150,425/150,0/8000 -dialrecall = 425/650,0/25 -record = 1400/500,0/15000 -info = 950/650,0/325,950/325,0/30,1400/1300,0/2600 -stutter = 425/650,0/25 - -[fr] -description = France -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1500,3500 -; Dialtone can also be 440+330 -dial = 440 -busy = 440/500,0/500 -ring = 440/1500,0/3500 -; CONGESTION - not specified -congestion = 440/250,0/250 -callwait = 440/300,0/10000 -; DIALRECALL - not specified -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 -; RECORDTONE - not specified -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330 -stutter = !440/100,!0/100,!440/100,!0/100,!440/100,!0/100,!440/100,!0/100,!440/100,!0/100,!440/100,!0/100,440 - -[gr] -description = Greece -ringcadence = 1000,4000 -dial = 425/200,0/300,425/700,0/800 -busy = 425/300,0/300 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/150,0/150,425/150,0/8000 -dialrecall = 425/650,0/25 -record = 1400/400,0/15000 -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,0 -stutter = 425/650,0/25 - -[hu] -description = Hungary -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1250,3750 -dial = 425 -busy = 425/300,0/300 -ring = 425/1250,0/3750 -congestion = 425/300,0/300 -callwaiting = 425/40,0/1960 -dialrecall = 425+450 -; RECORDTONE - not specified -record = 1400/400,0/15000 -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,0 -stutter = 350+375+400 - -[il] -description = Israel -ringcadence = 1000,3000 -dial = 414 -busy = 414/500,0/500 -ring = 414/1000,0/3000 -congestion = 414/250,0/250 -callwaiting = 414/100,0/100,414/100,0/100,414/600,0/3000 -dialrecall = !414/100,!0/100,!414/100,!0/100,!414/100,!0/100,414 -record = 1400/500,0/15000 -info = 1000/330,1400/330,1800/330,0/1000 -stutter = !414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,!414/160,!0/160,414 - - -[in] -description = India -ringcadence = 400,200,400,2000 -dial = 400*25 -busy = 400/750,0/750 -ring = 400*25/400,0/200,400*25/400,0/2000 -congestion = 400/250,0/250 -callwaiting = 400/200,0/100,400/200,0/7500 -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,0/1000 -stutter = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 - -[it] -description = Italy -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -dial = 425/200,0/200,425/600,0/1000 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/400,0/100,425/250,0/100,425/150,0/14000 -dialrecall = 470/400,425/400 -record = 1400/400,0/15000 -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,0 -stutter = 470/400,425/400 - -[lt] -description = Lithuania -ringcadence = 1000,4000 -dial = 425 -busy = 425/350,0/350 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/150,0/150,425/150,0/4000 -; DIALRECALL - not specified -dialrecall = 425/500,0/50 -; RECORDTONE - not specified -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,0 -; STUTTER - not specified -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 - -[jp] -description = Japan -ringcadence = 1000,2000 -dial = 400 -busy = 400/500,0/500 -ring = 400+15/1000,0/2000 -congestion = 400/500,0/500 -callwaiting = 400+16/500,0/8000 -dialrecall = !400/200,!0/200,!400/200,!0/200,!400/200,!0/200,400 -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,0 -stutter = !400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,!400/100,!0/100,400 - -[mx] -description = Mexico -ringcadence = 2000,4000 -dial = 425 -busy = 425/250,0/250 -ring = 425/1000,0/4000 -congestion = 425/250,0/250 -callwaiting = 425/200,0/600,425/200,0/10000 -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 -record = 1400/500,0/15000 -info = 950/330,0/30,1400/330,0/30,1800/330,0/1000 -stutter = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 - -[my] -description = Malaysia -ringcadence = 2000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/400,0/200 -congestion = 425/500,0/500 - -[nl] -description = Netherlands -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -ringcadence = 1000,4000 -; Most of these 425's can also be 450's -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/250,0/250 -callwaiting = 425/500,0/9500 -; DIALRECALL - not specified -dialrecall = 425/500,0/50 -; RECORDTONE - not specified -record = 1400/500,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -stutter = 425/500,0/50 - -[no] -description = Norway -ringcadence = 1000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/200,0/200 -callwaiting = 425/200,0/600,425/200,0/10000 -dialrecall = 470/400,425/400 -record = 1400/400,0/15000 -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,0 -stutter = 470/400,425/400 - -[nz] -description = New Zealand -;NOTE - the ITU has different tonesets for NZ, but according to some residents there, -; this is, indeed, the correct way to do it. -ringcadence = 400,200,400,2000 -dial = 400 -busy = 400/250,0/250 -ring = 400+450/400,0/200,400+450/400,0/2000 -congestion = 400/375,0/375 -callwaiting = !400/200,!0/3000,!400/200,!0/3000,!400/200,!0/3000,!400/200 -dialrecall = !400/100!0/100,!400/100,!0/100,!400/100,!0/100,400 -record = 1400/425,0/15000 -info = 400/750,0/100,400/750,0/100,400/750,0/100,400/750,0/400 -stutter = !400/100!0/100,!400/100,!0/100,!400/100,!0/100,!400/100!0/100,!400/100,!0/100,!400/100,!0/100,400 -unobtainable = 400/75,0/100,400/75,0/100,400/75,0/100,400/75,0/400 - -[ph] - -; reference http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf - -description = Philippines -ringcadence = 1000,4000 -dial = 425 -busy = 480+620/500,0/500 -ring = 425+480/1000,0/4000 -congestion = 480+620/250,0/250 -callwaiting = 440/300,0/10000 -; DIALRECALL - not specified -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 -; RECORDTONE - not specified -record = 1400/500,0/15000 -; INFO - not specified -info = !950/330,!1400/330,!1800/330,0 -; STUTTER - not specified -stutter = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 - - -[pl] -description = Poland -ringcadence = 1000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/500,0/500 -callwaiting = 425/150,0/150,425/150,0/4000 -; DIALRECALL - not specified -dialrecall = 425/500,0/50 -; RECORDTONE - not specified -record = 1400/500,0/15000 -; 950/1400/1800 3x0.33 on 1.0 off repeated 3 times -info = !950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000,!950/330,!1400/330,!1800/330,!0/1000 -; STUTTER - not specified -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 - -[pt] -description = Portugal -ringcadence = 1000,5000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/5000 -congestion = 425/200,0/200 -callwaiting = 440/300,0/10000 -dialrecall = 425/1000,0/200 -record = 1400/500,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 - -[ru] -; References: -; http://www.minsvyaz.ru/site.shtml?id=1806 -; http://www.aboutphone.info/lib/gost/45-223-2001.html -description = Russian Federation / ex Soviet Union -ringcadence = 1000,4000 -dial = 425 -busy = 425/350,0/350 -ring = 425/1000,0/4000 -congestion = 425/175,0/175 -callwaiting = 425/200,0/5000 -record = 1400/400,0/15000 -info = 950/330,1400/330,1800/330,0/1000 -dialrecall = 425/400,0/40 -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 - -[se] -description = Sweden -ringcadence = 1000,5000 -dial = 425 -busy = 425/250,0/250 -ring = 425/1000,0/5000 -congestion = 425/250,0/750 -callwaiting = 425/200,0/500,425/200,0/9100 -dialrecall = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -record = 1400/500,0/15000 -info = !950/332,!0/24,!1400/332,!0/24,!1800/332,!0/2024,!950/332,!0/24,!1400/332,!0/24,!1800/332,!0/2024,!950/332,!0/24,!1400/332,!0/24,!1800/332,!0/2024,!950/332,!0/24,!1400/332,!0/24,!1800/332,!0/2024,!950/332,!0/24,!1400/332,!0/24,!1800/332,0 -stutter = !425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,!425/100,!0/100,425 -; stutter = 425/320,0/20 ; Real swedish standard, not used for now - -[sg] -description = Singapore -; Singapore -; Reference: http://www.ida.gov.sg/idaweb/doc/download/I397/ida_ts_pstn1_i4r2.pdf -; Frequency specs are: 425 Hz +/- 20Hz; 24 Hz +/- 2Hz; modulation depth 100%; SIT +/- 50Hz -ringcadence = 400,200,400,2000 -dial = 425 -ring = 425*24/400,0/200,425*24/400,0/2000 ; modulation should be 100%, not 90% -busy = 425/750,0/750 -congestion = 425/250,0/250 -callwaiting = 425*24/300,0/200,425*24/300,0/3200 -stutter = !425/200,!0/200,!425/600,!0/200,!425/200,!0/200,!425/600,!0/200,!425/200,!0/200,!425/600,!0/200,!425/200,!0/200,!425/600,!0/200,425 -info = 950/330,1400/330,1800/330,0/1000 ; not currently in use acc. to reference -dialrecall = 425*24/500,0/500,425/500,0/2500 ; unspecified in IDA reference, use repeating Holding Tone A,B -record = 1400/500,0/15000 ; unspecified in IDA reference, use 0.5s tone every 15s -; additionally defined in reference -nutone = 425/2500,0/500 -intrusion = 425/250,0/2000 -warning = 425/624,0/4376 ; end of period tone, warning -acceptance = 425/125,0/125 -holdinga = !425*24/500,!0/500 ; followed by holdingb -holdingb = !425/500,!0/2500 - -[th] -description = Thailand -ringcadence = 1000,4000 -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -dial = 400*50 -busy = 400/500,0/500 -ring = 420/1000,0/5000 -congestion = 400/300,0/300 -callwaiting = 1000/400,10000/400,1000/400 -; DIALRECALL - not specified - use special dial tone instead. -dialrecall = 400*50/400,0/100,400*50/400,0/100 -; RECORDTONE - not specified -record = 1400/500,0/15000 -; INFO - specified as an announcement - use special information tones instead -info = 950/330,1400/330,1800/330 -; STUTTER - not specified -stutter = !400/200,!0/200,!400/600,!0/200,!400/200,!0/200,!400/600,!0/200,!400/200,!0/200,!400/600,!0/200,!400/200,!0/200,!400/600,!0/200,400 - -[uk] -description = United Kingdom -ringcadence = 400,200,400,2000 -; These are the official tones taken from BT SIN350. The actual tones -; used by BT include some volume differences so sound slightly different -; from Asterisk-generated ones. -dial = 350+440 -; Special dial is the intermittent dial tone heard when, for example, -; you have a divert active on the line -specialdial = 350+440/750,440/750 -; Busy is also called "Engaged" -busy = 400/375,0/375 -; "Congestion" is the Beep-bip engaged tone -congestion = 400/400,0/350,400/225,0/525 -; "Special Congestion" is not used by BT very often if at all -specialcongestion = 400/200,1004/300 -unobtainable = 400 -ring = 400+450/400,0/200,400+450/400,0/2000 -callwaiting = 400/100,0/4000 -; BT seem to use "Special Call Waiting" rather than just "Call Waiting" tones -specialcallwaiting = 400/250,0/250,400/250,0/250,400/250,0/5000 -; "Pips" used by BT on payphones. (Sounds wrong, but this is what BT claim it -; is and I've not used a payphone for years) -creditexpired = 400/125,0/125 -; These two are used to confirm/reject service requests on exchanges that -; don't do voice announcements. -confirm = 1400 -switching = 400/200,0/400,400/2000,0/400 -; This is the three rising tones Doo-dah-dee "Special Information Tone", -; usually followed by the BT woman saying an appropriate message. -info = 950/330,0/15,1400/330,0/15,1800/330,0/1000 -; Not listed in SIN350 -record = 1400/500,0/60000 -stutter = 350+440/750,440/750 - -[us] -description = United States / North America -ringcadence = 2000,4000 -dial = 350+440 -busy = 480+620/500,0/500 -ring = 440+480/2000,0/4000 -congestion = 480+620/250,0/250 -callwaiting = 440/300,0/10000 -dialrecall = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,0 -stutter = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 - -[us-old] -description = United States Circa 1950/ North America -ringcadence = 2000,4000 -dial = 600*120 -busy = 500*100/500,0/500 -ring = 420*40/2000,0/4000 -congestion = 500*100/250,0/250 -callwaiting = 440/300,0/10000 -dialrecall = !600*120/100,!0/100,!600*120/100,!0/100,!600*120/100,!0/100,600*120 -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,0 -stutter = !600*120/100,!0/100,!600*120/100,!0/100,!600*120/100,!0/100,!600*120/100,!0/100,!600*120/100,!0/100,!600*120/100,!0/100,600*120 - -[tw] -description = Taiwan -; http://nemesis.lonestar.org/reference/telecom/signaling/dialtone.html -; http://nemesis.lonestar.org/reference/telecom/signaling/busy.html -; http://www.iproducts.com.tw/ee/kylink/06ky-1000a.htm -; http://www.pbx-manufacturer.com/ky120dx.htm -; http://www.nettwerked.net/tones.txt -; http://www.cisco.com/univercd/cc/td/doc/product/tel_pswt/vco_prod/taiw_sup/taiw2.htm -; -; busy tone 480+620Hz 0.5 sec. on ,0.5 sec. off -; reorder tone 480+620Hz 0.25 sec. on,0.25 sec. off -; ringing tone 440+480Hz 1 sec. on ,2 sec. off -; -ringcadence = 1000,4000 -dial = 350+440 -busy = 480+620/500,0/500 -ring = 440+480/1000,0/2000 -congestion = 480+620/250,0/250 -callwaiting = 350+440/250,0/250,350+440/250,0/3250 -dialrecall = 300/1500,0/500 -record = 1400/500,0/15000 -info = !950/330,!1400/330,!1800/330,0 -stutter = !350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,!350+440/100,!0/100,350+440 - -[ve] -; Tone definition source for ve found on -; Reference: http://www.itu.int/ITU-T/inr/forms/files/tones-0203.pdf -description = Venezuela / South America -ringcadence = 1000,4000 -dial = 425 -busy = 425/500,0/500 -ring = 425/1000,0/4000 -congestion = 425/250,0/250 -callwaiting = 400+450/300,0/6000 -dialrecall = 425 -record = 1400/500,0/15000 -info = !950/330,!1440/330,!1800/330,0/1000 - - -[za] -description = South Africa -; http://www.cisco.com/univercd/cc/td/doc/product/tel_pswt/vco_prod/safr_sup/saf02.htm -; (definitions for other countries can also be found there) -; Note, though, that South Africa uses two switch types in their network -- -; Alcatel switches -- mainly in the Western Cape, and Siemens elsewhere. -; The former use 383+417 in dial, ringback etc. The latter use 400*33 -; I've provided both, uncomment the ones you prefer -ringcadence = 400,200,400,2000 -; dial/ring/callwaiting for the Siemens switches: -dial = 400*33 -ring = 400*33/400,0/200,400*33/400,0/2000 -callwaiting = 400*33/250,0/250,400*33/250,0/250,400*33/250,0/250,400*33/250,0/250 -; dial/ring/callwaiting for the Alcatel switches: -; dial = 383+417 -; ring = 383+417/400,0/200,383+417/400,0/2000 -; callwaiting = 383+417/250,0/250,383+417/250,0/250,383+417/250,0/250,383+417/250,0/250 -congestion = 400/250,0/250 -busy = 400/500,0/500 -dialrecall = 350+440 -; XXX Not sure about the RECORDTONE -record = 1400/500,0/10000 -info = 950/330,1400/330,1800/330,0/330 -stutter = !400*33/100,!0/100,!400*33/100,!0/100,!400*33/100,!0/100,!400*33/100,!0/100,!400*33/100,!0/100,!400*33/100,!0/100,400*33 diff --git a/AsteriskConfig/logger.conf b/AsteriskConfig/logger.conf deleted file mode 100644 index 0ac424ac..00000000 --- a/AsteriskConfig/logger.conf +++ /dev/null @@ -1,69 +0,0 @@ -; -; Logging Configuration -; -; In this file, you configure logging to files or to -; the syslog system. -; -; "logger reload" at the CLI will reload configuration -; of the logging system. - -[general] -; Customize the display of debug message time stamps -; this example is the ISO 8601 date format (yyyy-mm-dd HH:MM:SS) -; see strftime(3) Linux manual for format specifiers -;dateformat=%F %T -; -; This appends the hostname to the name of the log files. -;appendhostname = yes -; -; This determines whether or not we log queue events to a file -; (defaults to yes). -;queue_log = no -; -; This determines whether or not we log generic events to a file -; (defaults to yes). -;event_log = no -; -; -; For each file, specify what to log. -; -; For console logging, you set options at start of -; Asterisk with -v for verbose and -d for debug -; See 'asterisk -h' for more information. -; -; Directory for log files is configures in asterisk.conf -; option astlogdir -; -[logfiles] -; -; Format is "filename" and then "levels" of debugging to be included: -; debug -; notice -; warning -; error -; verbose -; dtmf -; -; Special filename "console" represents the system console -; -; We highly recommend that you DO NOT turn on debug mode if you are simply -; running a production system. Debug mode turns on a LOT of extra messages, -; most of which you are unlikely to understand without an understanding of -; the underlying code. Do NOT report debug messages as code issues, unless -; you have a specific issue that you are attempting to debug. They are -; messages for just that -- debugging -- and do not rise to the level of -; something that merit your attention as an Asterisk administrator. Debug -; messages are also very verbose and can and do fill up logfiles quickly; -; this is another reason not to have debug mode on a production system unless -; you are in the process of debugging a specific issue. -; -;debug => debug -console => notice,warning,error -;console => notice,warning,error,debug -;messages => notice,warning,error -;full => notice,warning,error,debug,verbose - -;syslog keyword : This special keyword logs to syslog facility -; -;syslog.local0 => notice,warning,error -; diff --git a/AsteriskConfig/modules.conf b/AsteriskConfig/modules.conf deleted file mode 100644 index c2f2ba7e..00000000 --- a/AsteriskConfig/modules.conf +++ /dev/null @@ -1,36 +0,0 @@ -; -; Asterisk configuration file -; -; Module Loader configuration file -; - -[modules] -autoload=yes -; -; Any modules that need to be loaded before the Asterisk core has been -; initialized (just after the logger has been initialized) can be loaded -; using 'preload'. This will frequently be needed if you wish to map all -; module configuration files into Realtime storage, since the Realtime -; driver will need to be loaded before the modules using those configuration -; files are initialized. -; -; An example of loading ODBC support would be: -;preload => res_odbc.so -;preload => res_config_odbc.so -; -; Uncomment the following if you wish to use the Speech Recognition API -;preload => res_speech.so -; -; If you want, load the GTK console right away. -; -noload => pbx_gtkconsole.so -;load => pbx_gtkconsole.so -; -noload => res_musiconhold.so -;load => res_musiconhold.so -; -; Load either OSS or ALSA, not both -; By default, load OSS only (automatically) and do not load ALSA -; -noload => chan_alsa.so -;noload => chan_oss.so diff --git a/AsteriskConfig/sip.conf b/AsteriskConfig/sip.conf deleted file mode 100644 index bb7a8b95..00000000 --- a/AsteriskConfig/sip.conf +++ /dev/null @@ -1,91 +0,0 @@ -[general] -bindport=5060 ; asterisk 1.6 - ; UDP Port to bind to (SIP standard port for unencrypted UDP - ; and TCP sessions is 5060) - ; bindport is the local UDP port that Asterisk will listen on -bindaddr=0.0.0.0 ; asterisk 1.6 - ; IP address to bind UDP listen socket to (0.0.0.0 binds to all) - ; You can specify port here too, like 123.123.123.123:5080 -udpbindaddr=0.0.0.0 ; asterisk 1.8 - ; IP address to bind UDP listen socket to (0.0.0.0 binds to all) - ; Optionally add a port number, 192.168.1.1:5062 (default is port 5060) - -tos_sip=cs3 ; Sets TOS for SIP packets. -tos_audio=ef ; Sets TOS for RTP audio packets. -tos_video=af41 ; Sets TOS for RTP video packets. -tos_text=af41 ; Sets TOS for RTP text packets. - -cos_sip=3 ; Sets 802.1p priority for SIP packets. -cos_audio=5 ; Sets 802.1p priority for RTP audio packets. -cos_video=4 ; Sets 802.1p priority for RTP video packets. -cos_text=3 ; Sets 802.1p priority for RTP text packets. - -maxexpiry=3600 ; Maximum allowed time of incoming registrations - ; and subscriptions (seconds) -minexpiry=60 ; Minimum length of registrations/subscriptions (default 60) -defaultexpiry=3600 ; Default length of incoming/outgoing registration -dynamic_exclude_static=yes ; Disallow all dynamic hosts from registering - ; as any IP address used for staticly defined - ; hosts. This helps avoid the configuration - ; error of allowing your users to register at - ; the same address as a SIP provider. -use_q850_reason=yes ; Set to yes add Reason header and use Reason header if it is available. - -;t1min=100 ; Minimum roundtrip time for messages to monitored hosts - ; Defaults to 100 ms -;timert1=500 ; Default T1 timer - ; Defaults to 500 ms or the measured round-trip - ; time to a peer (qualify=yes). -;timerb=32000 ; Call setup timer. If a provisional response is not received - ; in this amount of time, the call will autocongest - ; Defaults to 64*timert1 -rtptimeout=60 ; Terminate call if 60 seconds of no RTP or RTCP activity - ; on the audio channel - ; when we're not on hold. This is to be able to hangup - ; a call in the case of a phone disappearing from the net, - ; like a powerloss or grandma tripping over a cable. -rtpholdtimeout=300 ; Terminate call if 300 seconds of no RTP or RTCP activity - ; on the audio channel - ; when we're on hold (must be > rtptimeout) - -;allowguest=no ; Allow or reject guest calls (default is yes) -autocreatepeer=yes ; The Autocreatepeer option allows, - ; if set to Yes, any SIP ua to register with your Asterisk PBX as a peer. - ; This peer's settings will be based on global options. - ; The peer's name will be based on the user part of the Contact: header field's URL. - -context=phones ; Default context for incoming calls -allowoverlap=no ; Disable overlap dialing support. (Default is yes) - -disallow=all ; need to disallow=all before we can use allow= -allow=gsm ; GSM -allow=ulaw ; ISDN US -allow=alaw ; ISDN EU - -relaxdtmf=yes ; Relax dtmf handling -dtmfmode=auto ; Set default dtmfmode for sending DTMF. Default: rfc2833 - ; Other options: - ; info : SIP INFO messages (application/dtmf-relay) - ; shortinfo : SIP INFO messages (application/dtmf) - ; inband : Inband audio (requires 64 kbit codec -alaw, ulaw) - ; auto : Use rfc2833 if offered, inband otherwise - - -; Zoiper is used as a fixture for factory testing. -[zoiper] -secret=3078923984 -callerid=2101 -canreinvite=no -type=friend -context=sip-local -host=dynamic -dtmfmode=auto - -; This is a test SIM provided with the BTS. -[IMSI001010000000000] -callerid=2100 -canreinvite=no -type=friend -context=sip-local -host=dynamic - diff --git a/CLI/CLI.cpp b/CLI/CLI.cpp index 25533ff8..5a06b69b 100644 --- a/CLI/CLI.cpp +++ b/CLI/CLI.cpp @@ -1,25 +1,18 @@ /* * Copyright 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -30,6 +23,8 @@ #include #include #include +#include +#include #include @@ -45,6 +40,14 @@ #include #include #include +#include +#include +#include +#include + +namespace SGSN { + extern int sgsnCLI(int argc, char **argv, std::ostream &os); +}; #include @@ -79,13 +82,8 @@ static const char* standardResponses[] = { int Parser::execute(char* line, ostream& os) const { - // escape to the shell? - if (line[0]=='!') { - os << endl; - int retVal = system(line+1); - os << endl << "External call returned " << retVal << endl; - return SUCCESS; - } + LOG(INFO) << "executing console command: " << line; + // tokenize char *argv[mMaxArgs]; int argc = 0; @@ -149,7 +147,14 @@ int uptime(int argc, char** argv, ostream& os) { if (argc!=1) return BAD_NUM_ARGS; os.precision(2); - os << "Unix time " << time(NULL) << endl; + + time_t now = time(NULL); + const char* timestring = ctime(&now); + // no endl since ctime includes a "\n" in the string + os << "Unix time " << now << ", " << timestring; + + os << "watchdog timer expires in " << (gWatchdogRemaining() / 60) << " minutes" << endl; + int seconds = gBTS.uptime(); if (seconds<120) { os << "uptime " << seconds << " seconds, frame " << gBTS.time() << endl; @@ -167,6 +172,7 @@ int uptime(int argc, char** argv, ostream& os) } float days = hours / 24.0F; os << "uptime " << days << " days, frame " << gBTS.time() << endl; + return SUCCESS; } @@ -192,8 +198,6 @@ int showHelp(int argc, char** argv, ostream& os) if (c%cols==0) os << endl; } if (c%cols!=0) os << endl; - os << endl << "Lines starting with '!' are escaped to the shell." << endl; - os << endl << "Use , to detach from \"screen\", *not* ." << endl << endl; return SUCCESS; } @@ -248,9 +252,10 @@ int dumpTMSIs(const char* filename) fileout.open(filename, ios::out); // erases existing! // FIXME -- Check that the file really opened. // Fake an argument list to call printTMSIs. - char* subargv[] = {"tmsis", NULL}; + const char* subargv[] = {"tmsis", NULL}; int subargc = 1; - return tmsis(subargc, subargv, fileout); + // (pat) Cast makes gcc happy about const conversion. + return tmsis(subargc, const_cast(subargv), fileout); } @@ -286,12 +291,10 @@ int isIMSI(const char *imsi) { if (!imsi) return 0; - - size_t imsiLen = strlen(imsi); - if (imsiLen != 15) + if (strlen(imsi) != 15) return 0; - for (size_t i = 0; i < imsiLen; i++) { + for (unsigned i = 0; i < strlen(imsi); i++) { if (!isdigit(imsi[i])) return 0; } @@ -347,6 +350,7 @@ int sendsimple(int argc, char** argv, ostream& os) return SUCCESS; } + /** Submit an SMS for delivery to an IMSI. */ int sendsms(int argc, char** argv, ostream& os) { @@ -377,39 +381,13 @@ int sendsms(int argc, char** argv, ostream& os) return SUCCESS; } -/** DEBUGGING: Sends a special sms that triggers a RRLP message to an IMSI. */ -int sendrrlp(int argc, char** argv, ostream& os) -{ - if (argc!=3) return BAD_NUM_ARGS; - - char *IMSI = argv[1]; - - UDPSocket sock(0,"127.0.0.1",gConfig.getNum("SIP.Local.Port")); - unsigned port = sock.port(); - unsigned callID = random(); - - // Just fake out a SIP message. - const char form[] = "MESSAGE sip:IMSI%s@localhost SIP/2.0\nVia: SIP/2.0/TCP localhost;branch=z9hG4bK776sgdkse\nMax-Forwards: 2\nFrom: RRLP@localhost:%d;tag=49583\nTo: sip:IMSI%s@localhost\nCall-ID: %d@127.0.0.1:5063\nCSeq: 1 MESSAGE\nContent-Type: text/plain\nContent-Length: %lu\n\n%s\n"; - - char txtBuf[161]; - snprintf(txtBuf,160,"RRLP%s",argv[2]); - char outbuf[1500]; - snprintf(outbuf,1499,form,IMSI,port,IMSI,callID,strlen(txtBuf),txtBuf); - sock.write(outbuf); - sleep(2); - sock.write(outbuf); - sock.close(); - os << "RRLP Triggering message submitted for delivery" << endl; - - return SUCCESS; -} - - /** Print current usage loads. */ int printStats(int argc, char** argv, ostream& os) { + // FIXME -- This needs to take GPRS channels into account. See #762. if (argc!=1) return BAD_NUM_ARGS; + os << "== GSM ==" << endl; os << "SDCCH load: " << gBTS.SDCCHActive() << '/' << gBTS.SDCCHTotal() << endl; os << "TCH/F load: " << gBTS.TCHActive() << '/' << gBTS.TCHTotal() << endl; os << "AGCH/PCH load: " << gBTS.AGCHLoad() << ',' << gBTS.PCHLoad() << endl; @@ -417,7 +395,15 @@ int printStats(int argc, char** argv, ostream& os) os << "Paging table size: " << gBTS.pager().pagingEntryListSize() << endl; os << "Transactions: " << gTransactionTable.size() << endl; // 3122 timer current value (the number of seconds an MS should hold off the next RACH) - os << "T3122: " << gBTS.T3122() << " ms" << endl; + os << "T3122: " << gBTS.T3122() << " ms (target " << gConfig.getNum("GSM.Radio.PowerManager.TargetT3122") << " ms)" << endl; + os << "== GPRS ==" << endl; + // (pat) We are not using dynamic channel allocation so I removed GPRS.Channels.Max until such time as we do. + // Also I did not understand what is the point of printing out something that is in the config? + //os << "chans mn/mx/dn/up: " + // << gConfig.getNum("GPRS.Channels.Min") << '/' << gConfig.getNum("GPRS.Channels.Max") + // << '/' << gConfig.getNum("GPRS.Multislot.Max.Downlink") << '/' << gConfig.getNum("GPRS.Multislot.Max.Uplink") << endl; + os << "current PDCHs: " << GPRS::gL2MAC.macActiveChannels() << endl; + os << "utilization: " << 100 * GPRS::gL2MAC.macComputeUtilization() << "%" << endl; return SUCCESS; } @@ -438,20 +424,26 @@ int cellID(int argc, char** argv, ostream& os) if (argc!=5) return BAD_NUM_ARGS; // Safety check the args!! - char* MCC = argv[1]; - char* MNC = argv[2]; - if (strlen(MCC)!=3) { + if (!gConfig.isValidValue("GSM.Identity.MCC", argv[1])) { os << "MCC must be three digits" << endl; return BAD_VALUE; } - int MNCLen = strlen(MNC); - if ((MNCLen<2)||(MNCLen>3)) { + if (!gConfig.isValidValue("GSM.Identity.MNC", argv[2])) { os << "MNC must be two or three digits" << endl; return BAD_VALUE; } + if (!gConfig.isValidValue("GSM.Identity.LAC", argv[3])) { + os << "Invalid value for LAC" << endl; + return BAD_VALUE; + } + if (!gConfig.isValidValue("GSM.Identity.CI", argv[4])) { + os << "Invalid value for CI" << endl; + return BAD_VALUE; + } + gTMSITable.clear(); - gConfig.set("GSM.Identity.MCC",MCC); - gConfig.set("GSM.Identity.MNC",MNC); + gConfig.set("GSM.Identity.MCC",argv[1]); + gConfig.set("GSM.Identity.MNC",argv[2]); gConfig.set("GSM.Identity.LAC",argv[3]); gConfig.set("GSM.Identity.CI",argv[4]); return SUCCESS; @@ -461,15 +453,12 @@ int cellID(int argc, char** argv, ostream& os) /** Print table of current transactions. */ -int calls(int argc, char** /*argv*/, ostream& os) +int calls(int argc, char** argv, ostream& os) { - if (argc > 2) - return BAD_NUM_ARGS; - - //fix later -kurtis - //bool showAll = (argc == 2); - //size_t count = gTransactionTable.dump(os,showAll); - size_t count = gTransactionTable.dump(os); + bool showAll = false; + if (argc==2) showAll = true; + if (argc>2) return BAD_NUM_ARGS; + size_t count = gTransactionTable.dump(os,showAll); os << endl << count << " transactions in table" << endl; return SUCCESS; } @@ -477,7 +466,7 @@ int calls(int argc, char** /*argv*/, ostream& os) /** Print or modify the global configuration table. */ -int config(int argc, char** argv, ostream& os) +int rawconfig(int argc, char** argv, ostream& os) { // no args, just print if (argc==1) { @@ -498,23 +487,354 @@ int config(int argc, char** argv, ostream& os) if (i!=(argc-1)) val.append(" "); } bool existing = gConfig.defines(argv[1]); - if (gConfig.isStatic(argv[1])) { - os << argv[1] << " is static; change takes effect on restart" << endl; + string previousVal; + if (existing) { + previousVal = gConfig.getStr(argv[1]); } if (!gConfig.set(argv[1],val)) { - os << argv[1] << " change failed" << endl; - return BAD_VALUE; + os << "DB ERROR: " << argv[1] << " change failed" << endl; + return FAILURE; + } + if (gConfig.isStatic(argv[1])) { + os << argv[1] << " is static; change takes effect on restart" << endl; } if (!existing) { os << "defined new config " << argv[1] << " as \"" << val << "\"" << endl; - // Anything created by the CLI is optional. - //gConfig.makeOptional(argv[1]); } else { - os << "changed " << argv[1] << " to \"" << val << "\"" << endl; + os << argv[1] << " changed from \"" << previousVal << "\" to \"" << val << "\"" << endl; + } + return SUCCESS; +} + +int trxfactory(int argc, char** argv, ostream& os) +{ + if (argc!=1) return BAD_NUM_ARGS; + + signed val = gTRX.ARFCN(0)->getFactoryCalibration("sdrsn"); + if (val == 0 || val == 65535) { + os << "Reading factory calibration not supported on this radio." << endl; + return SUCCESS; + } + os << "Factory Information" << endl; + os << " SDR Serial Number = " << val << endl; + + val = gTRX.ARFCN(0)->getFactoryCalibration("rfsn"); + os << " RF Serial Number = " << val << endl; + + val = gTRX.ARFCN(0)->getFactoryCalibration("band"); + os << " GSM.Radio.Band = "; + if (val == 0) { + os << "multi-band"; + } else { + os << val; + } + os << endl; + + val = gTRX.ARFCN(0)->getFactoryCalibration("rxgain"); + os << " GSM.Radio.RxGain = " << val << endl; + + val = gTRX.ARFCN(0)->getFactoryCalibration("txgain"); + os << " TRX.TxAttenOffset = " << val << endl; + + val = gTRX.ARFCN(0)->getFactoryCalibration("freq"); + os << " TRX.RadioFrequencyOffset = " << val << endl; + + return SUCCESS; +} + +/** Audit the current configuration. */ +int audit(int argc, char** argv, ostream& os) +{ + ConfigurationKeyMap::iterator mp; + stringstream ss; + + // value errors + mp = gConfig.mSchema.begin(); + while (mp != gConfig.mSchema.end()) { + if (!gConfig.isValidValue(mp->first, gConfig.getStr(mp->first))) { + ss << mp->first << " \"" << gConfig.getStr(mp->first) << "\" (\"" << mp->second.getDefaultValue() << "\")" << endl; + } + mp++; + } + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| ERROR : Invalid Values [key current-value (default)] |" << endl; + os << "| To use the default value again, execute: rmconfig key |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + + // factory calibration warnings + signed sdrsn = gTRX.ARFCN(0)->getFactoryCalibration("sdrsn"); + if (sdrsn != 0 && sdrsn != 65535) { + string factoryValue; + string configValue; + + factoryValue = gConfig.mSchema["GSM.Radio.Band"].getDefaultValue(); + configValue = gConfig.getStr("GSM.Radio.Band"); + // only warn on band changes if the unit is not multi-band + if (gTRX.ARFCN(0)->getFactoryCalibration("band") != 0 && configValue.compare(factoryValue) != 0) { + ss << "GSM.Radio.Band \"" << configValue << "\" (\"" << factoryValue << "\")" << endl; + } + + factoryValue = gConfig.mSchema["GSM.Radio.RxGain"].getDefaultValue(); + configValue = gConfig.getStr("GSM.Radio.RxGain"); + if (configValue.compare(factoryValue) != 0) { + ss << "GSM.Radio.RxGain \"" << configValue << "\" (\"" << factoryValue << "\")" << endl; + } + + factoryValue = gConfig.mSchema["TRX.TxAttenOffset"].getDefaultValue(); + configValue = gConfig.getStr("TRX.TxAttenOffset"); + if (configValue.compare(factoryValue) != 0) { + ss << "TRX.TxAttenOffset \"" << configValue << "\" (\"" << factoryValue << "\")" << endl; + } + + factoryValue = gConfig.mSchema["TRX.RadioFrequencyOffset"].getDefaultValue(); + configValue = gConfig.getStr("TRX.RadioFrequencyOffset"); + if (configValue.compare(factoryValue) != 0) { + ss << "TRX.RadioFrequencyOffset \"" << configValue << "\" (\"" << factoryValue << "\")" << endl; + } + + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| WARNING : Factory Radio Calibration [key current-value (factory)] |" << endl; + os << "| To use the factory value again, execute: rmconfig key |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + } + + // cross check warnings + vector allWarnings; + vector warnings; + vector::iterator warning; + mp = gConfig.mSchema.begin(); + while (mp != gConfig.mSchema.end()) { + warnings = gConfig.crossCheck(mp->first); + allWarnings.insert(allWarnings.end(), warnings.begin(), warnings.end()); + mp++; + } + sort(allWarnings.begin(), allWarnings.end()); + allWarnings.erase(unique(allWarnings.begin(), allWarnings.end() ), allWarnings.end()); + warning = allWarnings.begin(); + while (warning != allWarnings.end()) { + ss << *warning << endl; + warning++; + } + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| WARNING : Cross-Check Values |" << endl; + os << "| To quiet these warnings, follow the advice given. |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + + // site-specific values + mp = gConfig.mSchema.begin(); + while (mp != gConfig.mSchema.end()) { + if (mp->second.getVisibility() == ConfigurationKey::CUSTOMERSITE) { + if (gConfig.getStr(mp->first).compare(gConfig.mSchema[mp->first].getDefaultValue()) == 0) { + ss << mp->first << " \"" << gConfig.mSchema[mp->first].getDefaultValue() << "\"" << endl; + } + } + mp++; + } + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| WARNING : Site Values Which Are Still Default [key current-value] |" << endl; + os << "| These should be set to fit your installation: config key value |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + + // non-default values + mp = gConfig.mSchema.begin(); + while (mp != gConfig.mSchema.end()) { + if (mp->second.getVisibility() != ConfigurationKey::CUSTOMERSITE) { + if (gConfig.getStr(mp->first).compare(gConfig.mSchema[mp->first].getDefaultValue()) != 0) { + ss << mp->first << " \"" << gConfig.getStr(mp->first) << "\" (\"" << mp->second.getDefaultValue() << "\")" << endl; + } + } + mp++; + } + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| INFO : Non-Default Values [key current-value (default)] |" << endl; + os << "| To use the default value again, execute: rmconfig key |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + + // unknown pairs + ConfigurationRecordMap pairs = gConfig.getAllPairs(); + ConfigurationRecordMap::iterator mp2 = pairs.begin(); + while (mp2 != pairs.end()) { + if (!gConfig.keyDefinedInSchema(mp2->first)) { + // also kindly ignore SIM.Prog keys for now so the users don't kill their ability to program SIMs + string family = "SIM.Prog."; + if (mp2->first.substr(0, family.size()) != family) { + ss << mp2->first << " \"" << mp2->second.value() << "\"" << endl; + } + } + mp2++; + } + if (ss.str().length()) { + os << "+---------------------------------------------------------------------+" << endl; + os << "| INFO : Custom/Deprecated Key/Value Pairs [key current-value] |" << endl; + os << "| To clean up any extraneous keys, execute: rmconfig key |" << endl; + os << "+---------------------------------------------------------------------+" << endl; + os << ss.str(); + os << endl; + ss.str(""); + } + + return SUCCESS; +} + +/** Print or modify the global configuration table. */ +int _config(string mode, int argc, char** argv, ostream& os) +{ + // no args, just print + if (argc==1) { + ConfigurationKeyMap::iterator mp = gConfig.mSchema.begin(); + while (mp != gConfig.mSchema.end()) { + if (mode.compare("customer") == 0) { + if (mp->second.getVisibility() == ConfigurationKey::CUSTOMER || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERSITE || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERTUNE || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERWARN) { + ConfigurationKey::printKey(mp->second, gConfig.getStr(mp->first), os); + } + } else if (mode.compare("developer") == 0) { + ConfigurationKey::printKey(mp->second, gConfig.getStr(mp->first), os); + } + mp++; + } + return SUCCESS; + } + + // one arg + if (argc==2) { + // matches exactly? print single key + if (gConfig.keyDefinedInSchema(argv[1])) { + ConfigurationKey::printKey(gConfig.mSchema[argv[1]], gConfig.getStr(argv[1]), os); + ConfigurationKey::printDescription(gConfig.mSchema[argv[1]], os); + os << endl; + // ...otherwise print all similar keys + } else { + int foundCount = 0; + ConfigurationKeyMap matches = gConfig.getSimilarKeys(argv[1]); + ConfigurationKeyMap::iterator mp = matches.begin(); + while (mp != matches.end()) { + if (mode.compare("customer") == 0) { + if (mp->second.getVisibility() == ConfigurationKey::CUSTOMER || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERSITE || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERTUNE || + mp->second.getVisibility() == ConfigurationKey::CUSTOMERWARN) { + ConfigurationKey::printKey(mp->second, gConfig.getStr(mp->first), os); + foundCount++; + } + } else if (mode.compare("developer") == 0) { + ConfigurationKey::printKey(mp->second, gConfig.getStr(mp->first), os); + foundCount++; + } + mp++; + } + if (!foundCount) { + os << argv[1] << " - no keys matched"; + if (mode.compare("customer") == 0) { + os << ", developer/factory keys can be accessed with \"devconfig.\""; + } else if (mode.compare("developer") == 0) { + os << ", custom keys can be accessed with \"rawconfig.\""; + } + os << endl; + } + } + return SUCCESS; + } + + // >1 args: set new value + string val; + for (int i=2; i warnings = gConfig.crossCheck(argv[1]); + vector::iterator warning = warnings.begin(); + while (warning != warnings.end()) { + os << "WARNING: " << *warning << endl; + warning++; + } + if (gConfig.isStatic(argv[1])) { + os << argv[1] << " is static; change takes effect on restart" << endl; + } + os << argv[1] << " changed from \"" << previousVal << "\" to \"" << val << "\"" << endl; + return SUCCESS; } +/** Print or modify the global configuration table. Customer access. */ +int config(int argc, char** argv, ostream& os) +{ + return _config("customer", argc, argv, os); +} + +/** Print or modify the global configuration table. Developer/factory access. */ +int devconfig(int argc, char** argv, ostream& os) +{ + return _config("developer", argc, argv, os); +} + /** Disable a configuration key. */ int unconfig(int argc, char** argv, ostream& os) { @@ -540,10 +860,44 @@ int unconfig(int argc, char** argv, ostream& os) return SUCCESS; } -/** Dump current configuration to a file. */ -int configsave(int argc, char** argv, ostream& os) + +/** Set a configuration value back to default or remove from table if custom key. */ +int rmconfig(int argc, char** argv, ostream& os) { - os << "obsolete" << endl; + if (argc!=2) return BAD_NUM_ARGS; + + if (!gConfig.defines(argv[1])) { + os << argv[1] << " is not in the table" << endl; + return BAD_VALUE; + } + + // TODO : removing of default values from DB disabled for now. Breaks webui. + if (gConfig.keyDefinedInSchema(argv[1])) { + if (!gConfig.set(argv[1],gConfig.mSchema[argv[1]].getDefaultValue())) { + os << "DB ERROR: " << argv[1] << " could not be set back to the default value" << endl; + return FAILURE; + } + + os << argv[1] << " set back to its default value" << endl; + vector warnings = gConfig.crossCheck(argv[1]); + vector::iterator warning = warnings.begin(); + while (warning != warnings.end()) { + os << "WARNING: " << *warning << endl; + warning++; + } + if (gConfig.isStatic(argv[1])) { + os << argv[1] << " is static; change takes effect on restart" << endl; + } + return SUCCESS; + } + + if (!gConfig.remove(argv[1])) { + os << "DB ERROR: " << argv[1] << " could not be removed from the configuration table" << endl; + return FAILURE; + } + + os << argv[1] << " removed from the configuration table" << endl; + return SUCCESS; } @@ -554,22 +908,29 @@ int regperiod(int argc, char** argv, ostream& os) { if (argc==1) { os << "T3212 is " << gConfig.getNum("GSM.Timer.T3212") << " minutes" << endl; - os << "SIP registration period is " << gConfig.getNum("SIP.RegistrationPeriod")/60 << " minutes" << endl; + os << "SIP registration period is " << gConfig.getNum("SIP.RegistrationPeriod") << " minutes" << endl; return SUCCESS; } if (argc>3) return BAD_NUM_ARGS; unsigned newT3212 = strtol(argv[1],NULL,10); - if ((newT3212<6)||(newT3212>1530)) { + if (!gConfig.isValidValue("GSM.Timer.T3212", argv[1])) { os << "valid T3212 range is 6..1530 minutes" << endl; return BAD_VALUE; } - // By defuault, make SIP registration period 1.5x the GSM registration period. - unsigned SIPRegPeriod = newT3212*90; + // By default, make SIP registration period 1.5x the GSM registration period. + unsigned SIPRegPeriod = newT3212 * 1.5; + char SIPRegPeriodStr[10]; + sprintf(SIPRegPeriodStr, "%u", SIPRegPeriod); if (argc==3) { - SIPRegPeriod = 60*strtol(argv[2],NULL,10); + SIPRegPeriod = strtol(argv[2],NULL,10); + sprintf(SIPRegPeriodStr, "%s", argv[2]); + } + if (!gConfig.isValidValue("SIP.RegistrationPeriod", SIPRegPeriodStr)) { + os << "valid SIP registration range is 6..2298 minutes" << endl; + return BAD_VALUE; } // Set the values in the table and on the GSM beacon. @@ -612,25 +973,29 @@ int page(int argc, char **argv, ostream& os) gBTS.pager().dump(os); return SUCCESS; } + return BAD_NUM_ARGS; +} + + +int testcall(int argc, char **argv, ostream& os) +{ if (argc!=3) return BAD_NUM_ARGS; char *IMSI = argv[1]; - if (strlen(IMSI)>15) { + if (strlen(IMSI)!=15) { os << IMSI << " is not a valid IMSI" << endl; return BAD_VALUE; } - Control::TransactionEntry dummy( - gConfig.getStr("SIP.Proxy.SMS").c_str(), + Control::TransactionEntry *transaction = new Control::TransactionEntry( + gConfig.getStr("SIP.Proxy.Speech").c_str(), GSM::L3MobileIdentity(IMSI), NULL, - GSM::L3CMServiceType::UndefinedType, + GSM::L3CMServiceType::TestCall, GSM::L3CallingPartyBCDNumber("0"), GSM::Paging); - gBTS.pager().addID(GSM::L3MobileIdentity(IMSI),GSM::SDCCHType,dummy,1000*atoi(argv[2])); + Control::initiateMTTransaction(transaction,GSM::TCHFType,1000*atoi(argv[2])); return SUCCESS; } - - int endcall(int argc, char **argv, ostream& os) { if (argc!=2) return BAD_NUM_ARGS; @@ -650,22 +1015,46 @@ int endcall(int argc, char **argv, ostream& os) void printChanInfo(unsigned transID, const GSM::LogicalChannel* chan, ostream& os) { os << setw(2) << chan->CN() << " " << chan->TN(); - os << " " << setw(8) << chan->typeAndOffset(); + os << " " << setw(9) << chan->typeAndOffset(); os << " " << setw(12) << transID; + os << " " << setw(6) << chan->active(); + os << " " << setw(5) << chan->recyclable(); char buffer[200]; snprintf(buffer,199,"%5.2f %4d %5d %4d", 100.0*chan->FER(), (int)round(chan->RSSI()), chan->actualMSPower(), chan->actualMSTiming()); os << " " << buffer; + + if (!chan->SACCH()) { + os << endl; + return; + } + const GSM::L3MeasurementResults& meas = chan->SACCH()->measurementResults(); - if (!meas.MEAS_VALID()) { - snprintf(buffer,199,"%5d %5.2f", - meas.RXLEV_FULL_SERVING_CELL_dBm(), - 100.0*meas.RXQUAL_FULL_SERVING_CELL_BER()); - os << " " << buffer; - } else { - os << " ----- ------"; + + if (meas.MEAS_VALID()) { + os << endl; + return; } + + snprintf(buffer,199,"%5d %5.2f", + meas.RXLEV_FULL_SERVING_CELL_dBm(), + 100.0*meas.RXQUAL_FULL_SERVING_CELL_BER()); + os << " " << buffer; + + if (meas.NO_NCELL()==0) { + os << endl; + return; + } + unsigned CN = meas.BCCH_FREQ_NCELL(0); + std::vector ARFCNList = gNeighborTable.ARFCNList(); + if (CN>=ARFCNList.size()) { + LOG(NOTICE) << "BCCH index " << CN << " does not match ARFCN list of size " << ARFCNList.size(); + os << endl; + return; + } + snprintf(buffer,199,"%8u %8d",ARFCNList[CN],meas.RXLEV_NCELL_dBm(0)); + os << " " << buffer; os << endl; } @@ -673,10 +1062,12 @@ void printChanInfo(unsigned transID, const GSM::LogicalChannel* chan, ostream& o int chans(int argc, char **argv, ostream& os) { - if (argc!=1) return BAD_NUM_ARGS; + bool showAll = false; + if (argc==2) showAll = true; + if (argc>2) return BAD_NUM_ARGS; - os << "CN TN chan transaction UPFER RSSI TXPWR TXTA DNLEV DNBER" << endl; - os << "CN TN type id pct dB dBm sym dBm pct" << endl; + os << "CN TN chan transaction active recyc UPFER RSSI TXPWR TXTA DNLEV DNBER Neighbor Neighbor" << endl; + os << "CN TN type id pct dB dBm sym dBm pct ARFCN dBm" << endl; //gPhysStatus.dump(os); //os << endl << "Old data reporting: " << endl; @@ -685,10 +1076,12 @@ int chans(int argc, char **argv, ostream& os) GSM::SDCCHList::const_iterator sChanItr = gBTS.SDCCHPool().begin(); while (sChanItr != gBTS.SDCCHPool().end()) { const GSM::SDCCHLogicalChannel* sChan = *sChanItr; - if (sChan->active()) { + if (sChan->active() || showAll) { Control::TransactionEntry *trans = gTransactionTable.find(sChan); - if (trans) printChanInfo(trans->ID(),sChan,os); - else printChanInfo(0,sChan,os); + int tid = 0; + if (trans) tid = trans->ID(); + printChanInfo(tid,sChan,os); + //if (showAll) printChanInfo(tid,sChan->SACCH(),os); } ++sChanItr; } @@ -697,10 +1090,12 @@ int chans(int argc, char **argv, ostream& os) GSM::TCHList::const_iterator tChanItr = gBTS.TCHPool().begin(); while (tChanItr != gBTS.TCHPool().end()) { const GSM::TCHFACCHLogicalChannel* tChan = *tChanItr; - if (tChan->active()) { + if (tChan->active() || showAll) { Control::TransactionEntry *trans = gTransactionTable.find(tChan); - if (trans) printChanInfo(trans->ID(),tChan,os); - else printChanInfo(0,tChan,os); + int tid = 0; + if (trans) tid = trans->ID(); + printChanInfo(tid,tChan,os); + //if (showAll) printChanInfo(tid,tChan->SACCH(),os); } ++tChanItr; } @@ -727,7 +1122,21 @@ int power(int argc, char **argv, ostream& os) int min = atoi(argv[1]); int max = atoi(argv[2]); - if (min>max) return BAD_VALUE; + if (min>max) { + os << "Min is larger than max" << endl; + return BAD_VALUE; + } + + if (!gConfig.isValidValue("GSM.Radio.PowerManager.MinAttenDB", argv[1])) { + os << "Invalid new value for min. It must be in range ("; + os << gConfig.mSchema["GSM.Radio.PowerManager.MinAttenDB"].getValidValues() << ")" << endl; + return BAD_VALUE; + } + if (!gConfig.isValidValue("GSM.Radio.PowerManager.MaxAttenDB", argv[2])) { + os << "Invalid new value for max. It must be in range ("; + os << gConfig.mSchema["GSM.Radio.PowerManager.MaxAttenDB"].getValidValues() << ")" << endl; + return BAD_VALUE; + } gConfig.set("GSM.Radio.PowerManager.MinAttenDB",argv[1]); gConfig.set("GSM.Radio.PowerManager.MaxAttenDB",argv[2]); @@ -748,6 +1157,12 @@ int rxgain(int argc, char** argv, ostream& os) if (argc==1) return SUCCESS; if (argc!=2) return BAD_NUM_ARGS; + if (!gConfig.isValidValue("GSM.Radio.RxGain", argv[1])) { + os << "Invalid new value for RX gain. It must be in range ("; + os << gConfig.mSchema["GSM.Radio.RxGain"].getValidValues() << ")" << endl; + return BAD_VALUE; + } + int newGain = gTRX.ARFCN(0)->setRxGain(atoi(argv[1])); os << "new RX gain is " << newGain << " dB" << endl; @@ -756,6 +1171,49 @@ int rxgain(int argc, char** argv, ostream& os) return SUCCESS; } +int txatten(int argc, char** argv, ostream& os) +{ + os << "current TX attenuation is " << gConfig.getNum("TRX.TxAttenOffset") << " dB" << endl; + if (argc==1) return SUCCESS; + if (argc!=2) return BAD_NUM_ARGS; + + if (!gConfig.isValidValue("TRX.TxAttenOffset", argv[1])) { + os << "Invalid new value for TX attenuation. It must be in range ("; + os << gConfig.mSchema["TRX.TxAttenOffset"].getValidValues() << ")" << endl; + return BAD_VALUE; + } + + int newAtten = gTRX.ARFCN(0)->setTxAtten(atoi(argv[1])); + os << "new TX attenuation is " << newAtten << " dB" << endl; + + gConfig.set("TRX.TxAttenOffset",newAtten); + + return SUCCESS; +} + + +int freqcorr(int argc, char** argv, ostream& os) +{ + os << "current freq. offset is " << gConfig.getNum("TRX.RadioFrequencyOffset") << endl; + if (argc==1) return SUCCESS; + if (argc!=2) return BAD_NUM_ARGS; + + if (!gConfig.isValidValue("TRX.RadioFrequencyOffset", argv[1])) { + os << "Invalid new value for freq. offset It must be in range ("; + os << gConfig.mSchema["TRX.RadioFrequencyOffset"].getValidValues() << ")" << endl; + return BAD_VALUE; + } + + int newOffset = gTRX.ARFCN(0)->setFreqOffset(atoi(argv[1])); + os << "new freq. offset is " << newOffset << endl; + + gConfig.set("TRX.RadioFrequencyOffset",newOffset); + + return SUCCESS; +} + + + int noise(int argc, char** argv, ostream& os) { if (argc!=1) return BAD_NUM_ARGS; @@ -767,9 +1225,50 @@ int noise(int argc, char** argv, ostream& os) return SUCCESS; } +int sysinfo(int argc, char** argv, ostream& os) +{ + if (argc!=1) return BAD_NUM_ARGS; + + const GSM::L3SystemInformationType1 *SI1 = gBTS.SI1(); + if (SI1) os << *SI1 << endl; + const GSM::L3SystemInformationType2 *SI2 = gBTS.SI2(); + if (SI2) os << *SI2 << endl; + const GSM::L3SystemInformationType3 *SI3 = gBTS.SI3(); + if (SI3) os << *SI3 << endl; + const GSM::L3SystemInformationType4 *SI4 = gBTS.SI4(); + if (SI4) os << *SI4 << endl; + const GSM::L3SystemInformationType5 *SI5 = gBTS.SI5(); + if (SI5) os << *SI5 << endl; + const GSM::L3SystemInformationType6 *SI6 = gBTS.SI6(); + if (SI6) os << *SI6 << endl; + + return SUCCESS; +} + + +int neighbors(int argc, char** argv, ostream& os) +{ + + os << "host C0 BSIC" << endl; + char cmd[200]; + sprintf(cmd,"sqlite3 -separator ' ' %s 'select IPADDRESS,C0,BSIC from neighbor_table'", + gConfig.getStr("Peering.NeighborTable.Path").c_str()); + FILE *result = popen(cmd,"r"); + char *line = (char*)malloc(200); + while (!feof(result)) { + if (!fgets(line, 200, result)) break; + os << line; + } + free(line); + os << endl; + pclose(result); + return SUCCESS; +} + + int crashme(int argc, char** argv, ostream& os) { - char *nullp = 0x0; + char *nullp = 0x0; // we actually have to output this, // or the compiler will optimize it out os << *nullp; @@ -781,9 +1280,15 @@ int stats(int argc, char** argv, ostream& os) { char cmd[200]; - if (argc==2) + if (argc==2) { + if (strcmp(argv[1],"clear")==0) { + gReports.clear(); + os << "stats table (gReporting) cleared" << endl; + return SUCCESS; + } sprintf(cmd,"sqlite3 %s 'select name||\": \"||value||\" events over \"||((%lu-clearedtime)/60)||\" minutes\" from reporting where name like \"%%%s%%\";'", gConfig.getStr("Control.Reporting.StatsTable").c_str(), time(NULL), argv[1]); + } else if (argc==1) sprintf(cmd,"sqlite3 %s 'select name||\": \"||value||\" events over \"||((%lu-clearedtime)/60)||\" minutes\" from reporting;'", gConfig.getStr("Control.Reporting.StatsTable").c_str(), time(NULL)); @@ -809,30 +1314,39 @@ void Parser::addCommands() { addCommand("uptime", uptime, "-- show BTS uptime and BTS frame number."); addCommand("help", showHelp, "[command] -- list available commands or gets help on a specific command."); - addCommand("exit", exit_function, "[wait] -- exit the application, either immediately, or waiting for existing calls to clear with a timeout in seconds"); + addCommand("shutdown", exit_function, "[wait] -- shut down or restart OpenBTS, either immediately, or waiting for existing calls to clear with a timeout in seconds"); addCommand("tmsis", tmsis, "[\"clear\"] or [\"dump\" filename] -- print/clear the TMSI table or dump it to a file."); addCommand("sendsms", sendsms, "IMSI src# message... -- send direct SMS to IMSI, addressed from source number src#."); addCommand("sendsimple", sendsimple, "IMSI src# message... -- send SMS to IMSI via SIP interface, addressed from source number src#."); - //apparently non-function now -kurtis - //addCommand("sendrrlp", sendrrlp, " -- send RRLP message to ."); addCommand("load", printStats, "-- print the current activity loads."); addCommand("cellid", cellID, "[MCC MNC LAC CI] -- get/set location area identity (MCC, MNC, LAC) and cell ID (CI)"); addCommand("calls", calls, "-- print the transaction table"); + addCommand("rawconfig", rawconfig, "[] OR [patt] OR [key val(s)] -- print the current configuration, print configuration values matching a pattern, or set/change a configuration value"); + addCommand("trxfactory", trxfactory, "-- print the radio's factory calibration and meta information"); + addCommand("audit", audit, "-- audit the current configuration for troubleshooting"); addCommand("config", config, "[] OR [patt] OR [key val(s)] -- print the current configuration, print configuration values matching a pattern, or set/change a configuration value"); - addCommand("configsave", configsave, " -- write the current configuration to a file"); + addCommand("devconfig", devconfig, "[] OR [patt] OR [key val(s)] -- print the current configuration, print configuration values matching a pattern, or set/change a configuration value"); addCommand("regperiod", regperiod, "[GSM] [SIP] -- get/set the registration period (GSM T3212), in MINUTES"); addCommand("alarms", alarms, "-- show latest alarms"); addCommand("version", version,"-- print the version string"); - addCommand("page", page, "[IMSI time] -- dump the paging table or page the given IMSI for the given period"); + addCommand("page", page, "print the paging table"); addCommand("chans", chans, "-- report PHY status for active channels"); addCommand("power", power, "[minAtten maxAtten] -- report current attentuation or set min/max bounds"); addCommand("rxgain", rxgain, "[newRxgain] -- get/set the RX gain in dB"); + addCommand("txatten", txatten, "[newTxAtten] -- get/set the TX attenuation in dB"); + addCommand("freqcorr", freqcorr, "[newOffset] -- get/set the new radio frequency offset"); addCommand("noise", noise, "-- report receive noise level in RSSI dB"); - addCommand("unconfig", unconfig, "key -- remove a config value"); + addCommand("rmconfig", rmconfig, "key -- set a configuration value back to its default or remove a custom key/value pair"); + addCommand("unconfig", unconfig, "key -- disable a configuration key by setting an empty value"); addCommand("notices", notices, "-- show startup copyright and legal notices"); addCommand("endcall", endcall,"trans# -- terminate the given transaction"); + addCommand("testcall", testcall, "IMSI time -- initiate a TCHF test call to a given IMSI with a given paging time"); + addCommand("sysinfo", sysinfo, "-- print current system information messages"); + addCommand("neighbors", neighbors, "-- dump the neighbor table"); + addCommand("gprs", GPRS::gprsCLI,"GPRS mode sub-command. Type: gprs help for more"); + addCommand("sgsn", SGSN::sgsnCLI,"SGSN mode sub-command. Type: sgsn help for more"); addCommand("crashme", crashme, "force crash of OpenBTS for testing purposes"); - addCommand("stats", stats,"[patt] -- print all, or selected, performance statistics"); + addCommand("stats", stats,"[patt] OR clear -- print all, or selected, performance counters, OR clear all counters"); } diff --git a/CLI/CLI.h b/CLI/CLI.h index 1db90918..ff4a3954 100644 --- a/CLI/CLI.h +++ b/CLI/CLI.h @@ -1,25 +1,15 @@ /* -* Copyright 2008 Free Software Foundation, Inc. +* Copyright 2009 Free Software Foundation, Inc. * -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. */ @@ -71,7 +61,13 @@ class Parser { private: - /** Parse and execute a command string. */ + /** + Parse and execute a command string. + @line a writeable copy of the original line + @cline the original line + @os output stream + @return status code + */ int execute(char* line, std::ostream& os) const; }; diff --git a/COPYING b/COPYING index b5af1510..28e4621b 100644 --- a/COPYING +++ b/COPYING @@ -693,6 +693,14 @@ Interaction; Use with the GNU General Public License"). This exemption of interfaces other than the GSM air interface from the requirements of Section 13 is an additional permission granted to you. +2. GSM "A5" cipher stream generation libraries + +Notwithstanding any other provision of this License, you have +permission to link the Program with GSM "A5" cipher-stream generation +libraries provided under any license that allows redistribution of +those libraries in binary form, provided that the function of any such +library is limited to the generation of cipher-steam bits. + Non-Permissive Terms Supplementing The License @@ -704,11 +712,9 @@ license does not include the right to use the OpenBTS trademark in commerce. This additional non-permissive term is consistent with Section 7 of the AGPLv3 license. - END OF ADDITIONAL TERMS - How to comply with Section 13 of the AGPLv3 license. The recommended method for compliance with Section 13 of the AGPLv3 license is diff --git a/Control/CallControl.cpp b/Control/CallControl.cpp index b10e169e..3e531f99 100644 --- a/Control/CallControl.cpp +++ b/Control/CallControl.cpp @@ -2,26 +2,18 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -96,6 +88,9 @@ unsigned allocateRTPPorts() + + + /** Force clearing on the GSM side. @param transaction The call transaction record. @@ -104,17 +99,22 @@ unsigned allocateRTPPorts() */ void forceGSMClearing(TransactionEntry *transaction, GSM::LogicalChannel *LCH, const GSM::L3Cause& cause) { + LOG(DEBUG); LOG(INFO) << "Q.931 state " << transaction->GSMState(); // Already cleared? if (transaction->GSMState()==GSM::NullState) return; // Clearing not started? Start it. if (!transaction->clearingGSM()) LCH->send(GSM::L3Disconnect(transaction->L3TI(),cause)); // Force the rest. + LOG(DEBUG); LCH->send(GSM::L3ReleaseComplete(transaction->L3TI())); LCH->send(GSM::L3ChannelRelease()); + LOG(DEBUG); transaction->resetTimers(); transaction->GSMState(GSM::NullState); - LCH->send(GSM::RELEASE); + LOG(DEBUG); + //LCH->send(GSM::RELEASE); + //LOG(DEBUG); } @@ -129,17 +129,18 @@ void forceSIPClearing(TransactionEntry *transaction) return; } + LOG(DEBUG); SIP::SIPState state = transaction->SIPState(); LOG(INFO) << "SIP state " << state; - //why aren't we checking for failed here? -kurtis + //why aren't we checking for failed here? -kurtis ; we are now. -david if (transaction->SIPFinished()) return; if (state==SIP::Active){ //Changes state to clearing - transaction->MODSendBYE(); + transaction->MODSendBYE(); //then cleared transaction->MODWaitForBYEOK(); } else if (transaction->instigator()){ //hasn't started yet, need to cancel - //Changes state to canceling + //Changes state to canceling transaction->MODSendCANCEL(); //then canceled transaction->MODWaitForCANCELOK(); @@ -283,6 +284,7 @@ bool assignTCHF(TransactionEntry *transaction, GSM::LogicalChannel *DCCH, GSM::T // Shut down the SIP side of the call. forceSIPClearing(transaction); + // Indicate failure. return false; } @@ -301,7 +303,7 @@ bool assignTCHF(TransactionEntry *transaction, GSM::LogicalChannel *DCCH, GSM::T */ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChannel* LCH, const GSM::L3Message *message) { - LOG(DEBUG) << "from " << transaction->subscriber() << " message " << *message; + if (message) { LOG(DEBUG) << "from " << transaction->subscriber() << " message " << *message; } // FIXME -- This dispatch section should be something more efficient with PD and MTI swtiches. @@ -356,7 +358,7 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne // Disconnect (1st step of MOD) // GSM 04.08 5.4.3.2 - if (const GSM::L3Disconnect *disc = dynamic_cast(message)) { + if (const GSM::L3Disconnect* disc = dynamic_cast(message)) { LOG(INFO) << "GSM Disconnect " << *transaction; gReports.incr("OpenBTS.GSM.CC.MOD.Disconnect"); bool early = transaction->GSMState() != GSM::Active; @@ -365,7 +367,7 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne LOG(NOTICE) << "abnormal terminatation: " << *disc; } /* late RLLP request */ - if (normal && !early && gConfig.defines("Control.Call.QueryRRLP.Late")) { + if (normal && !early && gConfig.getBool("Control.Call.QueryRRLP.Late")) { // Query for RRLP if (!sendRRLP(transaction->subscriber(), LCH)) { LOG(INFO) << "RRLP request failed"; @@ -397,8 +399,8 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne transaction->MODWaitForResponse(&valid);*/ } else { //if we received it, send a 4** instead - //transaction->MODSendERROR(NULL, 480, "Temporarily Unavailable", true); - transaction->MODSendERROR(NULL, 486, "Busy Here", true); + //transaction->MODSendERROR(NULL, 480, "Temporarily Unavailable", true); + transaction->MODSendERROR(NULL, 486, "Busy Here", true); transaction->MODWaitForERRORACK(true); } //transaction->GSMState(GSM::NullState); @@ -408,11 +410,14 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne } // Release (2nd step of MTD) - if (dynamic_cast(message)) { + if (const GSM::L3Release *rls = dynamic_cast(message)) { LOG(INFO) << "GSM Release " << *transaction; gReports.incr("OpenBTS.GSM.CC.MTD.Release"); + if (rls->haveCause() && (rls->cause().cause() > 0x10)) { + LOG(NOTICE) << "abnormal terminatation: " << *rls; + } /* late RLLP request */ - if (gConfig.defines("Control.Call.QueryRRLP.Late")) { + if (gConfig.getBool("Control.Call.QueryRRLP.Late")) { // Query for RRLP if (!sendRRLP(transaction->subscriber(), LCH)) { LOG(INFO) << "RRLP request failed"; @@ -524,6 +529,14 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne return false; } + if (dynamic_cast(message)) { + LOG(DEBUG) << "received Ciphering Mode Complete on " << *LCH << " for " << transaction->subscriber(); + // Although the spec (04.08 3.4.7) says you can start ciphering the downlink at this time, + // it also says you can start when you successfully decrypt an uplink layer 2 frame, + // which is what we do. + return false; + } + // Stubs for unsupported features. // We need to answer the handset so it doesn't hang. @@ -535,8 +548,11 @@ bool callManagementDispatchGSM(TransactionEntry *transaction, GSM::LogicalChanne return false; } - if (message) { LOG(NOTICE) << "no support for message " << *message << " from " << transaction->subscriber(); } - else { LOG(NOTICE) << "no support for unrecognized message from " << transaction->subscriber(); } + if (message) { + LOG(NOTICE) << "no support for message " << *message << " from " << transaction->subscriber(); + } else { + LOG(NOTICE) << "no support for unrecognized message from " << transaction->subscriber(); + } // If we got here, we're ignoring the message. @@ -641,6 +657,7 @@ bool updateSIPSignalling(TransactionEntry *transaction, GSM::LogicalChannel *LCH if (transaction->SIPFinished()) return true; bool GSMClearedOrClearing = GSMCleared || transaction->clearingGSM(); + //only checking for Clearing because the call is active at this state. Should not cancel if (transaction->MTDCheckBYE() == SIP::MTDClearing) { LOG(DEBUG) << "got SIP BYE " << *transaction; @@ -679,6 +696,61 @@ bool updateSignalling(TransactionEntry *transaction, GSM::LogicalChannel *LCH, u +bool outboundHandoverTransfer(TransactionEntry* transaction, GSM::TCHFACCHLogicalChannel *TCH) +{ + // By returning true, this function indicates to its caller that the call is cleared + // and no longer needs a channel on this BTS. + + // In this method, we are "BS1" in the ladder diagram. + // BS2 has alrady accepted the handover request. + + // Send the handover command. + TCH->send(GSM::L3HandoverCommand( + transaction->outboundCell(), + transaction->outboundChannel(), + transaction->outboundReference(), + transaction->outboundPowerCmd(), + transaction->outboundSynch() + )); + + // Start a timer for T3103, the handover failure timer. + GSM::Z100Timer T3103(gConfig.getNum("GSM.Timer.T3103")); + T3103.set(); + + // The next step for the MS is to send Handover Access to BS2. + // The next step for us is to wait for the Handover Complete message + // and see that the phone doesn't come back to us. + // BS2 is doing most of the work now. + // We will get a handover complete once it's over, but we don't really need it. + + // Q: What about transferring audio packets? + // A: There should not be any after we send the Handover Command. + + // Get the response. + // This is supposed to time out on successful handover, similar to the early assignment channel transfer.. + GSM::L3Frame *result = TCH->recv(T3103.remaining()); + if (result) { + // If we got here, the handover failed and we just keep running the call. + LOG(NOTICE) << "failed handover, received " << *result; + delete result; + // Restore the call state. + transaction->GSMState(GSM::Active); + return false; + } + + // If the phone doesn't come back, either the handover succeeded or + // the phone dropped the connection. Either way, we are clearing the call. + + // Invalidate local cache entry for this IMSI in the subscriber registry. + string imsi = string("IMSI").append(transaction->subscriber().digits()); + gSubscriberRegistry.removeUser(imsi.c_str()); + + LOG(INFO) "timeout following outbound handover; exiting normally"; + TCH->send(GSM::HARDRELEASE); + return true; +} + + /** Poll for activity while in a call. Sleep if needed to prevent fast spinning. @@ -689,9 +761,11 @@ bool updateSignalling(TransactionEntry *transaction, GSM::LogicalChannel *LCH, u */ bool pollInCall(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel *TCH) { + // See if the radio link disappeared. if (TCH->radioFailure()) { LOG(NOTICE) << "radio link failure, dropped call"; + gReports.incr("OpenBTS.GSM.CC.DroppedCalls"); forceSIPClearing(transaction); return true; } @@ -700,6 +774,10 @@ bool pollInCall(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel *TCH) // If this returns true, it means the call is fully cleared. if (updateSignalling(transaction,TCH)) return true; + // Check for outbound handover. + if (transaction->GSMState() == GSM::HandoverOutbound) + return outboundHandoverTransfer(transaction,TCH); + // Did an outside process request a termination? if (transaction->terminationRequested()) { // Cause 25 is "pre-emptive clearing". @@ -714,6 +792,7 @@ bool pollInCall(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel *TCH) // Transfer vocoder data. // If anything happened, then the call is still up. + // This is a blocking call, blocking 20 ms on average. if (updateCallTraffic(transaction,TCH)) return false; // If nothing happened, sleep so we don't burn up the CPU cycles. @@ -749,9 +828,28 @@ bool waitInCall(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel *TCH, @param transaction The transaction record for this call, will be cleared on exit. @param TCH The TCH+FACCH for the call. */ -void callManagementLoop(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel* TCH) +void Control::callManagementLoop(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel* TCH) { LOG(INFO) << " call connected " << *transaction; + if (gConfig.getBool("GSM.Cipher.Encrypt")) { + int encryptionAlgorithm = gTMSITable.getPreferredA5Algorithm(transaction->subscriber().digits()); + if (!encryptionAlgorithm) { + LOG(DEBUG) << "A5/3 and A5/1 not supported: NOT sending Ciphering Mode Command on " << *TCH << " for " << transaction->subscriber(); + } else if (TCH->decryptUplink_maybe(transaction->subscriber().digits(), encryptionAlgorithm)) { + // send Ciphering Mode Command + // start reception in new mode (GSM 04.08, 3.4.7) + // The spec says to start decrypting uplink at this time, but that would cause us to + // start decrypting before the Ciphering Mode Command is acknowledged, so we start + // maybe decrypting - try decoding without decrypting, and when a frame comes along + // that fails, we try decrypting, and if that passes than we start decrypting everything. + LOG(DEBUG) << "sending Ciphering Mode Command on " << *TCH << " for " << transaction->subscriber(); + TCH->send(GSM::L3CipheringModeCommand( + GSM::L3CipheringModeSetting(true, encryptionAlgorithm), + GSM::L3CipheringModeResponse(false))); + } else { + LOG(DEBUG) << "no ki: NOT sending Ciphering Mode Command on " << *TCH << " for " << transaction->subscriber(); + } + } gReports.incr("OpenBTS.GSM.CC.CallMinutes"); // poll everything until the call is finished // A rough count of frames. @@ -769,6 +867,7 @@ void callManagementLoop(TransactionEntry *transaction, GSM::TCHFACCHLogicalChann // Every minute, reset the watchdog timer. if ((fCount%(60*50))==0) { LOG(DEBUG) << fCount << " cycles of call management loop; resetting watchdog"; + gResetWatchdog(); gReports.incr("OpenBTS.GSM.CC.CallMinutes"); } } @@ -840,13 +939,12 @@ void Control::MOCStarter(const GSM::L3CMServiceRequest* req, GSM::LogicalChannel } throw UnexpectedMessage(); } - gReports.incr("OpenBTS.GSM.CC.MOC.Setup"); - + /* early RLLP request */ /* this seems to need to be sent after initial call setup -kurtis */ - if (gConfig.defines("Control.Call.QueryRRLP.Early")) { + if (gConfig.getBool("Control.Call.QueryRRLP.Early")) { // Query for RRLP if (!sendRRLP(mobileID, LCH)) { LOG(INFO) << "RRLP request failed"; @@ -1074,6 +1172,8 @@ void Control::MOCController(TransactionEntry *transaction, GSM::TCHFACCHLogicalC transaction->MOCInitRTP(); transaction->MOCSendACK(); + // FIXME -- We need to watch for a repeated OK in case the ACK got lost. + // Get the Connect Acknowledge message. while (transaction->GSMState()!=GSM::Active) { @@ -1107,7 +1207,7 @@ void Control::MTCStarter(TransactionEntry *transaction, GSM::LogicalChannel *LCH if (LCH->type()==GSM::FACCHType) veryEarly=true; /* early RLLP request */ - if (gConfig.defines("Control.Call.QueryRRLP.Early")) { + if (gConfig.getBool("Control.Call.QueryRRLP.Early")) { // Query for RRLP if (!sendRRLP(transaction->subscriber(), LCH)) { LOG(INFO) << "RRLP request failed"; @@ -1197,7 +1297,7 @@ void Control::MTCStarter(TransactionEntry *transaction, GSM::LogicalChannel *LCH } else { // For late assignment, send the TCH assignment now. - // This dispatcher on the next channel will continue the transaction. + // The dispatcher on the next channel will continue the transaction. assert(TCH); assignTCHF(transaction,LCH,TCH); } @@ -1233,7 +1333,7 @@ void Control::MTCController(TransactionEntry *transaction, GSM::TCHFACCHLogicalC if (transaction->MTCCheckForCancel()==SIP::MTDCanceling) { LOG(INFO) << "MTCCheckForCancel return Canceling"; transaction->MTDSendCANCELOK(); - //should probably send a 487 here -kurtis + //should probably send a 487 here // Cause 0x15 is "rejected" return abortAndRemoveCall(transaction,TCH,GSM::L3Cause(0x15)); } @@ -1285,6 +1385,21 @@ void Control::MTCController(TransactionEntry *transaction, GSM::TCHFACCHLogicalC } +void Control::TestCall(TransactionEntry *transaction, GSM::LogicalChannel *LCH) +{ + assert(LCH); + LOG(INFO) << LCH->type() << " transaction: "<< *transaction; + assert(transaction->L3TI()<7); + + // Mark the call as active. + transaction->GSMState(GSM::Active); + + LOG(INFO) << "starting test call"; + while (!transaction->terminationRequested()) { sleep(1); } + LOG(INFO) << "ending test call"; + LCH->send(GSM::L3ChannelRelease()); + gTransactionTable.remove(transaction); +} diff --git a/Control/CallControl.h b/Control/CallControl.h index e7f6db4c..1cbc5da6 100644 --- a/Control/CallControl.h +++ b/Control/CallControl.h @@ -1,25 +1,16 @@ /**@file GSM/SIP Call Control -- GSM 04.08, ISDN ITU-T Q.931, SIP IETF RFC-3261, RTP IETF RFC-3550. */ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -45,8 +36,6 @@ class TransactionEntry; void MOCStarter(const GSM::L3CMServiceRequest*, GSM::LogicalChannel*); /** Complete the MOC connection. */ void MOCController(TransactionEntry*, GSM::TCHFACCHLogicalChannel*); -/** Set up an emergency call, assuming very early assignment. */ -void EmergencyCall(const GSM::L3CMServiceRequest*, GSM::LogicalChannel*); //@} @@ -70,6 +59,14 @@ void initiateMTTransaction(TransactionEntry* transaction, GSM::ChannelType chanType, unsigned pageTime); +/** + This is the standard call manangement loop, regardless of the origination type. + This function returns when the call is cleared and the channel is released. + @param transaction The transaction record for this call, will be cleared on exit. + @param TCH The TCH+FACCH for the call. +*/ +void callManagementLoop(TransactionEntry *transaction, GSM::TCHFACCHLogicalChannel* TCH); + } diff --git a/Control/ControlCommon.cpp b/Control/ControlCommon.cpp index 5d639da5..313a3c4f 100644 --- a/Control/ControlCommon.cpp +++ b/Control/ControlCommon.cpp @@ -3,24 +3,16 @@ /* * Copyright 2008, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -37,6 +29,7 @@ #include #include +#include #include #include @@ -60,25 +53,29 @@ L3Message* getMessageCore(LogicalChannel *LCH, unsigned SAPI) unsigned timeout_ms = LCH->N200() * T200ms; L3Frame *rcv = LCH->recv(timeout_ms,SAPI); if (rcv==NULL) { - LOG(NOTICE) << "timeout"; + LOG(NOTICE) << "L3 read timeout"; throw ChannelReadTimeout(); } LOG(DEBUG) << "received " << *rcv; Primitive primitive = rcv->primitive(); if (primitive!=DATA) { - LOG(NOTICE) << "unexpected primitive " << primitive; + LOG(ERR) << "unexpected primitive " << primitive; delete rcv; throw UnexpectedPrimitive(); } L3Message *msg = parseL3(*rcv); - delete rcv; if (msg==NULL) { - LOG(NOTICE) << "unparsed message"; - throw UnsupportedMessage(); + LOG(WARNING) << "unparsed message:" << *rcv; + delete rcv; + return NULL; + //throw UnsupportedMessage(); } + delete rcv; + LOG(DEBUG) << *msg; return msg; } + // FIXME -- getMessage should return an L3Frame, not an L3Message. // This will mean moving all of the parsing into the control layer. // FIXME -- This needs an adjustable timeout. @@ -86,13 +83,33 @@ L3Message* getMessageCore(LogicalChannel *LCH, unsigned SAPI) L3Message* Control::getMessage(LogicalChannel *LCH, unsigned SAPI) { L3Message *msg = getMessageCore(LCH,SAPI); - // Handsets should not be sending us GPRS suspension requests. + // Handsets should not be sending us GPRS suspension requests when GPRS support is not enabled. // But if they do, we should ignore them. // They should not send more than one in any case, but we need to be // ready for whatever crazy behavior they throw at us. + + // The suspend procedure includes MS<->BSS and BSS<->SGSN messages. + // GSM44.018 3.4.25 GPRS Suspension Procedure and 9.1.13b: GPRS Suspension Request message. + // Also 23.060 16.2.1.1 Suspend/Resume procedure general. + // GSM08.18: Suspend Procedure talks about communication between the BSS and SGSN, + // and is not applicable to us when using the internal SGSN. + // Note: When call is finished the RR is supposed to include a GPRS resumption IE, but if it does not, + // 23.060 16.2.1.1.1 says the MS will do a GPRS RoutingAreaUpdate to get the + // GPRS service back, so we are not worrying about it. + // (pat 3-2012) Send the message to the internal SGSN. + // It returns true if GPRS and the internal SGSN are enabled. + // If we are using an external SGSN, we could send the GPRS suspend request to the SGSN via the BSSG, + // but that has no hope of doing anything useful. See ticket #613. + // First, We are supposed to automatically detect when we should do the Resume procedure. + // Second: An RA-UPDATE, which gets send to the SGSN, does something to the CC state + // that I dont understand yet. + // We dont do any of the above. unsigned count = gConfig.getNum("GSM.Control.GPRSMaxIgnore"); - while (count && dynamic_cast(msg)) { - LOG(NOTICE) << "ignoring GPRS suspension request"; + const GSM::L3GPRSSuspensionRequest *srmsg; + while (count && (srmsg = dynamic_cast(msg))) { + if (! SGSN::Sgsn::handleGprsSuspensionRequest(srmsg->mTLLI,srmsg->mRaId)) { + LOG(NOTICE) << "ignoring GPRS suspension request"; + } msg = getMessageCore(LCH,SAPI); count--; } @@ -100,6 +117,10 @@ L3Message* Control::getMessage(LogicalChannel *LCH, unsigned SAPI) } + + + + /* Resolve a mobile ID to an IMSI and return TMSI if it is assigned. */ unsigned Control::resolveIMSI(bool sameLAI, L3MobileIdentity& mobileID, LogicalChannel* LCH) { @@ -108,7 +129,10 @@ unsigned Control::resolveIMSI(bool sameLAI, L3MobileIdentity& mobileID, Logical LOG(DEBUG) << "resolving mobile ID " << mobileID << ", sameLAI: " << sameLAI; // IMSI already? See if there's a TMSI already, too. - if (mobileID.type()==IMSIType) return gTMSITable.TMSI(mobileID.digits()); + if (mobileID.type()==IMSIType) { + GPRS::GPRSNotifyGsmActivity(mobileID.digits()); + return gTMSITable.TMSI(mobileID.digits()); + } // IMEI? WTF?! // FIXME -- Should send MM Reject, cause 0x60, "invalid mandatory information". @@ -121,6 +145,7 @@ unsigned Control::resolveIMSI(bool sameLAI, L3MobileIdentity& mobileID, Logical if (sameLAI) IMSI = gTMSITable.IMSI(TMSI); if (IMSI) { // We assigned this TMSI already; the TMSI/IMSI pair is already in the table. + GPRS::GPRSNotifyGsmActivity(IMSI); mobileID = L3MobileIdentity(IMSI); LOG(DEBUG) << "resolving mobile ID (table): " << mobileID; free(IMSI); @@ -153,16 +178,15 @@ unsigned Control::resolveIMSI(bool sameLAI, L3MobileIdentity& mobileID, Logical void Control::resolveIMSI(L3MobileIdentity& mobileIdentity, LogicalChannel* LCH) { // Are we done already? - if (mobileIdentity.type()==IMSIType){ - //Cause the tmsi table to be touched - gTMSITable.TMSI(mobileIdentity.digits()); - return; - } + if (mobileIdentity.type()==IMSIType) return; // If we got a TMSI, find the IMSI. if (mobileIdentity.type()==TMSIType) { char *IMSI = gTMSITable.IMSI(mobileIdentity.TMSI()); - if (IMSI) mobileIdentity = L3MobileIdentity(IMSI); + if (IMSI) { + GPRS::GPRSNotifyGsmActivity(IMSI); + mobileIdentity = L3MobileIdentity(IMSI); + } free(IMSI); } @@ -178,6 +202,9 @@ void Control::resolveIMSI(L3MobileIdentity& mobileIdentity, LogicalChannel* LCH throw UnexpectedMessage(); } mobileIdentity = resp->mobileID(); + if (mobileIdentity.type()==IMSIType) { + GPRS::GPRSNotifyGsmActivity(mobileIdentity.digits()); + } delete msg; } diff --git a/Control/ControlCommon.h b/Control/ControlCommon.h index 58b88992..6f63f822 100644 --- a/Control/ControlCommon.h +++ b/Control/ControlCommon.h @@ -1,27 +1,19 @@ /**@file Declarations for common-use control-layer functions. */ /* -* Copyright 2008-2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -114,8 +106,8 @@ GSM::L3Message* getMessage(GSM::LogicalChannel* LCH, unsigned SAPI=0); /**@name Dispatch controllers for specific channel types. */ //@{ -void FACCHDispatcher(GSM::TCHFACCHLogicalChannel *TCHFACCH); -void SDCCHDispatcher(GSM::SDCCHLogicalChannel *SDCCH); +//void FACCHDispatcher(GSM::TCHFACCHLogicalChannel *TCHFACCH); +//void SDCCHDispatcher(GSM::SDCCHLogicalChannel *SDCCH); void DCCHDispatcher(GSM::LogicalChannel *DCCH); //@} @@ -141,6 +133,12 @@ void resolveIMSI(GSM::L3MobileIdentity& mobID, GSM::LogicalChannel* LCH); +/** + SMSCB sender function +*/ +void *SMSCBSender(void*); + + @@ -213,6 +211,8 @@ class RemovedTransaction : public ControlLayerException { :ControlLayerException(wTransactionID) {} }; + + //@} diff --git a/Control/DCCHDispatch.cpp b/Control/DCCHDispatch.cpp index ae313fe5..faff8cac 100644 --- a/Control/DCCHDispatch.cpp +++ b/Control/DCCHDispatch.cpp @@ -1,27 +1,19 @@ /**@file Idle-mode dispatcher for dedicated control channels. */ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -85,8 +77,6 @@ void DCCHDispatchRR(const L3RRMessage* req, LogicalChannel *DCCH) { LOG(DEBUG) << "checking MTI"<< (L3RRMessage::MessageType)req->MTI(); - // TODO SMS -- This needs to handle SACCH Measurement Reports. - assert(req); L3RRMessage::MessageType MTI = (L3RRMessage::MessageType)req->MTI(); switch (MTI) { @@ -94,8 +84,9 @@ void DCCHDispatchRR(const L3RRMessage* req, LogicalChannel *DCCH) PagingResponseHandler(dynamic_cast(req),DCCH); break; case L3RRMessage::AssignmentComplete: - AssignmentCompleteHandler(dynamic_cast(req), - dynamic_cast(DCCH)); + AssignmentCompleteHandler( + dynamic_cast(req), + dynamic_cast(DCCH)); break; default: LOG(NOTICE) << "unhandled RR message " << MTI << " on " << *DCCH; @@ -123,23 +114,41 @@ void DCCHDispatchMessage(const L3Message* msg, LogicalChannel* DCCH) + /** Example of a closed-loop, persistent-thread control function for the DCCH. */ +// (pat) DCCH is a TCHFACCHLogicalChannel or SDCCHLogicalChannel void Control::DCCHDispatcher(LogicalChannel *DCCH) { while (1) { try { // Wait for a transaction to start. - LOG(DEBUG) << "waiting for " << *DCCH << " ESTABLISH"; - DCCH->waitForPrimitive(ESTABLISH); - // Pull the first message and dispatch a new transaction. - gReports.incr("OpenBTS.GSM.RR.ChannelSiezed"); - const L3Message *message = getMessage(DCCH); - LOG(DEBUG) << *DCCH << " received " << *message; - DCCHDispatchMessage(message,DCCH); - delete message; + LOG(DEBUG) << "waiting for " << *DCCH << " ESTABLISH or HANDOVER_ACCESS"; + L3Frame *frame = DCCH->waitForEstablishOrHandover(); + LOG(DEBUG) << *DCCH << " received " << *frame; + gResetWatchdog(); + Primitive prim = frame->primitive(); + delete frame; + LOG(DEBUG) << "received primtive " << prim; + switch (prim) { + case ESTABLISH: { + // Pull the first message and dispatch a new transaction. + gReports.incr("OpenBTS.GSM.RR.ChannelSiezed"); + const L3Message *message = getMessage(DCCH); + LOG(INFO) << *DCCH << " received establishing messaage " << *message; + DCCHDispatchMessage(message,DCCH); + delete message; + break; + } + case HANDOVER_ACCESS: { + ProcessHandoverAccess(dynamic_cast(DCCH)); + break; + } + default: assert(0); + } } // Catch the various error cases. + catch (RemovedTransaction except) { LOG(ERR) << "attempt to use removed transaciton " << except.transactionID(); } diff --git a/Control/Makefile.am b/Control/Makefile.am index 39a1cfd9..98b9888a 100644 --- a/Control/Makefile.am +++ b/Control/Makefile.am @@ -38,9 +38,12 @@ libcontrol_la_SOURCES = \ MobilityManagement.cpp \ RadioResource.cpp \ DCCHDispatch.cpp \ + SMSCB.cpp \ RRLPServer.cpp +# TODO - move CollectMSInfo.cpp and RRLPQueryController.cpp to RRLP directory. + noinst_HEADERS = \ ControlCommon.h \ SMSControl.h \ diff --git a/Control/MobilityManagement.cpp b/Control/MobilityManagement.cpp index 12cff676..62ca5876 100644 --- a/Control/MobilityManagement.cpp +++ b/Control/MobilityManagement.cpp @@ -1,26 +1,18 @@ /**@file GSM/SIP Mobility Management, GSM 04.08. */ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -97,9 +89,11 @@ void Control::IMSIDetachController(const L3IMSIDetachIndication* idi, LogicalCha // The IMSI detach maps to a SIP unregister with the local Asterisk server. try { - // FIXME -- Resolve TMSIs to IMSIs. + // FIXME -- Resolve TMSIs to IMSIs. (pat) And when you do call GPRSNotifyGsmActivity() on it. if (idi->mobileID().type()==IMSIType) { - SIPEngine engine(gConfig.getStr("SIP.Proxy.Registration").c_str(), idi->mobileID().digits()); + const char *digits = idi->mobileID().digits(); + GPRS::GPRSNotifyGsmActivity(digits); + SIPEngine engine(gConfig.getStr("SIP.Proxy.Registration").c_str(), digits); engine.unregister(); } } @@ -122,7 +116,8 @@ void Control::IMSIDetachController(const L3IMSIDetachIndication* idi, LogicalCha */ bool sendWelcomeMessage(const char* messageName, const char* shortCodeName, const char *IMSI, LogicalChannel* DCCH, const char *whiteListCode = NULL) { - if (!gConfig.defines(messageName)) return false; + if (!gConfig.defines(messageName) || !gConfig.defines(shortCodeName)) return false; + if (!gConfig.getStr(messageName).length() || !gConfig.getStr(shortCodeName).length()) return false; LOG(INFO) << "sending " << messageName << " message to handset"; ostringstream message; message << gConfig.getStr(messageName) << " IMSI:" << IMSI; @@ -137,6 +132,56 @@ bool sendWelcomeMessage(const char* messageName, const char* shortCodeName, cons return true; } + +/** + Check if a phone is white-listed. + @param name name of subscriber + @return true if phone was already white-listed +*/ +bool isPhoneWhiteListed(string name) +{ + + // if name isn't in SR, then put it in with white-list flag off + string id = gSubscriberRegistry.imsiGet(name, "id"); + if (id.empty()) { + //we used to create a user here, but that's almost certainly wrong. -kurtis + LOG(CRIT) << "Checking whitelist of a user that doesn't exist. Reject"; + //return not-white-listed + return false; + } + // check flag + string whiteListFlag = gSubscriberRegistry.imsiGet(name, "whiteListFlag"); + if (whiteListFlag.empty()){ + LOG(CRIT) << "SR query error"; + return false; + } + return (whiteListFlag == "0"); +} + +/** + Generate white-list code. + @param name name of subscriber + @return the white-list code. + Also put it into the SR database. +*/ +string genWhiteListCode(string name) +{ + // generate code + uint32_t wlc = (uint32_t)rand(); + ostringstream os2; + os2 << hex << wlc; + string whiteListCode = os2.str(); + // write to SR + if (gSubscriberRegistry.imsiSet(name, "whiteListCode", whiteListCode)){ + LOG(CRIT) << "SR update error"; + return ""; + } + // and return it + return whiteListCode; +} + + + /** Controller for the Location Updating transaction, GSM 04.08 4.4.4. @param lur The location updating request. @@ -170,13 +215,38 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L unsigned newTMSI = 0; if (!preexistingTMSI) newTMSI = gTMSITable.assign(IMSI,lur); + // White-listing. + const string name = "IMSI" + string(IMSI); + if (gConfig.getBool("Control.LUR.WhiteList")) { + LOG(INFO) << "checking white-list for " << name; + if (!isPhoneWhiteListed(name)) { + // not white-listed. reject phone. + LOG(INFO) << "is NOT white-listed"; + DCCH->send(L3LocationUpdatingReject(gConfig.getNum("Control.LUR.WhiteListing.RejectCause"))); + if (!preexistingTMSI) { + // generate code (and put in SR) and send message if first time. + string whiteListCode = genWhiteListCode(name); + LOG(INFO) << "generated white-list code: " << whiteListCode; + sendWelcomeMessage("Control.LUR.WhiteListing.Message", "Control.LUR.WhiteListing.ShortCode", IMSI, DCCH, whiteListCode.c_str()); + } + // Release the channel and return. + DCCH->send(L3ChannelRelease()); + return; + } else { + LOG(INFO) << "IS white-listed"; + } + } else { + LOG(INFO) << "not checking white-list for " << name; + } + // Try to register the IMSI. // This will be set true if registration succeeded in the SIP world. bool success = false; + string RAND; try { SIPEngine engine(gConfig.getStr("SIP.Proxy.Registration").c_str(),IMSI); LOG(DEBUG) << "waiting for registration of " << IMSI << " on " << gConfig.getStr("SIP.Proxy.Registration"); - success = engine.Register(SIPEngine::SIPRegister); + success = engine.Register(SIPEngine::SIPRegister, DCCH, &RAND); } catch(SIPTimeout) { LOG(ALERT) "SIP registration timed out. Is the proxy running at " << gConfig.getStr("SIP.Proxy.Registration"); @@ -191,23 +261,111 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L return; } + // Query for classmark? + // This needs to happen before encryption. + // It can't be optional if encryption is desired. + if (IMSIAttach && (gConfig.getBool("GSM.Cipher.Encrypt") || gConfig.getBool("Control.LUR.QueryClassmark"))) { + DCCH->send(L3ClassmarkEnquiry()); + L3Message* msg = getMessage(DCCH); + L3ClassmarkChange *resp = dynamic_cast(msg); + if (!resp) { + if (msg) { + LOG(WARNING) << "Unexpected message " << *msg; + delete msg; + } + throw UnexpectedMessage(); + } + LOG(INFO) << *resp; + const L3MobileStationClassmark2& classmark = resp->classmark(); + if (!gTMSITable.classmark(IMSI,classmark)) + LOG(WARNING) << "failed access to TMSITable"; + delete msg; + } + + // Did we get a RAND for challenge-response? + if (RAND.length() != 0) { + // Get the mobile's SRES. + LOG(INFO) << "sending " << RAND << " to mobile"; + uint64_t uRAND; + uint64_t lRAND; + gSubscriberRegistry.stringToUint(RAND, &uRAND, &lRAND); + gReports.incr("OpenBTS.GSM.MM.Authenticate.Request"); + DCCH->send(L3AuthenticationRequest(0,L3RAND(uRAND,lRAND))); + L3Message* msg = getMessage(DCCH); + L3AuthenticationResponse *resp = dynamic_cast(msg); + if (!resp) { + if (msg) { + LOG(WARNING) << "Unexpected message " << *msg; + delete msg; + } + // FIXME -- We should differentiate between wrong message and no message at all. + throw UnexpectedMessage(); + } + LOG(INFO) << *resp; + uint32_t mobileSRES = resp->SRES().value(); + delete msg; + // verify SRES + try { + SIPEngine engine(gConfig.getStr("SIP.Proxy.Registration").c_str(),IMSI); + LOG(DEBUG) << "waiting for authentication of " << IMSI << " on " << gConfig.getStr("SIP.Proxy.Registration"); + ostringstream os; + os << hex << mobileSRES; + string SRESstr = os.str(); + success = engine.Register(SIPEngine::SIPRegister, DCCH, &RAND, IMSI, SRESstr.c_str()); + if (!success) { + gReports.incr("OpenBTS.GSM.MM.Authenticate.Failure"); + LOG(CRIT) << "authentication failure for IMSI " << IMSI; + DCCH->send(L3AuthenticationReject()); + DCCH->send(L3ChannelRelease()); + return; + } + if (gConfig.getBool("GSM.Cipher.Encrypt")) { + int encryptionAlgorithm = gTMSITable.getPreferredA5Algorithm(IMSI); + if (!encryptionAlgorithm) { + LOG(DEBUG) << "A5/3 and A5/1 not supported: NOT sending Ciphering Mode Command on " << *DCCH << " for " << mobileID; + } else if (DCCH->decryptUplink_maybe(mobileID.digits(), encryptionAlgorithm)) { + LOG(DEBUG) << "sending Ciphering Mode Command on " << *DCCH << " for " << mobileID; + DCCH->send(GSM::L3CipheringModeCommand( + GSM::L3CipheringModeSetting(true, encryptionAlgorithm), + GSM::L3CipheringModeResponse(false))); + } else { + LOG(DEBUG) << "no ki: NOT sending Ciphering Mode Command on " << *DCCH << " for " << mobileID; + } + } + gReports.incr("OpenBTS.GSM.MM.Authenticate.Success"); + } + catch(SIPTimeout) { + LOG(ALERT) "SIP authentication timed out. Is the proxy running at " << gConfig.getStr("SIP.Proxy.Registration"); + // Reject with a "network failure" cause code, 0x11. + DCCH->send(L3LocationUpdatingReject(0x11)); + // HACK -- wait long enough for a response + // FIXME -- Why are we doing this? + sleep(4); + // Release the channel and return. + DCCH->send(L3ChannelRelease()); + return; + } + } + // This allows us to configure Open Registration bool openRegistration = false; - if (gConfig.defines("Control.LUR.OpenRegistration")) { - if (!gConfig.defines("Control.LUR.OpenRegistration.Message")) { - gConfig.set("Control.LUR.OpenRegistration.Message","Welcome to the test network. Your IMSI is "); - } - Regexp rxp(gConfig.getStr("Control.LUR.OpenRegistration").c_str()); + string openRegistrationPattern = gConfig.getStr("Control.LUR.OpenRegistration"); + if (openRegistrationPattern.length()) { + Regexp rxp(openRegistrationPattern.c_str()); openRegistration = rxp.match(IMSI); - if (gConfig.defines("Control.LUR.OpenRegistration.Reject")) { - Regexp rxpReject(gConfig.getStr("Control.LUR.OpenRegistration.Reject").c_str()); + string openRegistrationRejectPattern = gConfig.getStr("Control.LUR.OpenRegistration.Reject"); + if (openRegistrationRejectPattern.length()) { + Regexp rxpReject(openRegistrationRejectPattern.c_str()); bool openRegistrationReject = rxpReject.match(IMSI); openRegistration = openRegistration && !openRegistrationReject; } } // Query for IMEI? - if (gConfig.defines("Control.LUR.QueryIMEI")) { + // FIXME -- This needs to happen before sending the REGISTER method, so we can put it in a SIP header. + // See ticket #1101. + unsigned rejectCause = gConfig.getNum("Control.LUR.UnprovisionedRejectCause"); + if (gConfig.getBool("Control.LUR.QueryIMEI")) { DCCH->send(L3IdentityRequest(IMEIType)); L3Message* msg = getMessage(DCCH); L3IdentityResponse *resp = dynamic_cast(msg); @@ -222,10 +380,8 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L string new_imei = resp->mobileID().digits(); if (!gTMSITable.IMEI(IMSI,new_imei.c_str())){ LOG(WARNING) << "failed access to TMSITable"; - } - - //query subscriber registry for old imei, update if neccessary - string name = string("IMSI") + IMSI; + } + //query subscriber registry for old imei, update if necessary string old_imei = gSubscriberRegistry.imsiGet(name, "hardware"); //if we have a new imei and either there's no old one, or it is different... @@ -241,29 +397,10 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L delete msg; } - // Query for classmark? - if (IMSIAttach && gConfig.defines("Control.LUR.QueryClassmark")) { - DCCH->send(L3ClassmarkEnquiry()); - L3Message* msg = getMessage(DCCH); - L3ClassmarkChange *resp = dynamic_cast(msg); - if (!resp) { - if (msg) { - LOG(WARNING) << "Unexpected message " << *msg; - delete msg; - } - throw UnexpectedMessage(); - } - LOG(INFO) << *resp; - const L3MobileStationClassmark2& classmark = resp->classmark(); - if (!gTMSITable.classmark(IMSI,classmark)) - LOG(WARNING) << "failed access to TMSITable"; - delete msg; - } - // We fail closed unless we're configured otherwise if (!success && !openRegistration) { LOG(INFO) << "registration FAILED: " << mobileID; - DCCH->send(L3LocationUpdatingReject(gConfig.getNum("Control.LUR.UnprovisionedRejectCause"))); + DCCH->send(L3LocationUpdatingReject(rejectCause)); if (!preexistingTMSI) { sendWelcomeMessage( "Control.LUR.FailedRegistration.Message", "Control.LUR.FailedRegistration.ShortCode", IMSI,DCCH); @@ -289,7 +426,7 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L DCCH->send(L3MMInformation(gConfig.getStr("GSM.Identity.ShortName").c_str())); } // Accept. Make a TMSI assignment, too, if needed. - if (preexistingTMSI || !gConfig.defines("Control.LUR.SendTMSIs")) { + if (preexistingTMSI || !gConfig.getBool("Control.LUR.SendTMSIs")) { DCCH->send(L3LocationUpdatingAccept(gBTS.LAI())); } else { assert(newTMSI); @@ -305,11 +442,11 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L delete resp; } - if (gConfig.defines("Control.LUR.QueryRRLP")) { + if (gConfig.getBool("Control.LUR.QueryRRLP")) { // Query for RRLP if (!sendRRLP(mobileID, DCCH)) { LOG(INFO) << "RRLP request failed"; - } + } } // If this is an IMSI attach, send a welcome message. @@ -321,6 +458,9 @@ void Control::LocationUpdatingController(const L3LocationUpdatingRequest* lur, L sendWelcomeMessage( "Control.LUR.OpenRegistration.Message", "Control.LUR.OpenRegistration.ShortCode", IMSI, DCCH); } + // set unix time of most recent registration + // No - this happending in the registration proxy. + //gSubscriberRegistry.setRegTime(name); } // Release the channel and return. diff --git a/Control/MobilityManagement.h b/Control/MobilityManagement.h index dcfb4418..2f0e2968 100644 --- a/Control/MobilityManagement.h +++ b/Control/MobilityManagement.h @@ -3,24 +3,14 @@ * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/Control/RRLPServer.cpp b/Control/RRLPServer.cpp index abdd69a0..3e33d4d9 100644 --- a/Control/RRLPServer.cpp +++ b/Control/RRLPServer.cpp @@ -98,9 +98,10 @@ RRLPServer::RRLPServer(L3MobileIdentity wMobileID, LogicalChannel *wDCCH) DCCH = wDCCH; // name of subscriber name = string("IMSI") + mobileID.digits(); + // don't continue if IMSI has a RRLP support flag and it's false //if IMEI tagging enabled, check if this IMEI (which is updated elsewhere) has RRLP disabled //otherwise just go on - if (gConfig.defines("Control.LUR.QueryIMEI")){ + if (gConfig.getBool("Control.LUR.QueryIMEI")){ //check supported bit string supported= gSubscriberRegistry.imsiGet(name, "RRLPSupported"); if(supported.empty() || supported == "0"){ @@ -192,6 +193,7 @@ bool RRLPServer::transact() // bounce off mobile if (apdus.size() == 0) { LOG(INFO) << "missing apdu for mobile"; + LOG(ERR) << "MS did not respond gracefully to RRLP message."; return false; } string apdu = apdus[0]; @@ -199,6 +201,7 @@ bool RRLPServer::transact() BitVector bv(apdu.size()*4); if (!bv.unhex(apdu.c_str())) { LOG(INFO) << "BitVector::unhex problem"; + LOG(ERR) << "MS did not respond gracefully to RRLP message."; return false; } @@ -206,13 +209,15 @@ bool RRLPServer::transact() // Receive an L3 frame with a timeout. Timeout loc req response time max + 2 seconds. L3Frame* resp = DCCH->recv(130000); if (!resp) { + LOG(INFO) << "timed out"; + LOG(ERR) << "MS did not respond gracefully to RRLP message."; return false; } LOG(INFO) << "RRLPQuery returned " << *resp; if (resp->primitive() != DATA) { LOG(INFO) << "didn't receive data"; switch (resp->primitive()) { - case ESTABLISH: LOG(INFO) << "channel establihsment"; break; + case ESTABLISH: LOG(INFO) << "channel establishment"; break; case RELEASE: LOG(INFO) << "normal channel release"; break; case DATA: LOG(INFO) << "multiframe data transfer"; break; case UNIT_DATA: LOG(INFO) << "datagram-type data transfer"; break; @@ -221,6 +226,7 @@ bool RRLPServer::transact() default: LOG(INFO) << "unrecognized primitive response"; break; } delete resp; + LOG(ERR) << "MS did not respond gracefully to RRLP message."; return false; } const unsigned PD_RR = 6; @@ -228,17 +234,20 @@ bool RRLPServer::transact() if (resp->PD() != PD_RR) { LOG(INFO) << "didn't receive an RR message"; delete resp; + LOG(ERR) << "MS did not respond gracefully to RRLP message."; return false; } const unsigned MTI_RR_STATUS = 18; if (resp->MTI() == MTI_RR_STATUS) { + ostringstream os2; int cause = resp->peekField(16, 8); delete resp; switch (cause) { case 97: LOG(INFO) << "MS says: message not implemented"; //Rejection code only useful if we're gathering IMEIs - if (gConfig.defines("Control.LUR.QueryIMEI")){ + if (gConfig.getBool("Control.LUR.QueryIMEI")){ + // flag unsupported in SR so we don't waste time on it again // flag unsupported in SR so we don't waste time on it again if (gSubscriberRegistry.imsiSet(name, "RRLPSupported", "0")) { LOG(INFO) << "SR update problem"; diff --git a/Control/RRLP_PDU_Test.cpp b/Control/RRLP_PDU_Test.cpp new file mode 100644 index 00000000..3694eb15 --- /dev/null +++ b/Control/RRLP_PDU_Test.cpp @@ -0,0 +1,23 @@ +#include "Configuration.h" +#include "RRLPQueryController.h" + +using namespace GSM; +using namespace GSM::RRLP; + +// compile one liner: +// g++ -DRRLP_TEST_HACK -L../GSM/.libs -L../CommonLibs/.libs -I../CLI -I../Globals -I../GSM -I../CommonLibs RRLPQueryController.cpp RRLP_PDU_Test.cpp -lcommon -lGSM -lpthread -o RRLP_PDU_Test + +// Required by various stuff - I think a TODO is to allow tests to work without +// having to copy this line around. +ConfigurationTable gConfig("OpenBTS.config"); + +int main() +{ + // This is a MsrPositionRsp with valid coordinates + // 38.28411340713501,237.95414686203003,22 (as parsed by the erlang automatically generated + // implementation - checked against a map). + BitVector test("0000011000111000000000000001011000100010000100011111111111111111010010101111101111011110101101100100000011010110110100111000111010100011110010010100111000000000010001000001100000011000110010000010000100010000"); + RRLPQueryController c(test); + return 0; +} + diff --git a/Control/RadioResource.cpp b/Control/RadioResource.cpp index 60a1edc9..4fa10e2a 100644 --- a/Control/RadioResource.cpp +++ b/Control/RadioResource.cpp @@ -1,29 +1,21 @@ /**@file GSM Radio Resource procedures, GSM 04.18 and GSM 04.08. */ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - */ @@ -33,6 +25,7 @@ #include #include +#include #include "ControlCommon.h" #include "TransactionTable.h" #include "RadioResource.h" @@ -41,6 +34,12 @@ #include #include +#include "../GPRS/GPRSExport.h" + +#include +#include +#include + #include #include @@ -49,12 +48,12 @@ - using namespace std; using namespace GSM; using namespace Control; +extern unsigned allocateRTPPorts(); @@ -67,6 +66,7 @@ using namespace Control; @param RA The request reference from the channel request message. @return channel type code, undefined if not a supported service */ +static ChannelType decodeChannelNeeded(unsigned RA) { // This code is based on GSM 04.08 Table 9.9. @@ -74,6 +74,7 @@ ChannelType decodeChannelNeeded(unsigned RA) unsigned RA4 = RA>>4; unsigned RA5 = RA>>5; + // Answer to paging, Table 9.9a. // We don't support TCH/H, so it's wither SDCCH or TCH/F. // The spec allows for "SDCCH-only" MS. We won't support that here. @@ -82,14 +83,15 @@ ChannelType decodeChannelNeeded(unsigned RA) if (RA4 == 0x01) return SDCCHType; // SDCCH if (RA4 == 0x02) return TCHFType; // TCH/F if (RA4 == 0x03) return TCHFType; // TCH/F + if ((RA&0xf8) == 0x78 && RA != 0x7f) return PSingleBlock1PhaseType; + if ((RA&0xf8) == 0x70) return PSingleBlock2PhaseType; int NECI = gConfig.getNum("GSM.CellSelection.NECI"); if (NECI==0) { if (RA5 == 0x07) return SDCCHType; // MOC or SDCCH procedures if (RA5 == 0x00) return SDCCHType; // location updating } else { - assert(NECI==1); - if (gConfig.defines("Control.VEA")) { + if (gConfig.getBool("Control.VEA")) { // Very Early Assignment if (RA5 == 0x07) return TCHFType; // MOC for TCH/F if (RA4 == 0x04) return TCHFType; // MOC, TCH/H sufficient @@ -103,12 +105,13 @@ ChannelType decodeChannelNeeded(unsigned RA) } // Anything else falls through to here. - // We are still ignoring data calls, GPRS, LMU. + // We are still ignoring data calls, LMU. return UndefinedCHType; } /** Return true if RA indicates LUR. */ +static bool requestingLUR(unsigned RA) { int NECI = gConfig.getNum("GSM.CellSelection.NECI"); @@ -117,10 +120,8 @@ bool requestingLUR(unsigned RA) } - - - -/** Decode RACH bits and send an immediate assignment; may block waiting for a channel. */ +/** Decode RACH bits and send an immediate assignment; may block waiting for a channel for an SOS call. */ +static void AccessGrantResponder( unsigned RA, const GSM::Time& when, float RSSI, float timingError) @@ -149,18 +150,39 @@ void AccessGrantResponder( static const int maxAge = GSM::RACHSpreadSlots[txInteger] + GSM::RACHWaitSParam[txInteger]; // Check burst age. int age = gBTS.time() - when; - LOG(INFO) << "RA=0x" << hex << RA << dec - << " when=" << when << " age=" << age - << " delay=" << timingError << " RSSI=" << RSSI; + ChannelType chtype = decodeChannelNeeded(RA); + int lur = requestingLUR(RA); + int gprs = (chtype == PSingleBlock1PhaseType) || (chtype == PSingleBlock2PhaseType); + + // This is for debugging. + if (GPRS::GPRSDebug && gprs) { + Time now = gBTS.time(); + LOG(NOTICE) << "RACH" <gConfig.getNum("GSM.MS.TA.Max")) { - LOG(WARNING) << "ignoring RACH burst with delay " << timingError; + LOG(NOTICE) << "ignoring RACH burst with delay="<load()>gConfig.getNum("GSM.CCCH.AGCH.QMax")) { - LOG(WARNING) "AGCH congestion"; + // (pat) The default value is 5, so about 1.25 second for a system + // with a C0T0 beacon with only one CCCH. + if ((int)AGCH->load()>gConfig.getNum("GSM.CCCH.AGCH.QMax")) { + LOG(CRIT) << "AGCH congestion"; return; } // Check for location update. // This gives LUR a lower priority than other services. + // (pat): LUR = Location Update Request Message if (requestingLUR(RA)) { // Don't answer this LUR if it will not leave enough channels open for other operations. if ((int)gBTS.SDCCHAvailable()<=gConfig.getNum("GSM.Channels.SDCCHReserve")) { unsigned waitTime = gBTS.growT3122()/1000; - LOG(WARNING) << "LUR congestion, RA=" << RA << " T3122=" << waitTime; + LOG(CRIT) << "LUR congestion, RA=" << RA << " T3122=" << waitTime; const L3ImmediateAssignmentReject reject(L3RequestReference(RA,when),waitTime); LOG(DEBUG) << "LUR rejection, sending " << reject; AGCH->send(reject); @@ -191,9 +216,24 @@ void AccessGrantResponder( // Allocate the channel according to the needed type indicated by RA. // The returned channel is already open and ready for the transaction. LogicalChannel *LCH = NULL; - switch (decodeChannelNeeded(RA)) { + switch (chtype) { case TCHFType: LCH = gBTS.getTCH(); break; case SDCCHType: LCH = gBTS.getSDCCH(); break; +#if 0 + // GSM04.08 sec 3.5.2.1.2 + case PSingleBlock1PhaseType: + case PSingleBlock2PhaseType: + { + L3RRMessage *msg = GPRS::GPRSProcessRACH(chtype, + L3RequestReference(RA,when), + RSSI,timingError,AGCH); + if (msg) { + AGCH->send(*msg); + delete msg; + } + return; + } +#endif // If we don't support the service, assign to an SDCCH and we can reject it in L3. case UndefinedCHType: LOG(NOTICE) << "RACH burst for unsupported service RA=" << RA; @@ -206,18 +246,22 @@ void AccessGrantResponder( // Nothing available? if (!LCH) { // Rejection, GSM 04.08 3.3.1.1.3.2. - // But since we recognize SOS calls already, - // we might as well save some AGCH bandwidth. unsigned waitTime = gBTS.growT3122()/1000; - LOG(WARNING) << "congestion, RA=" << RA << " T3122=" << waitTime; + // TODO: If all channels are statically allocated for gprs, dont throw an alert. + LOG(CRIT) << "congestion, RA=" << RA << " T3122=" << waitTime; const L3ImmediateAssignmentReject reject(L3RequestReference(RA,when),waitTime); LOG(DEBUG) << "rejection, sending " << reject; AGCH->send(reject); return; } + // (pat) gprs todo: Notify GPRS that the MS is getting a voice channel. + // It may imply abandonment of packet contexts, if the phone does not + // support DTM (Dual Transfer Mode.) There may be other housekeeping + // for DTM phones; haven't looked into it. + // Set the channel physical parameters from the RACH burst. - LCH->setPhy(RSSI,timingError); + LCH->setPhy(RSSI,timingError,gBTS.clock().systime(when.FN())); gReports.incr("OpenBTS.GSM.RR.RACH.TA.Accepted",(int)(timingError)); // Assignment, GSM 04.08 3.3.1.1.3.1. @@ -231,7 +275,11 @@ void AccessGrantResponder( LCH->channelDescription(), L3TimingAdvance(initialTA) ); - LOG(INFO) << "sending " << assign; + LOG(INFO) << "sending L3ImmediateAssignment " << assign; + // (pat) This call appears to block. + // (david) Not anymore. It got fixed in the trunk while you were working on GPRS. + // (doug) Adding in a delay to make sure SI5/6 get out before IA. + sleepFrames(20); AGCH->send(assign); // On successful allocation, shrink T3122. @@ -254,6 +302,256 @@ void* Control::AccessGrantServiceLoop(void*) return NULL; } +void abortInboundHandover(TransactionEntry* transaction, unsigned cause, GSM::LogicalChannel *LCH=NULL) +{ + LOG(DEBUG) << "aborting inbound handover " << *transaction; + char ind[100]; + unsigned holdoff = gConfig.getNum("GSM.Handover.FailureHoldoff"); + sprintf(ind,"IND HANDOVER_FAILURE %u %u %u", transaction->ID(),cause,holdoff); + gPeerInterface.sendUntilAck(transaction,ind); + + if (LCH) LCH->send(HARDRELEASE); + + gTransactionTable.remove(transaction); +} + + + +bool Control::SaveHandoverAccess(unsigned handoverReference, float RSSI, float timingError, const GSM::Time& timestamp) +{ + // In this function, we are "BS2" in the ladder diagram. + // This is called from L1 when a handover burst arrives. + + // We will need to use the transaction record to carry the parameters. + // We put this here to avoid dealing with the transaction table in L1. + TransactionEntry *transaction = gTransactionTable.inboundHandover(handoverReference); + if (!transaction) { + LOG(ERR) << "no inbound handover with reference " << handoverReference; + return false; + } + + if (timingError > gConfig.getNum("GSM.MS.TA.Max")) { + // Handover failure. + LOG(NOTICE) << "handover failure on due to TA=" << timingError << " for " << *transaction; + // RR cause 8: Handover impossible, timing advance out of range + abortInboundHandover(transaction,8); + return false; + } + + LOG(INFO) << "saving handover access for " << *transaction; + transaction->setInboundHandover(RSSI,timingError,gBTS.clock().systime(timestamp)); + return true; +} + + + + +void Control::ProcessHandoverAccess(GSM::TCHFACCHLogicalChannel *TCH) +{ + // In this function, we are "BS2" in the ladder diagram. + // This is called from the DCCH dispatcher when it gets a HANDOVER_ACCESS primtive. + // The information it needs was saved in the transaction table by Control::SaveHandoverAccess. + + + assert(TCH); + LOG(DEBUG) << *TCH; + + TransactionEntry *transaction = gTransactionTable.inboundHandover(TCH); + if (!transaction) { + LOG(WARNING) << "handover access with no inbound transaction on " << *TCH; + TCH->send(HARDRELEASE); + return; + } + + // clear handover in transceiver + LOG(DEBUG) << *transaction; + transaction->channel()->handoverPending(false); + + // Respond to handset with physical information until we get Handover Complete. + int TA = (int)(transaction->inboundTimingError() + 0.5F); + if (TA<0) TA=0; + if (TA>62) TA=62; + unsigned repeatTimeout = gConfig.getNum("GSM.Timer.T3105"); + unsigned sendCount = gConfig.getNum("GSM.Ny1"); + L3Frame* frame = NULL; + while (!frame && sendCount) { + TCH->send(L3PhysicalInformation(L3TimingAdvance(TA)),GSM::UNIT_DATA); + sendCount--; + frame = TCH->recv(repeatTimeout); + if (frame && frame->primitive() == HANDOVER_ACCESS) { + LOG(NOTICE) << "flushing HANDOVER_ACCESS while waiting for Handover Complete"; + delete frame; + frame = NULL; + } + } + + // Timed out? + if (!frame) { + LOG(NOTICE) << "timed out waiting for Handover Complete on " << *TCH << " for " << *transaction; + // RR cause 4: Abnormal release, no activity on the radio path + abortInboundHandover(transaction,4,TCH); + return; + } + + // Screwed up channel? + if (frame->primitive()!=ESTABLISH) { + LOG(NOTICE) << "unexpected primitive waiting for Handover Complete on " + << *TCH << ": " << *frame << " for " << *transaction; + delete frame; + // RR cause 0x62: Message not compatible with protocol state + abortInboundHandover(transaction,0x62,TCH); + return; + } + + // Get the next frame, should be HandoverComplete. + delete frame; + frame = TCH->recv(); + L3Message* msg = parseL3(*frame); + if (!msg) { + LOG(NOTICE) << "unparsable message waiting for Handover Complete on " + << *TCH << ": " << *frame << " for " << *transaction; + delete frame; + // RR cause 0x62: Message not compatible with protocol state + TCH->send(L3ChannelRelease(0x62)); + abortInboundHandover(transaction,0x62,TCH); + return; + } + delete frame; + + L3HandoverComplete* complete = dynamic_cast(msg); + if (!complete) { + LOG(NOTICE) << "expecting for Handover Complete on " + << *TCH << "but got: " << *msg << " for " << *transaction; + delete frame; + // RR cause 0x62: Message not compatible with protocol state + TCH->send(L3ChannelRelease(0x62)); + abortInboundHandover(transaction,0x62,TCH); + } + delete msg; + + // Send re-INVITE to the remote party. + unsigned RTPPort = allocateRTPPorts(); + SIP::SIPState st = transaction->inboundHandoverSendINVITE(RTPPort); + if (st == SIP::Fail) { + abortInboundHandover(transaction,4,TCH); + return; + } + + transaction->GSMState(GSM::HandoverProgress); + + while (1) { + // FIXME - the sip engine should be doing this + // FIXME - and checking for timeout + // FIXME - and checking for proceeding (stop sending the resends) + st = transaction->inboundHandoverCheckForOK(); + if (st == SIP::Active) break; + if (st == SIP::Fail) { + LOG(NOTICE) << "received Fail while waiting for OK"; + abortInboundHandover(transaction,4,TCH); + return; + } + } + st = transaction->inboundHandoverSendACK(); + LOG(DEBUG) << "status of inboundHandoverSendACK: " << st << " for " << *transaction; + + // Send completion to peer BTS. + char ind[100]; + sprintf(ind,"IND HANDOVER_COMPLETE %u", transaction->ID()); + gPeerInterface.sendUntilAck(transaction,ind); + + // Update subscriber registry to reflect new registration. + if (transaction->SRIMSI().length() && transaction->SRCALLID().length()) { + gSubscriberRegistry.addUser(transaction->SRIMSI().c_str(), transaction->SRCALLID().c_str()); + } + + // The call is running. + LOG(INFO) << "succesful inbound handover " << *transaction; + transaction->GSMState(GSM::Active); + callManagementLoop(transaction,TCH); +} + + +void Control::HandoverDetermination(const L3MeasurementResults& measurements, SACCHLogicalChannel* SACCH) +{ + // This is called from the SACCH service loop. + + // Valid measurements? + if (measurements.MEAS_VALID()) return; + + // Got neighbors? + unsigned N = measurements.NO_NCELL(); + if (N==0) return; + + if (N == 7) { + LOG(DEBUG) << "neighbor cell information not available"; + return; + } + + // Is our current signal OK? + int myRxLevel = measurements.RXLEV_SUB_SERVING_CELL_dBm(); + int localRSSIMin = gConfig.getNum("GSM.Handover.LocalRSSIMin"); + LOG(DEBUG) << "myRxLevel=" << myRxLevel << " dBm localRSSIMin=" << localRSSIMin << " dBm"; + if (myRxLevel > localRSSIMin) return; + + + // Look at neighbor cell rx levels + int best = 0; + int bestRxLevel = measurements.RXLEV_NCELL_dBm(best); + for (unsigned int i=1; ibestRxLevel) { + bestRxLevel = thisRxLevel; + best = i; + } + } + + // Does the best exceed the current by more than the threshold? + int threshold = gConfig.getNum("GSM.Handover.ThresholdDelta"); + LOG(DEBUG) << "myRxLevel=" << myRxLevel << " dBm, best neighbor=" << + bestRxLevel << " dBm, threshold=" << threshold << " dB"; + if (bestRxLevel < (myRxLevel + threshold)) return; + + + // OK. So we will initiate a handover. + LOG(DEBUG) << measurements; + int BCCH_FREQ_NCELL = measurements.BCCH_FREQ_NCELL(best); + int BSIC = measurements.BSIC_NCELL(best); + char* peer = gNeighborTable.getAddress(BCCH_FREQ_NCELL,BSIC); + if (!peer) { + LOG(CRIT) << "measurement for unknown neighbor BCCH_FREQ_NCELL " << BCCH_FREQ_NCELL << " BSIC " << BSIC; + return; + } + if (gNeighborTable.holdingOff(peer)) { + LOG(NOTICE) << "skipping handover to " << peer << " due to holdoff"; + return; + } + + // Find the transaction record. + TransactionEntry* transaction = gTransactionTable.findBySACCH(SACCH); + if (!transaction) { + LOG(ERR) << "active SACCH with no transaction record: " << *SACCH; + return; + } + if (transaction->GSMState() != GSM::Active) { + LOG(DEBUG) << "skipping handover for transaction " << transaction->ID() + << " due to state " << transaction->GSMState(); + return; + } + LOG(INFO) << "preparing handover of " << transaction->ID() + << " to " << peer << " with downlink RSSI " << bestRxLevel << " dbm"; + + // The handover reference will be generated by the other BTS. + // We don't set the handover reference or state until we get RSP HANDOVER. + + // Form and send the message. + string msg = string("REQ HANDOVER ") + transaction->handoverString(); + struct sockaddr_in peerAddr; + if (!resolveAddress(&peerAddr,peer)) { + LOG(ALERT) << "cannot resolve peer address " << peer; + return; + } + gPeerInterface.sendMessage(&peerAddr,msg.c_str()); +} @@ -270,6 +568,9 @@ void Control::PagingResponseHandler(const L3PagingResponse* resp, LogicalChannel char *IMSI = gTMSITable.IMSI(mobileID.TMSI()); if (IMSI) { mobileID = L3MobileIdentity(IMSI); + // (pat) Whenever the MS RACHes, we need to alert the SGSN. + // Not sure this is necessary in this particular case, but be safe. + GPRS::GPRSNotifyGsmActivity(IMSI); free(IMSI); } else { // Don't try too hard to resolve. @@ -314,6 +615,9 @@ void Control::PagingResponseHandler(const L3PagingResponse* resp, LogicalChannel case L3CMServiceType::MobileTerminatedShortMessage: MTSMSController(transaction, DCCH); return; + case L3CMServiceType::TestCall: + TestCall(transaction, DCCH); + return; default: // Flush stray MOC entries. // There should not be any, but... diff --git a/Control/RadioResource.h b/Control/RadioResource.h index 02b56c94..42b58b0f 100644 --- a/Control/RadioResource.h +++ b/Control/RadioResource.h @@ -1,27 +1,19 @@ /**@file GSM Radio Resource procedures, GSM 04.18 and GSM 04.08. */ /* -* Copyright 2008-2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -30,13 +22,17 @@ #include #include +#include namespace GSM { class Time; class TCHFACCHLogicalChannel; +class SACCHLogicalChannel; class L3PagingResponse; class L3AssignmentComplete; +class L3HandoverComplete; +class L3HandoverAccess; }; namespace Control { @@ -46,12 +42,25 @@ class TransactionEntry; -/** Find and compelte the in-process transaction associated with a paging repsonse. */ +/** + Determine whether or not to initiate a handover. + @param measurements The measurement results from the SACCH. + @param SACCH The SACCH in question. +*/ +void HandoverDetermination(const GSM::L3MeasurementResults &measurements, GSM::SACCHLogicalChannel* SACCH); + + +/** Find and complete the in-process transaction associated with a paging repsonse. */ void PagingResponseHandler(const GSM::L3PagingResponse*, GSM::LogicalChannel*); /** Find and compelte the in-process transaction associated with a completed assignment. */ void AssignmentCompleteHandler(const GSM::L3AssignmentComplete*, GSM::TCHFACCHLogicalChannel*); +/** Save handover parameters from L1 in the proper transaction record. */ +bool SaveHandoverAccess(unsigned handoverReference, float RSSI, float timingError, const GSM::Time& timestamp); + +/** Process the handover access; returns when the transaction is cleared. */ +void ProcessHandoverAccess(GSM::TCHFACCHLogicalChannel *TCH); /**@ Access Grant mechanisms */ //@{ @@ -177,7 +186,7 @@ class Pager { const GSM::L3MobileIdentity& addID, GSM::ChannelType chanType, TransactionEntry& transaction, - unsigned wLife=gConfig.getNum("SIP.Timer.B") + unsigned wLife=gConfig.getNum("GSM.Timer.T3113") ); /** @@ -214,6 +223,7 @@ class Pager { void *PagerServiceLoopAdapter(Pager*); + //@} // paging mech } diff --git a/Control/SMSCB.cpp b/Control/SMSCB.cpp new file mode 100644 index 00000000..14678c8c --- /dev/null +++ b/Control/SMSCB.cpp @@ -0,0 +1,199 @@ +/**@file SMSCB Control (L3), GSM 03.41. */ +/* +* Copyright 2010 Kestrel Signal Processing, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + +#include "ControlCommon.h" +#include +#include +#include +#include +#include +#include + + +static const char* createSMSCBTable = { + "CREATE TABLE IF NOT EXISTS SMSCB (" + "GS INTEGER NOT NULL, " + "MESSAGE_CODE INTEGER NOT NULL, " + "UPDATE_NUMBER INTEGER NOT NULL, " + "MSGID INTEGER NOT NULL, " + "LANGUAGE_CODE INTEGER NOT NULL, " + "MESSAGE TEXT NOT NULL, " + "SEND_TIME INTEGER DEFAULT 0, " + "SEND_COUNT INTEGER DEFAULT 0" + ")" +}; + + + +sqlite3* SMSCBConnectDatabase(const char* path, sqlite3 **DB) +{ + int rc = sqlite3_open(path,DB); + if (rc) { + LOG(EMERG) << "Cannot open SMSCB database on path " << path << ": " << sqlite3_errmsg(*DB); + sqlite3_close(*DB); + *DB = NULL; + return NULL; + } + if (!sqlite3_command(*DB,createSMSCBTable)) { + LOG(EMERG) << "Cannot create SMSCB table"; + return NULL; + } + // Set high-concurrency WAL mode. + if (!sqlite3_command(*DB,enableWAL)) { + LOG(EMERG) << "Cannot enable WAL mode on database at " << path << ", error message: " << sqlite3_errmsg(*DB); + } + return *DB; +} + + +void encode7(char mc, int &shift, unsigned int &dp, int &buf, char *thisPage) +{ + buf |= (mc & 0x7F) << shift--; + if (shift < 0) { + shift = 7; + } else { + thisPage[dp++] = buf & 0xFF; + buf = buf >> 8; + } +} + + +void SMSCBSendMessage(sqlite3* DB, sqlite3_stmt* stmt, GSM::CBCHLogicalChannel* CBCH) +{ + // Get the message parameters. + // These column numbers need to line up with the argeuments to the SELEECT. + unsigned GS = (unsigned)sqlite3_column_int(stmt,0); + unsigned messageCode = (unsigned)sqlite3_column_int(stmt,1); + unsigned updateNumber = (unsigned)sqlite3_column_int(stmt,2); + unsigned messageID = (unsigned)sqlite3_column_int(stmt,3); + char* messageText = strdup((const char*)sqlite3_column_text(stmt,4)); + unsigned languageCode = (unsigned)sqlite3_column_int(stmt,5); + unsigned sendCount = (unsigned)sqlite3_column_int(stmt,6); + unsigned rowid = (unsigned)sqlite3_column_int(stmt,7); + // Done with the database entry. + // Finalize ASAP to unlock the database. + sqlite3_finalize(stmt); + + // Figure out how many pages to send. + const unsigned maxLen = 40*15; + unsigned messageLen = strlen((const char*)messageText); + if (messageLen>maxLen) { + LOG(ALERT) << "SMSCB message ID " << messageID << " to long; truncating to " << maxLen << " char."; + messageLen = maxLen; + } + unsigned numPages = messageLen / 40; + if (messageLen % 40) numPages++; + unsigned mp = 0; + + LOG(INFO) << "sending message ID=" << messageID << " code=" << messageCode << " in " << numPages << " pages: " << messageText; + + // Break into pages and send each page. + for (unsigned page=0; page> 8; + thisPage[dp++] = languageCode & 0x0ff; + while (dp<82 && mp> 8, shift, dp, buf, thisPage); + // encode7(languageCode & 0xFF, shift, dp, buf, thisPage); + // encode7('\r', shift, dp, buf, thisPage); + while (dp<81 && mpsend(message); + } + free(messageText); + + // Update send count and send time in the database. + char query[100]; + sprintf(query,"UPDATE SMSCB SET SEND_TIME = %u, SEND_COUNT = %u WHERE ROWID == %u", + (unsigned)time(NULL), sendCount+1, rowid); + if (!sqlite3_command(DB,query)) LOG(ALERT) << "timestamp update failed: " << sqlite3_errmsg(DB); +} + + + + + + +void* Control::SMSCBSender(void*) +{ + // Connect to the database. + // Just keep trying until it connects. + sqlite3 *DB; + while (!SMSCBConnectDatabase(gConfig.getStr("Control.SMSCB.Table").c_str(),&DB)) { sleep(1); } + LOG(NOTICE) << "SMSCB service starting"; + + // Get a channel. + GSM::CBCHLogicalChannel* CBCH = gBTS.getCBCH(); + + while (1) { + // Get the next message ready to send. + const char* query = + "SELECT" + " GS,MESSAGE_CODE,UPDATE_NUMBER,MSGID,MESSAGE,LANGUAGE_CODE,SEND_COUNT,ROWID" + " FROM SMSCB" + " WHERE SEND_TIME==(SELECT min(SEND_TIME) FROM SMSCB)"; + sqlite3_stmt *stmt; + if (sqlite3_prepare_statement(DB,&stmt,query)) { + LOG(ALERT) << "Cannot access SMSCB database: " << sqlite3_errmsg(DB); + sleep(1); + continue; + } + // Send the message or sleep briefly. + int result = sqlite3_run_query(DB,stmt); + if (result==SQLITE_ROW) SMSCBSendMessage(DB,stmt,CBCH); + else sleep(1); + // Log errors. + if ((result!=SQLITE_ROW) && (result!=SQLITE_DONE)) + LOG(ALERT) << "SCSCB database failure: " << sqlite3_errmsg(DB); + } + // keep the compiler from whining + return NULL; +} + + +// vim: ts=4 sw=4 diff --git a/Control/SMSControl.cpp b/Control/SMSControl.cpp index 122a7bc4..b99ead2b 100644 --- a/Control/SMSControl.cpp +++ b/Control/SMSControl.cpp @@ -1,25 +1,19 @@ /**@file SMS Control (L3), GSM 03.40, 04.11. */ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -141,12 +135,17 @@ bool handleRPDU(TransactionEntry *transaction, const RLFrame& RPDU) body << submit.UD().decode(); } else if (contentType == "application/vnd.3gpp.sms") { + LOG(DEBUG) << "RPDU: " << RPDU; RPDU.hex(body); + LOG(DEBUG) << "RPDU result: " << body; } else { LOG(ALERT) << "\"" << contentType << "\" is not a valid SMS payload type"; } const char* address = NULL; - if (gConfig.defines("SIP.SMSC")) address = gConfig.getStr("SIP.SMSC").c_str(); + string tmpAddress = gConfig.getStr("SIP.SMSC"); + if (tmpAddress.length()) { + address = tmpAddress.c_str(); + } /* The SMSC is not defined, we are using an older version */ if (address == NULL) { @@ -293,7 +292,7 @@ void Control::MOSMSController(const GSM::L3CMServiceRequest *req, GSM::LogicalCh gReports.incr("OpenBTS.GSM.SMS.MOSMS.Complete"); /* MOSMS RLLP request */ - if (gConfig.defines("Control.SMS.QueryRRLP")) { + if (gConfig.getBool("Control.SMS.QueryRRLP")) { // Query for RRLP if (!sendRRLP(mobileID, LCH)) { LOG(INFO) << "RRLP request failed"; @@ -412,7 +411,6 @@ bool Control::deliverSMSToMS(const char *callingPartyDigits, const char* message delete CM; throw UnexpectedMessage(); } - // FIXME -- Check L3 TI. @@ -482,7 +480,7 @@ void Control::MTSMSController(TransactionEntry *transaction, GSM::LogicalChannel LOG(INFO) << "transaction: "<< *transaction; /* MTSMS RLLP request */ - if (gConfig.defines("Control.SMS.QueryRRLP")) { + if (gConfig.getBool("Control.SMS.QueryRRLP")) { // Query for RRLP if(!sendRRLP(transaction->subscriber(), LCH)){ LOG(INFO) << "RRLP request failed"; @@ -617,7 +615,7 @@ void Control::InCallMOSMSController(const CPData *cpData, TransactionEntry* tran here -kurtis */ /* MOSMS RLLP request */ - if (gConfig.defines("Control.SMS.QueryRRLP")) { + if (gConfig.getBool("Control.SMS.QueryRRLP")) { // Query for RRLP if (!sendRRLP(transaction->subscriber(), LCH)) { LOG(INFO) << "RRLP request failed"; diff --git a/Control/SMSControl.h b/Control/SMSControl.h index 226f595e..69e61fc1 100644 --- a/Control/SMSControl.h +++ b/Control/SMSControl.h @@ -1,25 +1,19 @@ /**@file Declarations for common-use control-layer functions. */ /* -* Copyright 2008-2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/Control/TMSITable.cpp b/Control/TMSITable.cpp index 01d40dd8..8e9030f9 100644 --- a/Control/TMSITable.cpp +++ b/Control/TMSITable.cpp @@ -1,25 +1,17 @@ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -38,6 +30,8 @@ #include #include +#include + using namespace std; using namespace Control; @@ -57,8 +51,12 @@ static const char* createTMSITable = { "PREV_MCC INTEGER, " // previous network MCC "PREV_MNC INTEGER, " // previous network MNC "PREV_LAC INTEGER, " // previous network LAC + "RANDUPPER INTEGER, " // authentication token + "RANDLOWER INTEGER, " // authentication token + "SRES INTEGER, " // authentication token "DEG_LAT FLOAT, " // RRLP result - "DEG_LONG FLOAT " // RRLP result + "DEG_LONG FLOAT, " // RRLP result + "kc varchar(33) default '' " ")" }; @@ -67,7 +65,22 @@ static const char* createTMSITable = { int TMSITable::open(const char* wPath) { + // FIXME -- We can't call the logger here because it has not been initialized yet. + int rc = sqlite3_open(wPath,&mDB); + if (rc) { + // (pat) Gee, how about if we create the directory first? + // OpenBTS crashes if the directory does not exist because the LOG() + // below will crash because the Logger class has not been initialized yet. + char dirpath[strlen(wPath)+100]; + strcpy(dirpath,wPath); + char *sp = strrchr(dirpath,'/'); + if (sp) { + *sp = 0; + mkdir(dirpath,0777); + rc = sqlite3_open(wPath,&mDB); // try try again. + } + } if (rc) { LOG(EMERG) << "Cannot open TMSITable database at " << wPath << ": " << sqlite3_errmsg(mDB); sqlite3_close(mDB); @@ -78,6 +91,10 @@ int TMSITable::open(const char* wPath) LOG(EMERG) << "Cannot create TMSI table"; return 1; } + // Set high-concurrency WAL mode. + if (!sqlite3_command(mDB,enableWAL)) { + LOG(EMERG) << "Cannot enable WAL mode on database at " << wPath << ", error message: " << sqlite3_errmsg(mDB); + } return 0; } @@ -198,7 +215,7 @@ void printAge(unsigned seconds, ostream& os) void TMSITable::dump(ostream& os) const { sqlite3_stmt *stmt; - if (sqlite3_prepare_statement(mDB,&stmt,"SELECT TMSI,IMSI,CREATED,ACCESSED FROM TMSI_TABLE")) { + if (sqlite3_prepare_statement(mDB,&stmt,"SELECT TMSI,IMSI,CREATED,ACCESSED FROM TMSI_TABLE ORDER BY ACCESSED DESC")) { LOG(ERR) << "sqlite3_prepare_statement failed"; return; } @@ -245,13 +262,97 @@ bool TMSITable::classmark(const char* IMSI, const GSM::L3MobileStationClassmark2 +int TMSITable::getPreferredA5Algorithm(const char* IMSI) +{ + char query[200]; + sprintf(query, "SELECT A5_SUPPORT from TMSI_TABLE WHERE IMSI=\"%s\"", IMSI); + sqlite3_stmt *stmt; + if (sqlite3_prepare_statement(mDB,&stmt,query)) { + LOG(ERR) << "sqlite3_prepare_statement failed for " << query; + return 0; + } + if (sqlite3_run_query(mDB,stmt)!=SQLITE_ROW) { + // Returning false here just means the IMSI is not there yet. + sqlite3_finalize(stmt); + return 0; + } + int cm = sqlite3_column_int(stmt,0); + sqlite3_finalize(stmt); + if (cm&1) return 3; + // if (cm&2) return 2; not supported + if (cm&4) return 1; + return 0; +} + + + +void TMSITable::putAuthTokens(const char* IMSI, uint64_t upperRAND, uint64_t lowerRAND, uint32_t SRES) +{ + char query[300]; + sprintf(query,"UPDATE TMSI_TABLE SET RANDUPPER=%llu,RANDLOWER=%llu,SRES=%u,ACCESSED=%u WHERE IMSI=\"%s\"", + upperRAND,lowerRAND,SRES,(unsigned)time(NULL),IMSI); + if (!sqlite3_command(mDB,query)) { + LOG(ALERT) << "cannot write to TMSI table"; + } +} + + + +bool TMSITable::getAuthTokens(const char* IMSI, uint64_t& upperRAND, uint64_t& lowerRAND, uint32_t& SRES) +{ + char query[200]; + sprintf(query,"SELECT RANDUPPER,RANDLOWER,SRES FROM TMSI_TABLE WHERE IMSI=\"%s\"",IMSI); + sqlite3_stmt *stmt; + if (sqlite3_prepare_statement(mDB,&stmt,query)) { + LOG(ERR) << "sqlite3_prepare_statement failed for " << query; + return false; + } + if (sqlite3_run_query(mDB,stmt)!=SQLITE_ROW) { + // Returning false here just means the IMSI is not there yet. + sqlite3_finalize(stmt); + return false; + } + upperRAND = sqlite3_column_int64(stmt,0); + lowerRAND = sqlite3_column_int64(stmt,1); + SRES = sqlite3_column_int(stmt,2); + sqlite3_finalize(stmt); + return true; +} + + + +void TMSITable::putKc(const char* IMSI, string Kc) +{ + char query[300]; + sprintf(query,"UPDATE TMSI_TABLE SET kc=\"%s\" WHERE IMSI=\"%s\"", Kc.c_str(), IMSI); + if (!sqlite3_command(mDB,query)) { + LOG(ALERT) << "cannot write Kc to TMSI table"; + } +} + + + +string TMSITable::getKc(const char* IMSI) +{ + char *Kc; + if (!sqlite3_single_lookup(mDB, "TMSI_TABLE", "IMSI", IMSI, "kc", Kc)) { + LOG(ERR) << "sqlite3_single_lookup failed to find kc for " << IMSI; + return ""; + } + string Kcs = string(Kc); + free(Kc); + return Kcs; +} + + + unsigned TMSITable::nextL3TI(const char* IMSI) { // FIXME -- This should be a single atomic operation. unsigned l3ti; if (!sqlite3_single_lookup(mDB,"TMSI_TABLE","IMSI",IMSI,"L3TI",l3ti)) { LOG(ERR) << "cannot read L3TI from TMSI_TABLE, using random L3TI"; - return random() % 8; + return random() % 7; } // Note that TI=7 is a reserved value, so value values are 0-6. See GSM 04.07 11.2.3.1.3. unsigned next = (l3ti+1) % 7; diff --git a/Control/TMSITable.h b/Control/TMSITable.h index 839991d5..0a9cc038 100644 --- a/Control/TMSITable.h +++ b/Control/TMSITable.h @@ -1,24 +1,17 @@ /* -* Copyright 2008-2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -98,6 +91,21 @@ class TMSITable { /** Set the classmark. */ bool classmark(const char* IMSI, const GSM::L3MobileStationClassmark2& classmark); + /** Get the preferred A5 algorithm (3, 1, or 0). */ + int getPreferredA5Algorithm(const char* IMSI); + + /** Save a RAND/SRES pair. */ + void putAuthTokens(const char* IMSI, uint64_t upperRAND, uint64_t lowerRAND, uint32_t SRES); + + /** Get a RAND/SRES pair. */ + bool getAuthTokens(const char* IMSI, uint64_t &upperRAND, uint64_t &lowerRAND, uint32_t &SRES); + + /** Save Kc. */ + void putKc(const char* IMSI, std::string Kc); + + /** Get Kc. */ + std::string getKc(const char* IMSI); + /** Get the next TI value to use for this IMSI or TMSI. */ unsigned nextL3TI(const char* IMSI); diff --git a/Control/TransactionTable.cpp b/Control/TransactionTable.cpp index d7a64531..aebb1b4a 100644 --- a/Control/TransactionTable.cpp +++ b/Control/TransactionTable.cpp @@ -29,6 +29,8 @@ #include #include +#include + #include #include @@ -100,8 +102,7 @@ TransactionEntry::TransactionEntry( const L3CMServiceType& wService, const L3CallingPartyBCDNumber& wCalling, GSM::CallState wState, - const char *wMessage, - bool wFake) + const char *wMessage) :mID(gTransactionTable.newID()), mSubscriber(wSubscriber),mService(wService), mL3TI(gTMSITable.nextL3TI(wSubscriber.digits())), @@ -111,9 +112,8 @@ TransactionEntry::TransactionEntry( mNumSQLTries(gConfig.getNum("Control.NumSQLTries")), mChannel(wChannel), mTerminationRequested(false), - mRemoved(false), - mFake(wFake) - + mHandoverOtherBSTransactionID(0), + mRemoved(false) { if (wMessage) mMessage.assign(wMessage); //strncpy(mMessage,wMessage,160); else mMessage.assign(""); //mMessage[0]='\0'; @@ -137,8 +137,8 @@ TransactionEntry::TransactionEntry( mNumSQLTries(gConfig.getNum("Control.NumSQLTries")), mChannel(wChannel), mTerminationRequested(false), - mRemoved(false), - mFake(false) + mHandoverOtherBSTransactionID(0), + mRemoved(false) { assert(mSubscriber.type()==GSM::IMSIType); mMessage.assign(""); //mMessage[0]='\0'; @@ -146,29 +146,6 @@ TransactionEntry::TransactionEntry( } -// Form for SOS transactions. -TransactionEntry::TransactionEntry( - const char* proxy, - const L3MobileIdentity& wSubscriber, - GSM::LogicalChannel* wChannel, - const L3CMServiceType& wService, - unsigned wL3TI) - :mID(gTransactionTable.newID()), - mSubscriber(wSubscriber),mService(wService), - mL3TI(wL3TI), - mSIP(proxy,mSubscriber.digits()), - mGSMState(GSM::MOCInitiated), - mNumSQLTries(2*gConfig.getNum("Control.NumSQLTries")), - mChannel(wChannel), - mTerminationRequested(false), - mRemoved(false), - mFake(false) -{ - mMessage.assign(""); //mMessage[0]='\0'; - initTimers(); -} - - // Form for MO-SMS transactions. TransactionEntry::TransactionEntry( const char* proxy, @@ -185,8 +162,8 @@ TransactionEntry::TransactionEntry( mNumSQLTries(gConfig.getNum("Control.NumSQLTries")), mChannel(wChannel), mTerminationRequested(false), - mRemoved(false), - mFake(false) + mHandoverOtherBSTransactionID(0), + mRemoved(false) { assert(mSubscriber.type()==GSM::IMSIType); if (wMessage!=NULL) mMessage.assign(wMessage); //strncpy(mMessage,wMessage,160); @@ -208,8 +185,8 @@ TransactionEntry::TransactionEntry( mNumSQLTries(gConfig.getNum("Control.NumSQLTries")), mChannel(wChannel), mTerminationRequested(false), - mRemoved(false), - mFake(false) + mHandoverOtherBSTransactionID(0), + mRemoved(false) { assert(mSubscriber.type()==GSM::IMSIType); mMessage[0]='\0'; @@ -217,11 +194,142 @@ TransactionEntry::TransactionEntry( } + +// Form for inbound handovers. +TransactionEntry::TransactionEntry(const struct sockaddr_in* peer, + unsigned wHandoverReference, + SimpleKeyValue ¶ms, + const char *proxy, + GSM::LogicalChannel *wChannel, + unsigned wHandoverOtherBSTransactionID) + :mID(gTransactionTable.newID()), + mService(GSM::L3CMServiceType::HandoverCall), + mSIP(proxy), + mGSMState(GSM::HandoverInbound), + mInboundReference(wHandoverReference), + mNumSQLTries(gConfig.getNum("Control.NumSQLTries")), + mChannel(wChannel), + mTerminationRequested(false), + mHandoverOtherBSTransactionID(wHandoverOtherBSTransactionID), + mRemoved(false) +{ + // This is used for inbound handovers. + // We are "BS2" in the handover ladder diagram. + // The message string was formed by the handoverString method. + + // Save the peer address. + bcopy(peer,&mInboundPeer,sizeof(mInboundPeer)); + + // Break into space-delimited tokens, stuff into a SimpleKeyValue and then unpack it. + //SimpleKeyValue params; + //params.addItems(args); + + const char* IMSI = params.get("IMSI"); + if (IMSI) mSubscriber = GSM::L3MobileIdentity(IMSI); + + const char* called = params.get("called"); + if (called) { + mCalled = GSM::L3CallingPartyBCDNumber(called); + mService = GSM::L3CMServiceType::MobileOriginatedCall; + } + + const char* calling = params.get("calling"); + if (calling) { + mCalling = GSM::L3CallingPartyBCDNumber(calling); + mService = GSM::L3CMServiceType::MobileTerminatedCall; + } + + const char* ref = params.get("ref"); + if (ref) mInboundReference = strtol(ref,NULL,10); + + const char* L3TI = params.get("L3TI"); + if (L3TI) mL3TI = strtol(L3TI,NULL,10); + + // Set the SIP state. + mSIP.state(SIP::HandoverInbound); + + const char* codec = params.get("codec"); + if (codec) mCodec = atoi(codec); + + const char* remoteUsername = params.get("remoteUsername"); + if (remoteUsername) mRemoteUsername = strdup(remoteUsername); + + const char* remoteDomain = params.get("remoteDomain"); + if (remoteDomain) mRemoteDomain = strdup(remoteDomain); + + const char* SIPUsername = params.get("SIPUsername"); + if (SIPUsername) mSIPUsername = strdup(SIPUsername); + + const char* SIPDisplayname = params.get("SIPDisplayname"); + if (SIPDisplayname) mSIPDisplayname = strdup(SIPDisplayname); + + const char* FromTag = params.get("FromTag"); + if (FromTag) mFromTag = strdup(FromTag); + + const char* FromUsername = params.get("FromUsername"); + if (FromUsername) mFromUsername = strdup(FromUsername); + + const char* FromIP = params.get("FromIP"); + if (FromIP) mFromIP = strdup(FromIP); + + const char* ToTag = params.get("ToTag"); + if (ToTag) mToTag = strdup(ToTag); + + const char* ToUsername = params.get("ToUsername"); + if (ToUsername) mToUsername = strdup(ToUsername); + + const char* ToIP = params.get("ToIP"); + if (ToIP) mToIP = strdup(ToIP); + + const char* CSeq = params.get("CSeq"); + if (CSeq) mCSeq = atoi(CSeq); + + const char * CallID = params.get("CallID"); + if (CallID) mCallID = CallID; + mSIP.callID(CallID); + + const char * CallIP = params.get("CallIP"); + if (CallIP) mCallIP = CallIP; + + const char * RTPState = params.get("RTPState"); + if (RTPState) mRTPState = RTPState; + + const char * SessionID = params.get("SessionID"); + if (SessionID) mSessionID = SessionID; + + const char * SessionVersion = params.get("SessionVersion"); + if (SessionVersion) mSessionVersion = SessionVersion; + + const char * RTPRemPort = params.get("RTPRemPort"); + if (RTPRemPort) mRTPRemPort = atoi(RTPRemPort); + + const char * RTPRemIP = params.get("RTPRemIP"); + if (RTPRemIP) mRTPRemIP = RTPRemIP; + + const char * RmtIP = params.get("RmtIP"); + if (RmtIP) mRmtIP = RmtIP; + + const char * RmtPort = params.get("RmtPort"); + if (RmtPort) mRmtPort = atoi(RmtPort); + + const char * SRIMSI = params.get("SRIMSI"); + if (SRIMSI) mSRIMSI = SRIMSI; + + const char * SRCALLID = params.get("SRCALLID"); + if (SRCALLID) mSRCALLID = SRCALLID; + + initTimers(); + +} + + TransactionEntry::~TransactionEntry() { // This should go out of scope before the object is actually destroyed. ScopedLock lock(mLock); + // Remove any FIFO from the gPeerInterface. + gPeerInterface.removeFIFO(mID); // Remove the associated SIP message FIFO. gSIPInterface.removeCall(mSIP.callID()); @@ -332,6 +440,8 @@ bool TransactionEntry::dead() const if (age < 30*1000) return false; // Failed? if (lSIPState==SIP::Fail) return true; + // Bad handover? + if (lSIPState==SIP::HandoverInbound) return true; // SIP Null state? if (lSIPState==SIP::NullState) return true; // SIP stuck in proceeding? @@ -411,9 +521,7 @@ void TransactionEntry::messageType(const char *wContentType) void TransactionEntry::runQuery(const char* query) const { // Caller should hold mLock and should have already checked mRemoved.. - for (unsigned i=0; icontacts, 0); + os << " RmtIP=" << osip_uri_get_host(con->url); + os << " RmtPort=" << osip_uri_get_port(con->url); + + os << " RTPState=" << + mSIP.RTPSession()->rtp.snd_time_offset << "," << + mSIP.RTPSession()->rtp.snd_ts_offset << "," << + mSIP.RTPSession()->rtp.snd_rand_offset << "," << + mSIP.RTPSession()->rtp.snd_last_ts << "," << + mSIP.RTPSession()->rtp.rcv_time_offset << "," << + mSIP.RTPSession()->rtp.rcv_ts_offset << "," << + mSIP.RTPSession()->rtp.rcv_query_ts_offset << "," << + mSIP.RTPSession()->rtp.rcv_last_ts << "," << + mSIP.RTPSession()->rtp.rcv_last_app_ts << "," << + mSIP.RTPSession()->rtp.rcv_last_ret_ts << "," << + mSIP.RTPSession()->rtp.hwrcv_extseq << "," << + mSIP.RTPSession()->rtp.hwrcv_seq_at_last_SR << "," << + mSIP.RTPSession()->rtp.hwrcv_since_last_SR << "," << + mSIP.RTPSession()->rtp.last_rcv_SR_ts << "," << + mSIP.RTPSession()->rtp.last_rcv_SR_time.tv_sec << "," << mSIP.RTPSession()->rtp.last_rcv_SR_time.tv_usec << "," << + mSIP.RTPSession()->rtp.snd_seq << "," << + mSIP.RTPSession()->rtp.last_rtcp_report_snt_r << "," << + mSIP.RTPSession()->rtp.last_rtcp_report_snt_s << "," << + mSIP.RTPSession()->rtp.rtcp_report_snt_interval << "," << + mSIP.RTPSession()->rtp.last_rtcp_packet_count << "," << + mSIP.RTPSession()->rtp.sent_payload_bytes; + + return os.str(); +} + void TransactionTable::init(const char* path) { // This assumes the main application uses sdevrandom. @@ -881,11 +1093,51 @@ void TransactionTable::init(const char* path) if (!sqlite3_command(mDB,createTransactionTable)) { LOG(ALERT) << "Cannot create Transaction Table"; } + // Set high-concurrency WAL mode. + if (!sqlite3_command(mDB,enableWAL)) { + LOG(ALERT) << "Cannot enable WAL mode on database at " << path << ", error message: " << sqlite3_errmsg(mDB); + } // Clear any previous entires. if (!sqlite3_command(gTransactionTable.DB(),"DELETE FROM TRANSACTION_TABLE")) LOG(WARNING) << "cannot clear previous transaction table"; } + + +void TransactionEntry::setOutboundHandover( + const GSM::L3HandoverReference& reference, + const GSM::L3CellDescription& cell, + const GSM::L3ChannelDescription2& chan, + const GSM::L3PowerCommandAndAccessType& pwrCmd, + const GSM::L3SynchronizationIndication& synch + ) +{ + if (mRemoved) throw RemovedTransaction(mID); + ScopedLock lock(mLock); + mOutboundReference = reference; + mOutboundCell = cell; + mOutboundChannel = chan; + mOutboundPowerCmd = pwrCmd; + mOutboundSynch = synch; + GSMState(GSM::HandoverOutbound); + return; +} + + +void TransactionEntry::setInboundHandover(float RSSI, float timingError, double timestamp) +{ + if (mRemoved) throw RemovedTransaction(mID); + ScopedLock lock(mLock); + mChannel->setPhy(RSSI,timingError,timestamp); + mInboundRSSI = RSSI; + mInboundTimingError = timingError; +} + + + + + + TransactionTable::~TransactionTable() { // Don't bother disposing of the memory, @@ -962,10 +1214,7 @@ bool TransactionTable::removePaging(unsigned key) if (itr==mTable.end()) return false; if (itr->second->removed()) return true; if (itr->second->GSMState()!=GSM::Paging) return false; - //no one to respond to if we're fake - if (!itr->second->fake()){ - itr->second->MODSendERROR(NULL, 480, "Temporarily Unavailable", true); - } + itr->second->MODSendERROR(NULL, 480, "Temporarily Unavailable", true); itr->second->remove(); return true; } @@ -1095,7 +1344,6 @@ bool TransactionTable::isBusy(const L3MobileIdentity& mobileID) if (itr->second->subscriber() != mobileID) continue; GSM::L3CMServiceType service = itr->second->service(); bool speech = - service==GSM::L3CMServiceType::EmergencyCall || service==GSM::L3CMServiceType::MobileOriginatedCall || service==GSM::L3CMServiceType::MobileTerminatedCall; if (!speech) continue; @@ -1155,6 +1403,7 @@ TransactionEntry* TransactionTable::find(const L3MobileIdentity& mobileID, unsig ScopedLock lock(mLock); for (TransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; + if (itr->second->HandoverOtherBSTransactionID() != transactionID) continue; if (itr->second->subscriber() != mobileID) continue; return itr->second; } @@ -1246,7 +1495,6 @@ TransactionEntry* TransactionTable::findLongestCall() for (TransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { if (itr->second->deadOrRemoved()) continue; if (!(itr->second->channel())) continue; - if (itr->second->service() == GSM::L3CMServiceType::EmergencyCall) continue; if (itr->second->GSMState() != GSM::Active) continue; long runTime = itr->second->stateAge(); if (runTime > longTime) { @@ -1274,6 +1522,47 @@ bool TransactionTable::RTPAvailable(short rtpPort) return avail; } +TransactionEntry* TransactionTable::inboundHandover(unsigned ref) +{ + // Yes, it's linear time. + // Even in a 6-ARFCN system, it should rarely be more than a dozen entries. + + ScopedLock lock(mLock); + + // Since clearDeadEntries is also linear, do that here, too. + clearDeadEntries(); + + // Brute force search. + for (TransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { + if (itr->second->deadOrRemoved()) continue; + if (itr->second->GSMState() != GSM::HandoverInbound) continue; + if (itr->second->inboundReference() == ref) { + return itr->second; + } + } + return NULL; +} + +TransactionEntry* TransactionTable::inboundHandover(const GSM::LogicalChannel* chan) +{ + // Yes, it's linear time. + // Even in a 6-ARFCN system, it should rarely be more than a dozen entries. + + ScopedLock lock(mLock); + + // Since clearDeadEntries is also linear, do that here, too. + clearDeadEntries(); + + // Brute force search. + for (TransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { + if (itr->second->deadOrRemoved()) continue; + if (itr->second->GSMState() != GSM::HandoverInbound) continue; + if (itr->second->channel() == chan) return itr->second; + } + return NULL; +} + + bool TransactionTable::duplicateMessage(const GSM::L3MobileIdentity& mobileID, const std::string& wMessage) { @@ -1292,4 +1581,31 @@ bool TransactionTable::duplicateMessage(const GSM::L3MobileIdentity& mobileID, c } + + +#if 0 +bool TransactionTable::outboundReferenceUsed(unsigned ref) +{ + // Called is expected to hold mLock. + for (TransactionMap::iterator itr = mTable.begin(); itr!=mTable.end(); ++itr) { + if (itr->second->deadOrRemoved()) continue; + if (itr->second->GSMState() != GSM::HandoverOutbound) continue; + if (itr->second->handoverReference() == ref) return true; + } + return false; +} + + +unsigned TransactionTable::generateHandoverReference(TransactionEntry *transaction) +{ + ScopedLock lock(mLock); + clearDeadEntries(); + unsigned ref = random() % 256; + while (outboundReferenceUsed(ref)) { ref = (ref+1) % 256; } + transaction->handoverReference(ref); + return ref; +} +#endif + + // vim: ts=4 sw=4 diff --git a/Control/TransactionTable.h b/Control/TransactionTable.h index 3cb417ae..cac3f917 100644 --- a/Control/TransactionTable.h +++ b/Control/TransactionTable.h @@ -90,15 +90,59 @@ class TransactionEntry { Timeval mStateTimer; ///< timestamp of last state change. TimerTable mTimers; ///< table of Z100-type state timers + /**@name Handover parameters */ + //@{ + /**@name Inbound */ + //@{ + struct ::sockaddr_in mInboundPeer; ///< other BTS in inbound handover + unsigned mInboundReference; ///< handover reference + float mInboundRSSI; ///< access burst RSSI in dB wrt full scale + float mInboundTimingError; ///< access burst timing error in symbol periods + unsigned mCSeq; + string mCallID; + unsigned mCodec; + string mRTPState; + string mSessionID; + string mSessionVersion; + string mRemoteUsername; + string mRemoteDomain; + string mSIPDisplayname; + string mSIPUsername; + string mFromTag; + string mFromUsername; + string mFromIP; + string mToTag; + string mToUsername; + string mToIP; + string mCallIP; + short mRTPRemPort; + string mRTPRemIP; + short mRmtPort; + string mRmtIP; + string mSRIMSI; + string mSRCALLID; + + //@} + /**@name Outbound */ + //@{ + struct ::sockaddr_in mOutboundPeer; ///< other BTS in outbound handover + GSM::L3CellDescription mOutboundCell; + GSM::L3ChannelDescription2 mOutboundChannel; + GSM::L3HandoverReference mOutboundReference; + GSM::L3PowerCommandAndAccessType mOutboundPowerCmd; + GSM::L3SynchronizationIndication mOutboundSynch; + //@} + //@} + unsigned mNumSQLTries; ///< number of SQL tries for DB operations GSM::LogicalChannel *mChannel; ///< current channel of the transaction bool mTerminationRequested; - volatile bool mRemoved; ///< true if ready for removal + unsigned mHandoverOtherBSTransactionID; - bool mFake; ///true if this is a fake message generated internally + volatile bool mRemoved; ///< true if ready for removal public: @@ -109,8 +153,7 @@ class TransactionEntry { const GSM::L3CMServiceType& wService, const GSM::L3CallingPartyBCDNumber& wCalling, GSM::CallState wState = GSM::NullState, - const char *wMessage = NULL, - bool wFake=false); + const char *wMessage = NULL); /** This form is used for MOC, setting mGSMState to MOCInitiated. */ TransactionEntry(const char* proxy, @@ -120,13 +163,6 @@ class TransactionEntry { unsigned wL3TI, const GSM::L3CalledPartyBCDNumber& wCalled); - /** This form is used for SOS calls, setting mGSMState to MOCInitiated. */ - TransactionEntry(const char* proxy, - const GSM::L3MobileIdentity& wSubscriber, - GSM::LogicalChannel* wChannel, - const GSM::L3CMServiceType& wService, - unsigned wL3TI); - /** Form for MO-SMS; sets yet-unknown TI to 7 and GSM state to SMSSubmitting */ TransactionEntry(const char* proxy, const GSM::L3MobileIdentity& wSubscriber, @@ -139,6 +175,14 @@ class TransactionEntry { const GSM::L3MobileIdentity& wSubscriber, GSM::LogicalChannel* wChannel); + /** Form used for handover requests; argument is taken from the message string. */ + TransactionEntry(const struct ::sockaddr_in* peer, + unsigned wHandoverReference, + SimpleKeyValue ¶ms, + const char *proxy, + GSM::LogicalChannel* wChannel, + unsigned otherTransactionID); + /** Delete the database entry upon destruction. */ ~TransactionEntry(); @@ -162,8 +206,6 @@ class TransactionEntry { const GSM::L3CallingPartyBCDNumber& calling() const { return mCalling; } - bool fake() const {return mFake; } - const char* message() const { return mMessage.c_str(); } void message(const char *wMessage, size_t length); const char* messageType() const { return mContentType.c_str(); } @@ -174,6 +216,64 @@ class TransactionEntry { GSM::CallState GSMState() const; void GSMState(GSM::CallState wState); + /** Inbound reference set in the constructor, no lock needed. */ + // FIXME -- These should probably all the const references. + unsigned inboundReference() const { return mInboundReference; } + unsigned Codec() { return mCodec; } + unsigned CSeq() { return mCSeq; } + string CallID() { return mCallID; } + string RemoteUsername() { return mRemoteUsername; } + string RemoteDomain() { return mRemoteDomain; } + string SIPUsername() { return mSIPUsername; } + string SIPDisplayname() { return mSIPDisplayname; } + string FromTag() { return mFromTag; } + string FromUsername() { return mFromUsername; } + string FromIP() { return mFromIP; } + string ToTag() { return mToTag; } + string ToUsername() { return mToUsername; } + string ToIP() { return mToIP; } + string CallIP() { return mCallIP; } + short RTPRemPort() { return mRTPRemPort; } + string RTPRemIP() { return mRTPRemIP; } + string RTPState() { return mRTPState; } + string SessionID() { return mSessionID; } + string SessionVersion() { return mSessionVersion; } + short RmtPort() { return mRmtPort; } + string RmtIP() { return mRmtIP; } + string SRIMSI() { return mSRIMSI; } + string SRCALLID() { return mSRCALLID; } + unsigned HandoverOtherBSTransactionID() { return mHandoverOtherBSTransactionID; } + + GSM::L3HandoverReference outboundReference() const { ScopedLock lock(mLock); return mOutboundReference; } + GSM::L3CellDescription outboundCell() const { ScopedLock lock(mLock); return mOutboundCell; } + GSM::L3ChannelDescription2 outboundChannel() const { ScopedLock lock(mLock); return mOutboundChannel; } + GSM::L3PowerCommandAndAccessType outboundPowerCmd() const { ScopedLock lock(mLock); return mOutboundPowerCmd; } + GSM::L3SynchronizationIndication outboundSynch() const { ScopedLock lock(mLock); return mOutboundSynch; } + + /** Set the outbound handover parameters and set the state to HandoverOutbound. */ + void setOutboundHandover( + const GSM::L3HandoverReference& reference, + const GSM::L3CellDescription& cell, + const GSM::L3ChannelDescription2& chan, + const GSM::L3PowerCommandAndAccessType& pwrCmd, + const GSM::L3SynchronizationIndication& synch + ); + + /** Set the inbound handover parameters on the channel; state should alread be HandoverInbound. */ + void setInboundHandover( + float wRSSI, + float wTimingError, + double wTimestamp + ); + + // This is thread-safe because mInboundPeer is only modified in the constructor. + const struct ::sockaddr_in* inboundPeer() const { return &mInboundPeer; } + + float inboundTimingError() const { ScopedLock lock(mLock); return mInboundTimingError; } + + //@} + + /** Initiate the termination process. */ void terminate() { ScopedLock lock(mLock); mTerminationRequested=true; } @@ -194,13 +294,6 @@ class TransactionEntry { SIP::SIPState MOCSendACK(); void MOCInitRTP() { ScopedLock lock(mLock); return mSIP.MOCInitRTP(); } - SIP::SIPState SOSSendINVITE(short rtpPort, unsigned codec); - SIP::SIPState SOSResendINVITE() { return MOCResendINVITE(); } - SIP::SIPState SOSCheckForOK() { return MOCCheckForOK(); } - SIP::SIPState SOSSendACK() { return MOCSendACK(); } - void SOSInitRTP() { MOCInitRTP(); } - - SIP::SIPState MTCSendTrying(); SIP::SIPState MTCSendRinging(); SIP::SIPState MTCCheckForACK(); @@ -230,6 +323,13 @@ class TransactionEntry { SIP::SIPState MTSMSSendOK(); + SIP::SIPState inboundHandoverSendINVITE(unsigned RTPPort) + { ScopedLock lock(mLock); return mSIP.inboundHandoverSendINVITE(this, RTPPort); } + SIP::SIPState inboundHandoverCheckForOK() + { ScopedLock lock(mLock); return mSIP.inboundHandoverCheckForOK(&mLock); } + SIP::SIPState inboundHandoverSendACK() + { ScopedLock lock(mLock); return mSIP.inboundHandoverSendACK(); } + bool sendINFOAndWaitForOK(unsigned info); void txFrame(unsigned char* frame) { ScopedLock lock(mLock); return mSIP.txFrame(frame); } @@ -287,6 +387,9 @@ class TransactionEntry { /** Dump information as text for debugging. */ void text(std::ostream&) const; + /** Genrate an encoded string for handovers. */ + std::string handoverString() const; + private: friend class TransactionTable; @@ -372,6 +475,13 @@ class TransactionTable { */ bool RTPAvailable(short rtpPort); + /** + Fand an entry by its handover reference. + @param ref The 8-bit handover reference. + @return NULL if ID is not found or was dead + */ + TransactionEntry* inboundHandover(unsigned ref); + /** Remove an entry from the table and from gSIPMessageMap. @param wID The transaction ID to search. @@ -398,6 +508,9 @@ class TransactionTable { */ TransactionEntry* find(const GSM::LogicalChannel *chan); + /** Find a transaction in the HandoverInbound state on the given channel. */ + TransactionEntry* inboundHandover(const GSM::LogicalChannel *chan); + /** Find an entry by its SACCH channel pointer; returns first entry found. Also clears dead entries during search. @@ -458,6 +571,9 @@ class TransactionTable { size_t dump(std::ostream& os, bool showAll=false) const; + /** Generate a unique handover reference. */ + //unsigned generateInboundHandoverReference(TransactionEntry* transaction); + private: friend class TransactionEntry; @@ -478,6 +594,9 @@ class TransactionTable { void innerRemove(TransactionMap::iterator); + /** Check to see if a given outbound handover reference is in use. */ + //bool outboundReferenceUsed(unsigned ref); + }; diff --git a/GPRS/BSSG.cpp b/GPRS/BSSG.cpp new file mode 100644 index 00000000..5c0f004b --- /dev/null +++ b/GPRS/BSSG.cpp @@ -0,0 +1,370 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "Defines.h" +#include "GPRSInternal.h" // For GPRSLOG() +#include "GSMConfig.h" +#include "Threads.h" +#include "BSSGMessages.h" +#include "BSSG.h" +#include "Utils.h" +#include "errno.h" + +#include +#include + +namespace BSSG { +BSSGMain gBSSG; + +const unsigned rbufSize = 3000; // Much bigger than any PDU message. + +#if _UNUSED_ +static int BSTLVParse(ByteType *data, int &rp, + IEIType::type expected_ieitype, int expected_length) +{ + int received_ieitype = data[rp++]; + int received_length = data[rp++]; + if (received_ieitype != expected_ieitype || received_length != expected_length) { + int bstype = data[NSMsg::UnitDataHeaderLen]; + LOG(ERR) << "Error in BSSG msg type="<BSS request reset everything. + // Dont know what we should do about reset. + BSSGWriteLowSide(BVCFactory(BSPDUType::BVC_RESET_ACK,0)); + break; + case BSSG::BSPDUType::BVC_RESET_ACK: // BSS->network and network->BSS? + break; + + case BSSG::BSPDUType::BVC_UNBLOCK: + BSSGWriteLowSide(BVCFactory(BSPDUType::BVC_UNBLOCK_ACK,0)); + break; + case BSSG::BSPDUType::BVC_UNBLOCK_ACK: + break; + + // We ignore all these: + case BSSG::BSPDUType::SUSPEND_ACK: // network->MS ACK + case BSSG::BSPDUType::SUSPEND_NACK: // network->MS NACK + case BSSG::BSPDUType::RESUME_ACK: // network->MS ACK + case BSSG::BSPDUType::RESUME_NACK: // network->MS NACK + case BSSG::BSPDUType::FLUSH_LL: // newtork->BSS forget this MS (it moved to another cell.) + case BSSG::BSPDUType::SGSN_INVOKE_TRACE: // network->BSS request trace an MS + LOG(WARNING) << "Unimplemented BSSG message:" << BSPDUType::name(bstype); + return; + + case BSSG::BSPDUType::SUSPEND: // MS->network request to suspend GPRS service. + case BSSG::BSPDUType::RESUME: // MS->network request to resume GPRS service. + case BSSG::BSPDUType::FLUSH_LL_ACK: // BSS->network + case BSSG::BSPDUType::LLC_DISCARDED: // BSS->network notification of lost PDUs (probably expired) + LOG(ERR) << "Invalid BSSG message:" << BSPDUType::name(bstype); + return; + } +} + +void NsRecvMsg(unsigned char *data, int nsize) +{ + NSPDUType::type nstype = (NSPDUType::type) data[0]; + // We dont need to see all the keep alive messages. + if (nstype != NSPDUType::NS_UNITDATA) { GPRSLOG(4) << "BSSG NsRecvMsg "<str() <mbsIsOpen = true; + + int failures = 0; + while (bssgp->mbsIsOpen && ++failures < 10) { + ssize_t rsize = recv(bssgp->mbsSGSockfd,buf,rbufSize,0); + if (rsize > 0) { + failures = 0; + NsRecvMsg(buf,rsize); + } else if (rsize == -1) { + LOG(ERR) << "Received -1 from BSSG recv(), error:"<mbsIsOpen = false; + // Send a message to the sendServiceLoop so that it will notice + // we have died and die also. + BSSGWriteLowSide(NsFactory(NSPDUType::NS_BLOCK)); + return NULL; +} + +// The send probably does not need to be in a separate thread. +// We could also have used a select or poll system call. +// But it was easier to use two threads. + +// OLD: Send this loop an NS_BLOCK message to kill this thread off; +// and we dont normally use that NS message. +// There is a BSSG-level BVC_BLOCK message that we would use to do a temporary data block. +void *sendServiceLoop(void *arg) +{ + BSSGMain *bssgp = (BSSGMain*)arg; + NSPDUType::type nstype = NSPDUType::NS_RESET; // init to anything. + do { + NSMsg *ulmsg = bssgp->mbsTxQ.read(); + // It is already wrapped up in an NS protocol. + int msgsize = ulmsg->size(); + ssize_t result = send(bssgp->mbsSGSockfd,ulmsg->begin(),msgsize,0); + nstype = ulmsg->getNSPDUType(); + int debug_level = 1; //(nstype == NSPDUType::NS_UNITDATA) ? 1 : 4; + if (GPRS::GPRSDebug & debug_level) { + GPRSLOG(debug_level) << "BSSG ===> sendServiceLoop sent " + <str()<mbsIsOpen /*&& nstype != NSPDUType::NS_BLOCK*/); + return NULL; +} + +static int opensock(uint32_t sgsnIp, int sgsnPort /*,int bssgPort*/ ) +{ + //int fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + int sockfd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sockfd < 0) { + LOG(ERR) << "Could not create socket for BSSGP"; + return -1; + } + + + /****** + { // We dont want to bind here. connect will pick a port for us. + int32_t bssgIp = INADDR_ANY; + struct sockaddr_in myAddr; + memset(&myAddr,0,sizeof(myAddr)); // be safe. + myAddr.sin_family = AF_INET; + myAddr.sin_addr.s_addr = htonl(bssgIp); + myAddr.sin_port = htons(bssgPort); + if (0 != bind(sockfd,(sockaddr*)&myAddr,sizeof(myAddr))) { + LOG(ERR) << "Could not bind NS socket to" + << LOGVAR(bssgIp) << LOGVAR(bssgPort) << LOGVAR(errno); + close(sockfd); + return -1; + } + } + ****/ + + struct sockaddr_in sgsnAddr; + memset(&sgsnAddr,0,sizeof(sgsnAddr)); + sgsnAddr.sin_family = AF_INET; + sgsnAddr.sin_addr.s_addr = sgsnIp; // This is already in network order. + sgsnAddr.sin_port = htons(sgsnPort); + if (0 != connect(sockfd,(sockaddr*)&sgsnAddr,sizeof(sgsnAddr))) { + LOG(ERR) << "Could not connect NS socket to" + << LOGVAR(sgsnIp) << LOGVAR(sgsnPort) << LOGVAR(errno); + close(sockfd); + return -1; + } else { + GPRSLOG(1) << "connected to SGSN at "<< inet_ntoa(sgsnAddr.sin_addr) <<" port "<= 40) { // wait 4 seconds + GPRSLOG(1) << LOGVAR(mbsResetReceived) + <write(ulmsg); + } else { + GPRSLOG(1) << "BSSG ===> writelowside " <str()< mbsRxQ; + InterthreadQueue mbsTxQ; + InterthreadQueue *mbsTestQ; // Only used for testing + Thread mbsRecvThread; + Thread mbsSendThread; + int mbsSGSockfd; + Bool_z mbsIsOpen; + + Bool_z mbsResetReceived; + Bool_z mbsResetAckReceived; + Bool_z mbsAliveReceived; + Bool_z mbsAliveAckReceived; + Bool_z mbsBlocked; // We dont implement blocking, but we track the state. + + // These are identifiers for the BSC and NS link, which we dont use. + UInt_z mbsBVCI; // Our BTS identifire. Must not be 0 or 1. + UInt_z mbsNSVCI; + UInt_z mbsNSEI; + + BSSGMain() { mbsSGSockfd = -1; } + + bool BSSGOpen(); + bool BSSGReset(); + + BSSGDownlinkMsg *BSSGReadLowSide(); +}; + +void BSSGWriteLowSide(NSMsg *ulmsg); + +extern BSSGMain gBSSG; + +}; + +#endif diff --git a/GPRS/BSSGMessages.cpp b/GPRS/BSSGMessages.cpp new file mode 100644 index 00000000..fa7519f4 --- /dev/null +++ b/GPRS/BSSGMessages.cpp @@ -0,0 +1,618 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "BSSG.h" +#include "BSSGMessages.h" +#include "GPRSInternal.h" +#include "Globals.h" +#include "LLC.h" +#define CASENAME(x) case x: return #x; + +namespace BSSG { + +const char *BSPDUType::name(int val) +{ + switch ((type)val) { + CASENAME(DL_UNITDATA) + CASENAME(UL_UNITDATA) + CASENAME(RA_CAPABILITY) + CASENAME(PTM_UNITDATA) + CASENAME(PAGING_PS) + CASENAME(PAGING_CS) + CASENAME(RA_CAPABILITY_UPDATE) + CASENAME(RA_CAPABILITY_UPDATE_ACK) + CASENAME(RADIO_STATUS) + CASENAME(SUSPEND) + CASENAME(SUSPEND_ACK) + CASENAME(SUSPEND_NACK) + CASENAME(RESUME) + CASENAME(RESUME_ACK) + CASENAME(RESUME_NACK) + CASENAME(BVC_BLOCK) + CASENAME(BVC_BLOCK_ACK) + CASENAME(BVC_RESET) + CASENAME(BVC_RESET_ACK) + CASENAME(BVC_UNBLOCK) + CASENAME(BVC_UNBLOCK_ACK) + CASENAME(FLOW_CONTROL_BVC) + CASENAME(FLOW_CONTROL_BVC_ACK) + CASENAME(FLOW_CONTROL_MS) + CASENAME(FLOW_CONTROL_MS_ACK) + CASENAME(FLUSH_LL) + CASENAME(FLUSH_LL_ACK) + CASENAME(LLC_DISCARDED) + CASENAME(SGSN_INVOKE_TRACE) + CASENAME(STATUS) + CASENAME(DOWNLOAD_BSS_PFC) + CASENAME(CREATE_BSS_PFC) + CASENAME(CREATE_BSS_PFC_ACK) + CASENAME(CREATE_BSS_PFC_NACK) + CASENAME(MODIFY_BSS_PFC) + CASENAME(MODIFY_BSS_PFC_ACK) + CASENAME(DELETE_BSS_PFC) + CASENAME(DELETE_BSS_PFC_ACK) + } + return "unrecognized PDU"; +} +std::ostream& operator<<(std::ostream& os, const BSPDUType::type val) +{ + os << "PDU_Type=" <<(int)val <<"=" <appendByte(((mMCC[1]-'0')<<4) | (mMCC[0]-'0')); // MCC digit 2, MCC digit 1 + vec->appendByte(((mMNC[2]-'0')<<4) | (mMCC[2]-'0')); // MNC digit 3, MCC digit 3 + vec->appendByte(((mMNC[1]-'0')<<4) | (mMNC[0]-'0')); // MNC digit 2, MNC digit 1 + vec->appendUInt16(mLAC); + vec->appendByte(mRAC); + // Add Routing Area Identification IE from GSM 04.08 10.5.5.15, excluding IEI type and length bytes. + // Add Cell Identity IE GSM 04.08 10.5.1.1, excluding IEI type + unsigned mCI = gConfig.getNum("GSM.Identity.CI"); + vec->appendUInt16(mCI); +} + +// GSM 08.18 sec 10 describes the PDU messages that the SGSN can send to the BSS. +// Cause values: 08.18 sec 11.3.8 +BSSGUplinkMsg *BVCFactory(BSPDUType::type bstype, + int arg1) // For reset, the bvci to reset; for others may be cause or tag . +{ + BSSGUplinkMsg *vec = new BSSGUplinkMsg(80); // Big enough for any message. + BVCI::type bvci; + + vec->setAppendP(0); // Setup vec for appending. + + // Add the NS header. + vec->appendByte(NSPDUType::NS_UNITDATA); + vec->appendByte(0); // unused byte. + vec->appendUInt16(gBSSG.mbsNSEI); + // Add the BSSG message type + vec->appendByte((ByteType)bstype); + + switch (bstype) { + case BSPDUType::BVC_RESET: + // See GSM 08.18 sec 8.4: BVC-RESET procedure; and 10.4.12: BVC-RESET message. + bvci = (BVCI::type) arg1; + vec->appendByte(IEIType::BVCI); + vec->appendLI(2); + vec->appendUInt16(bvci); + BVCAddCause(vec,0x8); // Cause 8: O&M Intervention + if (bvci != BVCI::SIGNALLING) { + BVCAddCellIdentifier(vec); + } + // We dont use the feature bitmap. + break; + case BSPDUType::BVC_RESET_ACK: + BVCAddBVCI(vec,bstype); + // There could be a cell identifier + // There coulde be a feature bitmap. + break; + case BSPDUType::BVC_BLOCK: + BVCAddBVCI(vec,bstype); + BVCAddCause(vec,arg1); + break; + case BSPDUType::BVC_BLOCK_ACK: // fall through + case BSPDUType::BVC_UNBLOCK: // fall through + case BSPDUType::BVC_UNBLOCK_ACK: + BVCAddBVCI(vec,bstype); + break; + case BSPDUType::FLOW_CONTROL_BVC_ACK: + BVCAddTag(vec,arg1); + break; + default: assert(0); + } + return vec; +} + +// Length Indicator GSM 08.16 10.1.2 +// And I quote: +// "The BSS or SGSN shall not consider the presence of octet 2a in a received IE +// as an error when the IE is short enough for the length to be coded +// in octet 2 only." +// (pat) If the length is longer than 127, it is written simply +// as a 16 bit number in network order, which is high byte first, +// so the upper most bit is 0 if the value is <= 32767 +static unsigned IEILength(unsigned int len) +{ + if (len < 127) return 0x80 + len; + assert(0); +} + + +void NSMsg::textNSHeader(std::ostream&os) const +{ + int nstype = (int)getNSPDUType(); + if (nstype == NSPDUType::NS_UNITDATA) { + os <<"NSPDUType="<(this)); + os << " LLC UL payload="<field)) +//#endif + +namespace BSSG { + +// BVCI defined GSM 08.18 5.4.1 +// See table 5.4 for which BVCI to use with each message. +// The SIGNALLING and PTP numbers are reserved. +// See LookupBVCI(BSPDUType::type bstype); +class BVCI { + public: + enum type { + SIGNALLING = 0, // For BSSG BVC messages other than data. + PTM = 1, // Point to multipoint + PTP = 2 // Any other value is a base station designator for Point-To-Point data. + // We only use one value, gBSSG.mbsBVCI, which must be >- PTP. + }; +}; + +/********************************************************** + BSSGSP Messages we need to support eventually: + DL-UNITDATA + Includes PDU type (DL-UNITDATA), TLLI, QoS Profile, PDU Lifetime, PDU. + optional: IMSI, oldTLLI, PFI (Packet Flow Identifier), etc. + UL-UNITDATA + Includes PDU type (UL-UNITDATA), TLLI, BVCI, Cell Identifier, PDU. + GMM-PAGING-PS/GMM-PAGING-CS (for packet or voice) + Includes PDU type (PAGING-PS), QoS Profile, P-TMSI IMSI. + Note: If TLLI is specified and already exists within a Radio Context in BSS + [because MS has communicated previously] it is used. + + BVCI Location Area Routing Area BSSArea Indication + Optional: P-TMSI, BVCI, Location area, Routing area. + GMM-RA-CAPABILITY, GMM-RA-CAPABILITY-UPDATE + Astonishingly, the BSS asks the SGSN for this info. + It is because the MS may be moving from BTS to BTS, so the SGSN + is a slightly more permanent repository, and makes handover easier between BTSs. + But note that the MS can also travel from SGSN to SGSN, so I think the location + of the MS info is arbitrary. + We are talking about information that is only kept around a short while anyway. + GMM-RADIO-STATUS + GMM-SUSPEND + GMM-RESUME + + Note that there are alot of messages to control the data-rate on the BSSG connection. + None of them are implemented in the SGSN, so we dont support them either. +**********************************************************/ + +class BSPDUType { + public: + // GSM08.18 sec11.3.26 table11.27: PDU Types + // GSM08.18 table 5.4 defines the BVCI to be used with each message. + // BVCIs are: PTP, PTM, SIG + enum type { + // PDUs between RL and BSSGP SAPs: + DL_UNITDATA = 0, // PTP network->MS + UL_UNITDATA = 1, // PTP MS->network + // PDUs between GMM SAPs. + RA_CAPABILITY = 2, // PTP network->BSS + PTM_UNITDATA = 3, // PTM not currently used + // PDUs between GMM SAPs: + PAGING_PS = 6, // PTP or SIG network->BSS request to page MS for packet connection. + PAGING_CS = 7, // PTP or SIG network->BSS request to page MS for RR connection. + RA_CAPABILITY_UPDATE = 8, // PTP BSS->network request for MS Radio Access Capabilities. + RA_CAPABILITY_UPDATE_ACK = 9,// PTP network->BSS Radio Access Capability and IMSI. + RADIO_STATUS = 0x0a, // PTP BSS->SGSN notification of error + + SUSPEND = 0x0b, // SIG MS->network request to suspend GPRS service. + SUSPEND_ACK = 0x0c, // SIG network->MS ACK + SUSPEND_NACK = 0x0d, // SIG network->MS NACK + RESUME = 0xe, // SIG MS->network request to resume GPRS service. + RESUME_ACK = 0xf, // SIG network->MS ACK + RESUME_NACK = 0x10, // SIG network->MS NACK + // PDUs between NM SAPs: + // We will not use the flow control stuff, block, unblock, etc. + BVC_BLOCK = 0x20, // SIG + BVC_BLOCK_ACK = 0x21, // SIG + BVC_RESET = 0x22, // SIG network->BSS request reset everything. + BVC_RESET_ACK = 0x23, // SIG BSS->network and network->BSS? + BVC_UNBLOCK = 0x24, // SIG + BVC_UNBLOCK_ACK = 0x25, // SIG + FLOW_CONTROL_BVC = 0x26, // PTP BSS->network inform maximum throughput on Gb I/F + FLOW_CONTROL_BVC_ACK = 0x27, // PTP network->BSS + FLOW_CONTROL_MS = 0x28, // PTP BSS->network inform maximum throughput for MS. + FLOW_CONTROL_MS_ACK = 0x29, // PTP network->BSS + FLUSH_LL = 0x2a, // SIG network->BSS forget this MS (it moved to another cell.) + FLUSH_LL_ACK = 0x2b, // SIG BSS->network + LLC_DISCARDED = 0x2c, // SIG BSS->network notification of lost PDUs (probably expired) + // We ignore all these: + SGSN_INVOKE_TRACE = 0x40, // network->BSS request trace an MS + STATUS = 0x41, // SIG BSS->network or network->BSS report error condition. + DOWNLOAD_BSS_PFC = 0x50, // PTP + CREATE_BSS_PFC = 0x51, // PTP + CREATE_BSS_PFC_ACK = 0x52, // PTP + CREATE_BSS_PFC_NACK = 0x53, // PTP + MODIFY_BSS_PFC = 0x54, // PTP + MODIFY_BSS_PFC_ACK = 0x55, // PTP + DELETE_BSS_PFC = 0x56, // PTP + DELETE_BSS_PFC_ACK = 0x57 // PTP + }; + static const char *name(int val); + static const unsigned LookupBVCI(BSPDUType::type bstype); +}; +std::ostream& operator<<(std::ostream& os, const BSPDUType::type val); + +class NsIEIType { + // GSM08.18 sec10.3 NS protocol IEI Types. + // The NS protocol doesnt do much. It specifies a procedure + // to make sure the link is alive, to reset it after failure, + // and to turn the entire link on and off (block/unblock.) + public: enum type { + IEINsCause, + IEINsVCI, + IEINsPDU, + IEINsBVCI, + IEINsNSEI, + }; +}; + +class NsCause { + public: enum type { + TransitNetworkFailure, + OAndMIntervention, + EquipmentFailure, + NSVCBlocked, + NSVCUnknown, + BVCIUnknown, + SemanticallyIncorrectPDU = 8, + PduNotCompatible = 10, + ProtocolError = 11, + InvalidEssentialIE = 12, + MissingEssentialIE = 13 + }; +}; + +class IEIType { + public: + // GSM08.18 sec11.3 table11.1: IEI Types + enum type { + AlignmentOctets = 0x00, + BmaxDefaultMS = 0x01, + BSSAreaIndication = 0x02, + BucketLeakRate = 0x03, + BVCI = 0x04, + BVCBucketSize = 0x05, + BVCMeasurement = 0x06, + Cause = 0x07, + CellIdentifier = 0x08, + ChannelNeeded = 0x09, + DRXParameters = 0x0a, + eMLPPPriority = 0x0b, + FlushAction = 0x0c, + IMSI = 0x0d, + LLCPDU = 0x0e, + LLCFramesDiscarded = 0x0f, + LocationArea = 0x10, + MobileId = 0x11, + MSBucketSize = 0x12, + MSRadioAccessCapability = 0x13, + OMCId = 0x14, + PDUInError = 0x15, + PDULifetime = 0x16, + Priority = 0x17, + QoSProfile = 0x18, + RadioCause = 0x19, + RACapUPDCause = 0x1a, + RouteingArea = 0x1b, + RDefaultMS = 0x1c, + SuspendReferenceNumber = 0x1d, + Tag = 0x1e, + TLLI = 0x1f, + TMSI = 0x20, + TraceReference = 0x21, + TraceType = 0x22, + TransactionId = 0x23, + TriggerId = 0x24, + NumberOfOctetsAffected = 0x25, + LSAIdentifierList = 0x26, + LSAInformation = 0x27, + PacketFlowIdentifier = 0x28, + PacketFlowTimer = 0x29, + AggregateBSSQoSProfile = 0x3a, // (ABQP) + FeatureBitmap = 0x3b, + BucketFullRatio = 0x3c, + ServiceUTRANCCO = 0x3d // (Cell Change Order) + }; + static const char *name(int val); + +}; +std::ostream& operator<<(std::ostream& os, const IEIType::type val); + +// Notes: +// BVC = BSSG Virtual Connection +// NS SDU = the BSSG data packet transmitted over NS. + +// GSM08.16 sec 10.3.7: Network Service PDU Type +class NSPDUType { + public: + enum type { + NS_UNITDATA = 0, + NS_RESET = 2, + NS_RESET_ACK = 3, + NS_BLOCK = 4, + NS_BLOCK_ACK = 5, + NS_UNBLOCK = 6, + NS_UNBLOCK_ACK = 7, + NS_STATUS = 8, + NS_ALIVE = 10, + NS_ALIVE_ACK = 11 + }; + static const char *name(int val); +}; +std::ostream& operator<<(std::ostream& os, const NSPDUType::type val); + +// GSM08.16 sec 9.2.10 +struct RN_PACKED NSUnitDataHeader { + ByteType mNSPDUType; + ByteType mUnused; + ByteType mBVCI[2]; +}; + +class NSMsg : public ByteVector, public Utils::Text2Str +{ + public: + // This is the NS header length only for NS_UNIT_DATA messages, + // which are the only ones we care about because they are the + // only ones that have BSSG and potentially user data in them. + static const unsigned UnitDataHeaderLen = sizeof(struct NSUnitDataHeader); + + // wlen should include the NS header + BSSG header + data + NSMsg(ByteType *wdata, int wlen) // Make one from downlink data. + : ByteVector(wdata,wlen) + { } + + // wlen should include the NS header + BSSG header + data + NSMsg(int wlen) // Make one for uplink data. + : ByteVector(wlen + NSMsg::UnitDataHeaderLen) // But we will add it in anyway + { + // Zero out the NS header; the type will be filled in later. + fill(0,0,NSMsg::UnitDataHeaderLen); + } + + // Make a new message from some other, taking over the ByteVector. + // Used to change the type of a BSSG message. + NSMsg(NSMsg *src) : ByteVector(*src) { + //assert(src->isOwner()); + //move(*src); // Grab the memory from src. + //assert(!src->isOwner()); no longer true with refcnts. + } + +// Passify the brain-dead compiler: +#define NSMsgConstructors(type1,type2) \ + type1(ByteType *data, int len) : type2(data,len) {} \ + type1(int wlen) : type2(wlen) {} \ + type1(NSMsg *src) : type2(src) {} + + // Fields in the 4 byte NS Header: + void setNSPDUType(NSPDUType::type nstype) { setByte(0,nstype); } + NSPDUType::type getNSPDUType() const { return (NSPDUType::type) getByte(0); } + + void textNSHeader(std::ostream&os) const; + virtual void text(std::ostream&os) const; + std::string str() const { return this->Text2Str::str(); } // Disambigute +}; + +class BSSGMsg : public NSMsg { + public: + NSMsgConstructors(BSSGMsg,NSMsg) + // Common fields in the BSSG Header: + void setPDUType(BSPDUType::type type) { setByte(NSMsg::UnitDataHeaderLen,(ByteType)type); } + BSPDUType::type getPDUType() const { return (BSPDUType::type) getByte(NSMsg::UnitDataHeaderLen); } + virtual void text(std::ostream &os) const; + virtual std::string briefDescription() const; +}; + +class BSSGUplinkMsgElt { + public: + //TODO: virtual void text(std::ostream&) const; + virtual void parseElement(const char *src, size_t &rp); +}; +class BSSGDownlinkMsgElt { + public: + //TODO: virtual void text(std::ostream&) const; +}; + +// Note that the ByteVector in NSMsg is allocated, and all the other ones in downlink messages +// are segments referring to this one. +//class BSSGDownlinkMsg : public NSMsg, public BSSGMsg +class BSSGDownlinkMsg : public BSSGMsg +{ + public: + NSMsgConstructors(BSSGDownlinkMsg,BSSGMsg) + + virtual void text(std::ostream &os) const { BSSGMsg::text(os); } +}; + +class BSSGUplinkMsg : public BSSGMsg +{ + public: + NSMsgConstructors(BSSGUplinkMsg,BSSGMsg) + + virtual void text(std::ostream &os) const { BSSGMsg::text(os); } +}; + + +// This is the QoS Profile in the header, when not including the 2 byte IEI prefix. +// GSM08.18 sec 11.3.28 +struct QoSProfile { + // Coded as value part of Bucket Leak Rate 'R' from sec 11.3.4 + // And I quote: + // The R field is the binary encoding of the rate information expressed in 100 bits/sec + // increments starting from 0 x 100 bits/sec until 65535 * 100 bits/sec (6Mbps) + // Note a) Bit Rate 0 means "Best Effort". + unsigned mPeakBitRate:16; + + // These are the bits of byte 3. + unsigned mSpare:2; + unsigned mCR:1; + unsigned mT:1; + unsigned mA:1; + unsigned mPrecedence:3; + + ByteType getB3() { return (mCR<<5)|(mT<<4)|(mA<<3)|mPrecedence; } + void setB3(ByteType bits) { + mPrecedence = bits & 0x7; + mA = (bits>>3)&1; + mT = (bits>>4)&1; + mCR = (bits>>5)&1; + } + + QoSProfile() { // Create a default QoSProfile + mPeakBitRate = 0; + setB3(0); + } + + // Get from the ByteVector, which is in network order: + void qosRead(ByteVector &src, size_t &wp) { + mPeakBitRate = src.getUInt16(wp); wp+=2; + ByteType byte3 = src.getByte(wp++); + setB3(byte3); + } + + // Write to ByteVector in network order. + void qosAppend(ByteVector *dest) { + dest->appendUInt16(mPeakBitRate); + dest->appendByte(getB3()); + } +}; + + +// GSM 08.18 sec 10.2.1 From SGSN to BSS. +class BSSGMsgDLUnitData : public BSSGDownlinkMsg +{ + public: + // Mandatory elements: + UInt32_z mbdTLLI; + UInt16_z mbdPDULifetime; + QoSProfile mbdQoS; // 3 bytes + Bool_z mbdHaveOldTLLI; + UInt32_z mbdOldTLLI; + // Optional elements: + //RLCMsgEltMSRACapabilityValuePart mbdRACap; + ByteVector mbdRACap; + ByteVector mbdIMSI; + ByteVector mbdPDU; + + BSSGMsgDLUnitData(BSSGDownlinkMsg*src) : BSSGDownlinkMsg(src) { + size_t wp = NSMsg::UnitDataHeaderLen; + parseDLUnitDataBody(*this,wp); + } + + // Parse body, excluding the NS header. + void parseDLUnitDataBody(ByteVector &src, size_t &wp); + void text(std::ostream &os) const; + std::string briefDescription() const; +}; + +BSSGDownlinkMsg* BSSGDownlinkMessageParse(ByteVector&src); + +// Routing Area Identification: GSM04.08 sec 10.5.5.15. +// 6 bytes. This is just a magic cookie as far as we are concerned. +struct RN_PACKED RoutingAreaID { + // First three bytes identify MCC Mobile Country Code and MNC Mobile Network Code. + // Weird encoding; see spec. + ByteType mMCCDigits12; // MCC Digit2, MCC Digit1. + ByteType mHighDigits; // MNC Digit3, MCC Digit3. + ByteType mMNCDigits12; // MNC Digit2, MNC Digit1. + unsigned mLAC:16; // Location Area Code. + unsigned mRAC:8; // Routing Area Code. + RoutingAreaID(unsigned MCC, unsigned MNC, unsigned LAC, unsigned RAC) { + // TODO, but maybe no one cares. + } +}; + + +// Cell Identifier: GSM 08.18 sec 11.2.9 +struct RN_PACKED CellIdentifier { + // Routing Area Identification: GSM 04.08 sec 10.5.5.15. + uint64_t RoutingAreaIdentification:48; + // Cell Identity: GSM04.08 sec 10.5.1.1 + // It is just a two byte int whose value determined by the administrator. + unsigned CellIdentity:16; +}; + +struct RN_PACKED CellIdentifierIEI { + ByteType iei; + ByteType length; + ByteType RoutingAreaID[6]; + ByteType CellIdentity[2]; +}; + +// This is the specific UlUnitData format that we use. +struct RN_PACKED BSSGMsgULUnitDataHeader { + NSUnitDataHeader mbuNS; + ByteType mbuPDUType; + ByteType mbuTLLI[4]; + ByteType mbuQoS[3]; + ByteType mbuCellIdIEI[10]; + ByteType mbuAlignmentIEI[2]; // spacer required to make size div by 4. + ByteType mLLCIEIType; + ByteType mLLCPDULength[2]; + // PDU data starts after this.. +}; + +//class BSSGMsgBVCReset { +// NSUnitDataHeader mbuNS; +//}; + +// GSM 08.18 sec 10.2.2 Packet Data from BSS to SGSN. +// It could be user data or a GMM or other message from a higher +// layer in the MS to the SGSN. +// There are several optional IEIs we do not include. +// Alignment octets are necessary so the start of the PDU IEI +// starts on a 32bit boundary, which is totally dopey because the PDU +// itself is unaligned inside the PDU IEI. Whatever. +// This structure is write-only; the internal fields are in network order and +// we dont need to read them, so we dont. +// The RLCEngine is the primary producer of these things, and always allocates +// a maximum size (1502 bytes plus headers) so it can just append the data in. +// The lifetime of these things is quite short; they live in the BSSG outgoing +// queue until the service thread sends them to the SGSN. +class BSSGMsgULUnitData : public BSSGUplinkMsg +{ + public: + static const unsigned HeaderLength = sizeof(BSSGMsgULUnitDataHeader); + int mTBFId; // For debugging, the associated tbf that processed us. + + BSSGMsgULUnitData(unsigned wLen, uint32_t wTLLI); + + // Return the pointer to the header within the ByteVector (its just 0) + // what a horrible language. + BSSGMsgULUnitDataHeader *getHeader() { return (BSSGMsgULUnitDataHeader*) begin(); } + BSSGMsgULUnitDataHeader *getHeader() const { return const_cast(this)->getHeader(); } + + //unused: void setBVCI(int wBVCI) { setUInt16(2,wBVCI); } + unsigned getBVCI() const { return getUInt16(2); } + //unused: void setTLLI(int wTLLI) { sethtonl(getHeader()->mbuTLLI,wTLLI); } + int getTLLI() const { return getntohl(getHeader()->mbuTLLI); } + + ByteVector getPayload() { + // Now the return value also owns memory. + //return tail(HeaderLength); + ByteVector result = *this; // Increments refcnt. + result.trimLeft(HeaderLength); + return result; + } + + // We set the length after assembling the complete pdu. + unsigned mLengthPosition; // Where the PDU Length goes in the ByteVector. + void setLength(); + + void text(std::ostream&os) const; +}; + +// Make a short BSSG Protocol signaling message. +BSSGUplinkMsg *BVCFactory(BSPDUType::type bssgtype, int arg1=0); +// Make a short NS Protocol message. +NSMsg *NsFactory(NSPDUType::type nstype, int cause=0); + +//class BSSGMsgULUnitData : public BSSGUplinkMsg { +// unsigned mbPDUType:8; +// unsigned mTLLI:32; +// QoSProfile mQoS; +// CellIdentifierIEI mCellIdentifier; // 10 bytes +//}; + +}; +#endif diff --git a/GPRS/ByteVector.cpp b/GPRS/ByteVector.cpp new file mode 100644 index 00000000..fbc0f470 --- /dev/null +++ b/GPRS/ByteVector.cpp @@ -0,0 +1,669 @@ +/* +* Copyright 2011 Range Networks, Inc. +* +* +* This software is distributed under the terms of the GNU Affero Public License. +* See the COPYING file in the main directory for details. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ +#include "ByteVector.h" + +// Set the char[2] array at ip to a 16-bit int value, swizzling bytes as needed for network order. +void sethtons(ByteType *cp,unsigned value) +{ + uint16_t tmp = htons(value); + ByteType *tp = (ByteType*)&tmp; + cp[0]=tp[0]; cp[1]=tp[1]; // Overkill but safe. +} + +// Set the char[4] array at ip to a 32-bit int value, swizzling bytes as needed for network order. +void sethtonl(ByteType *cp,unsigned value) +{ + uint32_t tmp = htonl(value); + ByteType *tp = (ByteType*)&tmp; + cp[0]=tp[0]; cp[1]=tp[1]; cp[2]=tp[2]; cp[3]=tp[3]; +} + +uint16_t getntohs(ByteType *cp) +{ + uint16_t tmp; + ByteType *tp = (ByteType*)&tmp; + tp[0]=cp[0]; tp[1]=cp[1]; + return ntohs(tmp); +} + +uint32_t getntohl(ByteType *cp) +{ + uint32_t tmp; + ByteType *tp = (ByteType*)&tmp; + tp[0]=cp[0]; tp[1]=cp[1]; tp[2]=cp[2]; tp[3]=cp[3]; + return ntohl(tmp); +} + +void ByteVector::clear() +{ + if (mData) { +#if BYTEVECTOR_REFCNT + if (decRefCnt() <= 0) { delete[] mData; RN_MEMCHKDEL(ByteVectorData) } +#else + delete[] mData; +#endif + } + mSizeBits = 0; + mData = NULL; +} + +void ByteVector::init(size_t size) +{ + //mBitInd = 0; + if (size == 0) { + mData = mStart = 0; + } else { +#if BYTEVECTOR_REFCNT + RN_MEMCHKNEW(ByteVectorData) + mData = new ByteType[size + mDataOffset]; + setRefCnt(1); + mStart = mData + mDataOffset; +#else + mData = new ByteType[size]; + mStart = mData; +#endif + } + mAllocEnd = mStart + size; + mSizeBits = size*8; +} + +// Make a full memory copy of other. +// We clone only the filled in area, not the unused allocated area. +void ByteVector::clone(const ByteVector &other) +{ + clear(); + init(other.size()); + memcpy(mStart,other.mStart,other.size()); +} + +// Make this a copy of other. +// It it owns memory, share it using refcnts. +// Formerly: moved ownership of allocated data to ourself. +void ByteVector::dup(const ByteVector &other) +{ + clear(); + mData=other.mData; + mStart=other.mStart; + mSizeBits=other.mSizeBits; + mAllocEnd = other.mAllocEnd; + //mBitInd = other.mBitInd; +#if BYTEVECTOR_REFCNT + if (mData) incRefCnt(); +#else + other.mData=NULL; +#endif +} + +// Return a segment of a ByteVector that shares the same memory as the original. +ByteVector ByteVector::segment(size_t start, size_t span) const +{ +#if NEW_SEGMENT_SEMANTICS + BVASSERT(start+span <= size()); + ByteVector result(*this); + result.mStart = mStart + start; + result.mSizeBits = span*8; + //result.mEnd = result.mStart + span; + //BVASSERT(result.mEnd<=mEnd); + return result; +#else + ByteType* wStart = mStart + start; + ByteType* wEnd = wStart + span; + BVASSERT(wEnd<=mEnd); + return ByteVector(wStart,wEnd); +#endif +} + +// This returns a segment that does not share ownership of the original memory, +// so when the original is deleted, this is destroyed also, and without warning. +// Very easy to insert bugs in your code, which is why it is called segmentTemp to indicate +// that it is a ByteVector for temporary use only. +const ByteVectorTemp ByteVector::segmentTemp(size_t start, size_t span) const +{ + BVASSERT(start+span <= size()); + ByteType* wStart = mStart + start; + ByteType* wEnd = wStart + span; + //BVASSERT(wEnd<=mEnd); + return ByteVectorTemp(wStart,wEnd); +} + +// Copy other to this starting at start. +// The 'this' ByteVector must be allocated large enough to hold other. +// Unlike Vector, the size() is increased to make it fit, up to the allocated size. +void ByteVector::setSegment(size_t start, ByteVector&other) +{ + BVASSERT(start <= size()); // If start == size(), nothing is copied. + BVASSERT(bitind() == 0); // This function only allowed on byte-aligned data. + ByteType* base = mStart + start; + int othersize = other.size(); + BVASSERT(mAllocEnd - base >= othersize); + memcpy(base,other.mStart,othersize); + //if (mEnd - base < othersize) { mEnd = base + othersize; } // Grow size() if necessary. + if (mSizeBits/8 < start+othersize) { mSizeBits = (start+othersize)*8; } +} + +// Copy part of this ByteVector to a segment of another. +// The specified span must not exceed our size, and it must fit in the target ByteVector. +// Unlike Vector, the size() of other is increased to make it fit, up to the allocated size. +void ByteVector::copyToSegment(ByteVector& other, size_t start, size_t span) const +{ + ByteType* base = other.mStart + start; + BVASSERT(start <= other.size()); // If start == size(), nothing is copied. + BVASSERT(base+span<=other.mAllocEnd); + //BVASSERT(mStart+span<=mEnd); + //BVASSERT(base+span<=other.mAllocEnd); + memcpy(base,mStart,span); + //if (base+span > other.mEnd) { other.mEnd = base+span; } // Increase other.size() if necessary. + if (other.size() < start+span) { other.mSizeBits = (start+span)*8; } +} + +/** Copy all of this Vector to a segment of another Vector. */ +void ByteVector::copyToSegment(ByteVector& other, size_t start /*=0*/) const +{ + copyToSegment(other,start,size()); +} + + +void ByteVector::append(const ByteType *bytes, unsigned len) +{ + memcpy(&mStart[grow(len)],bytes,len); +} + +// Does change size(). +void ByteVector::appendFill(ByteType byte, size_t span) +{ + memset(&mStart[grow(span)],byte,span); +} + +void ByteVector::append(const ByteVector&other) +{ + append(other.mStart,other.size()); + //BVASSERT(othersize <= mAllocEnd - mEnd); + //memcpy(mEnd,other.mStart,othersize); + //mEnd += othersize; +} + +// append a BitVector to this, converting the BitVector back to bytes. +void ByteVector::append(const BitVector&other) +{ + int othersizebits = other.size(); + int bitindex = bitind(); + if (bitindex) { + // Heck with it. Optimize this if you want to use it. + int iself = growBits(othersizebits); // index into this. + int iother = 0; // index into other + // First partial byte + int rem = 8-bitindex; + if (rem > othersizebits) rem = othersizebits; + setField(iself,other.peekField(iother,rem),rem); + iself += rem; iother += rem; + // Copy whole bytes. + for (; othersizebits-iother>=8; iother+=8, iself+=8) { + setByte(iself/8,other.peekField(iother,8)); + } + // Final partial byte. + rem = othersizebits-iother; + if (rem) { + setField(iself,other.peekField(iother,rem),rem); + } + return; + } else { + other.pack(&mStart[growBits(othersizebits)/8]); + } + //BVASSERT(othersize <= mAllocEnd - mEnd); + //other.pack(mEnd); + //mEnd += othersize; +} + +// Length Indicator: GSM08.16 sec 10.1.2 +// The length indicator may be 1 or 2 bytes, depending on bit 8, +// which is 0 to indicate a 15 bit length, or 1 to indicate a 7 bit length. +unsigned ByteVector::readLI(size_t &wp) +{ + unsigned byte1 = getByte(wp++); + if (byte1 & 0x80) { return byte1 & 0x7f; } + return (byte1 * 256) + getByte(wp++); +} + +// This is a two byte length indicator as per GSM 08.16 10.1.2 +void ByteVector::appendLI(unsigned len) +{ + if (len < 255) { + appendByte(len | 0x80); + } else { + BVASSERT(len <= 32767); + appendByte(0x7f&(len>>8)); + appendByte(0xff&(len>>8)); + } +} + +// The inverse of trimRight. Like an append but the new area is uninitialized. +void ByteVector::growRight(unsigned amt) +{ + BVASSERT(!bitind()); + BVASSERT(amt <= size()); + mSizeBits += 8*amt; +} + +// The inverse of trimLeft +ByteType* ByteVector::growLeft(unsigned amt) +{ + ByteType *newstart = mStart - amt; + BVASSERT(newstart >= mData + mDataOffset); + mSizeBits += 8*amt; + return mStart = newstart; +} + +void ByteVector::trimLeft(unsigned amt) +{ + BVASSERT(amt <= size()); + mStart += amt; + mSizeBits -= 8*amt; +} + +void ByteVector::trimRight(unsigned amt) +{ + BVASSERT(!bitind()); + BVASSERT(amt <= size()); + //mEnd -= amt; + mSizeBits -= 8*amt; +} + +// For appending. +// Grow the vector by the specified amount of bytes and return the index of that location. +unsigned ByteVector::grow(unsigned amt) +{ + unsigned writeIndex = sizeBytes(); // relative to mStart. + BVASSERT(bitind() == 0); // If it is not byte-aligned, cant use these functions; use setField instead. + setSizeBits(mSizeBits + 8*amt); + //BVASSERT(amt < sizeRemaining()); + //mSizeBits += 8*amt; + //unsigned writeIndex = mEnd - mStart; + //mEnd += amt; + //BVASSERT(mEnd <= mAllocEnd); + return writeIndex; +} + +// For appending. +// Grow the vector by amt in bits; return the old size in bits. +unsigned ByteVector::growBits(unsigned amt) +{ + int oldsizebits = sizeBits(); + setSizeBits(oldsizebits + amt); + return oldsizebits; +} + + +// GSM04.60 10.0b.3.1: Note that fields in RLC blocks use network order, +// meaning most significant byte first (cause they started on Sun workstations.) +// It is faster to use htons, etc, than unpacking these ourselves. +void ByteVector::setUInt16(size_t writeIndex,unsigned value) { // 2 byte value + BVASSERT(writeIndex <= size() - 2); + sethtons(&mStart[writeIndex],value); +} + +void ByteVector::setUInt32(size_t writeIndex, unsigned value) { // 4 byte value + BVASSERT(writeIndex <= size() - 4); + sethtonl(&mStart[writeIndex],value); +} + +// Does not change size(). +void ByteVector::fill(ByteType byte, size_t start, size_t span) { + ByteType *dp=mStart+start; + ByteType *end=dp+span; + BVASSERT(end<=mAllocEnd); + while (dp 0) { + char ch = getNibble(b,1); + ss.push_back(ch + (ch > 9 ? ('A'-10) : '0')); + numBits -= 4; + if (numBits >= 4) { + ch = getNibble(b,0); + ss.push_back(ch + (ch > 9 ? ('A'-10) : '0')); + } + b++; + numBits -= 4; + } + return ss; +} + +// This function returns "ByteVector(size=... data:...)" +std::string ByteVector::str() const +{ + std::ostringstream ss; + ss << *this; + return ss.str(); +} + +std::ostream& operator<<(std::ostream&os, const ByteVector&vec) +{ + int i, size=vec.size(); char buf[10]; + os <<"ByteVector(size=" <= 0 && bitIndex <= 7); + //return !!(getByte(byteIndex) & (1 << (7-bitIndex))); + return !!(getByte(byteIndex) & bitMasks[bitIndex]); +} + +// Get a bit from the specified byte, numbered like this: +// bits: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 +void ByteVector::setBit2(size_t byteIndex, unsigned bitIndex, unsigned val) +{ + BVASSERT(bitIndex >= 0 && bitIndex <= 7); + BVASSERT(byteIndex < size()); + ByteType mask = bitMasks[bitIndex]; + mStart[byteIndex] = val ? (mStart[byteIndex] | mask) : (mStart[byteIndex] & ~mask); +} + +#ifndef MIN +#define MIN(a,b) ((a)<(b)?(a):(b)) +#endif + +// Write a bit field starting at specified byte and bit, each numbered from 0 +void ByteVector::setField2(size_t byteIndex, size_t bitIndex, uint64_t value,unsigned lengthBits) +{ + BVASSERT(bitIndex >= 0 && bitIndex <= 7); + // Example: bitIndex = 2, length = 2; + // 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + // 0 0 X X 0 0 0 0 + // endpos = 4; nbytes = 0; lastbit = 4; nbits = 2; mask = 3; shift = 4; + + unsigned endpos = bitIndex + lengthBits; // 1 past the 0-based index of the last bit. + unsigned nbytes = (endpos-1) / 8; // number of bytes that will be modified, minus 1. + ByteType *dp = mStart + byteIndex + nbytes; + + unsigned lastbit = endpos % 8; // index of first bit not to be replaced, or 0. + // Number of bits to modify in the current byte, starting at the last byte. + unsigned nbits = lastbit ? MIN(lengthBits,lastbit) : MIN(lengthBits,8); + + for (int len = lengthBits; len > 0; dp--) { + // Mask of number of bits to be modified in this byte, starting from LSB. + unsigned mask = (1 << nbits) - 1; + ByteType val = value & mask; + value >>= nbits; + if (lastbit) { + // Shift val and mask so they are aligned with the bits to modify in the last byte, + // noting that we modify the last byte first, since we work backwards. + int shift = 8 - lastbit; + mask <<= shift; + val <<= shift; + } + *dp = (*dp & ~mask) | (val & mask); + len -= nbits; + nbits = MIN(len,8); + + lastbit = 0; + } +} + +void ByteVector::appendField(uint64_t value,unsigned lengthBits) +{ + setField(growBits(lengthBits),value,lengthBits); + /*** old + int endpos = mBitInd + lengthBits; // 1 past the 0-based index of the last bit. + int nbytes = (endpos-1) / 8; // number of new bytes needed. + if (mBitInd == 0) nbytes++; // if at 0, the next byte has not been alloced yet. + setField2(grow(nbytes),mBitInd,value,lengthBits); + mBitInd = endpos % 8; + ***/ +} + +// Read a bit field starting at specified byte and bit, each numbered from 0. +// Bit numbering is from high to low, like this: +// getField bitIndex: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 +// Note that this is inverted with respect to the numbering scheme used +// in many GSM specs, which looks like this: +// GSM specs: 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 +// Note that some GSM specs use low-to-high and some use high-to-low numbering. +// Generally, where the BitVector class is used they use low-to-high numbering, +// which is rectified in the FEC classes by the byteswapping the BitVector before being used. +uint64_t ByteVector::getField2(size_t byteIndex, size_t bitIndex, unsigned lengthBits) const +{ + ByteType *dp = mStart + byteIndex; + int len = (int) lengthBits; + BVASSERT(bitIndex >= 0 && bitIndex <= 7); + // Get first byte: + // This was for bitIndex running from low bit to high bit: + // int nbits = bitIndex+1; // Number of bits saved from byte. + // This is for bitIndex running from 0=>high bit to 7=>low bit: + int nbits = 8-bitIndex; // Number of bits saved from byte, ignoring len restriction. + // Example: bitIndex=3 => 0 | 0 | 0 | X | X | X | X | X => AND with 0x1f + + uint64_t accum = *dp++ & (0x0ff >> (8-nbits)); // Preserve right-most bits. + if (len < nbits) { accum >>= (nbits - len); return accum; } + len -= nbits; + + // Get the full bytes: + for (; len >= 8; len -= 8) { accum = (accum << 8) | *dp++; } + + // Append high bits of last byte: + if (len>0) { accum = (accum << len) | (*dp >> (8-len)); } + return accum; +} + +// This is static - there is no 'this' argument. +int ByteVector::compare(const ByteVector &bv1, const ByteVector &bv2) +{ + unsigned bv1size = bv1.sizeBits(), bv2size = bv2.sizeBits(); + unsigned minsize = MIN(bv1size,bv2size); + unsigned bytes = minsize/8; + int result; + // Compare the full bytes. + if (bytes) { + if ((result = memcmp(bv1.begin(),bv2.begin(),bytes))) {return result;} + } + // Compare the partial byte, if any. + unsigned rem = minsize%8; + if (rem) { + if ((result = (int) bv1.getField2(bytes,0,rem) - (int) bv2.getField2(bytes,0,rem))) {return result;} + } + // All bits the same. The longer guy wins. + return (int)bv1size - (int)bv2size; +} + +// We assume that if the last byte is a partial byte (ie bitsize % 8 != 0) +// then the remaining unused bits are all equal, should be 0. +// If they were set with setField, that will be the case. +bool ByteVector::eql(const ByteVector &other) const +{ + if (sizeBits() != other.sizeBits()) {return false;} // Quick check to avoid full compare. + return 0 == compare(*this,other); + //unsigned bytes = bvsize/8; + //ByteType *b1 = mStart, *b2 = other.mStart; + //for (int i = size(); i > 0; i--) { if (*b1++ != *b2++) return false; } + //return true; +} + +#ifdef TEST +void ByteVectorTest() +{ + unsigned byten, bitn, l, i; + const unsigned bvlen = 20; + ByteVector bv(bvlen), bv2(bvlen), pat(bvlen); + BitVector bitv(64); + int printall = 0; + int tests = 0; + + ByteVector bctest = ByteVector("12345"); + for (i = 0; i < 5; i++) { + assert(bctest.getByte(i) == '1'+i); + } + + bv.fill(3); + for (i = 0; i < bvlen; i++) { + assert(bv.getByte(i) == 3); + } + + for (byten = 0; byten <= 1; byten++) { + for (bitn = 0; bitn <= 7; bitn++) { + for (l = 1; l <= 33; l++) { + tests++; + uint64_t val = 0xffffffffffull & ((1ull< +#include +#include "MemoryLeak.h" +#include "BitVector.h" +#include "ScalarTypes.h" +#include "Logger.h" +// Originally based on BitVector, based on Vector + +// ByteVector is like a Vector but for objects of type... guess what? +// ByteVector also has an efficient append facility. +// A ByteVector consists of packed memory that is byte-aligned on the left side +// and bit-aligned on the right side. Both the left and right side can be moved +// back and forth within the constraints of the originally allocated memory. +// See: trimLeft, trimRight, growLeft, and the many append functions. +// The basic strategy is that ByteVectors are always allocated initially +// such that size() is the full allocated size, so if you want to use the append +// feature you must call setAppendP() (or conceivably trimRight) to set the location +// where you want to start appending. +// Exceeding the edges of the allocated memory area throws ByteVectorError +// There are two classes defined here: +// o ByteVector points to a memory area that it manages. +// All segments derived from a ByteVector share the same memory using refcnts. +// When the last segment is deleted, the memory is freed. +// o ByteVectorTemp is identical but does not 'own' the memory, rather it points into +// an area of memory that it does not manage. It allows you to use the rather extensive +// set of ByteVector manipulation functions on some other memory, or derive a segment +// using segmentTemp when you know for sure that the derived segment is temporary and +// will not outlive the original ByteVector. +// It is unwise to expand the left or right side of a ByteVectorTemp because there is +// no protection for exceeding the bounds of the memory area, however those functions +// are not currently over-ridden in ByteVectorTemp to remove them, but they should be eliminated. +// ByteVector is the base class and can refer to either ByteVectors that own memory, +// or ByteVectorTemps that do not. + +// I started inheriting from Vector but we could only reuse a few lines of code +// from there so it is not worth the trouble. It would be better to push +// the appending ability down into Vector. But appending creates a different +// concept of size() than Vector which would be a major change. +// To avoid confusion with Vector type functions resize() is not defined in ByteVector. + +#define NEW_SEGMENT_SEMANTICS 1 +#define BYTEVECTOR_REFCNT 1 // ByteVectors now use reference counting + +#define BVASSERT(expr) {if (!(expr)) throw ByteVectorError();} +class ByteVectorError {}; + +typedef uint8_t ByteType; +void sethtonl(ByteType *cp,unsigned value); +void sethtons(ByteType *cp,unsigned value); +uint16_t getntohs(ByteType *cp); +uint32_t getntohl(ByteType *cp); + +class ItemWithSize { + virtual size_t sizeBits() const =0; + virtual size_t sizeBytes() const =0; +}; + +class ByteVectorTemp; +class Dorky {}; // Used to force use of a particular constructor. + +class ByteVector //: public Vector + : public ItemWithSize +{ + ByteType* mData; ///< allocated data block, if any + ByteType* mStart; ///< start of useful data, always >=mData and <=mAllocEnd + unsigned mSizeBits; ///< size of useful data in bits. + ByteType *mAllocEnd; ///< end of allocated data + 1. + + //unsigned mBitInd; + unsigned bitind() { return mSizeBits % 8; } + +#if BYTEVECTOR_REFCNT + // The first mDataOffset bytes of mData is a short reference count of the number + // of ByteVectors pointing at it. + static const int mDataOffset = sizeof(short); + int setRefCnt(int val) { return ((short*)mData)[0] = val; } + int decRefCnt() { return setRefCnt(((short*)mData)[0] - 1); } + void incRefCnt() { setRefCnt(((short*)mData)[0] + 1); } +#endif + + + void init(size_t newSize); /** set size and allocated size to that specified */ + void initstr(const char *wdata, unsigned wlen) { init(wlen); setAppendP(0); append(wdata,wlen); } + void initstr(const char *wdata) { initstr(wdata,strlen(wdata)); } + void dup(const ByteVector& other); + + // Constructor: A ByteVector that does not own any memory, but is just a segment + // of some other memory. This constructor is private. + // The public way to do this is to use segmentTemp or create a ByteVectorTemp. + protected: + ByteVector(Dorky,ByteType*wstart,ByteType*wend) + : mData(0), mStart(wstart), mSizeBits(8*(wend-wstart)), mAllocEnd(wend) {} + //: mData(0), mStart(wstart), mEnd(wend), mAllocEnd(wend), mBitInd(0) {} + + public: + void clear(); // Release the memory used by this ByteVector. + // clone semantics are weird: copies data from other to self. + void clone(const ByteVector& other); /** Copy data from another vector. */ +#if BYTEVECTOR_REFCNT + int getRefCnt() { return mData ? ((short*)mData)[0] : 0; } +#endif + + const ByteType* begin() const { return mStart; } + ByteType*begin() { return mStart; } + + // This is the allocSize from mStart, not from mData. + size_t allocSize() const { return mAllocEnd - mStart; } + //size_t sizeBytes() const { return mEnd - mStart + !!mBitInd; } // size in bytes + size_t sizeBytes() const { return (mSizeBits+7)/8; } // size in bytes + size_t size() const { return sizeBytes(); } // size in bytes + //size_t sizeBits() const { return size() * 8 + mBitInd; } // size in bits + size_t sizeBits() const { return mSizeBits; } // size in bits + size_t sizeRemaining() const { // Remaining full bytes for appends + return (mAllocEnd - mStart) - sizeBytes(); + //int result = mAllocEnd - mEnd - !!mBitInd; + //return result >= 0 ? (size_t) result : 0; + } + + void resetSize() { mSizeBits = 8*allocSize(); } + + // Set the Write Position for appends. This is equivalent to setting size(). + void setAppendP(size_t bytep, unsigned bitp=0) { + BVASSERT(bytep + !!bitp <= allocSize()); + mSizeBits = bytep*8 + bitp; + //mEnd=mStart+bytep; mBitInd=bitp; + } + void setSizeBits(size_t bits) { + BVASSERT((bits+7)/8 <= allocSize()); + mSizeBits = bits; + //mEnd=mStart+(bits/8); mBitInd=bits%8; + } + + // Concat unimplemented. + //ByteVector(const ByteVector& source1, const ByteVector source2); + + /** Constructor: An empty Vector of a given size. */ + ByteVector(size_t wSize=0) { RN_MEMCHKNEW(ByteVector); init(wSize); } + + // Constructor: A ByteVector whose contents is from a string with optional length specified. + // A copy of the string is malloced into the ByteVector. + // ByteType is "unsigned char" so we need both signed and unsigned versions to passify C++. + ByteVector(const ByteType *wdata,int wlen) { RN_MEMCHKNEW(ByteVector) initstr((const char*)wdata,wlen); } + ByteVector(const ByteType *wdata) { RN_MEMCHKNEW(ByteVector) initstr((const char*)wdata); } + ByteVector(const char *wdata,int wlen) { RN_MEMCHKNEW(ByteVector) initstr(wdata,wlen); } + ByteVector(const char *wdata) { RN_MEMCHKNEW(ByteVector) initstr(wdata); } + + // Constructor: A ByteVector which is a duplicate of some other. + // They both 'own' the memory using refcounts. + // The const is tricky - other is modified despite the 'const', because only the ByteVector itself is 'const', + // the memory it points to is not. + // See also: alias. + ByteVector(ByteVector& other) : mData(0) { RN_MEMCHKNEW(ByteVector) dup(other); } + ByteVector(const ByteVector&other) : mData(0) { RN_MEMCHKNEW(ByteVector) dup(other); } + + ByteVector(const BitVector &other) { RN_MEMCHKNEW(ByteVector) init((other.size()+7)/8); setAppendP(0); append(other); } + virtual ~ByteVector() { RN_MEMCHKDEL(ByteVector) clear(); } + + // Make a duplicate of the vector where both point into shared memory, and increment the refcnt. + // Use clone if you want a completely distinct copy. + void operator=(ByteVector& other) { dup(other); } + + // The BitVector class implements this with a clone(). + // However, this gets called if a hidden intermediary variable is required + // to implement the assignment, for example: x = segment(...); + // In this case it is safer for the class to call clone to be safe, + // however, that is probably never what is wanted by the calling code, + // so I am leaving it out of here. Only use this type of assignment if the + // other does not own the memory. + // Update: With refcnts, these issues evaporate, and the two forms + // of assignment are identical. + void operator=(const ByteVector& other) { +#if BYTEVECTOR_REFCNT + dup(other); +#else + BVASSERT(other.mData == NULL); // Dont use assignment in this case. + set(other); +#endif + } + + static int compare(const ByteVector &bv1, const ByteVector &bv2); + bool eql(const ByteVector &other) const; + bool operator==(const ByteVector &other) const { return eql(other); } + bool operator!=(const ByteVector &other) const { return !eql(other); } + // This is here so you put ByteVectors in a map, which needs operator< defined. + bool operator<(const ByteVector &other) const { return compare(*this,other)<0; } + bool operator>(const ByteVector &other) const { return compare(*this,other)>0; } + bool operator<=(const ByteVector &other) const { return compare(*this,other)<=0; } + bool operator>=(const ByteVector &other) const { return compare(*this,other)>=0; } + + /**@name Functions identical to Vector: */ + // Return a segment of a ByteVector that shares the same memory as the original. + // The const is tricky - the original ByteVector itself is not modified, but the memory it points to is. + ByteVector segment(size_t start, size_t span) const; + // For the const version, since we are not allowed to modify the original + // to change the refcnt, we have to either return a segment that does not own memory, + // or a completely new piece of memory. So I am using a different name so that + // it is obvious that the memory ownership is being stripped off the ByteVector. + const ByteVectorTemp segmentTemp(size_t start, size_t span) const; + + // Copy other to this starting at start. + // The 'this' ByteVector must be allocated large enough to hold other. + void setSegment(size_t start, ByteVector&other); + + bool isOwner() { return !!mData; } // Do we own any memory ourselves? + + // Trim specified number of bytes from left or right in place. + // growLeft is the opposite: move the mStart backward by amt, throw error if no room. + // New space is uninitialized. + // These are for byte-aligned only, so we assert bit index is 0. + void trimLeft(unsigned amt); + void trimRight(unsigned amt); + ByteType *growLeft(unsigned amt); + void growRight(unsigned amt); + + void fill(ByteType byte, size_t start, size_t span); + void fill(ByteType byte, size_t start) { fill(byte,start,size()-start); } + void fill(ByteType byte) { fill(byte,0,size()); } + void appendFill(ByteType byte, size_t span); + // Zero out the rest of the ByteVector: + void appendZero() { + if (bitind()) appendField(0,8-bitind()); // 0 fill partial byte. + appendFill(0,allocSize() - size()); // 0 fill remaining bytes. + } + + // Copy part of this ByteVector to a segment of another. + // The specified span must not exceed our size, and it must fit in the target ByteVector. + void copyToSegment(ByteVector& other, size_t start, size_t span) const; + + /** Copy all of this Vector to a segment of another Vector. */ + void copyToSegment(ByteVector& other, size_t start=0) const; + + // pat 2-2012: I am modifying this to use the refcnts, so to get + // a segment with an incremented refcnt, use: alias().segment(...) + //ByteVector alias() { return segment(0,size()); } + ByteVector head(size_t span) const { return segment(0,span); } + //const ByteVector headTemp(size_t span) const { return segmentTemp(0,span); } + ByteVector tail(size_t start) const { return segment(start,size()-start); } + //const ByteVector tailTemp(size_t start) const { return segmentTemp(start,size()-start); } + + // GSM04.60 10.0b.3.1: Note that fields in RLC blocks use network order, + // meaning most significant byte first (cause they started on Sun workstations.) + // It is faster to use htons, etc, than unpacking these ourselves. + // Note that this family of functions all assume byte-aligned fields. + // See setField/appendField for bit-aligned fields. + unsigned grow(unsigned amt); + unsigned growBits(unsigned amt); + void setByte(size_t ind, ByteType byte) { BVASSERT(ind < size()); mStart[ind] = byte; } + void setUInt16(size_t writeIndex,unsigned value); // 2 byte value + void setUInt32(size_t writeIndex, unsigned value); // 4 byte value + void appendByte(unsigned value) { BVASSERT(bitind()==0); setByte(grow(1),value); } + void appendUInt16(unsigned value) { BVASSERT(bitind()==0); setUInt16(grow(2),value); } + void appendUInt32(unsigned value) { BVASSERT(bitind()==0); setUInt32(grow(4),value); } + ByteType getByte(size_t ind) const { BVASSERT(ind < size()); return mStart[ind]; } + ByteType getNibble(size_t ind,int hi) const { + ByteType val = getByte(ind); return hi ? (val>>4) : val & 0xf; + } + + unsigned getUInt16(size_t readIndex) const; // 2 byte value + unsigned getUInt32(size_t readIndex) const; // 4 byte value + ByteType readByte(size_t &rp) { return getByte(rp++); } + unsigned readUInt16(size_t &rp); + unsigned readUInt32(size_t &rp); + unsigned readLI(size_t &rp); // GSM8.16 1 or 2 octet length indicator. + + void append(const ByteVector&other); + void append(const BitVector&other); + void append(const ByteType*bytes, unsigned len); + void append(const char*bytes, unsigned len) { append((const ByteType*)bytes,len); } + void appendLI(unsigned len); // GSM8.16 1 or 2 octet length indicator. + void append(const ByteVector*other) { append(*other); } + void append(const BitVector*other) { append(*other); } + + // Set from another ByteVector. + // The other is typically a segment which does not own the memory, ie: + // v1.set(v2.segment()) The other is not a reference because + // the result of segment() is not a variable to which + // the reference operator can be applied. + // This is not really needed any more because the refcnts take care of these cases. + void set(ByteVector other) // That's right. No ampersand. + { +#if BYTEVECTOR_REFCNT + // Its ok, everything will work. + dup(other); +#else + BVASSERT(other.mData == NULL); // Dont use set() in this case. + clear(); + mStart=other.mStart; mEnd=other.mEnd; mAllocEnd = other.mAllocEnd; mBitInd = other.mBitInd; +#endif + } + + // Bit aligned operations. + // The "2" suffix versions specify both byte and bit index in the range 0..7. + // The "R1" suffix versions are identical to the "2" suffix versions, + // but with start bit numbered from 1 like this: 8 | 7 | ... | 1 + // This is just a convenience because all the specs number the bits this weird way. + // The no suffix versions take a bit pos ranging from 0 to 8*size()-1 + + // Get a bit from the specified byte, numbered like this: 0 | 1 | ... | 7 + bool getBit2(size_t byteIndex, unsigned bitIndex) const; + // Get a bit in same bit order, but with start bit numbered from 1 like this: 8 | 7 | ... | 1 + // Many GSM L3 specs specify numbering this way. + bool getBitR1(size_t byteIndex, unsigned bitIndex) const { + return getBit2(byteIndex,8-bitIndex); + } + bool getBit(unsigned bitPos) const { return getBit2(bitPos/8,bitPos%8); } + void setBit2(size_t byteIndex, unsigned bitIndex, unsigned val); + void setBit(unsigned bitPos, unsigned val) { setBit2(bitPos/8,bitPos%8,val); } + + // Set/get fields giving both byte and bit index. + void setField2(size_t byteIndex, size_t bitIndex, uint64_t value,unsigned lengthBits); + uint64_t getField2(size_t byteIndex, size_t bitIndex, unsigned lengthBits) const; + uint64_t getFieldR1(size_t byteIndex, size_t bitIndex, unsigned length) const { + return getField2(byteIndex,8-bitIndex,length); + } + + // Set/get bit field giving bit position treating the entire ByteVector as a string of bits. + void setField(size_t bitPos, uint64_t value, unsigned lengthBits) { + setField2(bitPos/8,bitPos%8,value,lengthBits); + } + uint64_t getField(size_t bitPos, unsigned lengthBits) const { // aka peekField + return getField2(bitPos/8,bitPos%8,lengthBits); + } + + // Identical to getField, but add lengthBits to readIndex. + uint64_t readField(size_t& readIndex, unsigned lengthBits) const { + uint64_t result = getField(readIndex,lengthBits); + readIndex += lengthBits; + return result; + } + + void appendField(uint64_t value,unsigned lengthBits); + // This works for Field<> data types. + void appendField(ItemWithValueAndWidth &item) { + appendField(item.getValue(),item.getWidth()); + } + + std::string str() const; + std::string hexstr() const; +}; + +class ByteVectorTemp : public ByteVector +{ + public: + ByteVectorTemp(ByteType*wstart,ByteType*wend) : ByteVector(Dorky(),wstart,wend) {} + ByteVectorTemp(size_t) { assert(0); } + + // Constructor: A ByteVector whose contents is from a string with optional length specified. + // These cannot be const because the ByteVectorTemp points into the original memory + // and has methods to modify it. + // ByteType is "unsigned char" so we need both signed and unsigned versions to passify C++. + // All the explicit casts are required. This is so brain dead. + ByteVectorTemp(ByteType *wdata,int wlen) : ByteVector(Dorky(),wdata,wdata+wlen) {} + ByteVectorTemp(ByteType *wdata) : ByteVector(Dorky(),wdata,wdata+strlen((char*)wdata)) {} + ByteVectorTemp(char *wdata,int wlen) : ByteVector(Dorky(),(ByteType*)wdata,(ByteType*)wdata+wlen) {} + + ByteVectorTemp(char *wdata) : ByteVector(Dorky(),(ByteType*)wdata,(ByteType*)wdata+strlen((char*)wdata)) {} + ByteVectorTemp(ByteVector& other) : ByteVector(Dorky(),other.begin(),other.begin()+other.size()) {} + + ByteVectorTemp(BitVector &) { assert(0); } +}; + +// Warning: C++ prefers an operator<< that is const to one that is not. +std::ostream& operator<<(std::ostream&os, const ByteVector&vec); +#endif diff --git a/GPRS/CS4.txt b/GPRS/CS4.txt new file mode 100644 index 00000000..adab832f --- /dev/null +++ b/GPRS/CS4.txt @@ -0,0 +1,59 @@ + +SACCH Encoding: +(Parity(0x10004820009ULL,40,224) + Set bits: 0,3,17,23,26,40 + g(D) = 1 + D3 + D17 + D23 + D26 + D40 + +From GSM05.03 sec5.1 +184 bits + Parity: + a) Parity bits: + The block of 184 information bits is protected by 40 extra bits used + for error correction and detection. These bits are added to the 184 bits + according to a shortened binary cyclic code (FIRE code) using the generator + polynomial: + g(D) = (D23 + 1)*(D17 + D3 + 1) + The encoding of the cyclic code is performed in a systematic form, which means that, in GF(2), the polynomial: + d(0)D223 + d(1)D222 +...+d(183)D40 + p(1)D38 +...+p(38)D + p(39) + where {p(0),p(1),...,p(39)} are the parity bits , when divided by g(D) yields a remainder equal to: + 1 + D + D2 +...+ D39. + ) Tail bits + Four tail bits equal to 0 are added to the information and parity bits, the result being a block of 228 bits. + u(k) = d(k) for k= 0,1,...,183 + u(k) = p(k-184) for k = 184,185,...,223 + u(k) = 0 for k = 224,225,226,227 (tail bits) + + +From GSM03.64 sec6.5.5 +CS-4: 431 bits. +---- + a) USF precoding: + The first three bits d(0),d(1),d(2) are block coded into twelve bits u'(0),u'(1),...,u'(11) according to the following + table: + d(0),d(1),d(2) u'(0),u'(1),...,u'(11) + 000 000 000 000 000 + 001 000 011 011 101 + 010 001 101 110 110 + 011 001 110 101 011 + 100 110 100 001 011 + 101 110 111 010 110 + 110 111 001 111 101 + 111 111 010 100 000 + b) Parity bits: + Sixteen parity bits p(0),p(1),...,p(15) are defined in such a way that in GF(2) the binary polynomial: + d(0)D446 +...+ d(430)D16 + p(0)D15 +...+ p(15), when divided by: + D16 + D12 + D5 + 1, yields a remainder equal to: + D15 + D14 + D13 + D12 + D11 + D10 + D9 + D8 + D7 + D6 + D5 + D4 + D3 + D2 + D+1. + The result is a block of 456 coded bits, {c(0),c(1),...,c(455)}: + c(k) = u'(k) for k = 0,1,...,11 + c(k) = d(k-9) for k = 12,13,...,439 + c(k) = p(k-440) for k = 440,441,...,455 + + + 5.1.4.5 Mapping on a burst + The mapping is given by the rule: + e(B,j) = i(B,j) and e(B,59+j) = i(B,57+j) for j = 0,1,...,56 + and + e(B+m,57) = q(2m) and e(B+m,58) = q(2m+1) for m = 0,1,2,3 + where + q(0),q(1),...,q(7) = 0,0,0,1,0,1,1,0 identifies the coding scheme CS-4. diff --git a/GPRS/FEC.cpp b/GPRS/FEC.cpp new file mode 100644 index 00000000..0826d5b5 --- /dev/null +++ b/GPRS/FEC.cpp @@ -0,0 +1,918 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "GPRSInternal.h" +#include "RLCMessages.h" +#include "RLCEngine.h" +#include "FEC.h" +#include "GSMTAPDump.h" +#include "../TransceiverRAD1/Transceiver.h" // For Transceiver::IGPRS +#define FEC_DEBUG 0 + +namespace GPRS { +static BitVector *decodeLowSide(const RxBurst &inBurst, int B, GprsDecoder &decoder, ChannelCodingType *ccPtr); +//static int sFecDebug = 1; + +//static Mutex testlock; Did not help. + +const int GPRSUSFEncoding[8] = { + // from table at GSM05.03 sec 5.1.4.2, specified in octal. + // 3 bits in, 12 bits out. + // Note that the table is defined as encoding bits 0,1,2 of usf, + // which is the post-byte-swapped version, not the original usf. + 00000, // 000 000 000 000 + 00335, // 000 011 011 101 + 01566, // 001 101 110 110 + 01653, // 001 110 101 011 + 06413, // 110 100 001 011 + 06726, // 110 111 010 110 + 07175, // 111 001 111 101 + 07240 // 111 010 100 000 +}; + + +// Do the reverse encoding on usf, and return the reversed usf, +// ie, the returned usf is byte-swapped. +static int decodeUSF(SoftVector &mC) +{ + // TODO: Make this more robust. + // Update: No dont bother, should always be zero anyway. + return (mC.bit(0)<<2) | (mC.bit(6)<<1) | mC.bit(4); +} + +ARFCNManager *PDCHCommon::getRadio() { return mchParent->mchOldFec->getRadio(); } +unsigned PDCHCommon::ARFCN() { return mchParent->mchOldFec->ARFCN(); } +unsigned PDCHCommon::CN() { return mchParent->mchOldFec->CN(); } +unsigned PDCHCommon::TN() { return mchParent->mchOldFec->TN(); } +PDCHL1Uplink *PDCHCommon::uplink() { return mchParent->mchUplink; } +PDCHL1Downlink *PDCHCommon::downlink() { return mchParent->mchDownlink; } +PDCHL1FEC *PDCHCommon::parent() { return mchParent; } +void PDCHL1FEC::debug_test() { /*printf("dt\n"); devassert(*mReservations[3]._vptr != NULL);*/ } +L1Decoder* PDCHCommon::getDecoder() const { return mchParent->mchOldFec->decoder(); } +void PDCHCommon::countGoodFrame() { getDecoder()->countGoodFrame(); } +void PDCHCommon::countBadFrame() { getDecoder()->countBadFrame(); } +float PDCHCommon::FER() const { return getDecoder()->FER(); } + +// Print out the USFs bracketing bsn on either side. +const char *PDCHCommon::getAnsweringUsfText(char *buf,RLCBSN_t bsn) +{ + PDCHL1FEC *pdch = parent(); + sprintf(buf," AnsweringUsf=%d %d [%d] %d %d", + pdch->getUsf(bsn-2),pdch->getUsf(bsn-1), pdch->getUsf(bsn),pdch->getUsf(bsn+1),pdch->getUsf(bsn+2)); + return buf; +} + +// Must return true if ch1 is before ch2. +bool chCompareFunc(PDCHCommon*ch1, PDCHCommon*ch2) +{ + if (ch1->ARFCN() < ch2->ARFCN()) return true; + if (ch1->ARFCN() > ch2->ARFCN()) return false; + if (ch1->TN() < ch2->TN()) return true; + return false; +} + +void PDCHL1FEC::mchStart() { + getRadio()->setSlot(TN(),Transceiver::IGPRS); + // Load up the GPRS filler idle burst tables in the transceiver. + // We could use any consecutive bsn, but lets use ones around the current time + // just to make sure they get through in case someone is triaging somewhere. + // Sending all 12 blocks is 2x overkill because the modulus in Transceiver::setModulus + // for type IGPRS is set the same as type I which is only 26, not 52. + RLCBSN_t bsn = FrameNumber2BSN(gBTS.time().FN()) + 1; + for (int i = 0; i < 12; i++, bsn = bsn + 1) { + GPRSLOG(1) <<"sendIdleFrame"<sendIdleFrame(bsn); + } + mchOldFec->setGPRS(true,this); + debug_test(); +} +void PDCHL1FEC::mchStop() { + getRadio()->setSlot(TN(),Transceiver::I); + mchOldFec->setGPRS(false,NULL); +} + +PDCHL1FEC::PDCHL1FEC(TCHFACCHLogicalChannel *wlogchan) : + PDCHCommon(this), mchOldFec(wlogchan->debugGetL1()), mchLogChan(wlogchan), mchTFIs(gTFIs) +{ + // Warning: These initializations use some of the variables in the init list above. + mchUplink = new PDCHL1Uplink(this); + mchDownlink = new PDCHL1Downlink(this); + Stats.countPDCH++; +} + +std::ostream& operator<<(std::ostream& os, PDCHL1FEC *ch) +{ + os << (ch ? ch->shortId() : "PDCH#(null)"); + return os; +} + +#if 0 +PDCHL1FEC::PDCHL1FEC() + : PDCHCommon(this) +{ + mchUplink = new PDCHL1Uplink(this); + mchDownlink = new PDCHL1Downlink(this); + mchOldFec = NULL; + mchLogChan = NULL; + mchTFIs = gTFIs; + //mchOpen(); +} +// We dont really need to remember the LogicalChannel, but maybe we'll need it in the future. +void PDCHL1FEC::mchOpen(TCHFACCHLogicalChannel *wlogchan) +{ + // setGPRS has two affects: getTCH will consider the channel in-use; + // and bursts start being delivered to us. + // Note that the getTCH function normally doesnt even pay attention to whether + // the channel is 'open' or not; it calls recyclable() that checks timers + // and reuses the chan if they have expired. + mchLogChan = wlogchan; + devassert(mchLogChan->inUseByGPRS()); + mchOldFec = mchLogChan->debugGetL1(); + //mchReady = true; // finally + +} +#endif + +PDCHL1FEC::~PDCHL1FEC() { + gL2MAC.macForgetCh(this); + delete mchUplink; + delete mchDownlink; +} + + +void PDCHL1FEC::mchDump(std::ostream&os, bool verbose) +{ + os << " PDCH"<tfiDump(os); + } +} + +//void PDCHL1Downlink::rollForward() +//{ +// // Calculate the TDMA paramters for the next transmission. +// // This implements GSM 05.02 Clause 7 for the transmit side. +// mchPrevWriteTime = mchNextWriteTime; +// mchTotalBursts++; +// mchNextWriteTime.rollForward(mchMapping.frameMapping(mchTotalBursts),mchMapping.repeatLength()); +//} + +#if FEC_DEBUG +GprsDecoder debugDecoder; +#endif + +// Send the specified BitVector at the specified block time. +void PDCHL1Downlink::transmit(RLCBSN_t bsn, BitVector *mI, const int *qbits, int transceiverflags) +{ + parent()->debug_test(); + // Format the bits into the bursts. + // GSM 05.03 4.1.5, 05.02 5.2.3 + // NO! Dont do a wait here. The MAC serviceloop does this for all channels. + // waitToSend(); // Don't get too far ahead of the clock. + + ARFCNManager *radio = getRadio(); + if (!radio) { + // For some testing, we might not have a radio connected. + // That's OK, as long as we know it. + GLOG(INFO) << "XCCHL1Encoder with no radio, dumping frames"; + return; + } + + int fn = bsn.FN(); + int tn = TN(); + + for (int qi=0,B=0; B<4; B++) { + Time nextWriteTime(fn,tn | transceiverflags); + mchBurst.time(nextWriteTime); + // Copy in the "encrypted" bits, GSM 05.03 4.1.5, 05.02 5.2.3. + //OBJLOG(DEBUG) << "transmit mI["<writeHighSideTx(mchBurst,"GPRS"); + fn++; // This cannot overflow because it is within an RLC block. + } +} + +static GSM::TypeAndOffset frame2GsmTapType(BitVector &frame) +{ + switch (frame.peekField(0,2)) { // Mac control field. + case MACPayloadType::RLCControl: + return TDMA_PACCH; + case MACPayloadType::RLCData: + default: + return TDMA_PDCH; + } +} + +void PDCHL1Downlink::send1Frame(BitVector& frame,ChannelCodingType encoding, bool idle) +{ + if (!idle && gConfig.getBool("Control.GSMTAP.GPRS")) { + // Send to GSMTAP. + gWriteGSMTAP(ARFCN(),TN(),gBSNNext.FN(), + frame2GsmTapType(frame), + false, // not SACCH + false, // this is a downlink + frame); // The data. + } + + switch (encoding) { + case ChannelCodingCS1: + // Process the 184 bit (23 byte) frame, leave result in mI. + //mchCS1Enc.encodeFrame41(frame,0); + //transmit(gBSNNext,mchCS1Enc.mI,qCS1,0); + mchEnc.encodeCS1(frame); + transmit(gBSNNext,mchEnc.mI,qCS1,0); + break; + case ChannelCodingCS4: + //std::cout << "WARNING: Using CS4\n"; + // This did not help the 3105/3101 errors: + //mchCS4Enc.initCS4(); // DEBUG TEST!! Didnt help. + //mchCS4Enc.encodeCS4(frame); // Result left in mI[]. + //transmit(gBSNNext,mchCS4Enc.mI,qCS4,0); + mchEnc.encodeCS4(frame); // Result left in mI[]. + transmit(gBSNNext,mchEnc.mI,qCS4,0); + break; + default: + LOG(ERR) << "unrecognized GPRS channel coding " << (int)encoding; + devassert(0); + } +} + + +// Return true if we send a block on the downlink. +bool PDCHL1Downlink::send1DataFrame( + RLCDownEngine *engdown, + RLCDownlinkDataBlock *block, // block to send. + int makeres, // 0 = no res, 1 = optional res, 2 = required res. + MsgTransactionType mttype, // Type of reservation + unsigned *pcounter) +{ + //ScopedLock lock(testlock); + TBF *tbf = engdown->getTBF(); + if (! setMACFields(block,mchParent,tbf,makeres,mttype,pcounter)) { return false; } + // The rest of the RLC header is already set, but we did not know the tfi + // when we created the RLCDownlinkDataBlocks (because tbf not yet attached) + // so set tfi now that we know. Update 8-2012: Above comment is stale because we + // make the RLCDownlinkBlocks on the fly now. + block->mTFI = tbf->mtTFI; + // block->mPR = 1; // DEBUG test; made no diff. + + tbf->talkedDown(); + + BitVector tobits = block->getBitVector(); // tobits deallocated when this function exits. + if (block->mChannelCoding == 0) { devassert(tobits.size() == 184); } + if (GPRSDebug & 1) { + RLCBlockReservation *res = mchParent->getReservation(gBSNNext); + std::ostringstream sshdr; + block->text(sshdr,false); //block->RLCDownlinkDataBlockHeader::text(sshdr); + ByteVector content(tobits); + GPRSLOG(1) << "send1DataFrame "<mtExpectedAckBSN) + << " "<str() : "") + << LOGVAR2("content",content); + //<< " enc="<mtChannelCoding <<" "<str() <<"\nbits:" <mChannelCoding,0); +#if FEC_DEBUG + BitVector *result = debugDecoder.getResult(); + devassert(result); + devassert(copybits == tobits); + if (result && !(*result == tobits)) { + int diffbit = -1; + char thing[500]; + for (int i = 0; i < (int)result->size(); i++) { + thing[i] = '-'; + if (result->bit(i) != tobits.bit(i)) { + if (diffbit == -1) diffbit = i; + thing[i] = '0' + result->bit(i); + } + } + thing[result->size()] = 0; + GPRSLOG(1) <<"encoding error" <size()) + <<"\n"<mUSF == 0); + BitVector tobits(RLCBlockSizeInBits[ChannelCodingCS1]); + msg->write(tobits); + delete msg; + //mchCS1Enc.encodeFrame41(tobits,0); + //transmit(bsn,mchCS1Enc.mI,qCS1,Transceiver::SET_FILLER_FRAME); + mchEnc.encodeCS1(tobits); + transmit(bsn,mchEnc.mI,qCS1,Transceiver::SET_FILLER_FRAME); +} + +void PDCHL1Downlink::bugFixIdleFrame() +{ + // DEBUG: We are only using this function to fix this problem for now. + if (gFixIdleFrame) { + // For this debug purpose, the mssage is sent on the next frame + // TODO: debug purpose only! This only works for one channel! + //Time tnext(gBSNNext.FN()); + //gBTS.clock().wait(tnext); + } + + // Did we make it in time? + { + Time tnow = gBTS.time(); + int fn = tnow.FN(); + int mfn = (fn / 13); // how many 13-multiframes + int rem = (fn - (mfn*13)); // how many blocks within the last multiframe. + int tbsn = mfn * 3 + ((rem==12) ? 2 : (rem/4)); + GPRSLOG(2) <<"idleframe"<write(mchIdleFrame); + delete dummymsg; + } + send1Frame(mchIdleFrame,ChannelCodingCS1,true); + ***/ +} + +// Return true if we send a block on the downlink. +bool PDCHL1Downlink::send1MsgFrame( + TBF *tbf, // The TBF sending the message, or NULL for an idle frame. + RLCDownlinkMessage *msg, // The message. + int makeres, // 0 = no res, 1 = optional res, 2 = required res. + MsgTransactionType mttype, // Type of reservation + unsigned *pcounter) // If non-null, incremented if a reservation is made. +{ + if (! setMACFields(msg,mchParent,msg->mTBF,makeres,mttype,pcounter)) { + delete msg; // oh well. + return false; // This allows some other tbf to try to use this downlink block. + } + + bool dummy = msg->mMessageType == RLCDownlinkMessage::PacketDownlinkDummyControlBlock; + bool idle = dummy && msg->isMacUnused(); + if (idle && 0 == gConfig.getNum("GPRS.SendIdleFrames")) { + delete msg; // Let the transceiver send an idle frame. + return false; // This return value will not be checked. + } + + if (tbf) { tbf->talkedDown(); } + + // Convert to a BitVector. Messages always use CS-1 encoding. + BitVector tobits(RLCBlockSizeInBits[ChannelCodingCS1]); + msg->write(tobits); + // The possible downlink debug things we want to see are: + // 2: Only non-dummy messages. + // 32: include messages with non-idle MAC header, means mUSF or mSP. + // 1024: all messages including dummy ones. + if (GPRSDebug) { + if ((!dummy && (GPRSDebug&2)) || (!idle && (GPRSDebug&32)) || (GPRSDebug&1024)) { + ByteVector content(tobits); + GPRSLOG(2|32|1024) << "send1MsgFrame "<mTBF<< " "<str() + << " " <str() : ""); + } + } + +#if 0 + // The below is what went out in release 3.0: + if (GPRSDebug & (1|32)) { + //RLCBlockReservation *res = mchParent->getReservation(gBSNNext); + //std::ostringstream ssres; + //if (res) ssres << res; + if (! idle || (GPRSDebug & 1024)) { + //ostringstream bits; + //tobits.hex(bits); + //GPRSLOG(1) << "send1MsgFrame "<mTBF<< " "<str() << "\nbits:"<mTBF<< " "<str() <<" " + << LOGVAR2("content",content); + // This res is unrelated to the message, and confusing, so dont print it: + //<<" "<<(res ? res->str() : ""); + } else if (msg->mUSF) { + GPRSLOG(32) << "send1MsgFrame "<mTBF<< " "<str() <<" "; + //<<" "<<(res ? res->str() : ""); + } + } +#endif + + delete msg; + send1Frame(tobits,ChannelCodingCS1,idle); + return true; +} + + +void PDCHL1Downlink::initBursts(L1FEC *oldfec) +{ + // unused: mchFillerBurst = GSM::TxBurst(GSM::gDummyBurst); // TODO: This may not be correct. + // Should probably be RLC Dummy control message. + + // Set up the training sequence since they'll be the same for all bursts. + // training sequence, GSM 05.02 5.2.3 + // (pat) Set from the BSC color-code from the original GSM channel. + GSM::gTrainingSequence[oldfec->TSC()].copyToSegment(mchBurst,61); +} + + +// Determine CS from the qbits. +ChannelCodingType GprsDecoder::getCS() +{ + // TODO: Make this more robust. + // Currently we only support CS1 or CS4, so just look at the first bit. + if (qbits[0]) { + return ChannelCodingCS1; + } else { + return ChannelCodingCS4; + } +} + +BitVector *GprsDecoder::getResult() +{ + switch (getCS()) { + case ChannelCodingCS4: + return &mD_CS4; + case ChannelCodingCS1: + return &mD; + default: devassert(0); // Others not supported yet. + return NULL; + } +} + +bool GprsDecoder::decodeCS4() +{ + // Incoming data is in SoftVector mC(456) and has already been deinterleaved. + // Convert the SoftVector directly into bits: data + parity: + // The first 12 bits need to be reconverted to 3 bits of usf. + // Yes, they do this even on uplink, where there is no usf 5.03 sec 5.1. + // Parity is run on the remaining 447 (=456-12+3) bits, which consists + // of 424 of useful data, 7 bits of zeros, and 16 bits of parity. + unsigned reverseUsf = decodeUSF(mC); + mDP_CS4.fillField(0,reverseUsf,3); + // We are grubbing into the arrays. TODO: move this into the classes somewhere. + float *in = mC.begin() + 12; + char *out = mDP_CS4.begin() + 3; + for (int i = 12; i < 456; i++) { + *out++ = *in++ > 0.5 ? 1 : 0; + } + BitVector parity(mDP_CS4.segment(440-12+3,16)); + parity.invert(); + unsigned syndrome = mBlockCoder_CS4.syndrome(mDP_CS4); + // Result is in mD_CS4. + return (syndrome==0); +} + +// Process the 184 bit frame, starting at offset, add parity, encode. +// Result is left in mI, representing 4 radio bursts. +void GprsEncoder::encodeCS1(const BitVector &src) +{ + encodeFrame41(src,0); +} + +static BitVector mCcopy; +void GprsEncoder::encodeCS4(const BitVector &src) +{ + //if (sFecDebug) GPRSLOG(1) <<"encodeCS4 src\n"< 0.5) { GPRSLOG(256) << "FEC:"<getReservation(bsn); + int thisUsf = pdch->getUsf(bsn-2); + // If we miss a reservation or usf, print it: + int missedRes = avg>0.4 && !result && (res||thisUsf); + if (missedRes || (GPRSDebug & (result?4:256))) { + std::ostringstream ss; + char buf[30]; + ss <<"writeLowSideRx "<str() : "") + //<getUsf(bsn-1) + //<<" ["<getUsf(bsn)<<"] "<getUsf(bsn+1)<<" "<getUsf(bsn+2) + <<" qbits="<= gBSNNext-1) { + if (cnt++ % 32 == 0) { + GLOG(ERR) << "Incoming burst at frame:"<(51*26))) { + mchNextWriteTime = now; + //mchNextWriteTime.TN(now.TN()); // unneeded? + mchNextWriteTime.rollForward(mchMapping.frameMapping(mchTotalBursts),mchMapping.repeatLength()); + GPRSLOG(2) <<"PDCHL1Downlink RESYNC" << LOGVAR(mchNextWriteTime) << LOGVAR(now); + } +} +#endif + + +// Dispatch an RLC block on this downlink. +// This must run once for every Radio Block (4 TDMA frames or so) sent. +// It should be kept only as far enough ahead of the physical layer so that it never stalls. +// Based on: TCHFACCHL1Encoder::dispatch() +void PDCHL1Downlink::dlService() +{ + // Get right with the system clock. + // NO: mchResync(); + static int debugCntTotal = 0, debugCntDummy = 0; + debugCntTotal++; + if ((GPRSDebug&512) || debugCntTotal % 1024 == 0) { + GPRSLOG(2) << "dlService sent total="<mtGetState() + <<" reqch:"<mtMS->msPacch + << " can not use downlink:"<parent(); + continue; + } + TBFState::type oldstate = tbf->mtGetState(); + + if (tbf->mtServiceDownlink(this)) { + GPRSLOG(2) <<"dlService"<mtMS->msPacch + <<" using ch:"<parent(); + // Move this tbf to end of the list so we may service someone else next time. + // TODO: If the tbf is using extended dynamic uplink, all ganged + // uplink channels are reserved at once, and so we are not sharing + // the other ganged uplinks with other TBFs that want to use them unless + // those TBFs also share this channel. + gL2MAC.macTBFs.erase(itr); + gL2MAC.macTBFs.push_back(tbf); + if (gFixIdleFrame) { bugFixIdleFrame(); } + return; + } + } + + // If nothing else, send a dummy message. + // We have to allocate it because we allocate all messages. + // Note that this message will have the MAC header fields USF and RRBP set by send1MsgFrame. + RLCMsgPacketDownlinkDummyControlBlock *dummymsg = new RLCMsgPacketDownlinkDummyControlBlock(); + send1MsgFrame(NULL,dummymsg,0,MsgTransNone,NULL); + debugCntDummy++; + + if (gFixIdleFrame) { bugFixIdleFrame(); } +} + + +// This is just a pass-through to TFIList. +void PDCHCommon::setTFITBF(int tfi, RLCDir::type dir, TBF *tbf) +{ + mchParent->mchTFIs->setTFITBF(tfi,dir,tbf); +} + +TBF *PDCHCommon::getTFITBF(int tfi, RLCDirType dir) +{ + TBF *tbf = mchParent->mchTFIs->getTFITBF(dir,tfi); + if (tbf == NULL) { + // Somehow we lost track of this tbf. Maybe the timers are set wrong, + // and we dumped it before the MS gave up on it. + GLOG(ERR) << "GPRS Radio Block Data Block with unrecognized tfi: " << tfi << " dir:"<active()); + assert(! decoder()->active()); + // TODO: Wait until an appropriate time, lock everything in sight before doing this. + ARFCNManager*radio = encoder()->getRadio(); + // Connect to the radio now. + mPDTCH.downstream(radio); + mPTCCH.downstream(radio); + mPDIdle.downstream(radio); + } + + void PDCHL1FEC::unsnarf() + { + assert(mlogchan); + ARFCNManager*radio = encoder()->getRadio(); + // TODO: Wait until an appropriate time, lock everything in sight before doing this. + mlogchan->downstream(radio); + // TODO: This is not thread safe. It would not work either, + // because the TRXManager function asserts that nobody got the channel earlier. + // Can we hook the channel inside the logical channel? + gBTS.addTCH(mlogchan); + } +#endif +}; diff --git a/GPRS/FEC.h b/GPRS/FEC.h new file mode 100644 index 00000000..c43013a6 --- /dev/null +++ b/GPRS/FEC.h @@ -0,0 +1,284 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +/**@file GPRS L1 radio channels, from GSM 05.02 and 05.03. */ + +#ifndef GPRSL1FEC_H +#define GPRSL1FEC_H + +#include +#include // for gBTS +#include +#include // for TxBurst +#include // for TCHFACCHLogicalChannel +#include "MAC.h" +using namespace GSM; +namespace GPRS { +class TBF; + +// GSM05.03 sec 5.1.4 re GPRS CS-4 says: 16 bit parity with generator: D16 + D12 + D5 + 1, +static const unsigned long sCS4Generator = (1<<16) + (1<<12) + (1<<5) + 1; + +class PDCHL1FEC; +class PDCHCommon +{ + public: + PDCHL1FEC *mchParent; + + PDCHCommon(PDCHL1FEC*wParent) { mchParent = wParent; } + ARFCNManager *getRadio(); + unsigned TN(); + unsigned ARFCN(); + unsigned CN(); + L1Decoder *getDecoder() const; + float FER() const; + void countGoodFrame(); + void countBadFrame(); + PDCHL1Uplink *uplink(); + PDCHL1Downlink *downlink(); + PDCHL1FEC *parent(); // If called from mchParent, returns itself. + TBF *getTFITBF(int tfi, RLCDirType dir); + void setTFITBF(int tfi, RLCDirType dir, TBF *TBF); + + const char *getAnsweringUsfText(char *buf, RLCBSN_t bsn); // Printable USFs around BSN for debugging + + char *shortId() { // Return a short printable id for this channel. + static char buf[20]; + sprintf(buf,"PDCH#%u:%u",getRadio()->ARFCN(),TN()); + return buf; + } +}; + +// For gprs channels, should we allocate them using getTCH(), +// which returns a TCHFACCHLogicalChannel which we have no use for, +// or should we get dedicated GPRS channels directly from TRXManager, +// which currently does not allow this. +// Answer: We are going to make the logical channels tri-state (inactive, RRactive, GPRSactive), +// and use getTCH to get them. + +// There is one of these classes for each GPRS Data Channel, PDTCH. +// Downstream it attaches to a single Physical channel in L1FEC via mchEncoder and mchDecoder. +// TODO: I did this wrong. This should be for a single ARFCN, but multiple +// upstream/downstream timeslots. +class PDCHL1FEC : + public L1UplinkReservation, + public PDCHCommon, + public USFList +{ + public: + PDCHL1Downlink *mchDownlink; + PDCHL1Uplink *mchUplink; + + L1FEC *mchOldFec; // The GSM TCH channel that this GPRS channel took over; + // it has the channel parameters. + + // Temporary: GPRS will not use anything in this LogicalChannel class, and we dont want + // the extra class hanging around, but currently the only way to dynamically + // allocate physical channels is via the associated logical channel. + TCHFACCHLogicalChannel *mchLogChan; + + public: + // The TFIs are a 5 bit handle for TBFs. The USFs are a 3 bit handle for uplink TBFs. + TFIList *mchTFIs;// Points to the global TFIList. Someday will point to the per-ARFCN TFIList. + + void debug_test(); + + PDCHL1FEC(TCHFACCHLogicalChannel *wlogchan); + + // Release the radio channel. + // GSM will start using it immediately. TODO: Do we want to set a timer + // so it is not reused immediately? + ~PDCHL1FEC(); + + // Attach this GPRS channel to the specified GSM channel. + //void mchOpen(TCHFACCHLogicalChannel *wlogchan); + + void mchStart(); + void mchStop(); + void mchDump(std::ostream&os,bool verbose); + + // Return a description of PDTCH, which is the only one we care about. + // (We dont care about the associated SDCCH, whose frame is used in GPRS for + // continuous timing advance.) + // The packet channel description is the same as channel description except: + // From GSM04.08 10.5.25 table 10.5.2.25a table 10.5.58, and I quote: + // "The Channel type field (5 bit) shall be ignored by the receiver and + // all bits treated as spare. For backward compatibility + // reasons, the sender shall set the spare bits to binary '00001'." + // This doesnt matter in the slightest, because the typeAndOffset would + // have been TCHF_0 whose enum value is 1 anyway. + L3ChannelDescription packetChannelDescription() + { + L1FEC *lf = mchOldFec; + return L3ChannelDescription((TypeAndOffset) 1, lf->TN(), lf->TSC(), lf->ARFCN()); + } +}; +std::ostream& operator<<(std::ostream& os, PDCHL1FEC *ch); + +// For CS-1 decoding, just uses SharedL1Decoder. +// For CS-4 decoding: Uses the SharedL1Decoder through deinterleaving into mC. +class GprsDecoder : public SharedL1Decoder +{ + Parity mBlockCoder_CS4; + BitVector mDP_CS4; + public: + BitVector mD_CS4; + short qbits[8]; + ChannelCodingType getCS(); // Determine CS from the qbits. + BitVector *getResult(); + GprsDecoder() : + mBlockCoder_CS4(sCS4Generator,16,431+16), + mDP_CS4(431+16), + mD_CS4(mDP_CS4.head(424)) + {} + bool decodeCS4(); +}; + +// CS-4 has 431 input data bits, which are always 424 real data bits (53 bytes) +// plus 7 unused bits that are set to 0, to make 431 data bits. +// The first 3 bits are usf encoded to 12 bits, to yield 440 bits. +// Then 16 bit parity bits yields 456 bits. +class GprsEncoder : public SharedL1Encoder +{ + Parity mBlockCoder_CS4; + public: + // Uses SharedL1Encoder::mC for result vector + // Uses SharedL1Encoder::mI for the 4-way interleaved result vector. + BitVector mP_CS4; // alias for parity part of mC + BitVector mU_CS4; // alias for usf part of mC + BitVector mD_CS4; // assembly area for parity. + GprsEncoder() : + SharedL1Encoder(), + mBlockCoder_CS4(sCS4Generator,16,431+16), + mP_CS4(mC.segment(440,16)), + mU_CS4(mC.segment(0,12)), + mD_CS4(mC.segment(12-3,431)) + {} + void encodeCS4(const BitVector&src); + void encodeCS1(const BitVector &src); +}; + + + +class PDCHL1Uplink : public PDCHCommon +{ + protected: +#if GPRS_ENCODER + //SharedL1Decoder mchCS1Dec; + GprsDecoder mchCS14Dec; +#else // This case does not compile yet. + L1Decoder mchCS1Dec; +#endif + + public: + static const RLCDirType mchDir = RLCDir::Up; + // The uplink queue: + // There will typically only be one guy on here, and we could probably dispense + // with the queue, but it is safer to do it this way to avoid thread problems. + // InterthreadQueue template adds "*" so it is really a queue of BitVector* + InterthreadQueue mchUplinkData; + + PDCHL1Uplink(PDCHL1FEC *wParent) : PDCHCommon(wParent) { } + + ~PDCHL1Uplink() {} + + void writeLowSideRx(const RxBurst &inBurst); + + // TODO: This needs to be per-MS. + // void setPhy(float wRSSI, float TimingError) { + // This function is inapplicable to packet channels, which have multiple + // MS listening to the same channel. + //assert(0); + //} +}; + + +// One of these for each PDCH (physical channel), attached to L1FEC. +// Accepts Radio Blocks from anybody. +// Based on SACCHL1Encoder and TCHFACCHL1Encoder +// This does on-demand sending of RLCBlocks down to the physical channel. +// We wait until the last minute so we can encode uplink assignments in the blocks +// as close as possible to the present time. +// TODO: When we support different encodings we may have to base this on L1Encoder directly +// and copy a bunch of routines from XCCHL1Encoder? +static const int qCS1[8] = { 1,1,1,1,1,1,1,1 }; +static const int qCS2[8] = { 1,1,0,0,1,0,0,0 }; // GSM05.03 sec 5.1.2.5 +static const int qCS3[8] = { 0,0,1,0,0,0,0,1 }; // GSM05.03 sec 5.1.3.5 +static const int qCS4[8] = { 0,0,0,1,0,1,1,0 }; // GSM0503 sec5.1.4.5; magically identifies CS-4. + +class PDCHL1Downlink : public PDCHCommon +{ + protected: +#if GPRS_ENCODER + GprsEncoder mchEnc; + //GSM::SharedL1Encoder mchCS1Enc; + //GSM::SharedL1Encoder mchCS4Enc; +#else + GSM::L1Encoder mchCS1Enc; +#endif + TxBurst mchBurst; ///< a preformatted burst template + //TxBurst mchFillerBurst; // unused ///< the filler burst for this channel + int mchTotalBursts; + //GSM::Time mchNextWriteTime, mchPrevWriteTime; + const TDMAMapping& mchMapping; + BitVector mchIdleFrame; + + // The mDownlinkData is used only for control messages, which can stack up. + //InterthreadQueue mchDownlinkMsgQ; + + public: + static const RLCDirType mchDir = RLCDir::Up; + + void initBursts(L1FEC*); + PDCHL1Downlink(PDCHL1FEC *wParent) : + PDCHCommon(wParent), + //mchCS1Enc(ChannelCodingCS1), +#if GPRS_ENCODER + //mchCS4Enc(ChannelCodingCS4), +#endif + mchTotalBursts(0), + mchMapping(wParent->mchOldFec->encoder()->mapping()), + mchIdleFrame((size_t)0) + { + initBursts(wParent->mchOldFec); + } + + ~PDCHL1Downlink() {} + + // Enqueue a downlink message. We dont use this for downlink data - those + // are sent by calling the RLCEngine when this queue is empty. + //void enqueueMsg(RLCDownlinkMessage *); + // The PDCH must feed the radio on time. This is the routine that does it. + void dlService(); + void transmit(RLCBSN_t bsn, BitVector *mI, const int *qbits, int transceiverflags); + //void rollForward(); + //void mchResync(); + int findNeedyUSF(); + + // Send the L2Frame down to the radio now. + void send1Frame(BitVector& frame,ChannelCodingType encoding, bool idle); + bool send1DataFrame(RLCDownEngine *tbfdown, RLCDownlinkDataBlock *block, int makeres,MsgTransactionType mttype,unsigned *pcounter); + bool send1MsgFrame(TBF *tbf,RLCDownlinkMessage *msg, int makeres, MsgTransactionType mttype,unsigned *pcounter); + void sendIdleFrame(RLCBSN_t bsn); + void bugFixIdleFrame(); +}; + +extern bool chCompareFunc(PDCHCommon*ch1, PDCHCommon*ch2); + +}; // namespace GPRS + + +#endif diff --git a/GPRS/GPRSCLI.cpp b/GPRS/GPRSCLI.cpp new file mode 100644 index 00000000..f921c31d --- /dev/null +++ b/GPRS/GPRSCLI.cpp @@ -0,0 +1,696 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "GPRSInternal.h" +#include "TBF.h" +#include "RLCEngine.h" +#include "MAC.h" +#include "FEC.h" +#include "RLCMessages.h" +#include "Interthread.h" +#include "BSSG.h" +#include "LLC.h" +#define strmatch(what,pat) (0==strncmp(what,pat,strlen(pat))) + +using namespace BSSG; +using namespace SGSN; + +#define BAD_NUM_ARGS 1 // See CLI/CLI.cpp + +#define RN_CMD_OPTION(opt) (argi1 && 0==strncmp(argv[1],o,strlen(o)) ? argc--,argv++,1 : 0) + +namespace GPRS { + + +static int gprsMem(int argc, char **argv, int argi, ostream&os) +{ + gMemStats.text(os); + return 0; +} + +static void printChans(bool verbose, ostream&os) +{ + PDCHL1FEC *pch; + RN_MAC_FOR_ALL_PDCH(pch) { pch->mchDump(os,verbose); } +} + +//static int gprsChans(int argc, char **argv, int argi, ostream&os) +//{ +// bool verbose=0; +// while (argi < argc) { +// if (strmatch(argv[argi],"-v")) { verbose = 1; argi++; continue; } +// os << "oops! unrecognized arg:" << argv[argi] << "\n"; +// return 0; +// } +// printChans(verbose,os); +// return 0; +//} + +static int gprsList(int argc, char **argv, int argi, ostream&os) +{ + bool xflag=0, aflag=0, listms=0, listtbf=0, listch=0; + int options = 0; + int id = -1; + while (argi < argc) { + if (strmatch(argv[argi],"ch")) { listch = 1; argi++; continue; } + if (strmatch(argv[argi],"tbf")) { listtbf = 1; argi++; continue; } + if (strmatch(argv[argi],"ms")) { listms = 1; argi++; continue; } + if (strmatch(argv[argi],"-v")) { options |= printVerbose; argi++; continue; } + if (strmatch(argv[argi],"-c")) { options |= printCaps; argi++; continue; } + if (strmatch(argv[argi],"-x")) { xflag = 1; argi++; continue; } + if (strmatch(argv[argi],"-a")) { aflag = 1; argi++; continue; } + if (isdigit(argv[argi][0])) { + if (id >= 0) goto oops; // already found a number + id = atoi(argv[argi]); + argi++; + continue; + } + oops: + os << "oops! unrecognized arg:" << argv[argi] << "\n"; + return 0; + } + + bool all = !(listch|listtbf|listms); + + if (all|listms) { + MSInfo *ms; + for (RListIterator itr(xflag ? gL2MAC.macExpiredMSs : gL2MAC.macMSs); itr.next(ms); ) { + if (id>=0 && (int)ms->msDebugId != id) continue; + if (aflag || ! ms->msDeprecated) { ms->msDump(os,(PrintOptions)options); } + } + } + if (all|listtbf) { + TBF *tbf; + //RN_MAC_FOR_ALL_TBF(tbf) { os << tbf->tbfDump(verbose); } + for (RListIterator itr(xflag ? gL2MAC.macExpiredTBFs : gL2MAC.macTBFs); itr.next(tbf); ) { + // If the id matches a MS, print the TBFs associated with that MS. + if (id>=0 && (int)tbf->mtDebugId != id && (int)tbf->mtMS->msDebugId != id) continue; + os << tbf->tbfDump(options&printVerbose) << endl; + } + } + if (all|listch) { + printChans(options&printVerbose,os); + } + return 0; +} + +static int gprsFree(int argc, char **argv, int argi, ostream&os) +{ + char *what = RN_CMD_ARG; + char *idstr = RN_CMD_ARG; + if (!idstr) return BAD_NUM_ARGS; + int id = atoi(idstr); + MSInfo *ms; TBF *tbf; + if (strmatch(what,"ms")) { + RN_MAC_FOR_ALL_MS(ms) { + if (ms->msDebugId == (unsigned)id) { + os << "Deleting " <msDelete(1); + return 0; + } + } + os << "MS# "<mtDebugId == (unsigned)id) { + os << "Deleting " <mtDelete(1); + return 0; + } + } + os << "TBF# "< itr(gL2MAC.macExpiredMSs); itr.next(ms); ) { + itr.erase(); + delete ms; + } + TBF *tbf; + for (RListIterator itr(gL2MAC.macExpiredTBFs); itr.next(tbf); ) { + itr.erase(); + delete tbf; + } + return 0; +} + +static int gprsStats(int argc, char **argv, int argi, ostream&os) +{ + if (!GPRSConfig::IsEnabled()) { + os << "GPRS is not enabled. See 'GPRS.Enable' option.\n"; + return 0; + } + GSM::Time now = gBTS.time(); + os << "GSM FN=" << now.FN() << " GPRS BSN=" << gBSNNext << "\n"; + os << "Current number of" + << " PDCH=" << gL2MAC.macPDCHs.size() + << " MS=" << gL2MAC.macMSs.size() + << " TBF=" << gL2MAC.macTBFs.size() + << "\n"; + os << "Total number of" + << " PDCH=" << Stats.countPDCH + << " MS=" << Stats.countMSInfo + << " TBF=" << Stats.countTBF + << " RACH=" << Stats.countRach + << "\n"; + os << "Downlink utilization=" << gL2MAC.macDownlinkUtilization << "\n"; + os << LOGVAR2("ServiceLoopTime",Stats.macServiceLoopTime) << "\n"; + return 0; +} + +#if 0 // pinghttp test code not linked in yet. +static int gprsPingHttp(int argc, char **argv, int argi, ostream&os) +{ + if (argi >= argc) { os << "syntax: gprs pinghttp address\n"; return 1; } + //char *addr = argv[argi++]; + os << "pinghttp unimplemented\n"; + return 0; +} +#endif + +// Start the service and allocate a channel. +// This is redundant - can call rach. +static int gprsStart(int argc, char **argv, int argi, ostream&os) +{ + // Start the thread, if not running. + char *modearg = RN_CMD_ARG; + if (modearg) { + gL2MAC.macSingleStepMode = strmatch(modearg,"s"); + if (!gL2MAC.macSingleStepMode) { os << "Unrecognized arg: "<getTBF(); + RLCUpEngine *upengine = new RLCUpEngine(ms,0); + TBF *uptbf = upengine->getTBF(); + uptbf->mtAttach(); // Assigns USF and TFI for the tbf, so we can see it in the messages. + downtbf->mtAttach(); + os << "uplink TBF for messages is:\n"; + os << uptbf->tbfDump(1); + os << "downlink TBF for messages is:\n"; + os << downtbf->tbfDump(1); + + os << "struct RLCMsgPacketUplinkDummyControlBlock : public RLCUplinkMessage\n"; + struct RLCMsgPacketUplinkDummyControlBlock msgupdummy(&rb); + msgupdummy.text(os); + + os << "\n\nstruct RLCMsgPacketDownlinkAckNack : public RLCUplinkMessage\n"; + RLCMsgPacketDownlinkAckNack msgdownacknack(&rb); + msgdownacknack.text(os); + + os << "\n\nstruct RLCMsgPacketControlAcknowledgement : public RLCUplinkMessage\n"; + RLCMsgPacketControlAcknowledgement msgcontrolack(&rb); + msgcontrolack.text(os); + + os << "\n\nstruct RLCMsgPacketResourceRequest : public RLCUplinkMessage\n"; + RLCMsgPacketResourceRequest resreq(&rb); + resreq.text(os); + + os << "\n\nclass RLCMsgPacketAccessReject : public RLCDownlinkMessage\n"; + RLCMsgPacketAccessReject msgreject(downtbf); + msgreject.text(os); + + os << "\n\nclass RLCMsgPacketTBFRelease : public RLCDownlinkMessage\n"; + RLCMsgPacketTBFRelease msgtbfrel(downtbf); + msgtbfrel.text(os); + + os << "\n\nclass RLCMsgPacketUplinkAckNack : public RLCDownlinkMessage\n"; + RLCMsgPacketUplinkAckNack *msgupacknack = upengine->engineUpAckNack(); + msgupacknack->text(os); + delete msgupacknack; + + os << "\n\nstruct RLCMsgPacketDownlinkDummyControlBlock : public RLCDownlinkMessage\n"; + RLCMsgPacketDownlinkDummyControlBlock msgdowndummy; + msgdowndummy.text(os); + + os << "\n\nL3ImmediateAssignment for Single Block Packet Assignment\n"; + //ms->msMode = RROperatingMode::PacketIdle; + sendAssignment(pdch,downtbf, &os); + + os << "\n\nclass RLCMsgPacketDownlinkAssignment : public RLCDownlinkMessage\n"; + //ms->msMode = RROperatingMode::PacketTransfer; + ms->msT3193.set(); + // To force the MS to send the message on PACH we can set T3191 + sendAssignment(pdch,downtbf, &os); + ms->msT3193.reset(); + + os << "\n\nclass RLCMsgPacketUplinkAssignment : public RLCDownlinkMessage\n"; + sendAssignment(pdch,uptbf, &os); + + return 0; +} + +static int gprsTestBSN(int argc, char **argv, int argi, ostream&os) +{ + RLCBSN_t bsn = 0; + int fn = 0; + for (fn = 0; fn < 100; fn++) { + bsn = FrameNumber2BSN(fn); + int fn2 = BSN2FrameNumber(bsn); + os << LOGVAR(fn) < 0; numwords--) { + mcw.writeField(dataword++,16); // Write as a 16 bit word. + } + + // The BitVector now looks like something the MS would send us. + // Go through the steps GPRS code uses to parse the incoming BitVector: + + // The GSM radio queues us an RLCRawBlock: + RLCRawBlock *rawblock = new RLCRawBlock(bsn,vec,0,0,ChannelCodingCS1); + + return rawblock; +} + +#if INTERNAL_SGSN==0 +static int gprsTestUl(int argc, char **argv, int argi, ostream&os) +{ + bool randomize = RN_CMD_OPTION("-r"); + int32_t mytlli = 6789; + MSInfo *ms = new MSInfo(mytlli); + RLCUpEngine *upengine = new RLCUpEngine(ms,0); + TBF *uptbf = upengine->getTBF(); + + InterthreadQueue testQ; + gBSSG.mbsTestQ = &testQ; // Put uplink blocks on our own queue. + + int payloadsize = RLCPayloadSizeInBytes[ChannelCodingCS1]; // 20 bytes / block. + int numbytes = 200; // Max PDU size is 1500; we will test 20*20 == 400 to start. + int numblocks = numbytes / payloadsize; + // The window size is only 64, so we would normlly have to wait for the ack before proceeding. + // If we single stepped the MAC service loop while doing this, the dlservice routine + // would do that. + int bsn; + int TFI = 1; // Use a fake tfi for this test. + for (int j = 0; j < numblocks; j++) { + if (randomize && j < numblocks-16) { + // Goof up the order to see if blocks are reassembled in proper order. + // I left the last few blocks alone to simplify. + bsn = (j & 0xffff0) + ~(j & 0xf); + } else { + bsn = j; + } + int final = bsn == numblocks-1; + RLCRawBlock *rawblock = fakeablock(bsn,TFI,final); + // Raw uplink blocks are dequeued by processRLCUplinkDataBlock which + // sends them to the uplink engine thusly: + RLCUplinkDataBlock *rb = new RLCUplinkDataBlock(rawblock); + //rb->text(os); + // TODO: call processRLCUplinkDataBlock to test tfis + delete rawblock; + uptbf->engineRecvDataBlock(rb,0); + // The final block has E bit set, which makes the RLCUpEngine call sendPDU(), + // which sends the blocks to the BSSG, which puts them on our own queue. + } + gBSSG.mbsTestQ = NULL; // Restore BSSG to normal use. + + // Examine results on the testq.Get the block from the BSSG transmit queue. + if (testQ.size() != 1) { + os << "Unexpected BSSG Queue size="<getTLLI() != expected) { + os << "ULUnitData msg wrong tlli=" << ulmsg->getTLLI() <getHeader(); + expected = BSPDUType::UL_UNITDATA; + if (ulhdr->mbuPDUType != expected) { + os << "ULUnitData msg wrong PDUType=" << ulhdr->mbuPDUType <size(); + expected = numbytes+hdrsize; + if (msgsize != expected) { + os << "BSSG UL UnitData msg wrong size" << LOGVAR(msgsize) << LOGVAR(expected)<<"\n"; + } + } + + // Check the data: + expected = 0; + int offset; + int bads = 0; + for (offset = 0; offset < numbytes; offset += sizeof(short), expected++) { + int got = ulmsg->getUInt16(hdrsize+offset); + if (got != expected) { + os << "BSSG data wrong at "< 10) break; + } + } + + ulmsg->text(os); + /*** + os << "Data from beginning was:\n"; + todo: dump the ulmsg directly. Use << this for ByteVector. + offset = 0; + for (int l = 0; l < 10; l++) { + os << offset << ":"; + for (int i = 0; i < 10; i++, offset+=2) { os <<" " <getUInt16(offset); } + os << "\n"; + } + ***/ + + delete ulmsg; + return 0; +} +#endif + + +static int gprsDebug(int argc, char **argv, int argi, ostream&os) +{ + if (argi < argc) { + int newval = strtol(argv[argi++],NULL,0); // strtol allows hex + gConfig.set("GPRS.Debug",newval); + GPRSSetDebug(newval); + } else if (! GPRSDebug) { + //GPRSSetDebug(3); + } + char buf[100]; sprintf(buf,"GPRSDebug=0x%x\n",GPRSDebug); + os << buf; + return 0; +} + +static int gprsSet(int argc, char **argv, int argi, ostream&os) +{ + char *what = RN_CMD_ARG; + if (!what) { return BAD_NUM_ARGS; } + char *val = RN_CMD_ARG; // may be null. + + if (strmatch(what,"clock") || strmatch(what,"sync")) { + if (val) { gFixSyncUseClock = atoi(val); } + os << LOGVAR(gFixSyncUseClock) << "\n"; + } else if (strmatch(what,"console")) { + if (val) { gLogToConsole = atoi(val); } + os << LOGVAR(gLogToConsole) << "\n"; + } else { + os << "gprs set: unrecognized argument: " << what << "\n"; + } + return 0; +} + +static int gprsStep(int argc, char **argv, int argi, ostream&os) +{ + if (!gL2MAC.macSingleStepMode) { + os << "error: MAC is not in single step mode\n"; + return 0; // disaster would ensue if we accidently started another serviceloop. + } + // We single step it ignoring the global clock, which + // might result in messages from the channel service routines. + ++gBSNNext; + gL2MAC.macServiceLoop(); + return 0; +} + +static int gprsConsole(int argc, char **argv, int argi, ostream&os) +{ + gLogToConsole = !gLogToConsole; // Default: toggle. + if (argi < argc) { gLogToConsole = atoi(argv[argi++]); } + os << "LogToConsole=" << gLogToConsole << "\n"; + return 0; +} + +static struct GprsSubCmds { + const char *name; + int (*subcmd)(int argc, char **argv, int argi,std::ostream&os); + const char *syntax; +} gprsSubCmds[] = { + { "list",gprsList, "list [ms|tbf|ch] [-v] [-x] [-c] [id] # list active objects of specified type;\n\t\t -v => verbose; -c => include MS Capabilities -x => list expired rather than active" }, + { "stat",gprsStats, "stat # Show GPRS statistics" }, + { "free",gprsFree, "free ms|tbf|ch id # Delete something" }, + { "freex",gprsFreeExpired, "freex # free expired ms and tbf structs" }, + { "debug",gprsDebug, "debug [level] # Set debug level; 0 turns off" }, + { "start",gprsStart, "start [step] # Start gprs, optionally in single-step-mode;\n\t\t- can also start by 'gprs rach'" }, + { "stop",gprsStop, "stop [-c] # stop gprs thread and if -c release channels" }, + { "step",gprsStep, "step # single step the MAC service loop (requires 'start step')." }, + { "set",gprsSet, "set name [val] # print and optionally set a variable - see source for names" }, + { "rach",gprsTestRach, "rach # Simulate a RACH, which starts gprs service" }, + { "testmsg",gprsTestMsg, "testmsg # Test message functions" }, + { "testbsn",gprsTestBSN, "testbsn # Test bsn<->frame number functions" }, +#if INTERNAL_SGSN==0 + { "testul",gprsTestUl, "testul [-r] # Send a test PDU through the RLCEngine; -r => randomize order " }, +#endif + { "console",gprsConsole, "console [0|1] # Send messages to console as well as /var/log/OpenBTS.log;\n\t\t (default=1 for debugging)" }, + { "mem",gprsMem, "mem # Memory leak detector - print numbers of structs in use" }, + { "test",gprsTest, "test # Temporary test" }, + // Dont have the source code for pinghttp linked in yet. + //{ "pinghttp",gprsPingHttp,"pinghttp address # Send an http request to address (dont use google.com)" }, + // The "help" command is handled internally by gprsCLI. + { NULL,NULL } +}; + +static void help(std::ostream&os) +{ + os << "gprs sub-commands to control GPRS radio mode. Syntax: gprs subcommand \n"; + os << "subcommands are:\n"; + struct GprsSubCmds *gscp; + for (gscp = gprsSubCmds; gscp->name; gscp++) { + os << "\t" << gscp->syntax; + //if (gcp->arg) os << " " << gcp->arg; + os << "\n"; + } + os << "Notes:\n"; + os << " Downlink utilization averaged over 5 seconds; 1.0 means full utilization;\n"; + os << " 2.0 means downlink requests exceeds available bandwidth by 2x, etc.\n"; +} + +// Set defaults for gprs debugging. +/******* +static void debugdefaults() +{ + static int inited = 0; + if (!inited) { + inited = 1; + GPRSDebug = 3; + gLogToConsole = 1; + } +} +*******/ + +// Should return: SUCCESS (0), BAD_NUM_ARGS(1), BAD_VALUE(2), FAILURE (5) +// but sadly, these are defined in CLI.cpp, so I guess we just return 0. +// Note: argv includes command name so argc==1 implies no args. +int gprsCLI(int argc, char **argv, std::ostream&os) +{ + //debugdefaults(); + ScopedLock lock(gL2MAC.macLock); + + if (argc <= 1) { help(os); return 1; } + int argi = 1; // The number of arguments consumed so far; argv[0] was "gprs" + char *subcmd = argv[argi++]; + + struct GprsSubCmds *gscp; + int status = 0; // maybe success + for (gscp = gprsSubCmds; gscp->name; gscp++) { + if (0 == strcasecmp(subcmd,gscp->name)) { + status = gscp->subcmd(argc,argv,argi,os); + if (status == BAD_NUM_ARGS) { + os << "wrong number of arguments\n"; + } + return status; + //if (gscp->arg == NULL || (argi < argc && 0 == strcasecmp(gscp->arg,argv[argi+1]))) { + //gscp->subcmd(argc,argv,argi + (gscp->arg?1:0),os); + //} + } + } + + if (strcasecmp(subcmd,"help")) { + os << "gprs: unrecognized sub-command: "< +// The user of this file must include these first, to avoid circular .h files: +//#include "GSMConfig.h" // For Time +//#include "GSMCommon.h" // For ChannelType + +// You must not include anything from the GSM directory to avoid circular calls +// that read files out of order, but we need transparent pointers to these classes, +// so they must be defined first. +namespace GSM { + class RxBurst; + class L3RRMessage; + class CCCHLogicalChannel; + class L3RequestReference; + class Time; +}; + +namespace GPRS { + +struct GPRSConfig { + static unsigned GetRAColour(); + static bool IsEnabled(); + static bool sgsnIsInternal(); +}; + +enum ChannelCodingType { // Compression/Coding schemes CS-1 to CS-4 coded as 0-3 + ChannelCodingCS1, + ChannelCodingCS2, + ChannelCodingCS3, + ChannelCodingCS4, + ChannelCodingMax = ChannelCodingCS4, +}; + +// See notes at GPRSCellOptions_t::GPRSCellOptions_t() +struct GPRSCellOptions_t { + unsigned mNMO; + unsigned mT3168Code; // range 0..7 + unsigned mT3192Code; // range 0..7 + unsigned mDRX_TIMER_MAX; + unsigned mACCESS_BURST_TYPE; + unsigned mCONTROL_ACK_TYPE; + unsigned mBS_CV_MAX; + bool mNW_EXT_UTBF; // Extended uplink TBF 44.060 9.3.1b and 9.3.1.3 + GPRSCellOptions_t(); +}; + +extern const int GPRSUSFEncoding[8]; + +extern GPRSCellOptions_t &GPRSGetCellOptions(); + +// The following are not in a class because we dont want to include the entire GPRS class hierarchy. + +// The function by which bursts are delivered to GPRS. +class PDCHL1FEC; +extern void GPRSWriteLowSideRx(const GSM::RxBurst&, PDCHL1FEC*); + + +// The function by which RACH messages are delivered to GPRS. +extern void GPRSProcessRACH(unsigned RA, const GSM::Time &when, float RSSI, float timingError); + +extern int GetPowerAlpha(); +extern int GetPowerGamma(); +extern unsigned GPRSDebug; +extern void GPRSSetDebug(int value); +extern void GPRSNotifyGsmActivity(const char *imsi); + +// Hook into CLI/CLI.cpp:Parser class for GPRS sub-command. +int gprsCLI(int,char**,std::ostream&); +int configGprsChannelsMin(); + +void gprsStart(); // External entry point to start gprs service. + +}; // namespace GPRS + +// GPRSLOG is no longer used outside the GPRS directory. +/**** + * #ifndef GPRSLOG + * #include "Logger.h" + * #define GPRSLOG(level) if (GPRS::GPRSDebug & (level)) \ + * Log(LOG_DEBUG).get() <<"GPRS,"<<(level)<<":" + * #endif +***/ + +#endif diff --git a/GPRS/GPRSInternal.h b/GPRS/GPRSInternal.h new file mode 100644 index 00000000..22e63980 --- /dev/null +++ b/GPRS/GPRSInternal.h @@ -0,0 +1,129 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef GPRSINTERNAL_H +#define GPRSINTERNAL_H +#include +#include "GPRSRLC.h" + + +namespace GPRS { + // FEC.h: + class PDCHL1FEC; + //class PTCCHL1Uplink; + //class PDIdleL1Uplink; + class PDCHL1Uplink; + class PDCHL1Downlink; + + // MAC.h: + class RLCBSN_t; + class MSInfo; + class L2MAC; + class L1UplinkReservation; + class L1USFTable; + + // GPRSL2RLCEngine.h: + class RLCEngine; + + //TBF.h: + class TBF; + + + // RLCEngine.h: + class RLCUpEngine; + class RLCDownEngine; + + // RLCHdr.h: + class RLCDownlinkBlock; + class RLCUplinkDataBlock; + class RLCDownlinkDataBlock; + struct MACDownlinkHeader; + struct MACUplinkHeader; + struct RLCDownlinkControlBlockHeader; + struct RLCUplinkControlBlockHeader; + struct RLCSubBlockHeader; + struct RLCSubBlockTLLI; + struct RLCDownlinkDataBlockHeader; + struct RLCUplinkDataBlockHeader; + + // RLCMessages.h: + class RLCMessage; + class RLCDownlinkMessage; + class RLCUplinkMessage; + struct RLCMsgPacketControlAcknowledgement; + struct RLCMsgElementPacketAckNackDescription; + struct RLCMsgElementChannelRequestDescription; + struct RLCMsgElementRACapabilityValuePart; + struct RLCMsgPacketDownlinkAckNack; + struct RLCMsgPacketResourceRequest; + struct RLCMsgPacketUplinkDummyControlBlock; + struct RLCMsgPacketResourceRequest; + //struct RLCMsgPacketMobileTBFStatus; + class RLCMsgPacketUplinkAssignment; + class RLCMsgPacketDownlinkAssignment; + class RLCMsgPacketAccessReject; + class RLCMsgPacketTBFRelease; + class RLCMsgPacketUplinkAckNack; + + // LLC.h: + class LLCFrame; + + //GPRSL2RLCElements.h:class RLCElement + //GPRSL2RLCElements.h:class RLCAckNackDescription : public RLCElement + //GPRSL2RLCElements.h:class RLCChannelQualityReport : public RLCElement + //GPRSL2RLCElements.h:class RLCPacketTimingAdvance : public RLCElement + //GPRSL2RLCElements.h:class RLCPacketPowerControlParameters : public RLCElement + //GPRSL2RLCElements.h:class RLCChannelRequestDescription : public RLCElement + + // GPRSL2RLCMessages.h: + //class RLCDownlinkMessage; + //class RLCPacketDownlinkAckNack; + //class RLCPacketUplinkAckNack; + //class RLCPacketDownlinkControlBlock; + //class RLCPacketUplinkControlBlock; + + int GetTimingAdvance(float timingError); + int GetPowerAlpha(); + int GetPowerGamma(); + extern unsigned GPRSDebug; + extern unsigned gGprsWatch; + extern std::string fmtfloat2(float num); +}; + +#include "Defines.h" +#include "GSMConfig.h" // For Time +#include "GSMCommon.h" // For ChannelType +#include "GPRSExport.h" +#include "Utils.h" + +#include "Logger.h" +// Redefine GPRSLOG to include the current RLC BSN when called in this directory. +#ifdef GPRSLOG +#undef GPRSLOG +#endif +// 6-18-2012: If someone sets Log.Level to DEBUG, show everything. +#define GPRSLOG(level) if (GPRS::GPRSDebug & (level) || IS_LOG_LEVEL(DEBUG)) \ + _LOG(DEBUG) <<"GPRS"<<(level)<<","< +#include +//#include +#include + +namespace GPRS { +class RLCBSN_t; + +// There are roughly 48 RLC blocks/second. +const unsigned RLCBlocksPerSecond = 48; +// TODO: Get this exact. +const double RLCBlockTime = 1.0 / RLCBlocksPerSecond; // In seconds +const unsigned RLCBlockTimeMsecs = 1000.0 / RLCBlocksPerSecond; // In mseconds + +extern RLCBSN_t FrameNumber2BSN(int fn); // convert frame -> BSN +extern int BSN2FrameNumber(RLCBSN_t bsn); // convert BSN -> frame + +extern RLCBSN_t gBSNNext; // The next Block Sequence Number that will be send on the downlink. + +class RLCDir +{ + public: + // Order matters: The first bit of a GlobalTFI is 0 for up, 1 for down, matching this. + // Either is used as a 'dont-care' dir in function parameters. + enum type { Up, Down, Either }; + static const char *name(int val) + { + switch ((type)val) { + case Up: return "RLCDir::Up"; + case Down: return "RLCDir::Down"; + case Either: return "RLCDir::Either"; + } + return "unknown"; // Makes gcc happy. + } +}; +#define RLCDirType RLCDir::type +std::ostream& operator<<(std::ostream& os, const RLCDir::type &mode); + + +extern unsigned RLCBlockSize[4]; +extern const unsigned RLCBlockSizeBytesMax; +extern int deltaBSN(int bsn1,int bsn2); + +class RLCBSN_t { // Type of radio block sequence numbers. -1 means invalid. + int32_t mValue; + public: + // Number of Radio Blocks in a hyperframe: there are 12 blocks every 52 frames. + // Hyperframe = 2048UL * 26UL * 51UL; + static const unsigned BSNPeriodicity = 2048UL * 26UL * 51UL * 12UL / 52UL; + + // Note: C++ default operator=() is ok. + RLCBSN_t() { mValue = -1; } + RLCBSN_t(int wValue) : mValue(wValue) {} + operator int() const { return mValue; } + + void normalize() { + mValue = mValue % BSNPeriodicity; + if (mValue<0) { mValue += BSNPeriodicity; } + } + + // Return v1 - v2, accounting for wraparound, assuming the values are + // less than half a hyperframe apart. + int BSNdelta(RLCBSN_t v2); + int BSNcompare(RLCBSN_t v2); + bool operator<(RLCBSN_t v2) { return BSNcompare(v2) < 0; } + bool operator<=(RLCBSN_t v2) { return BSNcompare(v2) <= 0; } + bool operator>(RLCBSN_t v2) { return BSNcompare(v2) > 0; } + bool operator>=(RLCBSN_t v2) { return BSNcompare(v2) >= 0; } + RLCBSN_t operator+(RLCBSN_t v2) { + RLCBSN_t result(this->mValue + v2.mValue); result.normalize(); return result; + } + RLCBSN_t operator-(RLCBSN_t v2) { + RLCBSN_t result(this->mValue - v2.mValue); result.normalize(); return result; + } + RLCBSN_t operator+(int32_t v2) { + RLCBSN_t result(this->mValue + v2); result.normalize(); return result; + } + RLCBSN_t operator-(int32_t v2) { + RLCBSN_t result(this->mValue - v2); result.normalize(); return result; + } + RLCBSN_t& operator++() { mValue++; normalize(); return *this; } // prefix + void operator++(int) { mValue++; normalize(); } // postfix + bool valid() const { return mValue >= 0; } + + //static const int invalid = -1; // An invalid value for an RLCBSN_t. + + // Return the bsn that is msecs in the future from a base bsn. + // Used to conveniently specify timeouts in terms of something we track, namely, gBSNNext. + RLCBSN_t addTime(int msecs) + { + // 20msecs is one BSN. + int future = (msecs + RLCBlockTimeMsecs/2) / RLCBlockTimeMsecs; + return *this + future; // The operator+ normalizes it. + } + + // Convert to GSM Frame Number. + unsigned FN() { return (unsigned) BSN2FrameNumber(mValue); } +}; + +// A timer based on BSNs. +// Must not extend greater than BSNPeriodicity/ into the future. +class GprsTimer { + Timeval mWhen; + bool mValid; + public: +#if 1 // TODO: Switch this to use the BSN based timers below, but needs testing. + GprsTimer() : mValid(false) {} + bool valid() const { return mValid; } + void setInvalid() { mValid = false; } + // Two functions for a countdown timer: + void setFuture(int msecs) { mValid = true; mWhen.future(msecs); } + bool expired() const { return mValid && mWhen.passed(); } + // Two functions for a countup timer: + void setNow() { mValid = true; mWhen.now(); } + int elapsed() const { return mValid ? mWhen.elapsed() : 0; } +#else + RLCBSN_t mWhen; // Inits to not valid. + public: + bool valid() { return mWhen.valid(); } + void setInvalid() { mValid = false; } + // setFuture and expired function as a countdown timer. + void setFuture(int msecs) { + mWhen = gBSNNext.addTime(msecs); + GPRSLOG(1) << format("*** setFuture %d bsnnext=%d when=%d\n",msecs,(int)gBSNNext,(int)mWhen); + } + bool expired() { + GPRSLOG(1) << format("*** expired valid=%d bsnnext=%d when=%d togo=%d\n", + valid(),(int)gBSNNext,(int)mWhen,(mWhen-gBSNNext)*RLCBlockTimeMsecs ); + return valid() && gBSNNext > mWhen; + } + // setNow and elapsed function as a countup timer. + void setNow() { mWhen = gBSNNext; } + // Elapsed time from a now() in msecs. + int elapsed() { + return valid() ? (int)(gBSNNext - mWhen) * RLCBlockTimeMsecs : 0; + } +#endif + long remaining() const { return -elapsed(); } +}; + +}; + +#endif diff --git a/GPRS/GPRSTDMA.h b/GPRS/GPRSTDMA.h new file mode 100644 index 00000000..415e0cdc --- /dev/null +++ b/GPRS/GPRSTDMA.h @@ -0,0 +1,69 @@ +/**@file GPRS TDMA parameters. */ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + + +#ifndef GPRSTDMA_H +#define GPRSTDMA_H + + +#include "GSMCommon.h" +#include "GSMTDMA.h" + +namespace GPRS { +// (pat) We wont use this to program the radio right now, and probably never. +// However, it is needed to init a LogicalChannel. The info needs to duplicate +// that for an RR TCH, which is what we are really using. +// Currently we will hook to the existing RR logical channels to get RLC blocks. +// In the future, we probably would not use this either, we would probably modify +// TRXManager to send the entire channel stream directly to us, and then direct +// the packets internally; not use the ARFCNManager::mDemuxTable, +// which is what this TDMA_MAPPING is primarily for. + +/** A macro to save some typing when we set up TDMA maps. */ +// This is copied from ../GSM/GSMTDMA.cpp +#define MAKE_TDMA_MAPPING(NAME,TYPEANDOFFSET,DOWNLINK,UPLINK,ALLOWEDSLOTS,C0ONLY,REPEAT) \ + const GSM::TDMAMapping g##NAME##Mapping(TYPEANDOFFSET,DOWNLINK,UPLINK,ALLOWEDSLOTS,C0ONLY, \ + REPEAT,sizeof(NAME##Frames)/sizeof(unsigned),NAME##Frames) + + +/** PDCH TDMA from GSM 03.64 6.1.2, GSM 05.02 Clause 7 Table 6 of 9. */ +// (pat) This was the orignal; does not look correct to me: +// const unsigned PDCHFrames[] = {0,1,2,3, 4,5,6,7, 8,9,10,11, 13,14,15,16, 16,17,18,19, +// 20,21,22,23, 25,26,27,28, 29,30,31,32, 33,34,35,36, 38,39,40,41, 42,43,44,45, 46,47,48,49}; + +// (pat) This is first line (PDTCH/F PACCH/F) of GSM05.02 clause 7 table 6 of 9 +// Note that we skip over frames 12, 25, 38 and 51, which are used for other purposes. +// TODO: I dont know if we are going to handle the frame mapping this way, +// or let the GPRS code handle all the frames, including the PTCCH (timing advance) slots. +const unsigned PDTCHFFrames[] = {0,1,2,3, 4,5,6,7, 8,9,10,11, 13,14,15,16, 17,18,19,20, + 21,22,23,24, 26,27,28,29, 30,31,32,33, 34,35,36,37, 39,40,41,42, 43,44,45,46, 47,48,49,50 }; +const unsigned PTCCHFrames[] = { 12, 38 }; +const unsigned PDIdleFrames[] = { 25, 51 }; + +// PDCH is the name of the packet data channel, comprised of PDTCH, PTCCH, and 2 idle frames. +MAKE_TDMA_MAPPING(PDTCHF,GSM::TDMA_PDTCHF,true,true,0xff,false,52); // Makes gPDTCHFMapping +MAKE_TDMA_MAPPING(PTCCH,GSM::TDMA_PTCCH,true,false,0xff,false,52); +MAKE_TDMA_MAPPING(PDIdle,GSM::TDMA_PDIDLE,true,false,0xff,false,52); + +const GSM::MappingPair gPDTCHPair(gPDTCHFMapping,gPDTCHFMapping); +const GSM::MappingPair gPTCCHPair(gPTCCHMapping); + + +}; // namespace GPRS + +#endif diff --git a/GPRS/MAC.cpp b/GPRS/MAC.cpp new file mode 100644 index 00000000..ab63f006 --- /dev/null +++ b/GPRS/MAC.cpp @@ -0,0 +1,2570 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +//#include +#include "GPRSInternal.h" +#include "GSMCommon.h" +#include "GSML3RRMessages.h" // for L3RRMessage +#include "GSML3RRElements.h" // for L3RequestReference +#define MAC_IMPLEMENTATION 1 +#include "MAC.h" +#include "FEC.h" +#include "TBF.h" +#include "RLCEngine.h" +#include "RLCMessages.h" +#if INTERNAL_SGSN==0 +#include "BSSG.h" +#endif +#include "Ggsn.h" // For GgsnInit + +#include + +extern bool gLogToConsole; + +namespace GPRS { + +struct TFIList *gTFIs; +RLCBSN_t gBSNNext = 0; // The next Block Sequence Number that will be sent on the downlink. +RLCBSN_t gBSNPrev = 0; +L2MAC gL2MAC; +Stats_t Stats; +static unsigned ChIdleCounter = 0; +static unsigned ChCongestionCounter = 0; +int ExtraClockDelay = 0; + +bool gFixTFIBug = 1; // Default bug fix on. +bool gFixSyncUseClock = 0; +int gFixIdleFrame = 0; // Default bug fix off now +int gFixDRX = 1; // Assume DRX mode if MS does not respond after first try. +bool gFixIAUsePoll = 1;// Default on. +// 6-28-2012: The ConvertForeignTLLI definitively does not work if done every time. +// The multitech modem appears to need this conversion in a special case, fixed in sgsn. +bool gFixConvertForeignTLLI = 0; // Was only needed for external SGSN, which was sending bad TLLIs. + +// This is the downlink queue from the Sgsn. +static InterthreadQueue2 sgsnDownlinkQueue; + +// Extended Dynamic Uplink TBF support. +// If we grant a USF for an uplink TBF with more uplink TNs than downlink TNs, +// first we must grant the same USF for every downlink channel for this TBF, +// and second we must not grant a USF to any other TBF for the uplink +// channels belonging to this TBF. +// This is per-ARFCN, but we pre-sort the channels so that we process one ARFCN +// at a time, and the timeslots in order from 0..7. The multislot configuration +// insures that we will see the downlink channel before we see the uplink one. +struct ExtDyn { + int sCurrentCN; + unsigned sReservedUplinkSlots; + + bool isUplinkReserved(PDCHL1FEC *pdch) { + return sReservedUplinkSlots & (1<TN()); + } + + void reserveUplink(PDCHL1Uplink *up) { + sReservedUplinkSlots |= (1<TN()); + } + + void edReset() { + sReservedUplinkSlots = 0; + sCurrentCN = -1; + } + + // We process one ARFCN at a time in order, so we only need 8 bits of memory. + // Reset it each time we start a new ARFCN. + void edSetCn(int cn) { + if (cn != sCurrentCN) { + sReservedUplinkSlots = 0; + sCurrentCN = cn; + } + } +} extDyn; + +// Get the number quietly. +// Use this for debug options we dont need the user to see. +int configGetNumQ(const char *name, int defaultvalue) +{ + if (gConfig.defines(name)) { + //return gConfig.getNum(name,defaultvalue); + const char *strval = gConfig.getStr(name).c_str(); + return strtol(strval,NULL,0); // strtol allows hex + } else { + return defaultvalue; + } +} + +// Dont bother with a fancy specification (eg: 2x4) because we are going +// to dynamically allocate channels soon. +int configGprsChannelsMinCn() { return gConfig.getNum("GPRS.Channels.Min.CN"); } +int configGprsChannelsMinC0() { return gConfig.getNum("GPRS.Channels.Min.C0"); } +int configGprsChannelsMin() { return configGprsChannelsMinC0() + configGprsChannelsMinCn(); } +#if GPRS_CHANNELS_MAX_SUPPORTED + // We are currently doing only static assignment, so take this out for now. +int configGprsChannelsMax() { return gConfig.getNum("GPRS.Channels.Max"); } +#endif +int configGprsMultislotMaxUplink() { return gConfig.getNum("GPRS.Multislot.Max.Uplink"); } +int configGprsMultislotMaxDownlink() { return gConfig.getNum("GPRS.Multislot.Max.Downlink"); } + +//struct GPRSConfig GPRSConfig; not needed. +unsigned GPRSDebug = 0; +unsigned gGprsWatch = 0; + +void GPRSSetDebug(int value) +{ + GPRSDebug = value; + if (GPRSDebug) { + // Dont change these so that we can test the normal use. + } +} + +bool GPRSConfig::IsEnabled() +{ + + // BEGINCONFIG + // 'GPRS.Enable',1,0,0,'Enable GPRS service: 0 or 1. If enabled, GPRS service is advertised in the C0T0 beacon, and GPRS service may be started on demand. See also GPRS.Channels.*' + // ENDCONFIG + if (gConfig.getNum("GPRS.Enable")) return true; + return false; // nope +} + +bool GPRSConfig::sgsnIsInternal() +{ + // 2-2012: I added an extra sql parameter GPRS.SGSN.External that you must set to use the + // external sgsn, in order to avoid people with existing sql from using it by accident. + const char *sql_sgsn_host = "GPRS.SGSN.Host"; + const char *sql_sgsn_external = "GPRS.SGSN.External"; + if (!gConfig.defines(sql_sgsn_host)) return true; + if (!gConfig.defines(sql_sgsn_external)) return true; + if (0 == gConfig.getNum(sql_sgsn_external)) return true; + return false; // Use external SGSN.Host +} + +// (mike) pretty sure this function is not used anywhere... +unsigned GPRSConfig::GetRAColour() +{ + // BEGINCONFIG + // 'GPRS.RA_COLOUR',0,0,0,'GPRS Routing Area Color as advertised in the C0T0 beacon' + // ENDCONFIG + if (gConfig.defines("GPRS.RA_COLOUR")) { + return gConfig.getNum("GPRS.RA_COLOUR"); + } + return 0; +} + +// GSM04.60 12.24. The T3192 code is placed in System Information 13, GSM04.08. +// The code specifies one of the following values in msec. +static unsigned T3192Codes[8] = { + 500, 1000, 1500, 0, 80, 120, 160, 200, +}; + +// 3GPP 04.60 12.24 GPRS Cell Options Notes: +// NMO is Network Mode of Operation: See GSM 03.60 6.3.3.1 +// Network Mode of Operation II (IE value 1) is the most brain-dead, +// no dual-transfer-mode, the GPRS attached MS uses CCCH. +// 4-24-2012: I am testing NMO 1 to be used in UMTS, however we cannot use +// it in GPRS until we support paging for CS calls on the GPRS data channel. +// BEGINCONFIG +// 'GPRS.CellOptions.T3168Code',5,1,0,'Timer 3168 in the MS controls the wait time after sending a Packet Resource Request to initiate a TBF before giving up or reattempting a Packet Access Procedure, which may imply sending a new RACH. This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. See GSM 04.60 12.24. Range 0..7 to represent 0.5sec to 4sec in 0.5sec steps.' +// 'GPRS.CellOptions.T3192Code',0,1,0,'Timer 3192 in the MS specifies the time MS continues to listen on PDCH after all downlink TBFs are finished, and is used to reduce unnecessary RACH traffic. This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. The value must be one of the codes described in GSM 04.60 12.24. Value 0 implies 500msec.' +// ENDCONFIG +// DRX_TIMER irrelevant since we dont use DRX mode at all. +// ACCESS_BURST_TYPE 0 => use 8 bit format of Packet Channel Request Message, +// (that is, if we were using PRACH, which we are not.) +// CONTROL_ACK_TYPE 1 >= default format for Packet Control Acknowledgement is RLC/MAC block, +// ie, not special. +// BS_CV_MAX is the max number of RLC blocks used by any RLC message. +// NW_EXT_UTBF is extended uplink TBF mode: 44.060 9.3.1b and 9.3.1.3 +GPRSCellOptions_t::GPRSCellOptions_t() : + mNMO(gConfig.getNum("GPRS.NMO")-1), // IE value is spec NMO - 1. + //mNMO(1), // Dont let customers change this. + mT3168Code(gConfig.getNum("GPRS.CellOptions.T3168Code")), // T3168Code 5 => 5000msec + mT3192Code(gConfig.getNum("GPRS.CellOptions.T3192Code")), // T3192Code 0 => 500msec. + // Took this out of the gConfig options. + // mDRX_TIMER_MAX(gConfig.getNum("GPRS.CellOptions.DRX_TIMER_MAX",0)), + mDRX_TIMER_MAX(7), // It is actually non-drx timer max. + // The MS uses the min of this and a param sent in gprs attach. + mACCESS_BURST_TYPE(0), + mCONTROL_ACK_TYPE(1), // Packet Control Acknowledgement is an RLC/BLOCK, not a RACH burst. + mBS_CV_MAX(1), // This is determined by the system, not the user. + mNW_EXT_UTBF(gConfig.getNum("GPRS.Uplink.Persist") > 0) +{ + // Sanity check some values. + if (RN_BOUND(mNMO,0,2) != mNMO) { + GLOG(ERR) << "NMO [Network Mode of Operation] must be 1,2,3"; // we subtracted 1 + mNMO = 1; + } + if (RN_BOUND(mT3168Code,0,7) != mT3168Code) { + GLOG(ERR) << "mT3168 value " << mT3168Code << " must be in range 0..7"; + mT3168Code = 1; + } + if (RN_BOUND(mT3192Code,0,7) != mT3192Code) { + GLOG(ERR) << "mT3192 value " << mT3192Code << " must be in range 0..7"; + mT3192Code = 0; + } +} + +// This function probes the config file the first time it is called. +GPRSCellOptions_t &GPRSGetCellOptions() +{ + static GPRSCellOptions_t *GPRSCellOptions = NULL; + if (!GPRSCellOptions) GPRSCellOptions = new GPRSCellOptions_t(); + return *GPRSCellOptions; +} + +USFList::USFList() +{ + //mRandomUSF=0; + for (int i=0;imuDeadTime.valid()) { + if (info->muDeadTime.expired()) { + info->muDeadTime.setInvalid(); // Can use the USF now. + info->muMS = 0; + } + return NULL; + } + return info->muMS; +} + +// Find or set a USF for this ms. Return -1 on failure. +int USFList::allocateUSF(MSInfo *ms) +{ + int usf, freeusf = -1; + // We want to run through the whole list to see if there was a USF for this MS previously. + for (usf = USFMIN; usf <= USFMAX; usf++) { + if (mlUSFs[usf].muMS == ms) { + // The MS can reuse its own USF. The muDeadTime is how long other MS cannot use it. + mlUSFs[usf].muDeadTime.setInvalid(); // USF is back in use. + return usf; + } + if (freeusf == -1 && getUSFMS(usf) == NULL) { freeusf = usf; } + } + if (freeusf >= USFMIN) { + mlUSFs[freeusf].muMS = ms; + return freeusf; + } + return -1; // All in use. +} + +// Free the USFs assigned to this ms. +// We could have saved the usf number in the MS to avoid searching for it, +// but the list is short and this does not happen often. +// If wReserve, the tbf died, so we must reserve the USF resource for 5 seconds. +// Note that the same MS can reuse its own reserved USF if it positively re-establishes communication. +int USFList::freeUSF(MSInfo *ms, bool wReserve) +{ + for (int usf = USFMIN; usf <= USFMAX; usf++) { + UsfInfo *info = &mlUSFs[usf]; + if (info->muMS == ms) { + if (wReserve) { + info->muDeadTime.setFuture(5000); // This is 5 seconds, not programmable. + } else { + info->muMS = 0; + info->muDeadTime.setInvalid(); // Make sure usf is reusable. + } + return usf; + } + } + return 0; +} + +// Return any USF that is assigned to an MS. +// Used when there is nothing else to do with the uplink channel. +// Not really random, the MS are serviced round-robin. +// We dont want to just give the USF to the MS with the oldest response time, +// because it could be out of range, or whatever, and we may never hear from it. +//int USFList::getRandomUSF() +//{ +// for (int i = USFMIN; i <= USFMAX; i++) { +// if (++mRandomUSF > USFMAX) { mRandomUSF = USFMIN; } +// if (mlUSFs[mRandomUSF]) { return mRandomUSF; } +// } +// return 0; // There are none. +//} + +void USFList::usfDump(std::ostream&os) +{ + int i; + os << "USFList=("; + for (i = USFMIN; i <= USFMAX; i++) { + os << " " << i << "=>"; + MSInfo *ms = mlUSFs[i].muMS; + if (ms) { + os << ms; + long remaining = mlUSFs[i].muDeadTime.remaining(); + if (remaining) { os << LOGVAR(remaining); } + } else { + os << "free"; + } + } + //os << RN_PRETTY_TEXT1(mRandomUSF); + os << ")\n"; +} + +int USFList::getUsf(unsigned upbsn) // bsn from the uplink burst. +{ + // GSM 05.02 6.3.2.2.1: The USF field in downlink block N signals + // that uplink block (N+1) is assigned to that MS. + // 7-6-2012 update: This is also used for extended uplink TBF mode now. + unsigned downbsn = upbsn-1; + return (sRememberUsfBsn[downbsn%32] == downbsn ? ((signed)sRememberUsf[downbsn%32]) : -1); +} + +void USFList::setUsf(unsigned downusf, unsigned downbsn) // Save usf for current downlink burst. +{ + sRememberUsf[downbsn%32] = downusf; + sRememberUsfBsn[downbsn%32] = downbsn; +} + + +void L2MAC::macConfigInit() +{ + GPRSSetDebug(configGetNumQ("GPRS.Debug",0)); + gGprsWatch = configGetNumQ("GPRS.WATCH",0); + gLogToConsole = configGetNumQ("Log.ToConsole",0); + + GPRSCellOptions_t& gco = GPRSGetCellOptions(); + // BEGINCONFIG + // 'GPRS.Counters.N3101',20,0,0,'Counts unused USF responses to detect nonresponsive MS. Should be > 8. See GSM04.60 sec 13.' + // 'GPRS.Counters.N3103',8,0,0,'Counts ACK/NACK attempts to detect nonresponsive MS. See GSM04.60 sec 13.' + // 'GPRS.Counters.N3105',12,0,0,'Counts unused RRBP responses to detect nonresponsive MS. See GSM04.60 sec 13.' + // 'GPRS.Timers.T3169',5000,0,0,'Nonresponsive uplink tbf timer, in msecs. See GSM04.60 sec 13' + // 'GPRS.Timers.T3191',5000,0,0,'Nonresponsive downlink tbf timer, in msecs. See GSM04.60 sec 13' + // 'GPRS.Timers.T3193',0,0,0,'Timer T3193 (in msecs) in the base station corresponds to T3192 in the MS, which is set by GPRS.CellOptions.T3192Code. The T3193 value should be slightly longer than that specified by the T3192Code. If 0, the BTS will fill in a default value based on T3192Code.' + // 'GPRS.Timers.T3195',5000,0,0, 'Nonresponsive MS timer, in msecs. See GSM04.60 sec 13' + // ENDCONFIG + macN3101Max = gConfig.getNum("GPRS.Counters.N3101"); + macN3103Max = gConfig.getNum("GPRS.Counters.N3103"); + macN3105Max = gConfig.getNum("GPRS.Counters.N3105"); + macT3169Value = gConfig.getNum("GPRS.Timers.T3169"); // in msecs. + macT3191Value = gConfig.getNum("GPRS.Timers.T3191"); // in msecs. + macT3193Value = gConfig.getNum("GPRS.Timers.T3193"); // fixed below. + macT3195Value = gConfig.getNum("GPRS.Timers.T3195"); // in msecs. + macT3168Value = (gco.mT3168Code + 1) * 500; // in msecs + //macTNonResponsive = gConfig.getNUM("GPRS.Timers.MS.NonResponsive") // in msecs + + // Spec says T3193 (in network) should be longer than T3192 (in MS). + // If unspecified, add 50 msecs to T3192. + // TODO: We should really have a dead zone near the end of this timer + // where we just leave the MS alone, but it doesnt hurt anything, + // just wastes airwaves trying to contact the MS in the wrong mode. + unsigned T3192Value = T3192Codes[gco.mT3192Code]; + if (macT3193Value == 0) { + macT3193Value = T3192Value + 50; // Add some extra msecs. + } + if (macT3193Value < T3192Value) { + static unsigned lastT3193Value = 0, lastT3192Value = 0; + if (lastT3193Value != macT3193Value || lastT3192Value != T3192Value) { + GLOG(ERR) << "T3193 value " << macT3193Value << " should be longer than" + " T3192 value " << T3192Value << + " (T3192code=" << gco.mT3192Code << ")"; + lastT3193Value = macT3193Value; + lastT3192Value = T3192Value; + } + } + static bool firsttime = true; + if (firsttime) { + GLOG(INFO) << "Note: GPRS T3192 = " << T3192Value; + GLOG(INFO) << "Note: GPRS T3193 = " << macT3193Value; + firsttime = 0; + } + + // These 'timers' are in seconds converted to RLC block counts. + // Note: We must keep the MS structure around long enough to handle downlink SGSN + // message responses to an uplink message, but this period is short. + // If either the MS or SGSN wants to start a new TBF, they send a new TLLI + // to create a new MSInfo struct. However, in the future we may use the statistics + // gathered from a previous TBF to determine the ChannelCoding (speed) to use + // for future TBFs. + // BEGINCONFIG + // 'GPRS.Timers.MS.Idle',600,0,0,'How long an MS is idle before the BTS forgets about it.' + // 'GPRS.Timers.Channels.Idle',6000,0,0,'How long a GPRS channel is idle before being returned to the pool of channels. Also depends on Channels.Min. Currently the channel cannot be returned to the pool while there is any GPRS activity on any channel.' + // 'GPRS.Channels.Congestion.Timer',60,0,0,'How long GPRS congestion exceeds the Congestion.Threshold before we attempt to allocate another channel for GPRS' + // 'GPRS.Channels.Congestion.Threshold',200,0,0,'The GPRS channel is considered congested if the desired bandwidth exceeds available bandwidth by this amount, specified in percent.' + // ENDCONFIG + macMSIdleMax = gConfig.getNum("GPRS.Timers.MS.Idle") * RLCBlocksPerSecond; + macChIdleMax = gConfig.getNum("GPRS.Timers.Channels.Idle") * RLCBlocksPerSecond; + macChCongestionMax = gConfig.getNum("GPRS.Channels.Congestion.Timer") * RLCBlocksPerSecond; + // database number specified in percent: + macChCongestionThreshold = gConfig.getNum("GPRS.Channels.Congestion.Threshold") / 100.0; + macDownlinkPersist = gConfig.getNum("GPRS.Downlink.Persist"); + static bool thisMessageHasBeenPrinted = false; + if (macDownlinkPersist && !thisMessageHasBeenPrinted) { + thisMessageHasBeenPrinted = true; + LOG(ALERT) << "GPRS.Downlink.Persist is not implemented and config value should be 0!"; + } + macDownlinkKeepAlive = gConfig.getNum("GPRS.Downlink.KeepAlive"); + macUplinkPersist = gConfig.getNum("GPRS.Uplink.Persist"); + macUplinkKeepAlive = gConfig.getNum("GPRS.Uplink.KeepAlive"); + + if (macSingleStepMode) { + // Set these to maximum values so we can single step the service loop + // without these timers going off. + macMSIdleMax = macChIdleMax = 0x7fffffff; + macT3191Value = macT3193Value = 0x7fffffff; + } + + gFixIdleFrame = configGetNumQ("GPRS.FixIdleFrame",gFixIdleFrame); + gFixTFIBug = configGetNumQ("GPRS.FixTFIBug",gFixTFIBug); // Default bug fix on. + gFixDRX = configGetNumQ("GPRS.FixDRX",(int)gFixDRX); // Default to 4 sendAssignment tries. + gFixIAUsePoll = configGetNumQ("GPRS.FixIAUsePoll",gFixIAUsePoll); + gFixConvertForeignTLLI = configGetNumQ("GPRS.FixForeignTlli",gFixConvertForeignTLLI); +} + +void L2MAC::macAddTBF(TBF *tbf) { + macTBFs.push_back(tbf); // Usually already locked, so lock is recursive + //macTBFs.push_back_safely(tbf); // Usually already locked, so lock is recursive +} + +void L2MAC::macForgetTBF(TBF *tbf, bool forever) +{ + GPRSLOG(2) << "forget "<tbfid(0); + macTBFs.remove(tbf); + // lock unnecessary, using macLock now: + //macTBFs.remove_safely(tbf); // Usually already locked, so lock is recursive + //ScopedLock lock2(macExpiredTBFs.mListLock); + if (forever) { + macExpiredTBFs.remove(tbf); // Just in case it was on this list. + GPRSLOG(2) << "delete ",tbf->tbfid(0); + delete tbf; + return; + } + macExpiredTBFs.push_front(tbf); + unsigned keepExpired = gConfig.getNum("GPRS.TBF.KeepExpiredCount"); + while (macExpiredTBFs.size() > keepExpired) { + TBF *tbf2 = macExpiredTBFs.back(); + macExpiredTBFs.pop_back(); // returns void, the nitwits. + GPRSLOG(2) << "delete ",tbf2->tbfid(0); + delete tbf2; + } +} + +void L2MAC::macAddMS(MSInfo *ms) { macMSs.push_back(ms); } + +void L2MAC::macForgetMS(MSInfo *ms, bool forever) +{ + macMSs.remove(ms); + // lock unnecessary, using macLock now: + //macMSs.remove_safely(ms); // Usually already locked, so lock is recursive + //ScopedLock lock2(macExpiredMSs.mListLock); + if (forever) { + macExpiredMSs.remove(ms); // Just in case it was on this list. + delete ms; + return; + } + macExpiredMSs.push_front(ms); + unsigned keepExpired = gConfig.getNum("GPRS.MS.KeepExpiredCount"); + while (macExpiredMSs.size() > keepExpired) { + MSInfo *ms2 = macExpiredMSs.back(); + macExpiredMSs.pop_back(); + delete ms2; + } +} + +// The MS list will be short. Just look through linearly. +MSInfo *L2MAC::macFindMSByTlli(uint32_t tlli, int create /*=0*/) +{ + MSInfo *ms; + RN_MAC_FOR_ALL_MS(ms) { + // When the MS performs a Detach procedure, it will change its existing tlli + // from a local tlli to a foreign tlli. Instead of having the SGSN inform us + // of these events, just ignore whether the tlli is local or foreign. + if (tlliEq(ms->msTlli, tlli) || tlliEq(ms->msOldTlli,tlli)) { + // This is very important. + // If the SGSN looks up an MS by TLLI it is because it is about to use it, + // and we dont want it to disappear before it does. + ms->msIdleCounter = 0; + return ms; + } + } + if (! create) { return NULL; } + ms = new MSInfo(tlli); + GPRSLOG(1) << "New MS:"<CN() == 0) { cnt++; } + } + return cnt; +} + +static void macAddOneChannel(TCHFACCHLogicalChannel *lchan) +{ + PDCHL1FEC *pch = new PDCHL1FEC(lchan); + gL2MAC.macPDCHs.push_back(pch); + gL2MAC.macPacchs.clear(); // Must rebuild the pacch list. + pch->mchStart(); + GLOG(INFO) << "GPRS AddChannel " << pch << " total active=" + <= configGprsChannelsMax()) { + return false; + } +#endif + + if (! macActiveChannels()) { + // When you first start the BTS you will not be able to allocate until some + // timer expires, which I measured as 4 seconds. + // So dont bother reporting until after 5 seconds. + time_t now; time(&now); + if (now - macStartTime < 5) { return false; } + // And dont print this message any more often than 10 seconds. + static time_t lastMessageTime = 0; + if (now - lastMessageTime < 10) { return false; } + GLOG(INFO) << "GPRS: Unable to allocate channel, all are busy"; + time(&lastMessageTime); + return false; + } + + return true; +} + +// Try to free a GPRS channel, returning it to GSM RR use. +// 5-24-2012: We must not free the channel that is our PACCH. +bool L2MAC::macFreeChannel() +{ + ChIdleCounter = ChCongestionCounter = 0; + if (macActiveChannels() <= configGprsChannelsMin()) { return false; } + + //PDCHL1FEC *pdch = gL2MAC.macPickChannel(); // pick the least busy channel; + PDCHL1FEC *pdch = gL2MAC.macPDCHs.back(); + GLOG(INFO) << "GPRS freeing channel" << pdch; + GPRSLOG(1) << "GPRS freeing channel " << pdch; + delete pdch; // Among other things, removes from macPDCHs before freeing it. + macPacchs.clear(); // Must rebuild the pacch list. + return true; +} + + +// This is called during channel destruction to clean up any references to the channel. +// The channel better not be in use. +// Delete any tbfs using the channel. Detach any MSs using the channel. +// Remove the channel from the list in use by GPRS. +void L2MAC::macForgetCh(PDCHL1FEC*pch) +{ + pch->mchStop(); // TODO: This should set a timer before the channel goes back to RR use. + + // Delete any tbfs that used this channel. + // Warning: delete tbf removes the tbf from the list we are traversing, so be careful. + TBF *tbf; + RN_MAC_FOR_ALL_TBF(tbf) { + if (tbf->canUseDownlink(pch->downlink()) || tbf->canUseUplink(pch->uplink())) { + tbf->mtCancel(MSStopCause::ShutDown,TbfNoRetry); // Deletes tbf. + } + } + // Detach any ms that might be using this channel. + // FIXME: This needs work. We need to send the MS new channel assignments which + // is a long procedure and may need a new state. + MSInfo *ms; + RN_MAC_FOR_ALL_MS(ms) { + if (ms->canUseDownlink(pch->downlink()) || ms->canUseUplink(pch->uplink())) { + ms->msReassignChannels(); + } + // TODO: Without its PACCH the MSInfo is useless, should kill it off? + if (ms->msPacch == pch) { ms->msPacch = NULL; } + } + macPDCHs.remove(pch); + macPacchs.clear(); // Must rebuild the pacch list. + GPRSLOG(1) << "macForgetChannel, remaining="<ARFCN()) { + resultmask |= 1 << ch->TN(); + } + } + return resultmask; +} + +PDCHL1FEC *L2MAC::macFindChannel(unsigned arfcn, unsigned tn) +{ + PDCHL1FEC *ch; + RN_MAC_FOR_ALL_PDCH(ch) { + if (arfcn == ch->ARFCN() && tn == ch->TN()) { return ch; } + } + return NULL; +} + +static void dumpPdch() +{ + + PDCHL1FEC *ch; + printf("PDCHs=%d:",gL2MAC.macPDCHs.size()); + RN_MAC_FOR_ALL_PDCH(ch) { printf(" %s",ch->shortId()); } + printf("\n"); + printf("PACCHs=%d",gL2MAC.macPacchs.size()); + RN_MAC_FOR_ALL_PACCH(ch) { printf(" %s",ch->shortId()); } + printf("\n"); +} + +// Given an list of adjacent channels, try to derive optimal PACCH +// and stick them in the macPacchs list. +static void macPacchAddAdjCh(PDCHL1FEC**alist,int asize) +{ + int downslots = configGprsMultislotMaxDownlink(); // TODO: add a separate chunk size. + int upslots = configGprsMultislotMaxUplink(); + int chunk = upslots>downslots ? upslots : downslots; + //printf("first chunk=%d\n",chunk); + chunk = RN_BOUND(chunk,1,4); + + if (asize < chunk) { + // We cannot optimize this adjacency set. + if (asize == 1) { + GLOG(WARNING) << "GPRS: single channel cannot be used multislot: "<= upslots) ? 2 : 0; + } + } + int full = asize/chunk; // How many full bandwidth pacch. + for (int i = 0; i < full; i++) { + int n = i*chunk+offset; + devassert(n < asize); + if (n < asize) { + GLOG(INFO)<1) { + // If asize == 8 then we can only get here if chunk == 3, but check anyway: + if (asize == 8 && chunk == 3) { + // In this case we will have have 3 pacchs of size 3,3,2 where + // the top pacch shares one tn with the one below. + //printf("c:adding %d asize=%d\n",6,asize);dumpPdch(); + gL2MAC.macPacchs.push_back(alist[6]); + } else if (asize > chunk) { + // Go ahead and make a pacch that shares with adjacent ch below. + // If it were a full ARFCN, it could share above or below, so it + // would not matter much what we pick for PACCH, but in that case, + // we would not be in this branch. + // So the only sharing opportunity is with the channels below. + int lastpacch = asize - leftover + offset; + if (lastpacch < 0 || lastpacch >= asize) { + GLOG(ERR) << "logic error in leftover PACCH calculation"; // Dont crash. + } else { + gL2MAC.macPacchs.push_back(alist[lastpacch]); + GLOG(INFO)<CN() && tn+1 == (int)ch->TN()) { + alist[asize++] = ch; // This is an adjacent channel. + } else { // This is not an adjacent ch + // Process a list of adjacent channels. + macPacchAddAdjCh(alist,asize); + asize = 0; // Start a new adjacency list. + } + cn = ch->CN(); + tn = ch->TN(); + } + if (asize) { macPacchAddAdjCh(alist,asize); } + + if (GPRSDebug) { printf("after macPacchRebuild:"); dumpPdch(); } + + // Check for disaster. This would happen if all the channels were singletons. + if (gL2MAC.macPacchs.size() == 0) { + GLOG(WARNING) << "GPRS: No channels found that can be used in multislot configuration"; + RN_MAC_FOR_ALL_PDCH(ch) { gL2MAC.macPacchs.push_back(ch); } + } +} + +// Return a GPRS channel to use. +// Try to pick the least busy channel. +// For an uplink it would be nice to make sure we pick a channel that has free USFs, +// but I'm not going to worry about it. +PDCHL1FEC *L2MAC::macPickChannel() +{ + int size = macPDCHs.size(); + if (size == 0) { return NULL; } // Dont think this can happen. + if (0) { + // Phase 1: Original code: + static int roundrobin = 0; + if (++roundrobin >= size) { roundrobin = 0; } + return macPDCHs[roundrobin]; + } + //printf("macPickChannel:"); dumpPdch(); + + // To give the phones better bandwidth, only return channels that can be + // used in an optimum (or close to optimum) multislot config. + // We keep the channels we will use as pacch in the macPacchs list. + + // Rebuild the pacch list if necessary. + // The list is cleared whenever we add/remove from macPDCHs. + if (macPacchs.size() == 0) { macPacchRebuild(); } + + //printf("macPickChannel after rebuild:"); dumpPdch(); + + // Determine the approximate load on each pacch and pick the least busy. + int npacch = macPacchs.size(); + devassert(npacch); + PDCHL1FEC *ch, *bestch = NULL; + int bestload = 0; // unneeded init to make gcc happy. + for (RListIterator itr(macPacchs); itr.next(ch); ) { + int load = 0; + MSInfo *ms; + RN_MAC_FOR_ALL_MS(ms) { + // TODO: Use totalsize instead of size, which requires changing the q type + // TODO: Add in the uplink load too. + // TODO: The PACCH for assignments that favor uplink over downlink + // are one off assignments that favor downlink over uplink, so test + // the load on all the assigned channels, not just PACCH. + // Add 1 so an unallocated pacch wins over an allocated one, even if not loaded. + if (ms->msPacch == ch) { + // The msTrafficMetric measures the relative past utilization of the channel in blocks sent, + // while downlinkqueuesize is in bytes. Multiply to kind of even out their influence. + // Add 1 in case nobody is sending anything we will still differentiate empty channels. + int msload = ms->msDownlinkQueue.size() + ms->msTrafficMetric * 30; + load += 1 + msload; + GPRSLOG(2) << "macPickChannel loop"< 1 || configGprsMultislotMaxUplink() > 1) { + const char *multislotmsg = "A multislot configuration, required for high-speed GPRS service, is suggested by the config options GPRS.Multislot.Max.Downlink or GPRS.Multislot.Max.Uplink"; +#if GPRS_CHANNELS_MAX_SUPPORTED + if (configGprsChannelsMax() <= 1) { + GLOG(WARNING) << multislotmsg << " but is not possible because GPRS.Channels.Max <= 1"; + } else +#endif + if (configGprsChannelsMin() <= 1) { + GLOG(WARNING) << multislotmsg << " but is unlikely to be achieved because GPRS.Channels.Min <= 1"; + } + } + + // Allocate initial channels, if specified. + // Update: Channel allocation will not work until OpenBTS has been + // running a few seconds. I suspect the channels are created with + // their recyclable timers running and we cannot allocate them until + // they expire. So dont even try. The mac service loop will try again later. + //int minchans = gConfig.getNum("GPRS.Channels.Min",0); + //if (minchans > 0) { + // for (int i = 0; i < minchans && i < 8; i++) { + // if (!macAddChannel()) break; + // } + //} + + if (! macSingleStepMode) { + macRunning = true; // set this first to avoid an unlikely race condition since + // the lock above is released before thread starts. + macThread.start(macThreadFunc,this); + } + GLOG(INFO) << "GPRS service thread started"; + return true; +} + +// External entry point. +void gprsStart() { gL2MAC.macStart(); } + +// This is for debugging and does not try to kill off MS and TBF, +// in fact, we probably want to leave those alone for post-mortem examination. +void L2MAC::macStop(bool channelstoo) +{ + ScopedLock lock(macLock); // prevents a RACH from interrupting us. + if (macRunning) { + macStopFlag = true; + macThread.join(); + } + + // Cant just delete the channels while the serviceloop is running or we crash. + if (channelstoo) { + for (int sanitychk = 0; sanitychk < 20 && macActiveChannels(); sanitychk++) { + if (! macFreeChannel()) break; + } + } + GPRSLOG(1) << "macStop successful\n"; +} + + + +L1UplinkReservation::L1UplinkReservation() +{ + ScopedLock lock(mLock); // Overkill. We dont need to lock this, no one is using it yet. + RLCBlockReservation *rp = &mReservations[0]; + for (int i = mReservationSize-1; i >= 0; i--,rp++) { rp->mrBSN = -1; } +} + +void mac_debug() +{ + //PDCHL1FEC *pdch; + //RN_MAC_FOR_ALL_PDCH(pdch) { + // pdch->debug_test(); + //} +} + + + +// Find an available RadioBlock on this uplink. +// If restype indicates RRBP, also return the RRBP +// (Relative Reserved Block Period) GSM04.60 10.4.5. +// The RRBP can specify reservations 3 - 6 BSN periods in the future: +// rrbp: -1: invalid; 0: 3 BSN; 1: 4 BSN; 2: 5 BSN; 3: 6 BSN. +// Otherwise, make a reservation after afterBSN and return the reserved absolute BSN +// as an integer, or -1 on failure. +RLCBSN_t L1UplinkReservation::makeReservationInt( + RLCBlockReservation::type restype, RLCBSN_t afterBSN, TBF *tbf, + RadData *rd, + int *prrbp, // RRBP 0..3 returned here. + MsgTransactionType mttype) +{ + ScopedLock lock(mLock); + RLCBSN_t bsn, first, lastplus1; + // On my Toshiba the BTS and radio are becoming desynchronized to the point + // where the uplink blocks arrive at the same time as the downlink + // blocks are sent! When this happens the RRBP reservations are not far + // enough in advance to be answered. To fix that, use a minimum RRBP + // greater than 0. + int minrrbp = gConfig.getNum("GPRS.RRBP.Min"); + if (tbf) { + // Count the reservations for reporting purposes. + switch (restype) { + case RLCBlockReservation::ForPoll: tbf->mtMS->msCountCcchReservations.addTotal(); break; + case RLCBlockReservation::ForRRBP: tbf->mtMS->msCountRbbpReservations.addTotal(); break; + default: break; + } + // This I/O is so stupid... + if (rd) { + GPRSLOG(1) << "makeReservation"<mtMS <mtMS <resync() +// DAB GPRS - inside of getNextMsgSendTime(). +// pat - Done, and it worked. + +// 12 blocks is one 52-multiframe. For now, add an extra multframe +// to the AGCH load to make sure it is in the future. +// Update: This seems to be too far in the future for the Multitech Modem. +// TODO: This could be reduced down to a few blocks in the future. +// Update: 12 blocks in the future did not work, trying 24. Now trying 36. +// Return the reservation time +// TODO: The DRX mode needs to know which channel the MS is on, based on IMSI. see GSM05.02 6.5 +RLCBSN_t L1UplinkReservation::makeCCCHReservation( + CCCHLogicalChannel *AGCH, + RLCBlockReservation::type type, TBF *tbf, RadData *rd, + bool forDRX, + MsgTransactionType mttype) // The sub-state that this reservation is for. +{ + mac_debug(); + // TODO: Add a separate GPRS qmax, since it seems like MS cant handle much delay. + int qmax = gConfig.getNum("GSM.CCCH.AGCH.QMax"); + if (qmax > 0 && AGCH->load()>(unsigned)qmax) { + if (type == RLCBlockReservation::ForRACH) { + GPRSLOG(1) << "RACH dropped due to AGCH congestion.\n"; + } else if (type == RLCBlockReservation::ForPoll) { + GLOG(INFO) << "CCCH congestion prevented Packet Downlink Assignment Message"; + } + return RLCBSN_t(-1); // invalid. + } + + RLCBSN_t resbsn; // BSN of the immediate assignment uplink reservation. + // advanceblocks is time between MS receiving reservation and reacting. + int advanceblocks = 4; // We will default to 4. + Time sendTime = AGCH->getNextMsgSendTime(); + unsigned fn = sendTime.FN(); + + if (1) { + // new way: + // Converting to an RLC block rounds down: + resbsn = FrameNumber2BSN(fn); + // We have to give the MS a little time to respond. + // GSM05.10 sec 6.11.1, and I quote: + // "If the MS is required to transmit a PACKET CONTROL ACKNOWLEDGEMENT subsequent + // to an assignment message (see 3GPP TS 04.60), the MS shall be ready to + // transmit and receive on the new assignment no later than the + // next occurrence of block B((x+2) mod 12) where block B(x) is + // radio block containing the PACKET CONTROL ACKNOWLEDGEMENT." + if (gConfig.defines("GPRS.MS.ResponseTime")) { + advanceblocks = gConfig.getNum("GPRS.MS.ResponseTime") + ExtraClockDelay; + } + + // We also have to add one to compensate for FrameNumber2BSN rounding down. + resbsn = resbsn + advanceblocks + 1; + if (forDRX) { + // FIXME TODO: Fix this total hack. + // We dont know which paging channel, so make sure the reservation is beyond any of them. + // The BS_PA_MFRMS number of 51-multiframes used for paging is + // advertised in the beacon as 2. + // TODO: This magic number should not be hard-coded here. + resbsn = resbsn + 22; // Should be enough. Note: Must be even for gFixIdleFrame. + } + } else { + // old way that worked: + + // For debugging, add a variable advance amount. + //int advanceframes = gConfig.getNum("GPRS.advanceframes",0); + advanceblocks = gConfig.getNum("GPRS.advanceblocks"); // This worked! + + resbsn = gBSNNext + (int32_t)(12 * AGCH->load() + advanceblocks); // This is in blocks. + } + mac_debug(); + return makeReservationInt(type,resbsn,tbf,rd,NULL,mttype); +} + +// Make an RRBP reservation if possible. Return the rrbp (range 0 to 3), or -1 if failed. +RLCBSN_t L1UplinkReservation::makeRRBPReservation(TBF *tbf, + int *prrbp, // The RRBP result, 0..3 + MsgTransactionType mttype) // The sub-state that this reservation is for. +{ + return makeReservationInt(RLCBlockReservation::ForRRBP,-1,tbf,NULL,prrbp,mttype); +} + +// Return the reservation for the specified block timeslot, or NULL if none. +// Return true if found, and return TFI in *TFI. +// bsn can be in the past or future. +RLCBlockReservation *L1UplinkReservation::getReservation(RLCBSN_t bsn) +{ + ScopedLock lock(mLock); + RLCBlockReservation *rp = &mReservations[bsn % mReservationSize]; + if (rp->mrBSN == bsn) { return rp;} + return NULL; +} + +// If TBF is NULL, clear the reservation. +// If a TBF is specified, only clear the res if it is for that TBF. +void L1UplinkReservation::clearReservation(RLCBSN_t bsn, TBF *tbf) +{ + ScopedLock lock(mLock); + RLCBlockReservation *rp = &mReservations[bsn % mReservationSize]; + if (tbf && rp->mrTBF != tbf) { return; } + rp->mrBSN = -1; + rp->mrTBF = NULL; // not necessary, but lets be neat. +} + +// See if this block the MS sent to us corresponds to a reservation, +// and if so, update the counters in the corresponding TBF. +RLCBlockReservation::type L1UplinkReservation::recvReservation( + RLCBSN_t bsn, // The BSN of a received block. + TBF**restbf, // Return tbf specified by reservation here, just for debugging. + RadData *prd, // Return radio data here. + PDCHL1FEC *ch) // Implicit in this pointer, but easier to pass it. +{ + ScopedLock lock(mLock); // This lock is probably no longer necessary. + RLCBlockReservation::type result = RLCBlockReservation::None; + RLCBlockReservation *rp = &mReservations[bsn % mReservationSize]; + *restbf = 0; + if (rp->mrBSN == bsn) { + result = rp->mrType; + if (prd) { *prd = rp->mrRadData; } + if (rp->mrTBF) { + devassert(rp->mrType != RLCBlockReservation::ForRACH); + TBF *rtbf = rp->mrTBF; + GPRSLOG(1) << "recvReservation " <mrType <mrSubType) + <mtMS <mtRecvAck(rp->mrSubType); + switch (rp->mrType) { + case RLCBlockReservation::ForPoll: rtbf->mtMS->msCountCcchReservations.addGood(); break; + case RLCBlockReservation::ForRRBP: rtbf->mtMS->msCountRbbpReservations.addGood(); break; + default: break; + } + } else { + GPRSLOG(1) << "recvReservation"<mrType<<" tbf=null" <mrBSN.valid()) { + os << " "; + os << res; + } + } + os << ")\n"; +} + + +// Return the USF of an MS that might want to use the uplink. +// TODO: Fix this not to worry about RRBP since I added makeReservation. +static +int findNeedyUSF(PDCHL1FEC *pdch) +{ + int usf; + GPRSLOG(512) << "findNeedyUSF start for "<getUSFMS(usf); + if (ms) { + // This USF on this channel is in use by this MS. + // Lets see if the MS wants to send something to us. + TBF *tbf; + RN_MS_FOR_ALL_TBF(ms,tbf) { + GPRSLOG(512) << "findNeedyUSF "<mtDir != RLCDir::Up) continue; + // We dont include state DataFinal, because then we have already + // received all the blocks and are waiting for an RRBP reservation, + // which does not need a USF. + // We will include DataReassign to let an uplink proceed + // during the reassignment process. + TBFState::type tstate = tbf->mtGetState(); + if (tstate != TBFState::DataTransmit && + tstate != TBFState::DataReassign) continue; + + // At this point, we know the TBF wants to use this uplink slot. + + // Does the tbf have both uplink and downlink on this channel? + // We would not have allocated the USF otherwise, so assert. + unsigned tn = pdch->TN(); + devassert(ms->msCanUseUplinkTn(tn)); + devassert(ms->msCanUseDownlinkTn(tn)); + + // For extended dynamic we must reserve all the uplink channels simultaneously. + if (ms->isExtendedDynamic()) { + // Make sure this is the first channel of the allocation. + // (This is only necessary if there are multiple down channels, + // which only occurs for a 2-down/3-up config.) + // We keep the lists sorted to facilitate this test. + if (tn != ms->msPCHUps.front()->TN()) {goto nextusf;} + PDCHL1Uplink *up2; + RN_FOR_ALL(PDCHL1UplinkList_t,ms->msPCHUps,up2) { + // The USF in downlink block N reserves uplink block N+1 + if (up2->parent()->getReservation(gBSNNext + 1)) { + // The extended dynamic TBF cannot use the uplink + // if there is a reservation on any of its uplink channels. + // If the extended dynamic tbf has exclusive use of + // all its channels, or at least, if there are no other + // PACCH sharing any channels, this will not happen except + // on the PACCH of this ms, because there could be multiple + // extended dynamic tbfs sharing the same PACCH. + goto nextusf; + } + } + // Success! Reserve all the uplinks belonging to this tbf. + // According to the spec, we only have to transmit the USF + // on the first downlink for the tbf, which will happen below. + // So all we have to is reserve the channels, we dont need + // to save the usf. Doesnt matter if we reserve the current + // timeslot since we are already past the test, so just do all. + RN_FOR_ALL(PDCHL1UplinkList_t,ms->msPCHUps,up2) { + extDyn.reserveUplink(up2); + } + // Fall through to return the usf for this tbf. + } + + + // If an uplink tbf is stalled, the MS is waiting for an acknak + // from the network, so there is not much point in giving it an uplink USF. + // An intereseting point is that it might get its acknack before + // this downlink block gets to it, so maybe we should go ahead and + // give it a usf anyway, but I didnt. It might get the usf below, anyway. + // 6-7-2012: Above is WRONG. The MS will continue sending the old + // blocks and it needs USF to do so; without them it will hang forever. + //if (tbf->stalled()) continue; + + int thisage = gBSNNext - ms->msLastUsfGrant; + GPRSLOG(512) << "findNeedyUSF for "<mtMS->msCountUSFGrant(besttbf->mtGetState() == TBFState::DataTransmit); + GPRSLOG(4)<mUSF = findNeedyUSF(pdch); + int dutyfactor = configGetNumQ("GPRS.UplinkDutyFactor",100); + int nope = false; + if (dutyfactor < 0) { // If it is zero, it is probably just an sql goof. + GLOG(ERR) << "Invalid GPRS.UplinkDutyFactor:"<mSP = 0; + block->mUSF = 0; + if (makeres) { + mac_debug(); + RLCBSN_t bsn = pdch->makeRRBPReservation(tbf,&rrbp,mttype); + if (bsn.valid()) { + if (tbf) { tbf->mtSetAckExpected(bsn,mttype); } + } else { + devassert(rrbp == -1); + if (makeres == 2) { return false; } // Caller will try again later. + } + } + if (rrbp != -1) { + block->setRRBP(rrbp); + if (pcounter) (*pcounter)++; + } + + // GSM 05.02 6.3.2.2.1: The USF field in downlink block N signals + // that uplink block (N+1) is assigned to that MS. + + if (! pdch->getReservation(gBSNNext + 1) && + ! disabledByDutyFactor() && + ! extDyn.isUplinkReserved(pdch)) { + block->mUSF = findNeedyUSF(pdch); + // Remember the usf sent on this channel at time gBSNNext + pdch->setUsf(block->mUSF,gBSNNext); // ok to be 0. + } else { + pdch->setUsf(0,gBSNNext); // ok to be 0. + } + return true; +} + + +// WARNING: This small function runs in a different thread than the rest of the GPRS code. +void GPRSProcessRACH(unsigned RA, + const GSM::Time &when, + float RSSI, float timingError) +{ + if (! GPRSConfig::IsEnabled()) return; + ChIdleCounter = 0; + GPRSLOG(1) << "Received RACH"< 0 && AGCH->load()>(unsigned)qmax) { + // GPRSLOG(1) << "RACH dropped due to AGCH congestion.\n"; + //} + ***/ + + // 5-24-2012: Each MS must have a single requestCh assigned. + // Since we dont know what MS rached us at this point, that + // implies we either have to use only one channel as the PAACH + // for all MS, or we would have to change the MS channel once + // we get a response back from it. For now, the former. + // When this was a random channel, all kinds of problems occurred. + PDCHL1FEC *chan = gL2MAC.macPickChannel(); // pick the least busy channel; + //PDCHL1FEC *chan = gL2MAC.macPDCHs.front(); // First channel is our PAACH. + if (chan == NULL) { + GPRSLOG(1) << "MAC serviceRACH failed to find available channel!\n"; + return; + } + + RLCBSN_t RBN; // check .valid for error. + switch (mRA & 0xf8) { + case 0x78: { // MS requests an uplink TBF. + // GSM04.18 sec3.5.2.1.3.1 says: if one phase access is requested, + // the network may grant either a one phase access or a single block + // packet access, which forces the MS to do a two phase access. + // In order to implement single-phase access, we would have to implement code + // to identify the MS using one of the contention resolution methods + // in GSM04.60 sec7.1.2.3, and detect failures via timeouts, making + // all the state machines more complicated, so we wont. + // So fall through to Single Phase Access... + + /*** + int TFI = chan->downlink()->allocateTFI(); + if (TFI >= 0) { + const GSM::L3RequestReference &reqref, // Specifies when the RACH burst occurred. + result = new L3ImmediateAssignment( + GSM::L3RequestReference(rachinfo->RA,rachinfo->when) + chan->uplink->packetChannelDescription(), + GSM::L3TimingAdvance(GetTimingAdvance(timingError)), true); + result->packetAssign()->setPacketUplinkAssignDynamic(TFI,CSNum); + return result; + } + // Else, unlikely case of all TFIs in use. Fall through to single phase access. + ****/ + } + case 0x70: { // MS requests a single uplink block. + + RBN = chan->makeCCCHReservation(AGCH,RLCBlockReservation::ForRACH,NULL,&mRadData,0,MsgTransNone); + + Time now = gBTS.time(); + + if (! RBN.valid()) { + // Abject failure. This is probably due to AGCH congestion, + // and we printed one message already. + GPRSLOG(1) << "serviceRACH failed to make a reservation at" + <packetChannelDescription(), + GSM::L3TimingAdvance(GetTimingAdvance(mRadData.mTimingError)), + true,false); // tbf, downlink + + + // The immediate assignment has a TFI, but we do not set it for a single block assignment. + L3IAPacketAssignment *pa = result.packetAssign(); + pa->setPacketUplinkAssignSingleBlock(RBN.FN()); + + // We dont know what MS we are talking to, so set the power params to global defaults. + // We could fiddle with the power gamma based on RSSI, but not implemented, + // and probably unnecessary. It is not really necessary to change these at all here. + pa->setPacketPowerOptions(GetPowerAlpha(),GetPowerGamma()); + + GPRSLOG(1) << "GPRS serviceRACH sending L3ImmediateAssignment:" << result; + AGCH->send(result); + break; + } + default: + devassert(0); + } +} + +void L2MAC::macServiceRachQ() +{ + RachInfo *rip; + while ((rip = macRachQ.readNoBlock())) { + GPRSLOG(1) << "GPRS: servicing RACHQ"; + LOGWATCHF("RACH %s\n",strrchr(timestr().c_str(),':')+1); + // TODO: Check the burst age again, in case start of GPRS service was delayed. + // If the RACH is too old, discard it. + + if (!macActiveChannels()) { + // No GPRS channels allocated; attempt to get one. + macAddChannel(); + } + if (!macActiveChannels()) { + GPRSLOG(1) << "GPRS: RACHQ failed to allocate channels"; + // Failed to allocate any channels at all. + // Toss the RACH. The MS will just have to try again later. + //macRachQ.write(rip); // We reordered the RACHs, but should not matter. + } else { + rip->serviceRach(); + } + delete rip; + } +} + +// The RLC block just arrived on pdch. Figure out to which TBF it belongs and +// pass it on to the RLCEngine for assembly into a PDU. +static void processRLCUplinkDataBlock(PDCHL1FEC *pdch, RLCRawBlock *src,TBF *restbf) +{ + RLCUplinkDataBlock *rb = new RLCUplinkDataBlock(src); + + // If this flag is set, data blocks are supposed to arrive + // only on odd numbered blocks + if (gFixIdleFrame && (0==(src->mBSN&1))) { + GPRSLOG(1) << "@@@OOPS: Even numbered uplink data block:"<mBSN; + } + + // Reassociate the block with the TBF to which it belongs: + //GPRSLOG(1) << "### Uplink Data Block tfi="<mTFI<< " bsn="<mBSN; + TBF *tbf = pdch->getTFITBF(rb->mTFI,RLCDir::Up); + if (tbf) { + // The rd data is from the RACH, and is pretty old; + // Use the new RSSI from RLCRawBlock. + //if (rd.mValid) { + // tbf->mtMS->setRadData(rd); + // tbf->setRadData(rd); + //} + // In the calling function processUplinkBlock we looked up the TBF by USF + // and already counted it. Here we are looking up the TBF by TFI. + // I am leaving in both calls in case there is a problem, but we wont + // double count the block for reporting purposes. + tbf->mtMS->setRadData(src->mRD); + tbf->mtMS->talkedUp(true); // Mark time, but dont double count. + + //GPRSLOG(1) << "### Uplink Data Block tfi="<mTFI + // <<" tbf="<engineRecvDataBlock(rb,pdch->TN()); + } else { + //GPRSLOG(1) << "### Uplink Data Block with unknown tfi="<mTFI + // << " bsn="<mBSN; + GLOG(WARNING) << "Uplink Data Block with TFI="<mTFI + << " bsn="<mBSN <<" unassociated with TBF"; + } +} + + +// The MS is requesting an uplink TBF. +// Create the TBF, then go ahead and try to attach it right now. +// The MS may not have channels assigned yet. +// (Happens if this is the first time we have heard from this MS; it is the second +// part of a two phase uplink assignment.) +// LISTEN UP: If there is already an uplink TBF running for this MS and you send a second +// Packet Uplink Assignment, the MS *immediately* starts using the new TFI +// for the in-progress TBF, which would cause that one to report a "fail". +// The data may or may not be lost, because if the TBF has not yet had any acknacks, +// the data will get through on the second TBF, else if there have been acknacks, +// the data is lost as far as we are concerned. +static void processUplinkResourceRequest( + PDCHL1FEC *requestch, // The channel the request arrived on. + RLCMsgPacketResourceRequest *rmsg, + RLCBlockReservation::type restype, RadData &rd) +{ + MSInfo *ms = rmsg->getMS(requestch,true); + if (!ms) { + // 3-20120: This happened if the MS tried to identify itself using a TFI, + // but the TBF for that TFI is already deleted. Just ignore it, nothing else we can do. + GPRSLOG(1) << "UplinkResourceRequest for unidentified MS on ch:" << requestch; + return; + } + ms->talkedUp(); + if (rd.mValid) { + ms->setRadData(rd.mRSSI,rd.mTimingError); + } + // Store the channel quality report info. + ms->msCValue.addPoint(rmsg->mCValue); + if (rmsg->mSignVarPresent) { ms->msSigVar.addPoint(rmsg->mSignVar); } // Not present for two phase access. + for (int tn=0; tn < 8; tn++) { + if (rmsg->mILevelPresent[tn]) { ms->msILevel.addPoint(rmsg->mILevelTN[tn]); } + } + + bool isRach = (restype == RLCBlockReservation::ForRACH); + if (isRach) { + // After an MS in PacketIdle mode contacts us, we send it a single + // block uplink assignment, and this is its answer. + // The MS will listen on this PDCH until T3168 expires. + // Regardless of what we do about this RACH. + // TODO: Should we only set this if the purpose of the rach is an assignment, + // not for measurements? + // This timer blocks downlink assignments so its a performance issue. + ms->msT3168.set(); + } + + // 6-8-2012: This is the previous code; now handled below. + // // If there is an existing uplink tbf for this MS, ignore this request. + // // We just drop it on the ground. + // // FIXME TODO: That is not right - the uplink tbf request is probably changing + // // the priority of the existing tbf, and we need to resend the (identical) assignment + // // to make the MS happy, or it will kill off the uplink TBF after some timer. + // TBF *existingtbf; + // if (ms->msCountActiveTBF(RLCDir::Up, &existingtbf)) { + // GPRSLOG(1) << ms << " dropping uplink request, duplicates:"<mTLLIPresent) { ms->msTlli = rmsg->mTLLI; } + + // TODO LATER: We may want to assign more channels based on params in the rmsg. + // If this uplink request was RACH initiated, we did not know the MS before + // we got this message, so channel assignment was a shot in the dark. + // We need to remember the requestch because the ms is already listening on it + // as PACCH, and will continue to do so unless and until we tell it otherwise. + if (ms->msPacch && ms->msPacch != requestch) { + // This does not really matter. + GPRSLOG(1) << ms <<" ch:"<msPacch<<" different from msg ch:"<msCountActiveTBF(RLCDir::Down, &atbf)) { + GLOG(ERR) << ms <<" RACH while TBF transmitting:"< RACH, anonymous --> + // Immediate downlink assignment with TLLI <--- Network sends + // One block assignment for RACH, anonymous <-- Network sends + // MS <-- downlink assignment + // MS ---> ack to downlink ---> + // MS <-- One block assignment, ignored. + // MS -- XXX --> No response to one block assignment. + // Then the MS should not send the PacketResourceRequest, so if we + // get here with a downlink tbf in transmit state, it is because + // the TBF failed and the MS is retrying. + // If they arrive in this order: + // MS <-- one block assignment + // MS --> Packet Resource Request + // After sending Packet Resource Request MS leaves "Packet Idle Mode". + // MS <-- Immediate Assignment. + // Then the downlink TBF will be in DataWaiting1 mode. + // We have two choices: + // o We could pretend we did not hear this Packet Resource Request + // and wait for the Immediate Assignment to arrive, but I dont think that will work + // o We establish the uplink TBF on PACCH and just let the SendAssignment code + // wait for the reservation time for this Downlink Immediate Assignment message to pass, + // after which that code will try again on PACCH. + + // We dont find out they are the same MS until now. + // The safe thing to do is cancel the immediate assignment and send a new + // assignment on the new PACCH. + // In any case, this sucks, because we did not deliver the downlink TBF reliably - + // unless I kept track of the TBFs that have been fully acked and resend the others? + //atbf->mtSetState(TBFState::DataReassign); + if (atbf->isTransmitting()) { + // I think we can retry the TBF immediately, but until that is proved, + // we will wait for the timeout before retrying. + atbf->mtCancel(MSStopCause::Rach,TbfRetryAfterWait); + } + ignore = true; + GLOG(WARNING) << ms <<" Ignoring RACH while TBF active:"<msCountTransmittingTBF(RLCDir::Up, &atbf)) { + if (isRach) { + // I dont know if we should kill this TBF or not. + // Before I supported uplink requests in downlinkacknack I thought the Blackberry was using + // RACH to request new uplink TBFs but there were still so many bugs then that I'm not sure. + atbf->mtCancel(MSStopCause::Rach,TbfRetryInapplicable); + GLOG(WARNING) << ms <<" RACH while uplink TBF transmitting, TBF cancelled:"<mtGetState() != TBFState::DataFinal) { + // GLOG(INFO) << ms <<" Uplink TBF reassignment request delayed until current TBF finished:"<msSetUplinkRequest(rmsg->mCRD); + // } else { + // atbf->mtSetState(TBFState::DataReassign); + // GLOG(INFO) << ms <<" Uplink TBF reassignment request: "<msDeassignChannels(); + ms->msPacch = requestch; + } + + // Allocate a new uplink TBF. + // If a tlli was present in the message we must send it to the sgsn for the one + // special case where an ms loses registration and tries to re-register; + // in this special case the request tlli will not match the msTlli because + // the ms has already undergone the change tlli procedure. + uint32_t tlli = rmsg->mTLLIPresent ? (uint32_t) rmsg->mTLLI : (uint32_t) ms->msTlli; + TBF *tbf = TBF::newUpTBF(ms,rmsg->mCRD,tlli,true); + if (tbf == NULL) return; + //tbf->mtUnAckMode = rmsg->mCRD.mRLCMode; + + GPRSLOG(1) <<"UplinkResourceRequest" <mTLLI) <str(); + // TODO: This switch is not very interesting... + switch (accessType) { // GSM04.60 table 11.2.16.2 + case 0: // Two Phase Access Request [second half of it] + case 3: // Mobility Management Procedure. + case 1: // Page Response + case 2: // Cell Update + processUplinkResourceRequest(pdch,rmsg,restype,rd); + break; + } +} + +static void processDownlinkAckNack(PDCHL1FEC *pdch,RLCMsgPacketDownlinkAckNack* rmsg,RadData &rd) +{ + int tfi = rmsg->getTFI(); + TBF *tbf = pdch->getTFITBF(tfi,RLCDir::Down); + if (tbf) { + MSInfo *ms = tbf->mtMS; + ms->talkedUp(); + ms->setRadData(rd); + // Store the Channel Quality Report for reporting purposes. + ms->msCValue.addPoint(rmsg->mCQR.mCValue); + ms->msRXQual.addPoint(rmsg->mCQR.mRXQual); + ms->msSigVar.addPoint(rmsg->mCQR.mSignVar); + for (int tn = 0; tn < 8; tn++) { + // Dont bother to differentiate this per timeslot. + if (rmsg->mCQR.mHaveILevel[tn]) {ms->msILevel.addPoint(rmsg->mCQR.mILevel[tn]);} + } + + tbf->engineRecvAckNack(rmsg); // process the ack/nack part of the msg. + if (rmsg->mHaveChannelRequest) { // Process the channel request, if any. + TBF *uptbf = TBF::newUpTBF(ms,rmsg->mCRD,ms->msTlli,false); + + if (uptbf) { + GPRSLOG(1) <<"Uplink TBF from downlink AckNack" <talkedUp(); + ms->setRadData(rd); + } else { + GLOG(ERR) << "TLLI"<str(); + + // We dont have to do anything; recvReservation informed the TBF. + uplinkCommon(rmsg->mTLLI,rd,"ControlAck"); +} + +static void processUplinkDummy(PDCHL1FEC *pdch, RLCMsgPacketUplinkDummyControlBlock* rmsg, RadData &rd) +{ + GPRSLOG(1) << "processDummyUplink "<str(); + uplinkCommon(rmsg->mTLLI,rd,"DummyUplinkControl"); +} + +// The src block is the decoded uplink BitVector from the radio. +// The BitVector we are passed was allocated; delete when we are finished. +static void processUplinkBlock(PDCHL1FEC *pdch, RLCRawBlock *src) +{ + // Possibly handle timers. See: XCCHL1Decoder:handleGoodFrame(); + // TODO ... + + char buf[40]; + GPRSLOG(1) <<"-----> processUplinkBlock mac type="<mmac.mPayloadType) << " ch:"<getAnsweringUsfText(buf,src->mBSN); + RadData rd; + TBF *restbf; + RLCUplinkMessage::MessageType mtype; + mac_debug(); + RLCBlockReservation::type restype = pdch->recvReservation(src->mBSN,&restbf,&rd,pdch); + + // Reset N3101. We are doing it based on the presence of the burst rather. + // Someday we should look inside the burst and make sure it really came from the MS we expected. + // This code works for persistent (extended) uplink mode too; in that mode + // the MS sends control blocks if it does not have data. + // TODO: Can elide code still extant down alot of these branches that resets msN3101. + int usf = pdch->getUsf(src->mBSN); + if (usf != -1) { + MSInfo *usfms = pdch->getUSFMS(usf); + if (usfms) { + usfms->msN3101 = 0; + usfms->talkedUp(); + } + } + + switch (src->mmac.mPayloadType) { + case MACPayloadType::RLCData: + if (restype != RLCBlockReservation::None) { + // This happened alot before fixing the transceiverRAD1. + GLOG(ERR) << "ERROR: Received reservation in RLC data block"; + } + processRLCUplinkDataBlock(pdch,src,restbf); + break; + case MACPayloadType::RLCControl: { + mtype = (RLCUplinkMessage::MessageType) src->mData.peekField(8,6); + GPRSLOG(1) << "processUplinkMessage:" <mMessageType) { + case RLCUplinkMessage::PacketControlAcknowledgement: + processControlAcknowledgement(pdch,(RLCMsgPacketControlAcknowledgement*)msg,src->mRD); + break; + case RLCUplinkMessage::PacketDownlinkAckNack: + processDownlinkAckNack(pdch,(RLCMsgPacketDownlinkAckNack*)msg,src->mRD); + break; + case RLCUplinkMessage::PacketResourceRequest: + processResourceRequest(pdch,(RLCMsgPacketResourceRequest*) msg,restype,src->mRD); + break; + case RLCUplinkMessage::PacketUplinkDummyControlBlock: + processUplinkDummy(pdch,(RLCMsgPacketUplinkDummyControlBlock*)msg,src->mRD); + break; + default: // Just ignore unrecognized messages. + GLOG(INFO) << "GPRS: Ignoring UplinkMessage:" + << RLCUplinkMessage::name(msg->mMessageType); + break; + } // switch msg->mMessageType + + delete msg; + break; + } + case MACPayloadType::RLCControlExt: { + mtype = (RLCUplinkMessage::MessageType) src->mData.peekField(8,6); + GLOG(INFO) << "GPRS: Ignoring uplink RLCControlExt block, msgtype="<str()<getRefCnt() == 2); + delete dmsg; + //devassert(dlmsg->getRefCnt() == 1); + + // Find or create this MS. + MSInfo *ms = NULL; + if (dlmsg->mbdHaveOldTLLI) { + ms = bssgMSChangeTLLI(dlmsg->mbdOldTLLI,dlmsg->mbdTLLI); + } + if (!ms) { ms = gL2MAC.macFindMSByTlli(dlmsg->mbdTLLI, true); } +#if 0 // Code prior to internal sgsn + ms->msDownlinkQueue.write(dlmsg); +#else + SGSN::GprsSgsnDownlinkPdu *dlpdu = new SGSN::GprsSgsnDownlinkPdu(dlmsg->mbdPDU,ms->msTlli,0,"BSSGMsgDlUnitData"); + //devassert(dlmsg->getRefCnt() == 2); + //dlpdu->mDlData = dlmsg->mbdPDU; + //dlpdu->mDescr = "BSSGMsgDLUnitData"; + delete dlmsg; + ms->msDownlinkQueue.write(dlpdu); +#endif + + // If the MS queue is too full, we should do flow control, + // but the sgsn does not implement it yet, so not much point. + break; + } + + // TODO: Other BSSG messages: + case BSSG::BSPDUType::RA_CAPABILITY: // network->BSS + case BSSG::BSPDUType::PTM_UNITDATA: // not currently used + // PDUs between GMM SAPs: + case BSSG::BSPDUType::PAGING_PS: // network->BSS request to page MS for packet connection. + case BSSG::BSPDUType::PAGING_CS: // network->BSS request to page MS for RR connection. + case BSSG::BSPDUType::RA_CAPABILITY_UPDATE_ACK: // network->BSS Radio Access Capability and IMSI. + case BSSG::BSPDUType::FLOW_CONTROL_BVC_ACK: // network->BSS + case BSSG::BSPDUType::FLOW_CONTROL_MS_ACK: // network->BSS + default: + // See the list of unimplemented messages in BsRecvMsg() + GPRSLOG(1) << "BSSG Downlink Message Ignored:"<mTlli); + delete dlpdu; // Done with that. + } + } +} + +// Compute a measure of GPRS channel utilization so we can decide when we need more bandwidth. +// For now we will compute only downlink bandwidth and ignore uplink. +// You cant just count how many RLC blocks are in TBFs, because the associated MS +// could be stalled and the TBF may not be using any bandwidth. +// The utilization is the approximate number of TBFs that are waiting to send blocks now, +// so 1.0 means the channel is pretty much full, and 2.0 means we could +// probably fully utilize two channels, at least at this instant. +// Return the utilization. +float L2MAC::macComputeUtilization() +{ + ScopedLock lock(macLock); // This function called from CLI. + int numReady = 0; + float utilization = 0.0; // Desired downlink utilization at this moment. + TBF *tbf; + RN_MAC_FOR_ALL_TBF(tbf) { + switch (tbf->mtGetState()) { + case TBFState::DataReadyToConnect: + case TBFState::DataWaiting1: + case TBFState::DataWaiting2: + numReady++; // These TBFs are waiting to send a message. + continue; + case TBFState::DataTransmit: + case TBFState::DataReassign: + //case TBFState::DataStalled: + utilization += tbf->engineDesiredUtilization(); + continue; + default: continue; + } + } + utilization += numReady; + + // We want to average the utilization over some timespan. + // This number should be picked to match the delay we use before closing + // GPRS chanels, whatever that will be. + const unsigned NumFramesToAverage = 48*5; // roughly 5 seconds worth of RLC blocks. + return macDownlinkUtilization = + (utilization + macDownlinkUtilization * (NumFramesToAverage-1)) / NumFramesToAverage; +} + +void L2MAC::macCheckChannels() +{ + int minChC0 = configGprsChannelsMinC0(); + int minChCn = configGprsChannelsMinCn(); + if (minChC0 < 0) { minChC0 = 0; } + if (minChCn < 0) { minChCn = 0; } + bool addedChannels = false; + //GPRSLOG(2)<<"macCheckChannel"< (int)macActiveChannels()) { + // Allocate from CN0. + int active0 = macActiveChannelsC(0); + { + TCHFACCHLogicalChannel *lchan; + for ( ; active0 < minChC0 && (lchan = gBTS.getTCH(true,true)); active0++) { + //GPRSLOG(2)<<"macCheckChannel loop"<TN()); + // We dont "open" the logical channel, which means we dont start + // the various timers referred to in L1Decoder::recyclable(). + // It probably doesnt matter whether it is 'open' or not, because + // we hook the bursts before they get to the GSM logical channel classes. + macAddOneChannel(lchan); + addedChannels = true; + } + } + + // Allocate from other ARFCNs + // This should probably allow a specified number of channels + // on each arfcn. + { + int activecn = (int)macActiveChannels() - active0; + int nfound, need = minChCn - activecn; + TCHFACCHLogicalChannel *results[8]; + // TODO: Prevent this from allocating from C0 if user misconfigures. + for (; need > 0 && (nfound = gBTS.getTCHGroup(need,results)); need -= nfound) { + for (int i = 0; i < nfound; i++) { + macAddOneChannel(results[i]); + addedChannels = true; + } + } + } + } + if (addedChannels) { + // We must keep the channel list sorted all the time because + // PACCH selection and extended uplink TBF both eexpect it. + gL2MAC.macPDCHs.sort(chCompareFunc); + } +#if 0 + int minchans = configGprsChannelsMin(); + if (minchans > 0) { + // We are doing startup. Allocate the initial channels from CN0. + int need = minchans - (int)macActiveChannels(); + TCHFACCHLogicalChannel *lchan; + // Allocate from CN0 first. + for ( ; need > 0 && (lchan = gBTS.getTCH(true,true)); need--) { + macAddOneChannel(lchan); + } + // If we still need more, allocate them from the end of the list. + int nfound; + TCHFACCHLogicalChannel *results[8]; + for ( ; need > 0 && (nfound = gBTS.getTCHGroup(need,results)); need -= nfound) { + for (int i = 0; i < nfound; i++) { + macAddOneChannel(results[i]); + } + } + } +#endif + + // TODO: Switch code to new channel allocator, but this code + // will move to where the channels are allocated, not here. + if (macTBFs.size()) { + ChIdleCounter = 0; + // If there are TBFs but no channels, try to allocate one. + // TBFs get added not only from the MAC but also indirectly by the BSSG. + // Note that we may not get the channel, in which case we will try each loop iteration. + if (!macActiveChannels()) { macAddChannel(); } + } else { + // No TBFs exist. + if (ChIdleCounter++ > macChIdleMax) { + // Return a channel to GSM RR use. + // We dont do this unless there is no activity at all, + // which means that if there are multiple channels allocated we cant + // downsize if the activity is merely low - it has to be stopped. + macFreeChannel(); + } + } + + // Maybe add another channel. + // We average the congestion measurement by incrementing it or decrementing it once each loop. + // todo: This test is too simple; needs to take into account how many channels allocated. + /**** TODO: This will be replaced by dynamic allocation. + if (macComputeUtilization() > macChCongestionThreshold) { + if (ChCongestionCounter++ > macChCongestionMax) { + macAddChannel(); + } + } else { + if (ChCongestionCounter > 0) { ChCongestionCounter--; } + } + ****/ +} + + +// Advance gBSNNext by the specified amount. +// The reason this is a function instead of just adding one to gBSNNext +// is to clean up old reservations behind us as we go. +static void advanceBSNNext(int amt) +{ + while (amt > 0) { + amt--; + gBSNPrev = gBSNNext; + ++gBSNNext; + + PDCHL1FEC *pdch; + RN_MAC_FOR_ALL_PDCH(pdch) { // for all channels assigned to GPRS. + RLCBlockReservation *res; + // For debug purposes, show unanswered reservations, and show them more nearly + // when they are supposed to arrive than when we actually clear them out below: + if (GPRSDebug) { + RLCBSN_t rprevdeb(gBSNNext-(BSNLagTime+ExtraClockDelay+2)); + res = pdch->getReservation(rprevdeb); + if (res) { + GPRSLOG(1) << "Reservation unanswered "<mrType<mrSubType)<<" "<mrTBF + <<" bsn=" <clearReservation(rprev,NULL); + } + } +} + +// After a crash the current time is going backwards occassionally. +// which is screwing up this code. +// If you reboot, it is ok, but I am putting in some code to deal with it. +// Here is the /var/log/OpenBTS.log: +// My message: +//Nov 10 22:48:49 ToshibaLap openbts: DEBUG GPRS:now=777040 waiting for 777044 +//Nov 10 22:48:49 ToshibaLap transceiver: ERR 3086998384 rnrad1Core.cpp:52:usbMsg: libusb_control_transfer failed: No such device (it may have been disconnected) +//Nov 10 22:48:49 ToshibaLap transceiver: ERR 3086998384 rnrad1Core.cpp:52:usbMsg: libusb_control_transfer failed: No such device (it may have been disconnected) +// My message: +//Nov 10 22:48:49 ToshibaLap openbts: DEBUG GPRS:unexpected gBSNNext delta: delta=(1306) gBSNNext=(179319) fnnext=(777049) fnnow=(775743) +// +// I am also getting this occassionally: +//Nov 10 22:48:45 ToshibaLap transceiver: ERR 3074030448 fusb.cpp:445:reload_read_buffer: No libusb events +// +// Work around it by checking if time has run backwards, and switching to +// calling sleep instead of catching up to the goofed up GSM clock. +static void serviceLoopSynchronize(bool firsttime) +{ + static double timeprev; + Time tstart = gBTS.time(); + + if (GPRSDebug) { + // This is just a debug check that the GSM master clock is sane. + // Apparently it is. + static int fnprev; + int fnnow = tstart.FN(); + if (!firsttime) { + int fndelta = GSM::FNDelta(fnnow,fnprev); + GPRSLOG(16) << "GSM FN delta="< 6) { + GPRSLOG(4) << "GSM FN ran forwards by " << fndelta << "\n"; + } + } + fnprev = fnnow; + } + + if (gFixSyncUseClock) { + // Work around the bug where time goes backwards after a crash. + // The whole BTS is probably non-operational, but at least I can keep debugging. + // Try just waiting about a block time between each loop. + sleepf(RLCBlockTime); + //useconds_t usecs = (useconds_t) (1000000.0 / 48.0); + //usleep(usecs); + } else { + // Wait for the prev frame to come around. + Time tprev(gBSNPrev.FN()); + tprev = tprev - configGetNumQ("GPRS.ThreadAdvance",0); + gBTS.clock().wait(tprev); + + // Check if the wait is screwing up: It is. + // On the ITX board the FN moves backwards 2 units sporadically, + // which (I hope) is because the gBTS clock was resynchronized with the radio + // causing it to run backwards. + Time tnow = gBTS.time(); + int deltaAfterWait = GSM::FNDelta(tnow.FN(),tprev.FN()); + // The deltaAfterWait is usually -1, so ignore that. + if (deltaAfterWait > 3 || deltaAfterWait < -1) { + GPRSLOG(2) << "gBTS.clock.wait unexpected wait time: "< 0 || delta < -(51*26)) { + // Set bsnFixed to the beginning frame of the RLC block containing tnow. + // | gBSNPrev | gBSNNext | ... | bsnFixed | + RLCBSN_t bsnFixed = FrameNumber2BSN(tnow.FN()); + // Then advance gBSNNext to the next block time modulo ghyperframe. + if (delta > 0) { + advanceBSNNext(bsnFixed - gBSNNext); + } else { + // This is really a disaster. + gBSNNext = bsnFixed + 1; + } + GPRSLOG(2) << "unexpected gBSNNext delta:" <debug_test(); + while (RLCRawBlock *src = pdch->uplink()->mchUplinkData.readNoBlock()) { + processUplinkBlock(pdch,src); + delete src; + } + } + + GPRSLOG(16) << "macServiceLoop: after uplink service"; + + // Step: Service unattached TBFs. (An attached TBF has resources + // assigned to its MS. Attached TBFs are handled by dlService().) + // This could have been combined in servicing the MS, + // but efficiency is not an issue and this seems more clear. + // Be a little careful since we are deleting from the list we are iterating. + { + TBF *tbf; + RN_MAC_FOR_ALL_TBF(tbf) { + // This call may remove the tbf from the list being iterated: + tbf->mtServiceUnattached(); + } + } + + // Step: Service the MSs; they may want to start new TBFs. + MSInfo *ms; + RN_MAC_FOR_ALL_MS(ms) { + ms->msService(); + } + GPRSLOG(16) << "macServiceLoop: after ms service"; + + // Step: Feed each downlink PDCH with RadioBlocks. + // As a side effect, this services all the [connected] TBFs in round robin order. + extDyn.edReset(); + RN_MAC_FOR_ALL_PDCH(pdch) { // for all channels assigned to GPRS. + extDyn.edSetCn(pdch->CN()); + pdch->downlink()->dlService(); + } + GPRSLOG(16) << "macServiceLoop: after downlink service"; + + // LONG RANGE TODO: At the end of a TBF, if the downlink TBF queue is low, + // we should send flow control to BSSG. See engineRecvAckNack + // But sadly, the SGSN does not implement flow control, so dont bother. + + // Step: Service the BSSG queue. + // This mostly means moving any downlink PDUs into their MS. + if (! GPRSConfig::sgsnIsInternal()) { +#if INTERNAL_SGSN==0 + processBSSGMessages(); +#endif + GPRSLOG(16) << "macServiceLoop: after bssg service"; + } else { + processSgsnMessages(); + } + + // Step: gather statistics about this loop. + if (GPRSDebug) { + double elapsed = timef() - starttime; + Stats.macServiceLoopTime.addPoint(elapsed); + } +} + +static void *macThreadFunc(void *arg) +{ + gL2MAC.macRunning = true; + + // Set the current RLC BSN. + //Time tstart = gBTS.time().FN(); + Time tstart = gBTS.time(); + int fnstart = tstart.FN(); + gBSNNext = FrameNumber2BSN(fnstart); + + ChIdleCounter = ChCongestionCounter = 0; + + // Lets start synced up on a 52-multiframe boundary. + if (0) { // why bother? + int offset = fnstart % 52; + if (offset) { + fnstart += (52 - offset); + gBTS.clock().wait(fnstart); + } + } + + GPRSLOG(1) << "macServiceLoop starting:" << LOGVAR(gBSNNext) < +namespace GPRS { +extern void mac_debug(); + +// (pat) About BSN (radio block sequence numbers): +// We need a period for our internal BSNs, and it is somewhat arbitrary. +// For timing, we need a cyclic period of at least 8 52-multiframes. (GSM03.64 Figure 19) +// There are specified timeouts of up to 5 seconds over which it would be convenient +// if radio block numbers were non-repeating, which would be about 1000 frames, 250 radio blocks. +// But there is no reason not to just use the hyperframe, so we will. +// A hyperframe is 2048 * 26 * 51 frames; (2048*26*51 * 12/52) = 626688 Radio Blocks. +// A 52 multiframe takes 240ms, so each radio block averages 20ms. +// btw, if the block sequence numbers went on forever, stored in an int, it would last 497 days. +// There are 52 frames for 12 blocks. + +// In GSM the downlink block numbers trail the uplink block numbers slightly. +// I observed the incoming blocks are this far behind gBSNNext. +// If you are expecting an answer at time N, +// look for it when gBSNext == N+BSNLagTime. +const int BSNLagTime = 4; + +extern bool gFixTFIBug; +extern bool gFixSyncUseClock; // For debugging: Use wall time for service loop synchronization + // instead of GSM frames. TODO: Get rid of this. +extern int gFixIdleFrame; // Works around this bug - see code. +extern int gFixDRX; // Works around DRX mode bug. The value is the number of assignments + // that are unanswered before we assume the MS is in DRX mode. + +// Set to 1 to request a poll in the ImmediateAssignment on CCCH. +// We still have to wait for the poll time anyway, so this is here +// only for debugging. +extern bool gFixIAUsePoll; +extern bool gFixConvertForeignTLLI; + + +typedef RList PDCHL1FECList_t; +typedef RList MSInfoList_t; + +struct Stats_t { + Statistic macServiceLoopTime; + UInt_z countPDCH; + UInt_z countMSInfo; + UInt_z countTBF; + UInt_z countRach; +}; +extern struct Stats_t Stats; + + + +// For now, there is only one pool of TFIs shared by all channels. +// It should be per-ARFCN, but I expect there to only be one. +// Sharing TFIs between channels on the ARFCN makes multislot tfi allocation easy. +// There are 32 uplink and 32 downlink TFIs, which should be enough for some time to come. +// If this gets congested, can be split up into one pool of TFIs per uplink/downlink channel, +// but then you have to be careful when allocating multislot tfis that the +// tfi is unique across all channels. +// TODO: When we allocate multislot tfis, the tfi must be unique in all slots. +// The easiest way to do that is to have a single tfilist for the entire system. +class TBF; +struct TFIList { + TBF *mTFIs[2][32]; // One list for uplink, one list for downlink. + + // 12-22-2011: It looked like the Blackberry abandoned an uplink TBF when + // a downlink TBF was established using the same TFI. This seems like a horrible + // bug in the MS, but to work around it, I added the gFixTFIBug which uses + // a single TFI space for both uplink and downlink. This eliminated + // a bunch of msStop calls with cause T3101, so I think this is a genuine + // problem with MSs and we need this fix in permanently. + // Update: The TFI is reserved during the time after a downlink ends, so the MS + // may have been justifiably upset about seeing it reissued for an uplink too soon. + RLCDir::type fixdir(RLCDir::type dir) { + return gFixTFIBug ? RLCDir::Up : dir; + } + + TFIList(); + + TBF *getTFITBF(RLCDirType dir, int tfi) { return mTFIs[fixdir(dir)][tfi]; } + void setTFITBF(int tfi,RLCDirType dir,TBF *tbf) { mTFIs[fixdir(dir)][tfi] = tbf; } + int findFreeTFI(RLCDirType dir); + void tfiDump(std::ostream&os); +}; +extern struct TFIList *gTFIs; +#if MAC_IMPLEMENTATION +TFIList::TFIList() { for (int i=0;i<32;i++) { mTFIs[0][i] = mTFIs[1][i] = NULL; } } +int TFIList::findFreeTFI(RLCDirType dir) +{ + static int lasttfi = 0; + dir = fixdir(dir); + // TODO: After the TBF the tfi may only be reused by the same MS for some + // period of time. See "TBF release" section. + // Temporary work around is to round-robin the tfis. + for (int i = 0; i < 32; i++) { + lasttfi++; + if (lasttfi == 32) { lasttfi = 0; } + if (!mTFIs[dir][lasttfi]) { return lasttfi; } + } +#if 0 + // DEBUG: start at 1 instead of 0 + for (int tfi = 1; tfi < 32; tfi++) { + // DEBUG: try incrementing tfi to avoid duplication errors: + if (tfi == lasttfi) { continue; } + if (!mTFIs[dir][tfi]) { + lasttfi = tfi; + return tfi; + } + } +#endif + return -1; +} + +void TFIList::tfiDump(std::ostream&os) +{ + int dir, tfi; + for (dir = 0; dir <= (gFixTFIBug ? 0 : 1); dir++) { + os << "TFI=("; + if (!gFixTFIBug) { + os << RLCDir::name(dir) << ":"; + } + for (tfi = 0; tfi < 32; tfi++) { + TBF *tbf = mTFIs[dir][tfi]; + if (tbf) { os << " " << tfi << "=>" << tbf; } + } + } + os << ")\n"; +} +#endif + +// The USF associates radio blocks with an MS. +// It is placed in the downlink block header to indicate that the next uplink block +// is allocated to the MS assigned that USF. +// There may be multiple simultaneous uplink TBFs from the same MS; all will use the same USF. +// The MS does that if a higher priority or throughput TBF comes in while one is in progress. +// The USF is only 3 bits, and 0x7 is reserved (to indicate PRACH) on PCCCH channels. +// We are not using PCCCH, but I am going to avoid 0x7 anyway. +// Additionally, for RACH initiated single block uplink assignments, we need a USF +// that is not in use by any MS, for which we will reserve USF=0. +// so really only 6 USFs (1-6) are available per uplink channel, which should be plenty. +// Note that there are alot more TFIs than USFs, because TFIs are per-TBF, +// while USFs are per-MS. +// The USFList information applies to an uplink channel, but it is used primarily by +// the downlink channel to set the USF in the MAC header of each downlink block. +// Note that there is no pointer to the TBFs (could be multiple ones) from this USF struct, +// because arriving uplink blocks are associated with their TBFs using the TFI, not the USF. +// The USFs are numbered 0..7. +const int USFMIN = 1; +const int USFMAX = 6; +const int USFTotal = 8; +#define USF_VALID(usf) ((usf) >= USFMIN && (usf) <= USFMAX) +class USFList +{ + //PDCHL1FEC *mlchan; // The channel these USFs are being used on. + // We use the usf as the index, so mlUSFs[0] is unused. + struct UsfInfo { + MSInfo *muMS; // The MS assigned this USF on this channel. + GprsTimer muDeadTime; // When a TBF dies reserve the USF for this ms until this expires. + }; + UsfInfo mlUSFs[USFTotal]; // Some slots in this array are unused. + + //int mRandomUSF; // Used to pick one of the USFs. + + // When the channel is running, we save the usf that is sent on each downlink block, + // so that we can correlate the uplink responses independently of their content. + // We save them in an array indexed by bsn, length only has to cover the difference + // between uplink and downlink BSNs plus some slop, so 32 is way overkill. + unsigned char sRememberUsf[32]; // The usf transmitted in the downlink block. + unsigned sRememberUsfBsn[32]; + + public: + USFList(); + + // Return the usf that was specified on the downlink burst, given the bsn from the uplink burst. + int getUsf(unsigned upbsn); + + // Remember the usf transmitted on specified downlink burst. OK to pass 0 for usf. + void setUsf(unsigned downusf, unsigned downbsn); // Save usf for current downlink burst. + + // Which MS is using this USF? + MSInfo *getUSFMS(int usf); + + int allocateUSF(MSInfo *ms); + int freeUSF(MSInfo *ms,bool wReserve); + //int getRandomUSF(); + void usfDump(std::ostream&os); +}; + +struct RachInfo +{ + unsigned mRA; + const GSM::Time mWhen; + RadData mRadData; + + // Gotta love this language. + RachInfo(unsigned wRA, const GSM::Time &wWhen, RadData wRD) + : mRA(wRA), mWhen(wWhen), mRadData(wRD) + { RN_MEMCHKNEW(RachInfo) } + ~RachInfo() { RN_MEMCHKDEL(RachInfo) } + void serviceRach(); +}; + + +// There is only one of these. +// It holds the lists used to find all the other stuff. +class L2MAC +{ + Thread macThread; + // The entire MAC runs in a single thread. + // This Mutex is used at startup to make sure we only start one. + // Also used to lock the serviceloop so the CLI does not modify MS or TBF lists simultaneously. + public: + mutable Mutex macLock; + + // The RACH bursts come in unsychronized to the rest of the GPRS code. + // The primary purpose of this queue is just to allow the MAC service loop + // to handle the RACH in its single thread by saving the RACH until the MAC service + // loop gets around to it. If GPRS is running, we dont really expect multiple + // simultaneous RACHs to queue up here because we service the RACH queue on each loop. + // However, if a RACH comes in while GPRS service is stopped and all channels + // are in use for RR connections, the as-yet-unserviced RACHs may queue up here. + // When GPRS service resumes, we should disard RACHs that are too old. + // Note that there could be multiple RACH for the same MS, a case we cannot detect. + InterthreadQueue macRachQ; + + // We are doing a linear search through these lists, but there should only be a few of them. + PDCHL1FECList_t macPDCHs; // channels assigned to GPRS. + PDCHL1FECList_t macPacchs; // The subset of macPDCHs that we assign as PACCH. + //Mutex macTbfListLock; + TBFList_t macTBFs; // active TBFs. + MSInfoList_t macMSs; // The MS we know about. + + // For debugging, we keep expired TBF and MS around for post-mortem examination: + TBFList_t macExpiredTBFs; + MSInfoList_t macExpiredMSs; + +#define RN_MAC_FOR_ALL_PDCH(ch) RN_FOR_ALL(PDCHL1FECList_t,gL2MAC.macPDCHs,ch) +#define RN_MAC_FOR_ALL_PACCH(ch) RN_FOR_ALL(PDCHL1FECList_t,gL2MAC.macPacchs,ch) +#define RN_MAC_FOR_ALL_MS(ms) for (RListIterator itr(gL2MAC.macMSs); itr.next(ms); ) +#define RN_MAC_FOR_ALL_TBF(tbf) for (RListIterator itr(gL2MAC.macTBFs); itr.next(tbf); ) + + L2MAC() + { + gTFIs = new TFIList(); + } + ~L2MAC() { delete gTFIs; } + + public: + unsigned macN3101Max; + unsigned macN3103Max; + unsigned macN3105Max; + unsigned macT3169Value; + unsigned macT3191Value; + unsigned macT3193Value; + unsigned macT3168Value; + unsigned macT3195Value; + unsigned macMSIdleMax; + unsigned macChIdleMax; + unsigned macChCongestionMax; + unsigned macDownlinkPersist; + unsigned macDownlinkKeepAlive; + unsigned macUplinkPersist; + unsigned macUplinkKeepAlive; + float macChCongestionThreshold; + Float_z macDownlinkUtilization; + + Bool_z macRunning; // The macServiceLoop is running. + time_t macStartTime; + Bool_z macStopFlag; // Set this to terminate the service thread. + Bool_z macSingleStepMode; // For debugging. + + MSInfo *macFindMSByTlli(uint32_t tlli, int create = 0); + void macAddMS(MSInfo *ms); + void macForgetMS(MSInfo *ms,bool forever); + + // When deleting tbfs, macForgetTBF could be called on a tbf already removed + // from the list, which is ok. + void macAddTBF(TBF *tbf); + void macForgetTBF(TBF *tbf,bool forever); + + void macServiceLoop(); + PDCHL1FEC *macPickChannel(); // pick the least busy channel; + PDCHL1FEC *macFindChannel(unsigned arfcn, unsigned tn); // find specified channel, or null + unsigned macFindChannels(unsigned arfcn); + bool macAddChannel(); // Add a GSM RR channel to GPRS use. + bool macFreeChannel(); // Restore a GPRS channel back to GSM RR use. + void macForgetCh(PDCHL1FEC*ch); + void macConfigInit(); + bool macStart(); // Fire it up. + void macStop(bool channelstoo); // Try to kill it. + int macActiveChannels(); // Is GPRS running, ie, are there channels allocated? + int macActiveChannelsC(unsigned cn); // Number of channels on specified 0-based ARFCN + float macComputeUtilization(); + void macCheckChannels(); + void macServiceRachQ(); +}; +extern L2MAC gL2MAC; + +//const int TFIInvalid = -1; +//const int TFIUnknown = -2; + +// The master clock is not exactly synced up with the radio clock. +// It is corrected at intervals. This means there is a variable delay +// between the time we send a message to the MS and when we can expect an answer. +// It does not affect RRBP reservations, which are relative, but it affects +// L3 messages sent on CCCH, which must specify an exact time for the response. +// I'm not sure what to do about this. +// I am just adding some ExtraDelay, and if reservations are unanswered, +// increasing this value until they start getting answered. +// We could probably set this back to 0 when we observe the time() run backwards, +// which means the clock is synced back up. +extern int ExtraClockDelay; // in blocks. + +// This specifies a Radio Block reservation in an uplink channel. +// Reservations are used for: +// o response to CCCH Immediate Assignment One BLock initiated by MS on RACH. +// In this case we dont even know what MS the message was coming from, so if +// it does not arrive, nothing we can do but wait for the MS to try again later. +// o For an Uplink TBF: The RLCUpEngine sends AckNack every N blocks. +// We could require a response, but I dont think we will unless it gets stalled. +// o For a stalled Uplink TBF: The RLCUpEngine sends an AckNack with an RRBP +// reservation. The MS may respond with any type of message. +// If that response does not arrive, the RLCUpEngine network immediately sends +// another AckNack. Serviced when Uplink serviced. +// o For a completed Uplink TBF: network sends a final acknack with RRBP reservation, +// which must be repeated until received. +// Could be handled by the engine or MsgTransaction. +// o For a Downlink TBF: send an RRBP reservation every N blocks, which we expect +// the MS to use to send us an AckNack, or some other message. If we dont get +// a response, send another RRBP reservation immediately. +// o For a stalled Downlink TBF: resend the oldest block with another RRBP reservation. +// Note: a completed Downlink TBF can be destroyed immediately, since we received +// the final ack nack. +// The following are handled by the MsgTransaction class: +// o response to Packet Polling Request message. +// If the message does not arrive, we may want to try again. +// o RRBP responses in downlink TBF data blocks or control blocks. +// If these dont arrive from the MS, it doesnt matter. +// The response type is actually unknown: it could be a Packet Downlink Ack/Nack, +// or anything else the MS wants to send. The uplink message will have all the required +// info so we dont have to save anything in the RLCBlockReservation; +// o Packet Control Acknowledgement responses. Could come from: +// - Packet uplink/downlink assignment message, which may require network to resend. +// - Packet TBF release message, which requires network to resend. + +// o WRONG: For an Uplink TBF: The RLCUpEngine sends AckNack after N blocks and provides an RRBP +// uplink response. The MS may respond with any type of message. +// If that response does not arrive, the RLCUpEngine network immediately sends +// another AckNack. +struct RLCBlockReservation : public Text2Str { + enum type { + None, + ForRACH, + ForPoll, // For the poll response to a downlink immediate assignment when + // the MS is in packet idle mode. see sendAssignment() + ForRRBP // This has many subtypes of type MsgTransactionType + }; + type mrType; // Primary type + MsgTransactionType mrSubType; // Subtype, only valid if mrType is ForRRBP + RLCBSN_t mrBSN; // The block number that has been reserved. + TBF *mrTBF; // tbf if applicable. (Not known for MS initiated RACH.) + // TODO: Is it stupid to save mrRadData? We will get new data when the MS responds. + RadData mrRadData; // Saved from a RACH to be put in MS when it responds. + static const char *name(RLCBlockReservation::type type); + void text(std::ostream&) const; +}; +std::ostream& operator<<(std::ostream& os, RLCBlockReservation::type &type); +std::ostream& operator<<(std::ostream& os, RLCBlockReservation &res); + +#if MAC_IMPLEMENTATION +const char *RLCBlockReservation::name(RLCBlockReservation::type mode) +{ + switch (mode) { + CASENAME(None) + CASENAME(ForRACH) + CASENAME(ForPoll) + CASENAME(ForRRBP) + default: return "unrecognized"; // Not reached, but makes gcc happy. + } +} +std::ostream& operator<<(std::ostream& os, RLCBlockReservation::type &type) +{ + os << RLCBlockReservation::name(type); + return os; +} +void RLCBlockReservation::text(std::ostream &os) const +{ + os << "res=("; + os << LOGVAR2("bsn",mrBSN) << " " << name(mrType); + if (mrTBF) { os << " " << mrTBF; } + os <<")"; +} +std::ostream& operator<<(std::ostream& os, RLCBlockReservation &res) +{ + res.text(os); + return os; +} +#endif + +// The uplink reservation system. +// It is used by both uplink and downlink parts. +// I put it in its own class to avoid clutter elsewhere. +// Note that reservations are kept around after the current time passes, +// so that uplink acknowledgements can be paired with the message to which they belong. +// +// In order to efficiently utilize the uplink resource, we need to make reservations +// of future uplink RLCBlocks for various purposes. +// There is a problem in that the RRBP field is limited to identifying +// blocks 3-6 blocks in the future. The obvious solution would be to reserve +// them first, but that will not work because the control messages occur asynchronously +// with respect to the data streams and (should) have higher priority. +// My K.I.S.S. system to efficiently use these resources is to reserve even numbered +// uplink blocks for RRBP responses, which are typically Ack/Nack blocks, +// but could actually be any type of block, and to reserve odd numbered uplink blocks +// for all other control blocks, which include Single-Block accesses initiated +// by RACH (in which case, there is no TFI or TLLI) and all other control block +// responses to network initiated messages. +// A minimum sized downlink response is 2 blocks long, so this method tends to fully +// utilize the channel and still limit latency (as opposed to alternative schemes +// that would assign fixed slots for various messages, or lump all the blocks together +// which would mean that RRBP responses would sometimes not have a reservation available.) +// Note that a single-block downlink resend as a result of an Ack/Nack message with +// a single Nack may be only one block long, so it is still possible to run +// out of odd numbered uplink blocks for RRBP responses. +// When downlink blocks are finally queued for transmission, any unreserved uplink +// blocks are utilized for current uplink data transfers using dynamic allocation using USF. +// Using this method, the reservations are also monotonically increasing in each +// domain (RRBP and non-RRBP) which makes it easy. +class L1UplinkReservation +{ + private: + // TODO: mLock no longer needed because RACH processing synchronous now. + Mutex mLock; // The reservation controller can be called from GSM threads, so protect it. + + public: + // There should only be a few reservations at a time, probably limited to + // around one per actively attached MS. + // Update 8-8-2012: well, the MS can be in two sub-modes of transmit mode simultaneously, + // specifically, persistent keep-alive and reassignment, so I am upgrading this + // with the sub-type. + // There are timeouts of up to 5 seconds (250 blocks), so we should keep history that long. + // The maximum reservation in advance is probably from AGCH, which can stack up + // waiting for CCCH downlink spots up to a maximum (defined by a config option) + // in AccessGrantResponder(), but currently 1.5 seconds. + // There is no penalty for making this array larger, so just go ahead and overkill it. + static const int mReservationSize = (2*500); + RLCBlockReservation mReservations[mReservationSize]; + + L1UplinkReservation(); + + private: + RLCBSN_t makeReservationInt(RLCBlockReservation::type restype, RLCBSN_t afterBSN, + TBF *tbf, RadData *rd, int *prrbp, MsgTransactionType mttype); + + public: + RLCBSN_t makeCCCHReservation(GSM::CCCHLogicalChannel *AGCH, + RLCBlockReservation::type type, TBF *tbf, RadData *rd, bool forDRX, MsgTransactionType mttype); + RLCBSN_t makeRRBPReservation(TBF *tbf, int *prrbp, MsgTransactionType ttype); + + // Get the reservation for the specified block timeslot. + // Return true if found, and return TFI in *TFI. + // bsn can be in the past or future. + RLCBlockReservation *getReservation(RLCBSN_t bsn); + + void clearReservation(RLCBSN_t bsn, TBF *tbf); + RLCBlockReservation::type recvReservation(RLCBSN_t bsn, TBF**restbf, RadData *prd,PDCHL1FEC *ch); + void dumpReservations(std::ostream&os); +}; + +extern bool setMACFields(MACDownlinkHeader *block, PDCHL1FEC *pdch, TBF *tbf, int makeres,MsgTransactionType mttype,unsigned *pcounter); +extern int configGetNumQ(const char *name, int defaultvalue); +extern int configGprsMultislotMaxUplink(); +extern int configGprsMultislotMaxDownlink(); + +// The USF is assigned in each downlink block to indicate if the uplink +// block in the same frame position is available for uplink data, +// or reserved for some other purpose. +// There are only 7 USFs available, so we have to share. +// TODO: MOVE TO UPLINK RESERVATION: +//class L1USFTable +//{ +// TBF *mUSFTable[8]; +// public: +// void setUSF(RLCBSN_t bsn, TBF* tbf) { mUSFTable[bsn % mUSFTableSize] = tbf; } +// TBF *getTBFByUSF(RLCBSN_t bsn) { return mUSFTable[bsn % mUSFTableSize]; } +// +// L1USFTable() { for (int i = mUSFTableSize-1; i>= 0; i--) { mUSFTable[i] = 0; } } +//}; + +}; // namespace GPRS + +#endif diff --git a/GPRS/MSInfo.cpp b/GPRS/MSInfo.cpp new file mode 100644 index 00000000..b12d530b --- /dev/null +++ b/GPRS/MSInfo.cpp @@ -0,0 +1,1127 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "MSInfo.h" +#include "TBF.h" +#include "FEC.h" +#include "RLCMessages.h" +#include "BSSG.h" +#include "RLCEngine.h" +#include "Sgsn.h" +#include // for strchr +#include // For fmtflags, setprecision + +namespace GPRS { + + +// This stores the multislot class info that we need from 3GPP 45.002 annex B +// We dont save Ttb or Trb because: +// Trb is 1 or less for all multislot classes >= 2, so we satisfy the constraint +// by leaving one tn timeslot between transmit and receive timeslots. +// Ttb is 1 or less for all multislot classes with Rx > 2. +// Also see 3GPP 45.002 table 6.4.2.2.1 +struct MultislotClass { + uint8_t mMultislotClass; + uint8_t mMultislotRx; // Max downlink channels. + uint8_t mMultislotTx; // Max uplink channels. + uint8_t mMultislotSum; // Max Rx + Tx +}; + +// The MS uses the idle frames in the 52-multiframe to make measurements. +// I think the Tta and Tra apply to the frames surrounding the idle frame. +// 45.008 10.2.3.2.1 says that if the MS does not have time to perform +// the measurement it can skip a transmit or receive timeslot +// adjacent to the idle frame. + +static const int sMultislotMax = 45; // Table entries numbered 1-45. +static MultislotClass sMultislotInfo[sMultislotMax] = { + {1, 1, 1, 2}, + {2, 2, 1, 3}, + {3, 2, 2, 3}, + {4, 3, 1, 4}, + {5, 2, 2, 4}, + {6, 3, 2, 4}, + {7, 3, 3, 4}, + {8, 4, 1, 5}, + {9, 3, 2, 5}, + {10, 4, 2, 5}, + {11, 4, 3, 5}, + {12, 4, 4, 5}, + {13, 3, 3, 16}, // These ones with sum=16 are class 2 MS, + {14, 4, 4, 16}, // ie, send and receive simultaneously. + {15, 5, 5, 16}, + {16, 6, 6, 16}, + {17, 7, 7, 16}, + {18, 8, 8, 16}, + {19, 6, 2, 16}, + {20, 6, 3, 16}, + {21, 6, 4, 16}, + {22, 6, 4, 16}, + {23, 6, 6, 16}, + {24, 8, 2, 16}, + {25, 8, 3, 16}, + {26, 8, 4, 16}, + {27, 8, 4, 16}, + {28, 8, 6, 16}, + {29, 8, 8, 16}, + {30, 5, 1, 6}, + {31, 5, 2, 6}, + {32, 5, 3, 6}, + {33, 5, 4, 6}, + {34, 5, 5, 6}, + {35, 5, 1, 6}, + {36, 5, 2, 6}, + {37, 5, 3, 6}, + {38, 5, 4, 6}, + {39, 5, 5, 6}, + {40, 6, 1, 7}, + {41, 6, 2, 7}, + {42, 6, 3, 7}, + {43, 6, 4, 7}, + {44, 6, 5, 7}, + {45, 6, 6, 7} +}; + + +// Return an entry from the 45.002 Annex B table. +// We use T(rb) and T(tb) instead of T(ra), T(ta) because the latter are only used +// when there is a requirement for adjacent cell measurements. +// We dont need to worry much about T(rb) and T(tb) because we will +// always just keep Trb >= 1. This constraint is satisfied by allocating +// adjacent channels when Rx <= 2 or Tx <= 2. +static MultislotClass getMultislotClass(MSInfo *ms) +{ + int multislotclass = ms->sgsnGetMultislotClass(ms->msTlli); + GPRSLOG(1) << ms <45 are illegal. + if (multislotclass <= 0 || multislotclass > sMultislotMax) { + return sMultislotInfo[0]; // No multislot capability. + } else { + MultislotClass result = sMultislotInfo[multislotclass-1]; + devassert((int)result.mMultislotClass == multislotclass); + return result; + } +} + +MSInfo::MSInfo(uint32_t tlli) + : msDebugId(++Stats.countMSInfo), + msTlli(tlli), + msPacch(0), + //msMode(RROperatingMode::PacketIdle), + msT3191(gL2MAC.macT3191Value), + msT3193(gL2MAC.macT3193Value), + msT3168(gL2MAC.macT3168Value) + //msTxxxx(5000) // Needs initialization to prevent abort when we test it, + // but we will set it again to the real value when we use it. +{ + gReports.incr("GPRS.MSInfo"); + gL2MAC.macAddMS(this); +} + +bool MSInfo::msIsSuspended() +{ + SGSN::GmmState::state state = sgsnGetRegistrationState(this->msTlli); + return state == SGSN::GmmState::GmmRegisteredSuspsended; +} + +// Extended dynamic mode means # channels up > # channels down. +bool MSInfo::msCanUseExtendedUplink() +{ + // The Blackberry and iphone set the GeranFeaturePackI bit, but + // the danged Multitech modems truncate the MS capabilities before + // the GeranFeaturePackI bit, even though they do support extended uplink TBF + // They are multislot class 12, so if the MS multislot class > 10, assume ok. + return gL2MAC.macUplinkPersist > 0 && this->msIsRegistered() + && (this->sgsnGetGeranFeaturePackI(this->msTlli) || + this->sgsnGetMultislotClass(this->msTlli) > 10); +} + +bool MSInfo::msIsRegistered() +{ + SGSN::GmmState::state state = sgsnGetRegistrationState(this->msTlli); + return state == SGSN::GmmState::GmmRegisteredNormal || + state == SGSN::GmmState::GmmRegisteredSuspsended; +} + +void MSInfo::msCountUSFGrant(bool penalize) +{ + // It does not matter that this is not the exact slot when the USF was granted, + // because the msLastUsfGrant is used only to compare the same value in other MS. + msLastUsfGrant = gBSNNext; + msNumDataUSFGrants++; + // Note: We are supposed to set N3101 if the uplink USF block + // is unused, not when we grant it - so add a bit to + // the N3101 max count to account for this. + if (penalize) { + msN3101++; + } +} + + +const char *RROperatingMode::name(RROperatingMode::type mode) +{ + switch (mode) { + CASENAME(PacketIdle) + CASENAME(PacketTransfer) + //CASENAME(DualTransfer) + //CASENAME(Camped) + } + return "unrecognized"; // Not reached, but makes gcc happy. +} +std::ostream& operator<<(std::ostream& os, const RROperatingMode::type &mode) +{ + os << RROperatingMode::name(mode); + return os; +} + + +void MSInfo::msDelete(bool forever) +{ + gL2MAC.macForgetMS(this,forever); // MS destruction happens in here. +} + +// Relinquish any USFs belonging to this MS, if they are no longer in use by any TBFs. +// This function is in the MS, not the TBF, because there could be multiple TBFs +// sharing the same USFs. (Each TBF has a unique TFI, but share the USF.) +void MSInfo::msCleanUSFs() +{ + bool anyactiveuplinks = false; + TBF *tbf; + RN_MS_FOR_ALL_TBF(this,tbf) { + //if (tbf->mtDir == RLCDir::Up && tbf->mtGetState() != TBFState::Deleting) + // Update 8-13: Dead TBFs are no longer needed to reserve USFs, but + // they are still attached, so ignore them for this purpose. + if (tbf->mtDir == RLCDir::Up && tbf->mtAttached && tbf->mtGetState() != TBFState::Dead) { + anyactiveuplinks = true; + break; + } + } + + int chcnt = 0; + int freedusf = 0; + if (!anyactiveuplinks) { + // Relinquish any USFs we may have. + PDCHL1Uplink *up; + RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) { + chcnt++; + freedusf = up->mchParent->freeUSF(this,false); + } + msNumDataUSFGrants = 0; + msAckNackUSFGrant = -1; + for (int u = 0; u < USFMAX; u++) { msUSFs[u] = 0; } // Not used, but be tidy. + } + GPRSLOG(1) <<"CleanUSFs"<mchParent->freeUSF(this,true); + } +} + +//void MSInfo::setUSFGrantBSN(int usf) +//{ + //devassert(usf >= USFMIN && usf <= USFMAX); + //msLastUSFGrantBSN = gBSNNext; +//} + + +// How many timeslots assigned so far? +//static int chsum(MSInfo *ms) +//{ +// return ms->msPCHDowns.size() + ms->msPCHUps.size(); +//} + +// The multitech modem supports: +// o precendence class (1,2,3,0) +// o mean throughput, which is not precise enough: +// mean=9 50,000 (~111 bit/s) +// mean=10 100,000 (~0.22 kbit/s) +// mean=11 200 000(~0.44 kbit/s) +// mean=12 500 000(~1.11 kbit/s) +// mean=13 1,000,000 (~2.2 kbit/s) <- class 2 shared x 8 +// mean=14 2,000,000 (~4.4 kbit/s) <- class 2 shared x 4 +// mean=15 5,000,000 (~11.1 kbit/s) <- class 2 shared x 2 +// mean=16 10,000,000 (~22 kbit/s) <- class 2 dedicated. +// mean=17 20,000,000 (~44 kbit/s) <- map to class 3 dedicated +// mean=18 50,000,000 (~111 kbit/s) <- map to class 4 dedicated. What about 4U,4D? +// +// So that leaves guaranteed and maximum UL/DL values from 1-63Kbps: +// The max UL/DL will determine the class, and guaranteed the sharing factor. +// That doesnt work very well because class 2 and class 4 have the same max UL/DL. +// An sql option can determine how to handle 'best effort', either as the best +// available or a specified class. + + +// This should always succeed unless there is a logical bug during startup channel allocation. +// 45.008 10.1.1.2 describes what the MS will do if the multislot config does not give +// it sufficient time to take measurements. +// There is a BA(GPRS) list of BCCH carriers to monitor - where? +// 45.008 10.1.1.2: The MS shall perform the measurements during the block +// period where the polling response is sent. +// +// Here are pictures of the multislot assignments we make, the numbers are TN. +// Tr is the number of receive set-up timeslots; +// Tt is the number of transmit set-up timeslots. +// We can use either Tra and Ttb or Trb and Ttb. +// For multislot classes less than 10, Tra < Tta so we must leave the extra timeslots +// between receive and transmit to utilize the maximum number of slots. +// We are trying to avoid moving PACCH; in the pictures below TN1 is the PACCH. +// GSM uplink trails downlink separated by two empty slots. +// Normal single channel, Tr = 2, Tt = 4 +// 1D | 2 | 3 | 4 | 5 | 6 | 7 | 8 +// 6 | 7 | 8 | 1U | 2 | 3 | 4 | 5 +// 2-down/2-up, Tr = 1, Tt = 3: Uses TN1,2 PACCH on TN1 or 2, chs: P+1 or P-1 +// 1D | 2D | 3 | 4 | 5 | 6 | 7 | 8 +// 6 | 7 | 8 | 1U | 2U | 3 | 4 | 5 +// 3-down/2-up, Tr = 1, Tt = 2: Uses TN8,1,2 PACCH on TN1 or 2, chs: P+1&-1 or P-1&-2 +// 1D | 2D | 3 | 4 | 5 | 6 | 7 | 8D +// 6 | 7 | 8 | 1U | 2U | 3 | 4 | 5 +// 4-down/1-up, Tr = 1, Tt = 2: Uses TN7,8,1,2 PACCH on TN1, chs: P+1&-1&-2 +// 1D | 2D | 3 | 4 | 5 | 6 | 7D | 8D +// 6 | 7 | 8 | 1U | 2 | 3 | 4 | 5 + +// The multislot class 12 and higher support 1-down/4-up, and they also have +// Tta = Tra so we can put the extra empty slots on either side, but that doesnt help. +// 3GPP 45.002 table 6.4.2.2.1 specifies that Tra applies in these cases, +// so we dont have a choice. +// For extended dynamic uplink the lowest numbered uplink must be the downlink. +// Note: There is something called "Shifted USF" but it only applies to super +// high multislot classes. +// 2-down/3-up, uses TN1,2,3 PACCH on TN1 or 2, chs: P+1&+2 or P+1&-1. +// 1D | 2D | 3 | 4 | 5 | 6 | 7 | 8 +// 6 | 7 | 8 | 1U | 2U | 3U | 4 | 5 +// 1-down/4-up, Tra=2, Ttb=1 uses TN1,2,3,4, ie P+1&P+2&P+3 +// 1D | 2 | 3 | 4 | 5 | 6 | 7 | 8 +// 6 | 7 | 8 | 1U | 2U | 3U | 4U | 5 + + +// The following are illegal because for extended dynamic uplink +// the lowest numbered uplink must be the downlink. +// Also, 3GPP 45.002 table 6.4.2.2.1 specifies that Tra applies in these cases, not Tta. +// 2-down/3-up, Tr = 1, Tt = 2: Uses TN8,1,2 PACCH on TN1 or 8, chs: P+1&-1 or P+1&+2 +// 1D | 2 | 3 | 4 | 5 | 6 | 7 | 8D +// 6 | 7 | 8U | 1U | 2U | 3 | 4 | 5 +// 1-down/4-up, Tr=1 Tt=2 uses TN8,1,2,3 PACCH on TN1, chs: P-1&P+1&P+2 +// 1D | 2 | 3 | 4 | 5 | 6 | 7 | 8 +// 6 | 7 | 8U | 1U | 2U | 3U | 4 | 5 + +// Here are Tt=1 alternatives: +// Aternative 4-down/1-up for class 12, Tr = 2, Tt = 1: Uses TN6,7,8,1 PACCH on TN1 +// chs: P-1&P-2&P-3 +// 1D | 2 | 3 | 4 | 5 | 6D | 7D | 8D +// 6 | 7 | 8 | 1U | 2 | 3 | 4 | 5 +// So we can do 3-down/2-up and 2-down/3-up on 3 adjacent channels, +// but if we want to do 4-down/1-up and 1-down/4-up without moving PACCH +// we need 6 adjacent channels. +// +// GPRS Classes of service. +// Full bandwidth full duplex (4D/1U to 1D/4U), requires 5xTN or 2 per ARFCN. +// Full bandwidth assymetric only takes 4 adjacent TN, ex: 3D/1U to 1D/4U +// These may be shared or not. Strategically we may move the PACCHs around later. +// The more typical 2D/2U and uses PACCH on every other ARFCN, has a guaranteed +// bandwidth of 2x because even though the neighbor may encroach on it, it can +// also encroach on its neighbor. +// How about Guaranteed Bandwidth and Minimum Bandwidth in up/down. +// The ones that make sense, assuming 1 channel = 12Kpbs, are: +// class 5 adjacent TN: 48D/48U half-duplex, +// class 4 adjacent TN: 48D/36U, 36D/48U half-duplex, <- must specify which +// Or we could call the above class 4, 4D and 4U. +// class 3 adjacent TN: 36D/36U half-duplex, no need to specify, can dynamically +// change between 36D/24U or 24D/36U full-duplex +// class 2 adjacent TN: 24D/24U full-duplex +// class 1, uses same as above but shared: 12U/12U. +// If you allocate 4 TNs on an ARFCN, if they are both class 2, then the bandwidth +// of the upper one can borrow from the lower one only when it is not busy, +// otherwise they share as follows: +// one shared D TN: 18D/24U and 30D/24U +// two shared D TN: 12D/24U and 36D/24U +// +// The sql options can specify reserved allocation of any number of classes, +// for example: '2x4,3x3,1x2'. +// For dynamic allocation, it could be based on one or more of the following: +// the QoS from the modem; +// the class to allocate, or maybe max and min classes. (default class 2) +// or maybe the only thing we support for dynamic is class 2. +// total number of dynamic gprs channels to allocate; +// number of voice TCH to leave open; +// +// We can let the multitech modem specify the QoS. + + +// Until we support per-imsi bandwidth, it would be first-come first serve, or we +// could have the customer program the QoS in the multitech modem. +// If more phones register than will fit, we need to know if we are allowed to share, +// so we really need the sharing number, too. +// Or we could just use the priority: +// Allowing multiple classes makes no sense at all until we support per-imsi bandwidth, +// so for now I guess it is just a single static and a single dynamic class: +// StaticAllocation '2x4', DynamicAllocationClass.Max '4' DynamicAllocationClass.Min '2' +// Or we could specify the bandwidth, for example: 2x48D/48U or 4x24D/24U +// + +const unsigned cTnPerArfcn = 8; +static unsigned addTn(unsigned tn, int offset) +{ + return (unsigned) (tn + offset) % cTnPerArfcn; +} +static bool tnavail(unsigned chmask,unsigned tn) +{ + return chmask & (1 << (tn % cTnPerArfcn)); +} + +// The tnlist describes the tn slots used in this multislot configuration. +bool MSInfo::msAddCh(unsigned chmask, const char *tnlist) +{ + const char *cp, *pp = strchr(tnlist,'P'); + if (!pp) { + devassert(pp); // Caller goofed. tnlist must include a 'P' for PACCH location. + return false; // And thats a failure. + } + int before = pp-tnlist; // Number of channels needed before PACCH. + // Check for channel availability. + unsigned tn, tnfirst = addTn(msPacch->TN(),- before); +#if 0 + unsigned tn = msPacch->TN(); + int after = strlen(tnlist)-before-1; // Number of channels needed after PACCH. + if (before && !tnavail(chmask,tn-1)) {return false;} + if (before>1 && !tnavail(chmask,tn-2)) {return false;} + if (after && !tnavail(chmask,tn+1)) {return false;} + if (after>1 && !tnavail(chmask,tn+2)) {return false;} +#endif + GPRSLOG(4) << "msAddCh" <TN(),- before); + for (tn=tnfirst,cp=tnlist; *cp; cp++, tn=addTn(tn,1)) { + GPRSLOG(4) << "msAddCh loop" <ARFCN(),tn); + switch (*cp) { + case 'D': // downlink only timeslot. + msPCHDowns.push_back(ch->downlink()); + break; + case 'U': // uplink only timeslot. + msPCHUps.push_back(ch->uplink()); + break; + case 'B': // bidirection timeslot other than PACCH. + msPCHDowns.push_back(ch->downlink()); + msPCHUps.push_back(ch->uplink()); + break; + case 'P': // PACCH, already added so skip. + break; + } + } + return true; // finished +} + +// Try for a specific number of down and up timeslots. +// If there are multiple ways to satisfy the request, try them all. +// The PACCH has already been added to the channels. +// The letters correspond to timeslots and mean: +// D: downlink, U=uplink, B=bidir, P=PACCH (which is also bidir.) +// Any bidir timeslot could be the PACCH. +bool MSInfo::msTrySlots(unsigned chmask,int down,int up) +{ + // For extended dynamic uplink (ie, if number uplink channels > number downlink) + // then the first channel must be bidirectional, and the USF of the first + // channel allocates the uplink for all TNs. + switch ((down<<4)+up) { + case 0x41: return msAddCh(chmask,"DDPD"); + case 0x14: return msAddCh(chmask,"PUUU"); + case 0x32: return msAddCh(chmask,"DPB") || msAddCh(chmask,"DBP"); + case 0x23: return msAddCh(chmask,"PBU") || msAddCh(chmask,"BPU"); + case 0x22: return msAddCh(chmask,"PB") || msAddCh(chmask,"BP"); + // These are oddballs that would only be used for testing: + case 0x31: return msAddCh(chmask,"DPD") || msAddCh(chmask,"DDP"); + case 0x13: return msAddCh(chmask,"PUU"); + case 0x21: return msAddCh(chmask,"PD") || msAddCh(chmask,"DP"); + case 0x12: return msAddCh(chmask,"PU"); + default: devassert(0); return false; + } +} + +// Assign channels for the requested timeslots. +// If the multislot request cannot be satisfied, try others. +bool MSInfo::msAssignChannels2(int maxdown, int maxup, int sum) +{ + PDCHL1FEC *pdch1; + if (msPacch) { + pdch1 = msPacch; + } else { // Does this happen? + pdch1 = gL2MAC.macPickChannel(); + } + if (pdch1 == NULL) { + GPRSLOG(1) << "msAssignChannels failed" <uplink()); + msPCHDowns.push_back(pdch1->downlink()); + msPacch = pdch1; + +#if 0 + if (maxdown <= 1 && maxup <= 1) { return; } + + // Look for adjacent channels before (minus) and after (plus) our PACCH tn. + // We look up to two channels in both directions. + int plus = 0, minus = 0; + PDCHL1FEC *cplus[2]; cplus[0] = cplus[1] = 0; + PDCHL1FEC *cminus[2]; cminus[0] = cminus[1] = 0; + + unsigned tn = pdch1->TN(); + for (plus = 0; plus < 2; plus++) { + cplus[plus] = gL2MAC.macFindChannel(pdch1->ARFCN(),(unsigned)(tn+plus)%8); + if (!cplus[plus]) break; + } + for (minus = 0; minus < 2; minus++) { + cminus[minus] = gL2MAC.macFindChannel(pdch1->ARFCN(),(unsigned)(tn-minus)%8); + if (!cminus[minus]) break; + } +#endif + + unsigned mask = gL2MAC.macFindChannels(pdch1->ARFCN()); + GPRSLOG(2)<= 2 ? 2 : 1; + int down21 = maxdown >= 2 ? 2 : 1; + if (maxdown >= 4) { + msTrySlots(mask,4,1) || msTrySlots(mask,3,up21) || msTrySlots(mask,2,up21); + } else if (maxup >= 4) { + msTrySlots(mask,1,4) || msTrySlots(mask,down21,3) || msTrySlots(mask,down21,2); + } else if (maxdown == 3) { + msTrySlots(mask,3,up21) || msTrySlots(mask,2,up21); + } else if (maxup == 3) { + msTrySlots(mask,down21,3) || msTrySlots(mask,down21,2); + } else if (maxdown == 2) { + // The sum test is only needed for multislot class 3, + // which is the only class that can not do 2-down/2-up. + if (sum == 3) up21 = 1; + msTrySlots(mask,2,up21); + } else if (maxup == 2) { + if (sum == 3) down21 = 1; + msTrySlots(mask,down21,2); + } + + msPCHUps.sort(chCompareFunc); + msPCHDowns.sort(chCompareFunc); + +#if 0 + if (pdch2) { + if (maxdown > 1) { msPCHDowns.push_back(pdch2->downlink()); } + + // Do we want to add a third or fourth channel? They will be downlink only. + // We can satisfy multislot constraints (Tra=1 and Ttb=2) using + // either or both of the timeslots immediately prior to the two + // channels already allocated, but note that these ones cannot be PACCH. + if ((int)msPCHDowns.size() < maxdown && chsum(this) < slots.mMultislotSum) { + int tn2 = pdch2->TN(); + int tn = tn1ARFCN(),tn); + if (pdch3) { + msPCHDowns.push_back(pdch3->downlink()); + + // Can we do a 4-down config? + // In that case we will sacrifice an uplink TN. + if ((int)msPCHDowns.size() < maxdown && 5 <= slots.mMultislotSum) { + if (--tn < 0) { tn = 7; } + PDCHL1FEC *pdch4 = gL2MAC.macFindChannel(pdch1->ARFCN(),tn); + if (pdch4) { + msPCHDowns.push_back(pdch4->downlink()); + maxup = 1; // Limit to one channel up now. + } + } + } + } + + // The chsum test is only needed for multislot class 3, + // which is the only class that can not do 2-down/2-up. + if (maxup > 1 && chsum(this) < slots.mMultislotSum) { + msPCHUps.push_back(pdch2->uplink()); + } + } // have pdch2. +#endif + return true; +} + +bool MSInfo::msAssignChannels() +{ + // Get the channels we will use... + // For now, just use the one the request came in on. + // It would be better to make sure we pick a channel that has free USFs, + // but I'm not going to worry about it. + if (msPCHDowns.size() == 0) { + devassert(msPCHUps.size() == 0); + + // Check for multislot. Limit to sql options. + // User can control via sql: Multislot.Max, which can be over-ridden + // by: Multislot.Max.Uplink, Multislot.Max.Downlink. + int maxdown=1, maxup = 1; + maxdown = configGprsMultislotMaxDownlink(); + maxup = configGprsMultislotMaxUplink(); + // Defend against garbage input: + if (maxdown < 1) {maxdown = 1;} + if (maxup < 1) {maxup = 1;} + + MultislotClass slots; + if (maxdown > 1 || maxup > 1) { + // Check the multislot class of the phone: + slots = getMultislotClass(this); + + // Limit to phone capabilities. + if (maxdown > (int)slots.mMultislotRx) { maxdown = slots.mMultislotRx; } + if (maxup > (int)slots.mMultislotTx) { maxup = slots.mMultislotTx; } + GPRSLOG(1)< maxdown && ! msCanUseExtendedDynamic()) { + // maxdown = maxup; + //} + + // Update: This test moved into msAssignChannels2 + // This test is only needed for multislot class 3, + // which is the only class that can not do 2-down/2-up, + // and needs to be converted to 2-down/1-up, + // but we'll go ahead and do an exhaustive check. + //while (maxdown + maxup > slots.mMultislotSum) { + // if (maxup > 1) { + // maxup--; + // } else if (maxdown > 1) { + // maxdown--; + // } else { + // break; // This is impossible, but be safe. + // } + //} + } + + msAssignChannels2(maxdown,maxup,slots.mMultislotSum); + + LOGWATCHF("Channel Assign, max:down/up=%d/%d ch down/up=%d/%d\n", + maxdown,maxup,msPCHDowns.size(),msPCHUps.size()); + + // If we are multislot, log a message: + if (msPCHDowns.size() > 1) { + std::ostringstream os; + msDumpChannels(os); + LOG(INFO) << "Multislot assignment for "<isTransmitting()) { + GLOG(ERR) << "DeassignChannels while TBF transmitting:"<mtCancel(MSStopCause::Goof,TbfNoRetry); + } + } + // We dont call this if there are any active TBFs, but + // there could be attached TBFs that have not started yet. + // We must de-attach them to release the channels. + msPCHDowns.clear(); + msPCHUps.clear(); +} + +// TODO: +void MSInfo::msReassignChannels() +{ + msDeassignChannels(); + // TODO? + // tbf->mtDeReattach(); +} + +unsigned MSInfo::msGetDownlinkQueuedBytes() +{ + // Figure out how many bytes stacked up in the queue for this MS. + unsigned nbytes = 0; + TBF *tbf; + RN_MS_FOR_ALL_TBF(this,tbf) { + if (tbf->mtDir != RLCDir::Down) continue; + nbytes += tbf->engineDownPDUSize(); + } + return nbytes; +} + +// The MS is in PacketTransfer mode if any TBFs are currently running, stalled or not. +RROperatingMode::type MSInfo::getRROperatingMode() +{ + if (msCountTBF2(RLCDir::Either,TbfMTransmitting,NULL)) { + return RROperatingMode::PacketTransfer; + } else { + return RROperatingMode::PacketIdle; + } +} + + +// Count how many TBFs exist in the specified direction (which may be Either) +// are in the specified TbfMacroState. +// Normally there will only be one TBF. +// We also have to prevent starting a bunch of redundant nearly identical +// TBFs when the phone sends us a bunch of RACHes in a row. +int MSInfo::msCountTBF1(RLCDir::type dir, TbfMacroState tbfmstate, TBF**ptbf) const +{ + int count = 0; + TBF *tbf; + RN_MS_FOR_ALL_TBF(this,tbf) { + if (dir == RLCDir::Either || tbf->mtDir == dir) { + if (tbfmstate == TbfMActive && !tbf->isActive()) continue; + if (tbfmstate == TbfMTransmitting && !tbf->isTransmitting()) continue; + // We always ignore tbfs that are in the process of being deleted. + if (tbf->mtGetState() == TBFState::Deleting) continue; + if (tbf->mtGetState() == TBFState::Unused) continue; // shouldnt happen + if (ptbf) { *ptbf = tbf; } + count++; + } + } + return count; +} + +// Count TBFs for this real MS, which means all MSInfo belonging to the real MS. +int MSInfo::msCountTBF2(RLCDir::type dir, TbfMacroState tbfmstate, TBF**ptbf) +{ + int count = msCountTBF1(dir,tbfmstate,ptbf); + if (msAltTlli) { + MSInfo *ms2 = gL2MAC.macFindMSByTlli(msAltTlli,false); + if (ms2 == NULL) { + // The old MSInfo expired naturally, and we will never have to worry about it again. + msAltTlli = 0; + } else { + count += ms2->msCountTBF1(dir,tbfmstate,ptbf); + } + } + return count; +} + +int MSInfo::msCountActiveTBF(RLCDir::type dir, TBF**ptbf) +{ + return msCountTBF2(dir,TbfMActive,ptbf); +} + +int MSInfo::msCountTransmittingTBF(RLCDir::type dir, TBF**ptbf) +{ + return msCountTBF2(dir,TbfMTransmitting,ptbf); +} + +// Downlink time slots as defined by GSM04.60 12.18 +// And I quote: Bit 8 indicates the status of timeslot 0, +// bit 7 indicates the status of timeslot 1, etc. +// Note that TN() runs 0..7 (see Time::incTN()) +unsigned char MSInfo::msGetDownlinkTimeslots(MultislotSymmetry sym) +{ + unsigned char result = 0; + //if (sym == MultislotSymmetric && msPCHUps.size() < msPCHDowns.size()) { + // // If the uplink and downlink tn size are assymetric, + // // use the smaller array, which is always valid in both directions. + // PDCHL1Uplink *up; + // RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) { + // result |= (1 << (7 - up->TN())); + // } + //} else { + PDCHL1Downlink *down; + RN_FOR_ALL(PDCHL1DownlinkList_t,msPCHDowns,down) { + result |= (1 << (7 - down->TN())); + } + //} + return result; +} + + +//std::ostream& operator<<(std::ostream& os, const Statistic &stat) { stat.text(os); return os; } + +void SignalQuality::dumpSignalQuality(std::ostream&os) const +{ + ios_base::fmtflags savedfoobarflags = os.flags(); + os.precision(2); + os << "\t" << fixed; + os << LOGVAR2("TimingError",msTimingError); + os << LOGVAR2("RSSI",msRSSI); + os << LOGVAR2("CV",msCValue); + os << LOGVAR2("ILev",msILevel); + os << LOGVAR2("RXQual",msRXQual); + os << LOGVAR2("SigVar",msSigVar); + os << LOGVAR2("ChCoding",msChannelCoding); + os.flags(savedfoobarflags); // What were these guys thinking? + + //ChannelCodingType ccup = msGetChannelCoding(RLCDir::Up); + //ChannelCodingType ccdown = msGetChannelCoding(RLCDir::Down); + //int cc = min((int)ccup,(int)ccdown); + //if (ccup == ccdown) { + // os << LOGVAR2("ChannelCoding",(int)ccup); + //} else { + // os << format(" ChannelCoding=%dup/%ddown",ccup,ccdown); + //} + os << "\n"; +} + + +void SignalQuality::setRadData(RadData &rd) +{ + msRSSI.addPoint((int)rd.mRSSI); + msTimingError.addPoint(rd.mTimingError); +} + +void SignalQuality::setRadData(float wRSSI,float wTimingError) +{ + msRSSI.addPoint((int)wRSSI); + msTimingError.addPoint(wTimingError); +} + +// Determine whether we should use slow or fast channel coding for the specified direction. +ChannelCodingType MSInfo::msGetChannelCoding(RLCDirType wdir) const +{ + // Initial channel coding is determined from RSSI from most recent burst from MS. + // If the signal strength was low (less than -40db) then use the slow speed. + // TODO: For subsequent TBFs we should use statistics from previous TBFs. + // BEGINCONFIG + // 'GPRS.ChannelCodingControl.RSSI',-40,0,0,'If the initial signal strength is less than this amount in DB GPRS uses a lower bandwidth but more robust encoding CS-1' + // ENDCONFIG + + // Allow user full control over the codecs with these options: + const char *option = (wdir == RLCDir::Up) ? "GPRS.Codecs.Uplink" : "GPRS.Codecs.Downlink"; + const char *codecs = gConfig.getStr(option).c_str(); + // We only support CS1 and CS4. + bool cs1allowed = strchr(codecs,'1'); + bool cs4allowed = strchr(codecs,'4'); + if (cs1allowed && cs4allowed) { + // Choose codec based on initial signal strength: + int fastRSSI = gConfig.getNum("GPRS.ChannelCodingControl.RSSI"); + return (msRSSI.getCurrent() < fastRSSI) ? ChannelCodingCS1 : ChannelCodingCS4; + } else if (cs4allowed) { + return ChannelCodingCS4; + } else { + return ChannelCodingCS1; + } +} + +// UNUSED +// Not a MSInfo member function, but still related to MSInfo. +// This function is (was) used to implement CHANGE-TLLI from the BSSG interface. +// 3-2012: (pat) removed as procedurally incorrect. See notes at MSInfo struct. +// We need to keep a separate MSInfo for each TLLI. +// We might want to move some of the running timer info from the MSInfo for the old-TLLI +// to the MSInfo for the new-TLLI, but probably not even that because at the time this +// happens, the MSInfo for the new-TLLI will be the most recently used one, because +// we just received the attach-accept message. +// 6-2012: (pat) Changed my mind and replaced with msChangeTlli, msAliasTlli. +MSInfo *bssgMSChangeTLLI(uint32_t oldTLLI,uint32_t newTLLI) +{ + MSInfo *ms = NULL; + GPRSLOG(1) << "MSChangeTLLI"<msChangeTlli(newTLLI); + } else if ((ms = gL2MAC.macFindMSByTlli(oldTLLI, false))) { + ms->msAliasTlli(newTLLI); + ms->msChangeTlli(newTLLI); + } + //if ((ms = gL2MAC.macFindMSByTlli(oldTLLI, false))) { + // if (oldTLLI == ms->msTLLI) { + // ms->msSetTLLI(newTLLI); + // } + //} + return ms; +} + + +// This code is used with the integrated SGSN. +// The MS is identified by multilple TLLIs. +// The sgsn places the current tlli to be used in downlink in each message. +// This function makes sure that newTlli is the current one. +void MSInfo::msChangeTlli(uint32_t newTlli) +{ + if (! tlliEq(newTlli,msTlli)) { + // If the message is in the queue for this MS, the MS must have been + // identified by either msTlli or msOldTlli. + devassert(tlliEq(newTlli,msOldTlli)); + MSInfo *ms2 = gL2MAC.macFindMSByTlli(newTlli,false); + if (ms2 != this) { + // This is kind of a serious problem. + // It would only happen if the MS just happens to use a TLLI that is assigned by the SGSN. + LOG(ERR) << "Changing TLLI of"<msDelete(false); + } + + msOldTlli = msTlli; + msDeprecated = false; + if (msAltTlli) { + MSInfo *ms3 = gL2MAC.macFindMSByTlli(msAltTlli,false); + if (ms3 && ms3 != this) { msDeprecated = true; } + } + } + // The newTlli may differ by the TLLI_LOCAL_BIT, so always set msTlli. + msTlli = newTlli; +} + +// In addition to alias tllis, we also accept foreign TLLIs, see macFindMSByTlli. +void MSInfo::msAliasTlli(uint32_t otherTlli) +{ + if (otherTlli == 0) return; + if (!tlliEq(otherTlli,msTlli) && !tlliEq(otherTlli, msOldTlli)) { + MSInfo *ms2 = gL2MAC.macFindMSByTlli(otherTlli,false); + if (ms2) { + devassert(ms2 != this); + // Set the AltTlli so these two MSInfo structs reference each other, + // since they are the same MS, and wont try to start simultaneous TBFs. + // Conceivably there could be more than just two TLLIs, but I think it + // is ok because the MS uses them serially and it will all just work out. + // IMPORTANT: The alt tlli is NOT the old tlli. + // See the comments at MSinfo + ms2->msAltTlli = this->msTlli; + this->msAltTlli = ms2->msTlli; + } else { + // This code is processed by MAC when this message is first seen. + // Set oldTlli so that we will know the TLLI is the same MS. + // This may be switched by msChangeTlli when the message is processed. + this->msOldTlli = otherTlli; + } + } +} + +// Return index in data history arrays arrays, which is the current 48-block-multiframe +unsigned StatHits::histind() +{ + // There are approx 48 blocks per second. + unsigned now = (gBSNNext / 48); // Current 48-block-multiframe, modulo the hyperframe. + unsigned i = now % cNumHist; + if (now != mWhen[i]) { + // Saved data is over 10 seconds old; clear it. + mHistory[i].clear(); + } + mWhen[i] = now; + return i; +} + +void StatHits::getStats(float *pER, int *pTotal, float *pWorstER, int *pWorstTotal) +{ + *pWorstER = 0.0; + *pWorstTotal = 0; + int total = 0, good = 0; + int now = (gBSNNext / 48); // Current 48-block-multiframe, modulo the hyperframe. + + StatTotalHits *hp = &mHistory[0]; + for (int i = 0; i < cNumHist; i++, hp++) { + int age = (int)now - (int)mWhen[i]; // age of data in history bucket. +//printf(" i=%d now=%d when=%d age=%d",i,now,(int)mWhen[i],age); + if (age < 0) { age += RLCBSN_t::BSNPeriodicity / 48; } // Account for BSN wrap. + if (age >= cNumHist) { hp->clear(); continue; } // Data too old. + if (!hp->mTotal) {continue;} // empty + good += hp->mGood; + total += hp->mTotal; + float thisER = (float)(hp->mTotal-hp->mGood) / hp->mTotal; + if (thisER > *pWorstER) { + *pWorstER = thisER; + *pWorstTotal = hp->mTotal; + } + } + *pTotal = total; + *pER = total ? (float)(total-good)/total : 0; +//printf(" total=%d ER=%g\n",total,*pER); +} + +// Format a number with up to 2 significant digits if < 1, +// but dont go less than .01 or to exponential notation. +// Kind of amazing there is no default format for this. +std::string fmtfloat2(float num) +{ + if (num < 0.005) { // < 0.01 is 0 + return string("0"); + } else if (num < 0.2) { // .01 - .19, 2 sig digit + return format("%.2f",num).substr(1); + } else if (num < 1) { // .2 - .9, 1 sig digit ok. + return format("%.1f",num).substr(1); + } else if (num < 10) { // 1.0 - 9.9, 2 sig digit + return format("%.1f",num); + } else { // 10 - infinity, whatever digits needed. + return format("%.0f",num); + } +} + + +static void putER(std::ostream&os, const char*label, float er, int total) +{ + // Like this: "99.9% (7) low: 4% (2)" + os << label << fmtfloat2(er) << "% (" << total << ")"; +} + +// Print the average for the last N seconds and worst second. +void StatHits::textRecent(std::ostream &os) +{ + float avgER, worstER; + int total, worstTotal; + getStats(&avgER,&total,&worstER,&worstTotal); + putER(os,"",avgER,total); + // Dont bother to print worst if there is none. + if (worstER) { putER(os," low:",worstER,worstTotal); } +} + +void StatHits::textTotal(std::ostream&os) +{ + float er = mTotal.mTotal ? ((double)mTotal.mTotal - mTotal.mGood) / mTotal.mTotal : 0; + putER(os,"",er,(int)mTotal.mTotal); +} + +void MSInfo::msDumpCommon(std::ostream&os) const +{ + os << "\t"; + os << LOGVAR(msNumDataUSFGrants); + os << LOGVAR(msAckNackUSFGrant); + if (msOldTlli) os << LOGHEX(msOldTlli); + if (msAltTlli) os << LOGHEX(msAltTlli); + if (msDeprecated) os << LOGVAR(msDeprecated); + if (msPacch) { os << " Pacch="; msPacch->shortId(); } + os << LOGVAR2("idle",msIdleCounter); + //os << LOGVAR(msTimingErrorCount); + os << "\n"; +} + +void MSInfo::msDumpChannels(std::ostream &os) const +{ + PDCHL1Uplink *up; PDCHL1Downlink *down; + os <<" channels:"; + int howmany = 0; + RN_FOR_ALL_CONST(PDCHL1DownlinkList_t,msPCHDowns,down) { + if (howmany++ == 0) os << " down=("; + os << format(" %d:%d",down->CN(),down->TN()); + } + if (howmany) os << ")"; + howmany = 0; + RN_FOR_ALL_CONST(PDCHL1UplinkList_t,msPCHUps,up) { + if (howmany++ == 0) os << " up=("; + int tn = up->TN(); + os << format(" %d:%d,usf=%d",up->CN(),tn,(int)msUSFs[tn]); + } + if (howmany) os << ")"; +} + +void MSStat::msStatDump(const char *indent, std::ostream &os) +{ + os << indent; + os << " dataER:"; msCountBlocks.textTotal(os); + os << " recent:"; msCountBlocks.textRecent(os); + { + int fails = msCountTbfFail + msCountTbfNoConnect; + float tbfER = (float)fails/msCountTbfs; + //os << format(" tbfER:%.1f%% (%d)", 100.0*tbfER,(int)msCountTbfs); + os << " tbfER:"; + putER(os,"",tbfER,(int)msCountTbfs); + } + os << "\n" << indent; + os << " rrbpER:"; msCountRbbpReservations.textTotal(os); + os << " recent:"; msCountRbbpReservations.textRecent(os); + os << " ccchER:"; msCountCcchReservations.textTotal(os); + os << " recent:"; msCountCcchReservations.textRecent(os); + os << "\n"; +} + +void MSInfo::msDump(std::ostream&os, SGSN::PrintOptions options) +{ + int i; + RROperatingMode::type rrmode = getRROperatingMode(); + os << this // Dumps the operator<< value, which is sprintf(MS#%d,msDebugId) + //<< LOGHEX(msTlli) // The TLLI is in the default id now, so do not reprint it. + << LOGVAR(rrmode) + << " Bytes:" << msBytesUp << "up/" << msBytesDown << "down" + // The TrafficMetric is total number of blocks sent and received, + // decayed by 1/2 every 24 blocks, so max is 48/channel. + // The metric will be > 100% if multiple channels are used simultaneously. + // Note: channel = uplink or downlink, so single slot MS with uplink and downlink TBFs + // could reach 200%. Even a one-way TBF uses some of the other direction so can exceed 100%. + //<< format(" Utilization=%.1f%%",100.0 * msTrafficMetric / 48.0) + << " Utilization=" << fmtfloat2(100.0 * msTrafficMetric / 48.0) << "%" + << "\n"; + os << "\t"; sgsnPrint(msTlli,options | SGSN::printNoMsId,os); + dumpSignalQuality(os); + msStatDump("\t",os); + + if (!(options & SGSN::printVerbose)) {return;} + + // In case the queue is completely stalled or suspended, add a new data point for + // the current max delay if it is greater. + if (msDownlinkQueue.size()) { + double curage = msDownlinkQOldest.elapsed()/1000.0; + if (curage > msDownlinkQDelay.getCurrent()) { msDownlinkQDelay.addPoint(curage); } + } + os << "\t" << LOGVAR2("DownlinkQ:bytes",msDownlinkQStat) + << LOGVAR2("delay",msDownlinkQDelay) + << "\n"; + + os << "\t TBFs:" <mtDir == RLCDir::Up ? "(up)" : "(down)"); + os << " "<mtDir; + } + os << ")\n"; + + os << "\t USFs=("; + for (i = 0; i < 8; i++) { os << " " << msUSFs[i]; } + os << " )\n"; + + os << "\t"; msDumpChannels(os); os << "\n"; + msDumpCommon(os); +} + +string MSInfo::id() const +{ + char buf[100]; + sprintf(buf," MS#%d,TLLI=%x", msDebugId,(uint32_t)msTlli); + if (msOldTlli) { sprintf(buf+strlen(buf),",%x",(uint32_t)msOldTlli); } + return string(buf); +} +std::ostream& operator<<(std::ostream& os, const MSInfo*ms) +{ + if (ms) { + os << ms->id(); // not efficient, but only for debugging. + } else { + os << " MS#(null)"; + } + return os; +} + +}; // namespace GPRS diff --git a/GPRS/MSInfo.h b/GPRS/MSInfo.h new file mode 100644 index 00000000..4d9ec11f --- /dev/null +++ b/GPRS/MSInfo.h @@ -0,0 +1,601 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ +#ifndef MSINFO_H +#define MSINFO_H + +#include +//#include + +#include "GPRSInternal.h" +#include "GPRSRLC.h" +//#include "RLCHdr.h" +#include "RList.h" +//#include "BSSG.h" +#include "Utils.h" +#include "SgsnExport.h" + +#define CASENAME(x) case x: return #x; + +namespace GPRS { +struct RadData; + +typedef RList PDCHL1DownlinkList_t; +typedef RList PDCHL1UplinkList_t; +typedef RList TBFList_t; + +// A way to describe a collection of tbf states. +// See TBF.h isActive() and isTransmitting(). +enum TbfMacroState { + TbfMAny, // any state + TbfMActive, // any active state + TbfMTransmitting // any transmitting state. +}; + +// This should be in TBF.h but classes TBF and MSInfo are circularly referential. +enum TbfCancelMode { + TbfRetryInapplicable, // Tbf retry is inapplicable to an uplink tbf. + TbfNoRetry, // Kill the tbf forever + TbfRetryAfterRelease, // Retry tbf after sending a TbfRelease message. + // If the tbf release message fails, fall back to RetryAfterTimeout. + TbfRetryAfterWait // Retry tbf after timeout. + }; + +enum MultislotSymmetry { + MultislotSymmetric, // Use only symmetric multislot assignment, eg 2-down/2-up. + MultislotFull // Use full multislot assignment, which may be assymetric. +}; + +// GSM03.64 sec 6.2 +// Note: The RROperatingMode is a state of the MS and known only at the layer2 MAC level. +// The GSM spec says essentially that the RROperatingMode is simply whether +// the MS thinks there is an active TBF running, which is not particularly useful to us. +// What we need to track is whether the MS is listening to +// CCCH (PacketIdle mode) or PDCH (PacketTransfer mode.) Generally when all TBFs are +// finished the MS goes back to PacketIdle mode, but after a downlink transaction +// we keep it camped on PDCH for a time determined by the T3192 timer, whose value +// we broadcast in the System Information messages. +// +// Note: For T3192 timeout determination, dont forget to add the time delay +// of the outgoing message queue, but that should be short. +// But messages enqueued on CCCH may have long delays. +// +// This mode has almost nothing to do with Mobility Management Status, which is up in Layer3. +// When the docs talk about "GPRS attached" they usually mean Mobility Mangement State, not this. +// An MS in GMM state "Ready" (meaning the MS and SGSN have negotiated an SGSN supplied +// TLLI for the MS instead of using the random TLLI the MS uses for its first uplink message) +// will usually be in PacketIdle mode unless there is an ongoing TBF transaction. +// For a non dual-transfer-mode MS, the MS must relinquish (or suspend) its GMM "Ready" state +// to make a voice call, in which case it would no longer be in PacketIdle mode, +// but that has almost no meaning at the MAC level. If the voice call ends and the MS wants +// to use GPRS again, it will send us another RACH and we dont need to know that +// a voice call was made in the meanwhile. +class RROperatingMode { + public: + enum type { + PacketIdle, + PacketTransfer, + //DualTransfer, + // This mode was unnecessary: + //Camped // This is our own mode, not an official RROperatingMode. + // It marks the time MS is camped on Packet Channel between PacketTransfer + // and PacketIdle, when the T3192 timer is running. + // If a new transfer does not commence before T3192 expires, + // MS is in PacketIdle mode. + }; + static const char *name(RROperatingMode::type mode); +}; + +std::ostream& operator<<(std::ostream& os, const RROperatingMode::type &mode); + +// When we lose contact with the MS or something bad happens, we stop talking to it. +// This says why. Reserve the first 100 numbers so the MsgTransactionType can be used +// as a stop cause when the corresponding MsgTransactionType counter expires. +class MSStopCause { + public: + enum type { + AssignCounter = 1, + // These were the values in release 3.0: + //ShutDown = 2, + //Stuck = 3, + //Reassign = 4, // Reassignment failed. + ShutDown = 102, + Stuck = 103, + //Reassign = 104, // Reassignment failed. + Rach = 105, // Running TBF killed by a RACH. + ReleaseCounter = 106, + ReassignCounter = 107, + NonResponsive = 108, // MS does not talk to us any more. + Goof = 109, + N3101 = 3101, + N3103 = 3103, + N3105 = 3105, + T3191 = 3191, + T3168 = 3168, + CauseUnknown = 9999 // Used for unrecoverable internal inconsistency. + }; + //int mValue; + //MSStopCause(/*enum MsgTransactionType*/ int wMsgTransType) : mValue(wMsgTransType) {} +}; + + +struct SignalQuality { + // TODO: Get the Channel Quality Report from packet downlink ack/nack GSM04.60 11.2.6 + Statistic msTimingError; + Statistic msRSSI; // Dont bother saving RSSI as a float + Statistic msChannelCoding; + Statistic msCValue; + Statistic msILevel; + Statistic msRXQual; + Statistic msSigVar; + void setRadData(RadData &rd); + void setRadData(float wRSSI,float wTimingError); + void dumpSignalQuality(std::ostream &os) const; +}; + +struct StatTotalHits { + // Use ints instead of unsigned in case some statistic is buggy and runs backwards. + Int_z mTotal; + Int_z mGood; + void clear() { mGood = mTotal = 0; } +}; + +// keep the history of some success rate over the last few seconds for reporting purposes. +class StatHits { + // Keep totals for each of the last NumHist 48-block-multiframes, which is approx one second. + public: static const int cNumHist = 20; + private: + StatTotalHits mTotal; // Totals for all time. + StatTotalHits mHistory[cNumHist]; // Recent history. + UInt_z mWhen[cNumHist]; // When the historical data in this history bucket was collected. + // Return index in arrays above, which is the current 48-block-multiframe + unsigned histind(); + + public: + void addTotal() { + mTotal.mTotal++; + mHistory[histind()].mTotal++; + } + void addGood() { // increment only good count, not total. + mTotal.mGood++; + mHistory[histind()].mGood++; + } + void addMiss() { addTotal(); } + void addHit() { // increment good and total. + unsigned i = histind(); + mTotal.mTotal++; mTotal.mGood++; + mHistory[i].mTotal++; mHistory[i].mGood++; + } + + void getStats(float *pER, int *pTotal, float *pWorstER, int *pWorstTotal); + void textRecent(std::ostream &os); // Print the average for the last N seconds and worst second. + void textTotal(std::ostream&os); // Print totals. +}; + +// More MS statistics. Separate from MSInfo just because MSInfo is so large. +struct MSStat { + // This is a measure of the instantaneous traffic, used to pick the least busy channel. + // It is incremented every time a block is sent/received, and decayed on a regular time schedule. + UInt_z msTrafficMetric; + + StatHits msCountCcchReservations; + StatHits msCountRbbpReservations; + + StatHits msCountBlocks; // Counts both uplink and downlink. + + UInt_z msConnectTime; + void msAddConnectTime(unsigned msecs) { msConnectTime += msecs; } + UInt_z msCountTbfs, msCountTbfFail, msCountTbfNoConnect; + UInt_z msBytesUp, msBytesDown; + + + //UInt_z msCountCcchReservations; + //UInt_z msCountCcchReservationReplies; + //UInt_z msCountRbbpReservations; + //UInt_z msCountRbbpReservationReplies; + //void service() { + // // There are approx 48 blocks per second. + // if (gBSNNext % 48 != 0) { return; } + // unsigned ind = (gBSNNext / 48) % numHist; + // msHistoryCcchReservations.sethits(int,msCountCcchReservations,msCountCcchReplies); + // msHistoryRbbpReservations.sethits(int,msCountRbbpReservations,msCountRbbpReplies); + // fivesecavg.countCcchReservations = msCountCcchReservations. + //} + + // We use talkUp/talkDown to determine when the MS is non-responsive: + GprsTimer msTalkUpTime; // When the MS last talked to us. + GprsTimer msTalkDownTime; // When we last talked to the MS. + + // Called at every uplink/downlink communication from MS. + // These are not strictly statistics because they are also used to kill a non-responsive MS. + // These timers differ from the persistent mode timers in that those + // count only data, and these count anything. + void talkedUp(bool doubleCount=false) { msTalkUpTime.setNow(); if (!doubleCount) {msTrafficMetric++;} } + void talkedDown() { msTalkDownTime.setNow(); msTrafficMetric++; } + + // Dump all except traffic metric. + void msStatDump(const char *indent,std::ostream &os); +}; + +// MS Info a.k.a. Radio Context. There is one of these for each TLLI (not per-MS, per-TLLI) +// GSM04.08 4.7.1.4 talks about GPRS attach, P-TMSI, and TLLI. +// An MS, for our purposes in L2, is defined by its TLLI. No TLLI, no MSInfo struct. +// The TLLI identifies the MS in all transactions except the initial RACH. +// The RACH creates an anonymous packet uplink assignment for the MS, still identified +// only by the RACH time, to transmit one block, which will be a Packet Resource Request +// containing the all important TLLI. However, knowing the TLLI does not identify +// a unique MS; the MS may (and usually does) use several of them. +// Note that the MS can pick a "random" TLLI for itself when it does its first +// GPRS-attach, but the SGSN issues a new "local" TLLI based on P-TMSI on AttachAccept. +// The TLLI is specific to the sgsn, so if you switched sgsns, you would have a new TLLI, +// however, the MS remembers its old TLLIs and will try calling in with them, +// converted to foreign TLLIs. +// +// The spec is entirely botched up about whether the critical information needed +// to communicate with the MS is in L3 (the SGSN) or L2 (the BTS.) +// The SGSN stores the MS capabilities (RACap and DRX mode), which is ok but unnecessary, +// since the MS retransmits them in every RAUpdate. +// (The SGSN copies would be forwarded to the new cell during a cell change though.) +// The problem is that we need to remember the radio parameters for the MS +// (RSSI and TimingError), and a whole bunch of ad-hoc timers running in the MS +// here in L2, where we identify MS using TLLI, +// but the mapping of TLLI to MS (as known by IMSI) is in L3. Whoops! +// Also note that during a TLLI reassignment procedure using BSSG, the SGSN commands +// the BTS to switch TLLIs *after* it has received the Attach Complete Message, +// which has already arrived [possibly but not always] using the new TLLI, +// so here at L2 the MSInfo for the new TLLI already exists. +// We are supposed to combine the two RadioContexts (MSInfos) when we get +// the TLLI reassignment, and then recognize either TLLI. +// The RSSI and TimingError are updated separately per-TLLI (ie, per-MSInfo struct) +// because the MS initiates each conversation. +// +// The entire communication system between the MS and the SGSN can best +// be described in terms of two different state-universes, corresponding +// to the GPRS-Registration state: Registered (GPRS-Attached) or not. +// In the pre-gprs-attach state the MS may call in with several TLLIs, +// and we dont know how to correlate them to an actual MS. +// In this case it is quite easy to lose communication with the MS, because +// when an incoming RACH+PacketResourceRequest is answered, a new PACCH +// may be assigned at random, and it may conflict with a previous assignment +// that is on-going or in-flight on AGCH. +// Also in this state I think there is simply no way to know for sure the state of the +// per-MS timers, which are needed to know how to send the Immediate Assignment. +// TODO: Maybe we should use a single PACCH timeslot for all unregistered TLLIs, +// which implies communicationg the registration state from the SGSN to L2. + +// In the Registered-state we know that the new and old TLLI are the same physical MS, +// and communication is more secure. We can assign a new PACCH for the MS. +// By Registered we mean that both the SGSN and the MS agree on the +// Registration state and the P-TMSI, which agreement is handshaked in both +// directions (3 messages) and consumated by the AttachComplete message sent by MS to SGSN. + +#define TLLI_LOCAL_BIT 0x40000000 +#define TLLI_MASK_LOCAL(tlli) ((tlli) & ~ TLLI_LOCAL_BIT) +static __inline__ uint32_t tlliEq(uint32_t tlli1, uint32_t tlli2) { + // Temporarily provide a way to disable this in case it does not work: + if (gConfig.getBool("GPRS.LocalTLLI.Enable")) { + return TLLI_MASK_LOCAL(tlli1) == TLLI_MASK_LOCAL(tlli2); + } else { + return tlli1 == tlli2; + } +} + + +// vvv OLD COMMENT: +// Formerly I changed the TLLI in the MSInfo struct to an oldTLLI on the command of the SGSN, +// but that is incorrect - if the MS later sends another RACH using the oldTLLI, +// we need to respond with that oldTLLI, not the newTLLI, although possibly we +// should just ignore it in that case. +// The RACH creates an anonymous packet uplink assignment for the MS, still identified +// only by the RACH time, to transmit one block, which will be a Packet Resource Request +// with a TLLI. If that TLLI maps to an old TLLI, it is because we have already +// succeeded with the Attach Complete, and therefore it is safe to use the new TLLI. +// Therefore, an MSInfo structure has one and only one TLLI, and the sole purpose of +// TLLI is as a layer-2 transport identifier for the MS. +// ^^^ OLD COMMENT + +// The MSInfo struct needs to hang around as long as the MS is in packet-transfer mode, +// which means as long as it has TBFs, or the MS is in the T3192 period when it is camped +// on the PACCH channel instead of the CCCH channel. +// If we lose a connection with an MS, we keep the dead TBF around too until we +// are sure it is no longer in use (config option, default 5 seconds), +// so we dont need the MSInfo to survive after that. Doesn't hurt to keep it around, either. +// An unused MSInfo eventually decays and is destroyed; this delay must be longer +// than the expected use of the MSInfo by the SGSN - at least several seconds. +// The SGSN is what remembers GPRS-attached MS, and will send us both a TLLI and the +// MS capabilities (ie, multislot) in any transactions so that we can recreate the MSInfo at need. +class MSInfo : public SGSN::MSUEAdapter, public SignalQuality, public MSStat +{ + public: + unsigned msDebugId; + + // Use of TLLI is in GSM04.08 4.7.1.5: P-TMSI handling. + // From GSM03.03 sec 2.6: Structure of TLLI: + // local TLLI is built by MS that has a valid P-TMSI: + // top 2 bits 11, lower 30 bits are low 30 bits of P-TMSI. + // foreign TLLI is built by an MS that has a valid P-TMSI from elswhere: + // top 2 bits 10, lower 30 bits from P-TMSI; + // random TLLI is built by an MS with no P-TMSI: + // top top 5 bits 01111, lower 27 bits random. + // auxiliary TLLI is built by SGSN: + // top 5 bits 01110, lower 27 bits at SGSN discretion. + // GSM03.03 sec 2.4: Structure of TMSI + // The 32-bit TMSI has only local significance (within VLR or SGSN) and + // is created at manufacturer discretion, however, for SGSN top + // 2 bits must be 11, and it may not be all 1s, which value is reserved + // to mean invalid. We also reserve value 0 to mean unset. + // TMSI is stored in hex notation in 4 octets, and always ciphered. + // GPRS-L2 doesn't care about any of the above, the SGSN knows those things. + // In GPRS-L2 we just use whatever TLLI we are told. + + // An MSInfo structure is created as a result of an MS communication with a TLLI. + // No TLLI, no MSInfo structure. These things can time out and die whenever + // they want - their lifetime only needs to be as long as the RSSI and TimingError is valid. + // They will be recreated if the MS RAChes us again. + // In the spec they can also be created by a page from the SGSN, but not implemented here. + // The MSInfo struct corresponds to an SgsnInfo in the SGSN, but because + // either can be deleted any time we dont keep pointers between them, we always + // look them up by TLLI for communication to/from the SGSN. + // + // This is as per the spec: + // The msTlli is the TLLI we (the L2 layer) always use to communicate with the MS. + // It is initialzied from the TLLI the MS used to communicate with us. + // It is only changed if we get a 'change tlli' command from the SGSN, + // which happens only after a successfully completed attach procedure, + // (which is a fully acknowledged procedure with 3 way handshake) + // at which time msTlli becomes msOldTlli, and we must subsequently recognize + // either msTlli (the new sgsn-assigned one) or msOldTlli (the original one) + // for uplink communication, but use only msTlli for downlink communication. + // + // This is not as per the spec: + // After the attach is successful, we use only the assigned TLLI, + // only one MSInfo structure, and everything is copascetic. + // However, prior to a successful attach, I have seen the MS just use + // several different TLLIs in uplink messages one right after the other, + // maybe trying to find one that already works? + // So many of these MSInfo refer to the same phone. + // The spec deals with this by letting these things time out and die, + // however, this results in conflicting in-flight assignments on AGCH + // for different TLLIs that, in fact, refer to the same phone. + // The spec does not provide any way for the L2 layer (us) to find that out. + // So I introduced the AltTlli, which points to another MSInfo struct that + // refers the same phone. It MUST NOT be used for communication with the MS, + // however, it can be used to avoid launching conflicting assignments. + // The Sgsn sends us AltTlli as the AliasTlli in the DownlinkQPdu. + UInt32_z msTlli; // Identifies the MS, and is the TLLI used for downlink communication. + UInt32_z msOldTlli; // Also identifies the MS, and must be recognized in uplink communication. + UInt32_z msAltTlli; // Used to 'point' to another MSInfo struct that refers to the same MS, + // as reported to us by the SGSN, which knows these things. + // We save the TLLI instead of using a pointer because the other MSInfo + // could disappear at any time. + Bool_z msDeprecated; // This MS has been replaced by some other, which is another way + // of saying that the active MS's oldTlli points to this one. + + + // If the MS is in packet-idle state, there should be no TBFs. + TBFList_t msTBFs; // TBFs for this MS, both uplink and downlink. +#define RN_MS_FOR_ALL_TBF(ms,tbf) for (RListIterator itr(ms->msTBFs); itr.next(tbf); ) + Int_z msUSFs[8]; // USF in each timeslot. + // These are used by the RLCEngine to know when the MS has been granted a USF, ie, a chance to respond. + Int_z msNumDataUSFGrants; // Total number of USF grants; reset when last TBF detached. + Int_z msAckNackUSFGrant; // The msNumDataUSFGrants value of the last acknack. + + // Note: The uplink/downlink channels must all be in the same ARFCN that the MS is + // camped on, and for multislot, follow strict timeslot adjacency rules. + // The spec says that it is possible for different simultaneous TBFs for the same MS + // to use different channel assignments, for exmaple, if the MS is sending a low-data-rate + // TBF1 on a single channel and then wants to send a high-data-rate TBF2, it will interrupt + // TBF2, may request a multislot allocation, and do TBF2 first. + // Or another example, if TBF2 comes in and the TBF1 channel is on is congested, + // the MAC can pick a different channel for TBF2. + // However, we are not going to support that. The channels will be assigned to the MS + // permanently, and all TBFs will use the same ones, which is why these lists are + // here instead of in the TBF, where they really belong. + // We may, however, someday change the channel assignments dynamically based on the + // relative utilization of up and down links, for example, change from 4 down 1 up + // to 4 up 1 down, etc., but if we do that we will have to wait until there are + // no active TBFs, or reconfigure the existing TBFs. Having all the channels + // here in the MSInfo instead of scattered in different TBFs will make + // such reconfiguration easier. + PDCHL1UplinkList_t msPCHUps; // uplink channels assigned to the MS; usually just one. + PDCHL1DownlinkList_t msPCHDowns; // downlink channels assigned to the MS; usually just one. + bool msCanUseUplinkTn(unsigned tn); + bool msCanUseDownlinkTn(unsigned tn); + + // This is the channel the MS is listening to for messages. + // It is set before the msPCH assignments above. + // How did the MS come to be listening to this channel, you wonder? + // When a RACH comes in to the BTS, we do not know what MS it belongs to, so we pick + // the least busy GPRS channel and tell the MS to send its request on that channel. + // From then on the MS listens to that channel until we tell it differently + // in a channel assignment, which should be the next thing we send to it. + + // TODO: Is the below correct? Doesnt the MS always need to monitor PAACH which must + // be one of its assigned channels? + // It is possible for the msPCH assignments to not include the msPacch in several cases: + // 1. If we deliberately give the MS a different channel assignment + // for an uplink/downlink transfer, maybe because GPRS is underutilized and + // we decided to close the channel. (Channel closing not implemented in first draft.) + // 2. Maybe we decide on a different set of channels to satisfy this particular + // MSs multislot requirements. + // 3. If a previously attached MS starts a new RACH request, which will assign + // a new PCH at random (because we dont know what MS it is yet) and for some + // reason we havent cleared the msPCH channels, maybe because our timeouts are out of phase. + // In this weird case a BSSG downlink command might try to talk to the MS on + // the old channels, which might even possibly work if the new assignment and the + // old happen to be the same. The special cases are complicated. + // Note that if we used one-phase uplink access, this case 2 would extend in time out + // into the uplink transfer, but we wont do that. + PDCHL1FEC *msPacch; + + //RROperatingMode::type msMode; // Our belief about the state of MS: packet-idle, packet-transfer. + + + UInt_z msIdleCounter; // Counts how long MS is without TBFs; eventually we delete it. + UInt_z msStalled; // If MS is blocked, this is why, for error reporting. + + //Bool_z msUplinkRequest; // Request from phone to establish uplink was delayed due to existing uplink tbf. + //RLCMsgChannelRequestDescriptionIE msUplinkRequestCRD; // The CRD for delayed request above. + + + // GSM04.18 11.1.2: + // T3141 - Started at Immediate Assignment, stopped when MS starts TBF. + // We dont need it because we poll instead. + + // Note: Using Z100Timer is overkill for our single-threaded application; + // could just use RLCBlockTime counters instead. + // Counters and Timers defined in GSM04.60 sec 13. + // We dont calculate N3105 and N3101 exactly the way the spec says, + // but doesnt matter if they are off by 1 or 2. + // NOTE: The blackberry sometimes waits 3 block periods before it starts + // answering USFs, so N3101 better be bigger than that. + UInt_z msN3101; // Number of unacknowledged USF grants (off by one.) + + // GSM04.60 sec 13: + // Note: The MS may take advantage of this time period by keeping the TBF open + // after a PDU finishes, and not sending anything for a long time, then + // sending sending additional PDUs in the same TBF later, but before the timer expires. + GSM::Z100Timer msT3191; // Waiting for acknowledgement of final TBF data block. + + // GSM04.60 sec 13: + GSM::Z100Timer msT3193; // After downlink TBF finished, MS camps on PDCH this long. + // MS runs same timer but called T3192. + + // GSM04.60 sec 13: + GSM::Z100Timer msT3168; // MS camped on PDCH waiting for uplink assignment. + // This timer is defined to be in the MS, not the BTS, + // and we do not really need to track it as long as we + // are sure we send the downlink assignment message + // before this timer expires. The timer value is in + // the sql and broadcast in the beacon. + // However, I am using the timer as a way of tracking + // whether the assignment is for a RACH, rather + // than setting some other variable. + + // GSM04.60 sec 13: + //GSM::Z100Timer msT3169; // Final timeout for dead tbf. + //GSM::Z100Timer msT3195; // Final timeout for dead tbf. + //GSM::Z100Timer msTxxxx; // Combined T3169, T3191, T3195 - timeout for + // resource release after abnormal condition, during which time USF and TFI may not be reused. + + // When this MS was last granted a USF. + // We use this when multiple MS are in contention for the uplink to make it fair. + RLCBSN_t msLastUsfGrant; + + // Called when a USF is granted for this MS. + // If penalize, if the MS does not answer we kill of the tbf. + void msCountUSFGrant(bool penalize); + + // Incoming downlink data queue. + // This queue is not between separate threads for BSSG, + // and it is no longer for the internal sgsn either. + //InterthreadQueue msDownlinkQueue; + InterthreadQueue2 > msDownlinkQueue; + Statistic msDownlinkQStat; + Statistic msDownlinkQDelay; + Timeval msDownlinkQOldest; // The timeval from the last guy in the queue. + + // Can this TBF use the specified uplink? + bool canUseUplink(PDCHL1Uplink*up) { + return msPCHUps.find(up); + } + // Can this TBF use the specified downlink? + bool canUseDownlink(PDCHL1Downlink*down) { + return msPCHDowns.find(down); + } + // Return the downlink channels as a bitmask for PacketDownlinkAssignment msg. + unsigned char msGetDownlinkTimeslots(MultislotSymmetry sym); + + //PDCHL1Downlink *msPrimaryDownlink() { return msPCHDowns.front(); } + bool msAssignChannels(); // Get channel(s) for this MS. + private: + bool msAddCh(unsigned chmask, const char *tnlist); + bool msTrySlots(unsigned chmask,int down,int up); + bool msAssignChannels2(int maxdown, int maxup, int sum); + public: + void msDeassignChannels(); // Release all channels for this MS. + void msReassignChannels(); // Not implemented specially yet. + + //int msLastUplinkMsgBSN; // When did we last hear from this MS? + + MSInfo(uint32_t tlli); + // Use msDelete instead of calling ~MSinfo() directly. + void msDelete(bool forever=0); // If forever, do not move to expired list, just kill it. + + void msAddTBF(TBF *tbf) { + devassert(! msTBFs.find(tbf)); + msTBFs.push_back(tbf); + } + void msForgetTBF(TBF *tbf) { + devassert(msTBFs.find(tbf)); + msTBFs.remove(tbf); + } + + + // Called when a TBF goes dead. If it was the last active uplink TBF, surrender our USFs. + void msCleanUSFs(); + void msFailUSFs(); + + unsigned msGetDownlinkQueuedBytes(); + //TBF * msGetDownlinkActiveTBF(); + private: + int msCountTBF1(RLCDirType dir, enum TbfMacroState tbfstate, TBF**ptbf=0) const; + int msCountTBF2(RLCDirType dir, enum TbfMacroState tbfstate, TBF**ptbf=0); + public: + int msCountActiveTBF(RLCDirType dir, TBF**ptbf=0); + int msCountTransmittingTBF(RLCDirType dir, TBF**ptbf=0); + void msService(); + void msStop(RLCDir::type dir, MSStopCause::type cause, TbfCancelMode cmode, int unsigned howlong); + MSStopCause::type msStopCause; + //void msRestart(); + ChannelCodingType msGetChannelCoding(RLCDirType wdir) const; + int msGetTA() const { return GetTimingAdvance(msTimingError.getCurrent()); } + // All MS use the same power params at the moment. + int msGetAlpha() const { return GetPowerAlpha(); } + int msGetGamma() const { return GetPowerGamma(); } + void msDump(std::ostream&os, SGSN::PrintOptions options); + void msDumpCommon(std::ostream&os) const; + void msDumpChannels(std::ostream&os) const; + RROperatingMode::type getRROperatingMode(); + string id() const; + void msAliasTlli(uint32_t newTlli); + void msChangeTlli(uint32_t newTlli); + + //void msSetUplinkRequest(RLCMsgChannelRequestDescriptionIE &wCRD) { + // msUplinkRequest = true; + // msUplinkRequestCRD = wCRD; + //} + + // These are the functions required by the MSUEAdapter: + uint32_t msGetHandle() { return msTlli; } + string msid() const { return id(); } + //void msWriteHighSide(ByteVector &dlpdu, uint32_t tlli, const char *descr) { + //msDownlinkQueue.write(new GprsSgsnDownlinkPdu(dlpdu,tlli,descr)); + //} + void msDeactivateRabs(unsigned rabMask) {} // no-op in GPRS. + + bool msIsSuspended(); // Is the MS in suspended mode? + bool msIsRegistered(); // Is the MS GPRS registered? + bool isExtendedDynamic() { return msPCHUps.size() > msPCHDowns.size(); } + bool msCanUseExtendedUplink(); +}; +extern unsigned gMSDebugId; + +std::ostream& operator<<(std::ostream& os, const MSInfo*ms); + +MSInfo *bssgMSChangeTLLI(unsigned oldTLLI,unsigned newTLLI); + +}; // namespace +#endif diff --git a/GPRS/Makefile.am b/GPRS/Makefile.am new file mode 100644 index 00000000..4fe2e255 --- /dev/null +++ b/GPRS/Makefile.am @@ -0,0 +1,63 @@ +# +# Copyright 2008 Free Software Foundation, Inc. +# +# This software is distributed under the terms of the GNU Public License. +# See the COPYING file in the main directory for details. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +include $(top_srcdir)/Makefile.common + +AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) + +#AM_CXXFLAGS = -O0 -g +CXXFLAGS = -g -O0 + +noinst_LTLIBRARIES = libGPRS.la + + +libGPRS_la_SOURCES = \ + MSInfo.cpp \ + RLCEngine.cpp \ + TBF.cpp \ + MAC.cpp \ + FEC.cpp \ + RLCEngine.cpp \ + RLCMessages.cpp \ + ByteVector.cpp \ + GPRSCLI.cpp \ + RLC.cpp \ + MsgBase.cpp +#BSSGMessages.cpp +#BSSG.cpp + +noinst_HEADERS = \ + ByteVector.h \ + FEC.h \ + GPRSExport.h \ + GPRSInternal.h \ + GPRSTDMA.h \ + MAC.h \ + MsgBase.h \ + GPRSRLC.h \ + RLCEngine.h \ + RLCHdr.h \ + RLCMessages.h \ + RList.h \ + ScalarTypes.h \ + TBF.h \ + MSInfo.h +# BSSG.h +# BSSGMessages.h diff --git a/GPRS/MsgBase.cpp b/GPRS/MsgBase.cpp new file mode 100644 index 00000000..ae7cf25e --- /dev/null +++ b/GPRS/MsgBase.cpp @@ -0,0 +1,108 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include + +#include "MsgBase.h" + +void MsgCommonWrite::_define_vtable() {} +void MsgCommonLength::_define_vtable() {} +void MsgCommonText::_define_vtable() {} + +// Copied from same functions in L3Frame: +static const unsigned fillPattern[8] = {0,0,1,0,1,0,1,1}; + +void MsgCommonWrite::writeField(const ItemWithValueAndWidth&item,const char*) +{ + mResult.writeField(wp,item.getValue(),item.getWidth()); +} + +void MsgCommonWrite::writeField(uint64_t value, unsigned len, const char *, Type2Str) +{ + mResult.writeField(wp,value,len); +} + +void MsgCommonWrite::writeOptFieldLH(uint64_t value, unsigned len, int present, const char *) +{ + if (present) { writeH(); writeField(value,len); } else { writeL(); } +} + +// pat added: write an Optional Field controlled by an initial 0/1 field. +void MsgCommonWrite::writeOptField01(uint64_t value, unsigned len, int present, const char*) +{ + if (present) { write1(); writeField(value,len); } else { write0(); } +} + +void MsgCommonWrite::writeH() +{ + unsigned fillBit = fillPattern[wp%8]; // wp is in MsgCommon + writeField(!fillBit,1); +} + + +void MsgCommonWrite::writeL() +{ + unsigned fillBit = fillPattern[wp%8]; // wp is in MsgCommon + writeField(fillBit,1); +} + +void MsgCommonWrite::writeBitMap(bool*bitmap,unsigned bitmaplen, const char*name) +{ + for (unsigned i=0; i str; cp--) { + if (*cp != lastch) { + if (cp < end-6) { strcpy(cp+2,"..."); } + break; + } + } +} +#endif + +#define TOHEX(v) ((v) + ((v) < 10 ? '0' : ('a'-10))) +void MsgCommonText::writeBitMap(bool*bitmap,unsigned bitmaplen, const char*name) +{ + char txtbits[bitmaplen+6], *tp = txtbits; + unsigned i, accum = 0; + for (i=0; i +#include // For size_t +#include "Defines.h" +#include "BitVector.h" +#include "ScalarTypes.h" +typedef const char *Type2Str(int); +const char *tohex(int); + +// This is a package to help write downlink messages and message elements. +// (Note: A message element class is a helper class that writes part of a message, +// but is not a final class.) +// Also see Field<> and Field_z<> types. +// You write a single function writeCommon(), and this package uses it to instantiate the +// functions from MsgBase in your message class, namely: +// void writeBody(BitVector &dest, size_t &wp) +// void writeBody(BitVector &dest) +// int length() +// void text(std::ostream&os) +// To use: +// Step 1: For both message classes and message element classes: +// - Create a single function named: writeCommon(MsgCommon& dest) +// which writes out the message using the functions defined in MsgCommon, +// or the macros: WRITE_ITEM, WRITE_FIELD, etc. +// The primary output function is writeField() +// Step 2: Your message classes only (not message element classes) +// need to define the functions above. You can do this by inheriting from MsgBody, +// or just writing these yourself. +// For examples see class MsgBody or class RLCDownlinkMessage. + +class MsgCommon +{ + public: + size_t wp; + MsgCommon() : wp(0) {} + MsgCommon(size_t wwp) : wp(wwp) {} + + // Ignore this. g++ emits the vtable and typeid in the translation unit where + // the first virtual method (meeting certain qualifications) is defined, so we use this + // dummy method to control that and put the vtables in MsgBase.cpp. + // Do not change the order of these functions here or you will get inscrutable link errors. + // What a foo bar language. + virtual void _define_vtable() {} // Must be the first function. + + // The primary function to write bitfields. + virtual void writeField( + uint64_t value, // The value to be output to the BitVector by write(). + unsigned len, // length in bits of value. + const char *name=0, // name to be output by text() function; if not supplied, + // then this var does not appear in the text() + Type2Str cvt=0) = 0; // optional function to translate value to a string in text(). + + // This is used primarily to write variables of type Field or Field_z, + // for which the width is defined in the type declaration. + virtual void writeField(const ItemWithValueAndWidth&item, const char *name = 0) = 0; + + // Same as above, but an optional field whose presence is controlled by present. + virtual void writeOptFieldLH( // For fields whose presence is indicated by H, absence by L. + uint64_t value, unsigned len, int present, const char*name = 0) = 0; + // An alternative idiom to writeOptField01() is: + // if (dst.write01(present)) writeField(value,len,name); + virtual void writeOptField01( // For fields whose presence is indicated by 1, absence by 0. + uint64_t value, unsigned len, int present, const char*name = 0) = 0; + virtual void writeH() {} + virtual void writeL() {} + virtual void write0() {} + virtual void write1() {} + virtual bool write01(bool present) {return present;} + virtual void writeBitMap(bool*value,unsigned bitmaplen, const char*name) = 0; + + // getStream() returns the ostream for the text() function, or NULL for + // length() or write() functions. You can use it to make your function + // do something special for text(). + virtual std::ostream* getStream() { return NULL; } +}; + +#define WRITE_ITEM(name) writeField(name,#name) +#define WRITE_OPT_ITEM01(name,opt) writeOptField01(name,name.getWidth(),opt,#name) + +#define WRITE_FIELD(name,width) writeField(name,width,#name) +#define WRITE_OPT_FIELD01(name,width,opt) writeOptField01(name,width,opt,#name) +#define WRITE_OPT_FIELDLH(name,width,opt) writeOptFieldLH(name,width,opt,#name) +class MsgCommonWrite : public MsgCommon { + BitVector& mResult; + public: + void _define_vtable(); + MsgCommonWrite(BitVector& wResult) : mResult(wResult) {} + MsgCommonWrite(BitVector& wResult, size_t &wp) : MsgCommon(wp), mResult(wResult) {} + void writeField(uint64_t value, unsigned len, const char * name=0, Type2Str =0); + void writeField(const ItemWithValueAndWidth&item, const char *name = 0); + + void write0() { writeField(0,1); } + void write1() { writeField(1,1); } + bool write01(bool present) { writeField(present,1); return present; } + void writeH(); + void writeL(); + + // Write an Optional Field controlled by an initial L/H field. + void writeOptFieldLH(uint64_t value, unsigned len, int present, const char * name); + + // Write an Optional Field controlled by an initial 0/1 field. + void writeOptField01(uint64_t value, unsigned len, int present, const char * name); + + // Write a bitmap. + void writeBitMap(bool*value,unsigned bitmaplen, const char*name); +}; + +class MsgCommonLength : public MsgCommon { + public: + void _define_vtable(); + void writeH() { wp++; } + void writeL() { wp++; } + void write0() { wp++; } + void write1() { wp++; } + bool write01(bool present) { wp++; return present;} + void writeField(uint64_t, unsigned len, const char* =0, Type2Str =0) { wp+=len; } + //virtual void writeField(const ItemWithValueAndWidth&item, const char *name= 0) { wp = item.getWidth(); } + virtual void writeField(const ItemWithValueAndWidth&item, const char *) { wp = item.getWidth(); } + void writeOptFieldLH(uint64_t, unsigned len, int present, const char*) { + wp++; if (present) wp += len; + } + void writeOptField01(uint64_t, unsigned len, int present, const char*) { + wp++; if (present) wp += len; + } + void writeBitMap(bool*,unsigned bitmaplen, const char*) { wp+=bitmaplen; } +}; + +class MsgCommonText : public MsgCommon { + std::ostream& mos; + public: + void _define_vtable(); + MsgCommonText(std::ostream &os) : mos(os) { } + + void writeField(const ItemWithValueAndWidth&item, const char *name = 0) { + if (name) { mos << " " << name << "=(" << item.getValue() << ")"; } + } + void writeField(uint64_t value, unsigned, const char*name=0, Type2Str cvt=0) { + if (name) { + mos << " " << name << "="; + if (cvt) { mos << cvt(value); } else { mos << value; } + } + } + + void writeOptFieldLH(uint64_t value, unsigned, int present, const char*name = 0) { + if (name && present) { mos << " " << name << "=(" << value << ")"; } + } + + void writeOptField01(uint64_t value, unsigned, int present, const char*name = 0) { + if (name && present) { mos << " " << name << "=(" << value << ")"; } + } + void writeBitMap(bool*value,unsigned bitmaplen, const char*name); + std::ostream* getStream() { return &mos; } +}; + + +// This is the base class for the message, that defines the functions that use MsgCommon. +/*** +class MsgBody { + public: + virtual void writeCommon(MsgCommon &dest) const = 0; + + void writeOptional01(MsgCommon &dest, bool control) const { + if (control) { + dest.write1(); + writeCommon(dest); + } else { + dest.write0(); + } + } + + void writeBody(BitVector &vdst, size_t &wwp) const { + MsgCommonWrite dest(vdst,wwp); + writeCommon(dest); + wwp = dest.wp; + } + void writeBody(BitVector &vdst) const { + MsgCommonWrite dest(vdst); + writeCommon(dest); + } + int lengthBodyBits() const { + MsgCommonLength dest; + writeCommon(dest); + return dest.wp; + } + void textBody(std::ostream&os) const { + MsgCommonText dest(os); + writeCommon(dest); + } +}; +***/ + +/*** +#define INHERIT_MSG_BASE \ + void write(BitVector &dest, size_t &wp) { MsgBase::write(dest,wp); } \ + void write(BitVector &dest) { MsgBase::write(dest); } \ + int length() { return MsgBase::length(); } \ + void text(std::ostream&os) const { MsgBase::text(os); } +***/ + +#endif diff --git a/GPRS/RLC.cpp b/GPRS/RLC.cpp new file mode 100644 index 00000000..5931ab88 --- /dev/null +++ b/GPRS/RLC.cpp @@ -0,0 +1,83 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include "GPRSRLC.h" +#include "GSMCommon.h" + +namespace GPRS { + +int deltaBSN(int bsn1,int bsn2) +{ + static const int halfModulus = RLCBSN_t::BSNPeriodicity/2; + int delta = bsn1 - bsn2; + if (delta>=halfModulus) delta -= RLCBSN_t::BSNPeriodicity; + else if (delta<-halfModulus) delta += RLCBSN_t::BSNPeriodicity; + return delta; +} + + +// Based on GSM::FNDelta +// We assume the values are within a half periodicity of each other. +int RLCBSN_t::BSNdelta(RLCBSN_t v2) +{ + RLCBSN_t v1 = *this; + //int delta = v1.mValue - v2.mValue; + //if (delta>=halfModulus) delta -= BSNPeriodicity; + //else if (delta<-halfModulus) delta += BSNPeriodicity; + //return RLCBSN_t(delta); + return RLCBSN_t(deltaBSN(v1.mValue,v2.mValue)); +} + +// Return 1 if v1 > v2; return -1 if v1 < v2, using modulo BSNPeriodicity. +int RLCBSN_t::BSNcompare(RLCBSN_t v2) +{ + int delta = BSNdelta(v2); + if (delta>0) return 1; + if (delta<0) return -1; + return 0; +} + +// (pat) Return the block radio number for a frame number. +RLCBSN_t FrameNumber2BSN(int fn) +{ + // The RLC blocks use a 52-multiframe, but each 13-multiframe is identical: + // the first 12 frames are 3 RLC blocks, and the last frame is for timing or idle. + int mfn = (fn / 13); // how many 13-multiframes + int rem = (fn - (mfn*13)); // how many blocks within the last multiframe. + RLCBSN_t result = mfn * 3 + ((rem==12) ? 2 : (rem/4)); + result.normalize(); + return result; +} + + +// Return the Block Sequence Number for a frame number. +// There are 12 radio blocks per 52 frames, +int BSN2FrameNumber(RLCBSN_t absn) // absolute block sequence number. +{ + // One extra frame is inserted after every 3 radio blocks, + // so 3 radio blocks take 13 frames. + int bsn = absn; // Convert to int so we do math on int, not RLCBSN_t + int result = ((int)bsn / 3) * 13 + ((int)bsn % 3) * 4; + assert(result >= 0 && (unsigned) result <= GSM::gHyperframe); + return result; +} + +std::ostream& operator<<(std::ostream& os, const RLCDir::type &dir) +{ + os << RLCDir::name(dir); + return os; +} +}; diff --git a/GPRS/RLCEngine.cpp b/GPRS/RLCEngine.cpp new file mode 100644 index 00000000..210ac44f --- /dev/null +++ b/GPRS/RLCEngine.cpp @@ -0,0 +1,1330 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + + +// File contains RLCUpEngine and RLCDownEngine, which are classes that extend +// TBF (Temporary Block Flow) to do the data block uplink/downlink movement functions for a TBF. + +#include "BSSG.h" +#include "RLCEngine.h" +#include "TBF.h" +#include "MAC.h" +#include "FEC.h" +#include "Sgsn.h" +#include "RLCMessages.h" + +namespace GPRS { + +// If WaitForStall is true, a stalled TBF will send only one block at a time +// until it gets a response from the MS. +// If false, stalled downlink TBFs transfer the blocks continually +// until the time out. +//static int WaitForStall = false; + +/** RLC block size in bits for given coding standard, GSM 04.60 Table 10.2.1, plus MAC header. */ +// Index is a ChannelCodingType, 0-3 for CS-1 to CS-4. +const unsigned RLCBlockSizeBytesMax = 53; +unsigned RLCBlockSizeInBits[4] = +{ + // (pat) MAC header, plus RLC data block in octets, plus spare bits. + // Table 10.2.1 does not include the 8-bit MAC header, so add 1. + (1+22) * 8 + 0, // CS-1 + (1+32) * 8 + 7, // CS-2 + (1+38) * 8 + 3, // CS-3 + (1+52) * 8 + 7 // CS-4 + + // (pat) What was here before, but 319 does not appear correct: + // 184, // CS-1 22 octets plus MAC header + // 271, // CS-2 32 octets plus MAC header + // 319, // CS-3 38 octets plus MAC header + // 431 // CS-4 52 octets plus MAC header +}; + +unsigned RLCPayloadSizeInBytes[4] = +{ + // Table 10.2.1 includes the 2 octets for the RLC header, so subtract those out + // for the payload size. + (22-2), // CS-1 + (32-2), // CS-2 + (38-2), // CS-3 + (52-2) // CS-4 +}; + + + +/* + Arithmetic for the sequence numbers and arrays. + The index for mSt.VB, mSt.TxQ, mSt.VN and mSt.RxQ is straight BSN. + The downengine base index into mSt.VB is mSt.VA. + The upengine base index into mSt.VN us mSt.VQ. + In any of these arrays, the current window is base index through + (base index + mWS) % mSNS. +*/ +unsigned RLCEngineBase::addSN(int sn1, int sn2) const // Allow negative numbers. +{ + return ((unsigned)((int)sn1 + (int)sn2)) % (unsigned) mSNS; +} + +void RLCEngineBase::incSN(unsigned &psn) +{ + psn = addSN(psn,1); +} + +// Previously this returned -mWS <= result <= +mWS +// Which results in the following, which should return identical numbers, returning: +// deltaSN(64, 0) = 64 +// deltaSN(63, 127) = -64 +// The above is the all important stall condition. +// I tried to fix this returning a number in the range: -mWS < result <= +mWS, +// but then it failed somewhere else. +// Instead of being so careful around the edge condition, I am switching to deltaSNS +// Note that this is the opposite edge condition of deltaBSN. +int RLCEngineBase::deltaSN(unsigned sn1, unsigned sn2) const +{ + int delta = sn1 - sn2; + assert(!(delta >= (int) mSNS)); + assert(!(delta <= - (int) mSNS)); + if (delta <= -(int) mWS) delta += mSNS; // modulo the sequence space + if (delta > (int) mWS) delta -= mSNS; + return delta; +} + +// New delta functions +// Assume that sn1 >= sn2 in modulo arithmetic and do the delta on that basis. +// This function is safer than deltaSN at the edge condition, which is the stall condition. +// For safety we will allow an over-run of one. +// -1 <= result < +mSNS +int RLCEngineBase::deltaSNS(unsigned sn1, unsigned sn2) const +{ + int delta = sn1 - sn2; + assert(!(delta >= (int) mSNS)); + assert(!(delta <= - (int) mSNS)); + if (delta < -1) delta += mSNS; // modulo the sequence space + if (delta >= (int)mSNS) delta -= mSNS; + return delta; +} + +// Are the RLC BSNs equal? Using a function is just documentation that +// we are doing a comparison of BSNs. +bool RLCEngineBase::deltaEQ(unsigned sn1, unsigned sn2) const +{ + return sn1 == sn2; +} + +static bool queueFrontExistsAndIsNotTlliChangeCommand(MSInfo *ms) +{ + SGSN::GprsSgsnDownlinkPdu *dlmsg = ms->msDownlinkQueue.front(); + if (!dlmsg) { return false; } + if (dlmsg->mTlli != ms->msTlli) { + // We will terminate the downlink TBF and leave this new TBF with + // the TLLI change command sitting in the queue to handle + // at the next time we talk to this MS. + LOGWATCHF("TLLI change command from %x to %x\n",(unsigned)ms->msTlli,(unsigned)dlmsg->mTlli); + return false; + } + return true; +} + +// We leave persistent mode when mDownFinished is set. +bool RLCDownEngine::dlPersistentMode() { return !mDownFinished && (gL2MAC.macDownlinkPersist > 0); } +//bool RLCDownEngine::dataAvail() { return mDownPDU.size() || mtMS->msDownlinkQueue.size(); } +bool RLCDownEngine::dataAvail() { + return mDownPDU.size() || queueFrontExistsAndIsNotTlliChangeCommand(mtMS); +} + + +// Advance mSt.VA forward over blocks that have been previously acked; +// This is the only function permitted to modify VA. +// End condition is VA == TxQNum meaning VA is one beyond last in queue. +void RLCDownEngine::advanceVA() +{ + //for (; mSt.VA= VA + // Even though we are allowed mWS (64) outstanding blocks, + // we are only going to send only 63 to stay away from the edge condition, + // both for our own code, and possibly for bugs on the MS side as well. + if (deltaSNS(mSt.VS,mSt.VA) >= (int)(mWS-1)) { + mDownStalled = true; + mSt.VS = mSt.VA; // Start over from oldest unacked block. + } +#else + //if ((mSt.VS >= mSt.TxQNum || mSt.VS - mSt.VA >= mWS)) + if (mSt.VS >= mSt.TxQNum || deltaSN(mSt.VS,mSt.VA) >= (int)mWS) { + mDownStalled = true; + mSt.VS = mSt.VA; // Start over from oldest unacked block. + } +#endif + } +} + + +// (pat) Process an AckNack block from the MS (cell phone) +// Here we just mark the blocks that got through ok. +// The serviceloop will resend the blocks via engineService() +void RLCDownEngine::engineRecvAckNack(const RLCMsgPacketDownlinkAckNack *msg) +{ + mtRecvAck(MsgTransTransmit); // Make sure. + // mtMS->msN3101 = 0; Removed 6-7: 3101 is for uplink only. + mDownStalled = false; // Until proven otherwise. + // Mark the acks. + mNumDownBlocksSinceAckNack = 0; + const RLCMsgPacketAckNackDescriptionIE& AND = msg->mAND; + if (AND.mFinalAckIndication) { + // All done. We need to ack the entire area covered by the window, + // but we will overkill and ack the entire queue to be safe. + for (unsigned i=0; imControlAck bit in the + // downlink assignment, so it may be a bug in the Blackberry. + // Update 6-12: The above bug is definitively fixed. However, TBFs still get stuck here + // due to non-responsive MS, but that case is also detected by N3105. + bool stuck = (AND.mSSN == mPrevAckSsn); + if (stuck && !receivedNewAcks) { + LOGWATCHF("T%s STUCK at %d\n",getTBF()->tbfid(1),(int)AND.mSSN); + if ((int)mTotalBlocksSent - (int)mPrevAckBlockCount > configGetNumQ("GPRS.TBF.Downlink.NStuck",250)) { + mtCancel(MSStopCause::Stuck,TbfRetryAfterRelease); + goto finished; + } + } else { + mPrevAckBlockCount = mTotalBlocksSent; + } + mPrevAckSsn = AND.mSSN; + mResendSsn = AND.mSSN; // Default is to resend negatively acked blocks. + if (stuck || mDownFinished || mDownStalled) { + mResendSsn = mSt.TxQNum; // In these cases, resend all blocks. + } else { + // The downlink acknack lags behind VS by an amount that depends on how + // many downchannels are being used. If it looks like the SSN + // was out of sync farther than that, resend those blocks too. + int slip = 6 * mtMS->msPCHDowns.size(); + if (deltaSNS(mSt.TxQNum,mResendSsn) > slip) { + mResendSsn = addSN(mSt.TxQNum,-slip); + } + } + + // TEST!!! This was a total hack to test the bitmap interpretation. + // If we missed any blocks, resend them all. + // Worked in that RLCEngine resent all blocks, but did not help at all. + //if (cntmissed && AND.mSSN) { + // for (int i=1; i <= AND.mbitmapsize; i++) { + // absn = AND.mSSN - i; + // mSt.VB[absn] = false; // resend all the blocks referenced in the bitmap. + // if (absn == 0) break; + // } + //} + } + + advanceVA(); + + //if (deltaSN(mSt.VA,mSt.TxQNum) >= 0) // Did the MS ack all the blocks? + if (deltaEQ(mSt.VA,mSt.TxQNum)) { // Did the MS ack all the blocks? + mAllAcked = true; + GPRSLOG(1) << getTBF() <str() <str() <str() <setAppendP(0); +#else + mUpPDU = new BSSG::BSSGMsgULUnitData(RLC_PDU_MAX_LEN,mtMS->msTlli); + mUpPDU->mTBFId = mtDebugId; +#endif + } + // Do not use str() here, because it tries to print the pdu contents, + // but the pdu is incomplete. + mUpPDU->append(seg); + GPRSLOG(4096) << "addUpPDU:after="<hexstr(); +} + + +// Send the completed PDU on its way. +void RLCUpEngine::sendPDU() +{ + GPRSLOG(2048) <<"sendPDU"<size())<hexstr()); + mtMS->msBytesUp += mUpPDU->size(); +#if INTERNAL_SGSN + mtMS->sgsnWriteLowSide(*mUpPDU,getTBF()->mtTlli); + delete mUpPDU; // decrements refcnt; llc may have saved a ref to the bytevector. +#else + mUpPDU->setLength(); + if (GPRSConfig::sgsnIsInternal()) { + // LLC does not want the BSSG header. + // The payload is an LLC message. + ByteVector payload = mUpPDU->getPayload(); + //SGSN::Sgsn::sgsnWriteLowSide(payload,mtMS->msTlli); + mtMS->sgsnWriteLowSide(payload,getTBF()->mtTlli); + delete mUpPDU; // decrements refcnt; llc may have saved a ref to the bytevector. + } else { + BSSG::BSSGWriteLowSide(mUpPDU); + } +#endif + mUpPDU = 0; +} + +struct seghdr { + unsigned LI; + unsigned M; + unsigned E; +}; + +static void dumpsegs(seghdr *segs, int n) +{ + for (int j = 0; j < n; j++) { + struct seghdr& seg = segs[j]; + GLOG(INFO) <<"\t" << LOGVAR(seg.LI) << LOGVAR(seg.M) << LOGVAR(seg.E); + } +} + +// Process any blocks that have been received, advancing the window. +// The MS is allowed to send multiple PDUs in a TBF, so we have to handle it. +// Even in unack mode, we will assemble the blocks in mSt.RxQ to descramble multislot transmissions. +void RLCUpEngine::engineUpAdvanceWindow() +{ + mIncompletePDU = false; + //if (BSN==mSt.VQ) { + // while (mSt.VN[mSt.VQ] && mSt.VQ!=mSt.VR) { mSt.VQ = addSN(mSt.VQ,1); } + //} + // TODO: Unacknowledged mode. + while (mSt.RxQ[mSt.VQ /*% mSNS*/]) { + RLCUplinkDataBlock *block = mSt.RxQ[mSt.VQ]; + mSt.RxQ[mSt.VQ] = NULL; // redundant; they will also be deleted when suredly past + //mSt.VN[mSt.VQ] = false; // We cant do this here. + mSt.VQ = addSN(mSt.VQ,1); + + RLCUplinkDataSegment payload(block->getPayload()); + GPRSLOG(64) << "RLCUpEngine payload=" << payload.hexstr() <mE) { + // Whole payload is appended to the current PDU. + GPRSLOG(64) << "RLCUpEngine E=1,"<mmac.isFinal()) { sendPDU(); } // Annex B Example 6. + } else { + // The RLC block contains length indicator octets. Crack them out. + struct seghdr segs[16]; + + int n; // Number of length indicators; last one has E==1. + bool end = 0; + for (n = 0; !end; n++) { + if (n == 16) { + GLOG(ERR) << "GPRS: more than 16 segments per RLC block"; + dumpsegs(segs,15); + break; // This block is almost certianly trash... + } + segs[n].LI = payload.LIByteLI(); + segs[n].E = payload.LIByteE(); + end = segs[n].E; + segs[n].M = payload.LIByteM(); + payload.set(payload.tail(8)); + } + unsigned original_size = payload.size(); + + // GSM 04.60 sec 10.4.14 and Appendix B. + // Use the length indicators to slice up the payload into segments. + for (int i = 0; i < n; i++) { + unsigned lenbytes = segs[i].LI; + unsigned sizebytes = payload.size()/8; + GPRSLOG(64) << "RLCUpEngine seg:"<mmac.isFinal()) { + GPRSLOG(64) << "RLCUpEngine mIncompletePDU"; + mIncompletePDU = true; // But we dont currently use this. + } + } else { + // Sanity check here. Log any bogus blocks. + if (lenbytes > sizebytes) { + GLOG(ERR)<<"Uplink PDU with with nonsensical segments:"; + //GLOG(INFO)<<"\tpayloadlen="<mE); + dumpsegs(segs,n); + // what to do? Just save what there really is. + lenbytes = sizebytes; + } + } + BitVector foo(payload.segment(0,8*lenbytes)); + addUpPDU(foo); + if (segs[i].LI) { sendPDU(); } + payload.set(payload.tail(8*lenbytes)); + } + // Final M bit means add rest of the payload to the nextpdu. + if (payload.size() && segs[n-1].M) { + GPRSLOG(64) << "RLCUpEngine M=1:"<mmac.isFinal()) { + if (mUpPersistentMode && ! mtPerformReassign) { + mtUpState = RlcUpQuiescent; + } else { + mtUpState = RlcUpFinished; + } + // 5-22-2012: Re-enabled this: + if (mUpPDU && !mIncompletePDU) { sendPDU(); } + } else { + // This is a new block, so we are not quiescent any more. + if (mtUpState == RlcUpQuiescent) { mtUpState = RlcUpTransmit; } + } + delete block; + } +} + +// Amazingly, we need to give the MS an RRBP reservation in case it wants to change +// the priority of the TBF. All those zillions of USFs we sent it, and on which +// it responded with control blocks, were not enough. +// Update: Some MSs (iphone) send the packet resource request on the USF, ie, using PACCH. +bool RLCUpEngine::sendNonFinalAckNack(PDCHL1Downlink *down) +{ + MSInfo *ms = mtMS; + // Before we issue another ack nack, wait until we have given this MS + // an uplink block to respond via usf. This is an efficiency issue + // if the uplink is being used by other MS as well. However, if the MAC does not + // find any other pressing need for the current uplink block, it will be granted + // to any one of the active uplink TBFs, so we may get a block that way too. + // Note there can be multiple uplink channels, and a USF could be granted for + // this MS on any of them. Note that this would get complicated if there + // are multiple uplink TBFs. + if (ms->msAckNackUSFGrant == ms->msNumDataUSFGrants) { return false; } + + // These messages are sent without acknowledgement, except for the last one. + // If we were more clever, we would insure that the USF in this message + // is for the intended MS recipient, then if we received an uplink block + // we would know the message was (probably) received. (It can still fail + // because the USF in the header is sent with better error correction.) + // These messages are not specifically counted. The TBF cancellation + // occurs if the MS stops answering USFs. + // It would be nice to detect stuck (non-advancing) uplinks, but the MS + // will do that, cancel the TBF, stop answering USFs, and then we will cancel. + RLCMsgPacketUplinkAckNack * msg = engineUpAckNack(); + GPRSLOG(1) <str(); + //down->send1MsgFrame(getTBF(),msg,0,MsgTransNone,NULL); + down->send1MsgFrame(getTBF(),msg,1,MsgTransTransmit,NULL); + ms->msAckNackUSFGrant = ms->msNumDataUSFGrants; // Remember this number. + mNumUpBlocksSinceAckNack = 0; + return true; +} + +#if UPLINK_PERSIST +// For persistent uplink we need to know when all current TBFs have completed +// so the uplink is quiescent. +// bool RLCUpEngine::isQuiescent() +//{ +// return mSt.VQ == mSt.VR && NULL==mUpPDU; +//} +#endif + +// See if this up engine wants to send something on the downlink. +// It would be an AckNack message. +// Return true if we sent something. +bool RLCUpEngine::engineService(PDCHL1Downlink *down) +{ + TBF *tbf = getTBF(); + if (! tbf->isPrimary(down)) { return false; } + + if (mtUpState == RlcUpFinished) { + finalstate: + // Send the final acknack + // We wait for the MS to send a PacketControlAcknowledgment + // to the final acknack msg we sent it, to make sure it got it. + // Otherwise, it may keep trying to send us blocks forever. + // An alternate strategy would be to resend the acknack whenever + // we receive a data block from it, but it would be harder to tell + // when to release the resources. + if (mtGotAck(MsgTransDataFinal,true)) { // Woo hoo! + mtFinishSuccess(); + return false; // We did not need to use the downlink. + } + if (mtMsgPending()) { return false; } + RLCMsgPacketUplinkAckNack * msg = engineUpAckNack(); + GPRSLOG(1) <str(); + int result = down->send1MsgFrame(tbf,msg,2,MsgTransDataFinal,&mtN3103); + if (result) { + if (mtUnAckMode) { + // We dont bother to find out if the acknack gets through. + // In fact, the only reason we used an RRBP was + // to allow the MS to initiate another uplink transfer. + mtFinishSuccess(); + } else { + mtMsgSetWait(MsgTransDataFinal); // Wait for response before trying again. + } + } + return result; + } + + if (mUpStalled || mNumUpBlocksSinceAckNack >= mNumUpPerAckNack) { + // But absolutely do not send two reservations at once: + if (! mtMsgPending(MsgTransTransmit)) return sendNonFinalAckNack(down); + } + +#if UPLINK_PERSIST + // Persistent uplink uses Extended Uplink TBF defined in 44.060 9.3.1b and 9.5 + if (mUpPersistentMode) { + //LOGWATCHF("X%s UpState=%d keepalive=%d persist=%d\n",tbf->tbfid(1),mtUpState,mtUpKeepAliveTimer.elapsed(),mtUpPersistTimer.elapsed()); + // Note: The link is 'quiescent' if there are no new unique blocks, + // but there could be lots of duplicate blocks, so we still need to + // send acknacks. + if (mtUpState == RlcUpQuiescent) { + if (mtUpPersistTimer.elapsed() > (int)gL2MAC.macUplinkPersist || mtPerformReassign) { + // Time to kill the TBF. + // As per 44.060 9.5 we do that by sending a PacketUplinkAckNack with the + // final ack bit set. But since we are constantly transmiting USFs, there + // could be a new uplink TBF starting right now, so first we have to + // stop transmitting USFs, then wait to make sure the MS didnt start + // another TBF, before we can kill it off. + mtUpState = RlcUpPersistFinal; + // There could be USFs sent in this period, so we need to wait out + // this block period, and a USF in block N gets replied in block N+1, + // so we have to wait for N+2, or 3 block periods total. + mDataPersistFinalEndBSN = gBSNNext + 3; + return false; + } else if (mtUpKeepAliveTimer.elapsed() > (int)gL2MAC.macUplinkKeepAlive) { + // This updates the keepalive timer if the message is sent: + bool result = sendNonFinalAckNack(down); + if (result) LOGWATCHF("K%s keepalive=%d persist=%d\n",getTBF()->tbfid(1), + mtUpKeepAliveTimer.elapsed(),mtUpPersistTimer.elapsed()); + return result; + } + } else if (mtUpState == RlcUpPersistFinal) { + // We hang out in this state until the expiry BSN is reached. + // If a new TBF starts in the meantime it will throw us + // back into DataTransmit state. + if (gBSNNext <= mDataPersistFinalEndBSN) {return false;} + // Now it is really time to finish. + mtUpState = RlcUpFinished; + mtSetState(TBFState::DataFinal); + goto finalstate; + } + //GLOG(ERR) << "persistent mode timer expiration: invalid TBF state:"<msT3168.reset(); // Way overkill, this should not be running. + mtSetState(TBFState::DataTransmit); + break; + case TBFState::Dead: + GLOG(ERR) <msN3101 = 0; + // Mark the ack flags and save the block. + unsigned BSN = block->mBSN; // This is modulo mSNS + LOGWATCHF("B%s tn=%d block=%d cc=%d %s %s\n",getTBF()->tbfid(1),tn,BSN,(int)block->mUpCC, + block->mmac.isFinal()?"final":"",mSt.VN[BSN]?"dup":""); + if (mSt.RxQ[BSN]) { + // If BSN < VQ in modulo arithmetic, this is a duplicate block that + // we have already scanned past, and we dont need to save it. + // However, to be safe, we wont do the above test, instead we'll just save the + // block and delete it when it is surely past below. + // If we had more energy, we might check that the two blocks are the same. + if (mSt.VN[BSN] == false) { GLOG(ERR) << getTBF() << " VN out of sync" <msCountBlocks.addMiss(); + } else { + mUniqueBlocksReceived++; + mtMS->msCountBlocks.addHit(); + } + mSt.VN[BSN]=true; + mSt.RxQ[BSN]=block; + // We must use deltaSN, not deltaSNS, because we dont know which is higher. + // Have to subtract 1 first to keep the edge condition from failing. + int VRm1 = addSN(mSt.VR,-1); + int deltaR = deltaSN(BSN,VRm1); + if (deltaR>0) { + unsigned past = addSN(mSt.VR, -mWS - 2); // -2 to be safe + mSt.VR=addSN(BSN,1); + unsigned pastend = addSN(mSt.VR, -mWS - 2); + + // We clear out the VN behind us. + // Since the acknack msg only stretches back the 64 acks before VR, + // it is safe to clear before those. + // 12-28-2012: Change to -2 from -1. + //mSt.VN[(mSt.VR - mWS - 2) % mSNS] = false; + + for ( ; past != pastend; incSN(past)) { + mSt.VN[past] = false; + if (mSt.RxQ[past]) { delete mSt.RxQ[past]; mSt.RxQ[past] = 0; } + } + } + + mUpStalled = block->mmac.mSI; + //mtSetState(mUpStalled ? TBFState::DataStalled : TBFState::DataTransmit); + mNumUpBlocksSinceAckNack++; + mTotalBlocksReceived++; + + // Add any finished blocks to the PDU, possibly send PDUs. + engineUpAdvanceWindow(); // sets mtUpState to RlcUpFinished if TBF is complete. + + if (mtUpState == RlcUpFinished) { + // We always send a final ack/nack for both acknowledged and unacknowledged mode. + // old: Calls mtMsgReset() - very important. update: now each msgtransaction has its own type. + mtSetState(TBFState::DataFinal); + } +} + + + +// Send Packet Uplink Ack/Nack, GSM 04.60 11.2.6 +RLCMsgPacketUplinkAckNack * RLCUpEngine::engineUpAckNack() +{ + RLCMsgPacketAckNackDescriptionIE AND; + // Spec says that if mFinalAckIndication is set, the rest is ignored. + AND.mFinalAckIndication = (mtUpState == RlcUpFinished); + // GSM04.60 9.1.8. + // Also read 12.3: Ack/Nack description carefully. + // The phrase: "Mapping of the bitmap is defined on sub-clause 11" means look at the + // start of section 11, to learn that the bitmap is indexed backwards relative to SSN which + // is defined as the most recent block received (aka V(R) in the receiver), + // then modulo 128, but the bit encoding itself runs from MSB to LSB, + // so it ends up going forwards, but the bits of interest + // are at the high end of the bitmap. + AND.mSSN = mSt.VR; // Thats right: VR, not VQ. + for (int i = 1; i <= AND.mbitmapsize; i++) { + //AND.mBitMap[AND.mbitmapsize - i] = mSt.VN[(mSt.VR-i) % mSNS]; + AND.mBitMap[AND.mbitmapsize - i] = mSt.VN[addSN(mSt.VR,-i)]; + } + RLCMsgPacketUplinkAckNack *msg = new RLCMsgPacketUplinkAckNack(getTBF(), AND); +#if UPLINK_PERSIST + if (mUpPersistentMode) { + mtUpKeepAliveTimer.setNow(); + } + LOGWATCHF("A%s SSN=%d state=%d keepalive=%d persist=%d\n",getTBF()->tbfid(1),(int)AND.mSSN, + mtUpState,mtUpKeepAliveTimer.elapsed(),mtUpPersistTimer.elapsed()); +#endif + return msg; +} + + +// Is the downlink engine stalled, waiting for acknack messages? +// Assumes the downlink is unifinished. +// Stalled applies only to acknowledged mode. +// We never send more than SNS blocks in a single TBF, +// so we dont have to worry about wraparound. +//bool RLCDownEngine::stalled() +//{ +// if (!mDownStalled && !mtUnAckMode && +// (mSt.VS >= mSt.TxQNum || mSt.VS - mSt.VA >= mWS)) { +// mDownStalled = true; +// } +// return mDownStalled; +//} + +// Deliver a PDU to the MS. +// Before FAST_TBF: This function primes the RLCEngine by chopping the PDU into RLCBlocks, +// placing them in the mSt.TxQ. They will be physically sent by the serviceloop. +void RLCDownEngine::engineWriteHighSide(SGSN::GprsSgsnDownlinkPdu *dlmsg) +{ +#if FAST_TBF + // We dont do the pdus one at a time. Just leave the PDU on the queue and the + // engine will pull it off. + mtMS->msDownlinkQueue.write_front(dlmsg); +#else + // The pdus are sent one at a time. + TBF *tbf = getTBF(); + tbf->mtDescription = dlmsg->mDescr; + // The mDownPDU owns the malloced memory. Many of the other downlink blocks are segments into it. + // The mDownPDU is deleted automatically with the RLCDownEngine. + mDownPDU = dlmsg->mDlData; + LOGWATCHF("engineWriteHighSide %d tn=%d\n",mDownPDU.size(),tbf->mtMS->msPacch->TN()); + //assert(mDownPDU.getRefCnt() == 2); // Not true during a retry. + mDownlinkPdu = dlmsg; + //assert(mDownPDU.getRefCnt() == 1); + //std::cout<<"engineWriteHighSide, ref="<mDlData.getRefCnt()<<"\n"; + GPRSLOG(1) <mDescr<getPayloadSize(); + block->mBSN = bsn++; + if (remaining >= payloadsize) { + block->mE = 1; // No extension octet follows. + // In this case the payload ByteVector segment points directly into the mDownPDU. + // I tried allocating here, did not help the cause=3105 failure. + //block->mPayload.clone(mDownPDU.segment(fp,payloadsize)); + block->mPayload.set(mDownPDU.segmentTemp(fp,payloadsize)); + remaining -= payloadsize; + fp += payloadsize; + } else { + // The data will not fill the block. + // In this case, we will use a ByteVector with allocated memory. + // The confusing "singular case" mentioned in GSM04.60 10.4.14 does not happen + // to us because we do not put multiple PDUs in a block. Yet. + block->mE = 0; // redundant + ByteVector payload(payloadsize); // Allocate a new one. What horrible syntax. + ByteVector seg = mDownPDU.segmentTemp(fp,remaining); + + // We have to add an extension octet to specify the PDU segment length. + int sbh = RLCSubBlockHeader::makeoctet(remaining,0,1); + payload.setByte(0, sbh); + + // Add the PDU segment. + payload.setSegment((size_t)1, seg); + + // Note: unused RLC data field filled with 0x2b as per 04.60 10.4.16 + int fillsize = payloadsize - remaining - 1; + payload.fill(0x2b,remaining+1,fillsize); + // Try cloning the memory here, but didnt help + // block->mPayload.clone(payload); + block->mPayload = payload; // dups the memory + remaining = 0; + } + // The TFI is not set yet! TFI will be set by send1Frame just before transmit. + //block->mTFI = mtTFI; + if (!remaining) { block->mFBI = true; } + mSt.TxQ[mSt.TxQNum++] = block; + } +#endif +} + +// Return the block at vs. +// If vs is past the end of the previously sent blocks, then: +// if we have sent the FBI, return that block, +// otherwise get the next block, or NULL if no more data avail, +// which can only happen in persistent mode. +RLCDownlinkDataBlock *RLCDownEngine::getBlock(unsigned vs, + int tn) // Timeslot Number, for debugging only. +{ +#if FAST_TBF + mTotalDataBlocksSent++; + if (vs == mSt.TxQNum) { + // Did we finish? Look at the next-to-last block to see if it has fbi set. + RLCDownlinkDataBlock *prev = mSt.TxQ[addSN(vs,-1)]; + if (prev && prev->mFBI) { return prev; } + + // Manufacture the next block. + mUniqueDataBlocksSent++; + mtMS->msCountBlocks.addHit(); + // Clean up behind ourselves when wrapping around. + if (mSt.TxQ[mSt.TxQNum]) { delete mSt.TxQ[mSt.TxQNum]; mSt.TxQ[mSt.TxQNum] = 0; } + RLCDownlinkDataBlock *block = engineFillBlock(mSt.TxQNum,tn); + if (block == NULL) { return NULL; } + mAllAcked = false; // It is a brand new block. + GPRSLOG(4096) << "getBlock"<str(); + mSt.TxQ[mSt.TxQNum] = block; + //mSt.sendTime[mSt.TxQNum] = gBSNNext; + mSt.VB[mSt.TxQNum] = false; // block needs an ack. + incSN(mSt.TxQNum); + } else { + mtMS->msCountBlocks.addMiss(); + } +#endif + assert(mSt.TxQ[vs]); + return mSt.TxQ[vs]; +} + +#if FAST_TBF +// Depending on dlPersistentmode, when we run out of data we can either terminate +// the downlink tbf by setting the fbi indicator in the last block sent, +// or we can leave the TBF open pending further data, which means the routine +// will send NULL when data exhausted. +// This routine always returns NULL if it is out of data, but in non-persistent-mode +// the caller notices the FBI bit and does not call it any more after +// receiving the last block. +RLCDownlinkDataBlock* RLCDownEngine::engineFillBlock(unsigned bsn, + int tn) // Timeslot Number, for debugging only. +{ + const int maxPdus = 10; + int li[maxPdus]; + int mbit[maxPdus]; // mbit = 1 implies a new PDU starts after the current one. + ByteVector pdus[maxPdus]; + int pducnt = 0; + int licnt = 0; + bool fbi = false; // final block indicator + + // Create the block. + RLCDownlinkDataBlock *block = new RLCDownlinkDataBlock(mtChannelCoding()); + int payloadsize = block->getPayloadSize(); + int payloadavail = payloadsize; + + bool nonIdle = !!mDownPDU.size(); + + // First make a list of the pdus to go in the rlc block. + + // Dont think it is possible for licnt to reach maxPdus without pducnt hitting maxPdus first, but test anyway. + while (payloadavail>0 && pducnt < maxPdus && licnt < maxPdus) { + + // Is there any more data? + if (mDownPDU.size() == 0) { + // For testing, if SinglePduMode send just one pdu at a time: + // The first pdu was loaded by engineWriteHighSide, so we just ignore the q. + if (configGetNumQ("GPRS.SinglePduMode",0)) {break;} + if (queueFrontExistsAndIsNotTlliChangeCommand(mtMS)) { + SGSN::GprsSgsnDownlinkPdu *dlmsg = mtMS->msDownlinkQueue.readNoBlock(); + assert(dlmsg); + mtMS->msDownlinkQDelay.addPoint(dlmsg->mDlTime.elapsed()/1000.0); + mDownPDU = dlmsg->mDlData; + mtMS->msBytesDown += mDownPDU.size(); + if (! dlmsg->isKeepAlive()) { nonIdle = true; } + // Remember the last sdu for possible retry on failure. + if (mDownlinkPdu) {delete mDownlinkPdu;} + mDownlinkPdu = dlmsg; + getTBF()->mtDescription = dlmsg->mDescr; + LOGWATCHF("pdu %d\n",mDownPDU.size()); + GPRSLOG(1) <mDescr<= mSNS-1) { + LOGWATCHF("debug: Skipping wrap-around\n"); + fbi = true; + break; + } + } + } + + int sdusize = mDownPDU.size(); // sdu remaining bytes + if (sdusize == 0) {break;} // No more incoming data. + + if (sdusize > payloadavail || (sdusize == payloadavail && pducnt)) { + if (pducnt) { mbit[licnt-1] = 1; } + pdus[pducnt++] = mDownPDU.head(payloadavail); + mDownPDU.trimLeft(payloadavail); + payloadavail = 0; + } else if (sdusize == payloadavail && pducnt == 0) { + // Special case for single pdu exactly fills the block. + // If this is the final block, we set FBI and can ommit the length indicator. + // Otherwise we put out all but the last byte of sdu and use a special zero length indicator. + // The next rlc block will get the final byte of this sdu. + // I disabled this special case out, just to be safe; the penalty + // is occassionally sending an extra block with only one byte in it. + if (0 && mtMS->msDownlinkQueue.size() == 0 && ! dlPersistentMode()) { + // This is the final block, so the fbi indicator tells the MS + // the data ends at the end of the block and we do not need + // the 'singular case' 0 li field. + fbi = true; // Make sure to set this here, in case an asynchronous + // process adds data between here and the time we + // check msDownlinkQueue.size() at the end of this loop. + pdus[pducnt++] = mDownPDU; + mDownPDU.trimLeft(payloadavail); + //LOGWATCHF("debug: exactly full block\n"); + } else { + // Not the final block; need to add the 'singular case' 0 li field, + // and final byte of the pdu will go in the next block. + li[licnt] = 0; + mbit[licnt] = 0; + licnt++; + payloadavail--; // For the li field. + pdus[pducnt++] = mDownPDU.head(payloadavail); + mDownPDU.trimLeft(payloadavail); + //LOGWATCHF("debug: singular case block\n"); + } + payloadavail = 0; + } else { // sdusize < payloadavail + if (payloadavail == 1) {break;} // too small to use. + if (pducnt) { mbit[licnt-1] = 1; } + pdus[pducnt++] = mDownPDU; + mDownPDU.trimLeft(sdusize); + li[licnt] = sdusize; + mbit[licnt] = 0; // Until proven otherwise. + licnt++; + payloadavail--; // For the li field + payloadavail -= sdusize; + } + } + + if (pducnt == 0) { + // There is no data ready to go. + delete block; + return NULL; + } + + if (!dataAvail() && !dlPersistentMode()) { fbi = true; } + + if (configGetNumQ("GPRS.SinglePduMode",0)) { + // For testing, send just one pdu at a time: + if (mDownPDU.size() == 0) { fbi = true; } + } + + if (fbi) mDownFinished = true; + block->mFBI = fbi; + block->mBSN = bsn; + block->mIdle = !nonIdle; + + // DEBUG: + //static unsigned lastbsn = 0; + //if (fbi) { lastbsn = 0; } + //if (bsn < lastbsn) { + // // This is a BUG. + // printf("BUG\n"); + // GPRSLOG(1) << "BUG HERE"<tbfid(1),tn,bsn,(int)block->mChannelCoding,mSt.TxQNum,fbi); + if (licnt == 0) { + // Entire block is payload. + block->mE = 1; // No extension octet follows. + assert(pducnt == 1); + assert(pdus[0].size() == (unsigned)payloadsize); + block->mPayload = pdus[0]; + } else { + block->mPayload = ByteVector(payloadsize); + block->mPayload.setAppendP(0); + // Add the licnts + for (int i = 0; i < licnt; i++) { + // Add the extension octet to specify the PDU segment length. + int sbh = RLCSubBlockHeader::makeoctet(li[i],mbit[i],i == licnt-1); + if (GPRSDebug) sprintf(report+strlen(report)," li=%d:%d:%d",li[i],mbit[i],i==licnt-1); + block->mPayload.appendByte(sbh); + } + // Add the pdu segments. + for (int j = 0; j < pducnt; j++) { + block->mPayload.append(pdus[j]); + if (GPRSDebug) sprintf(report+strlen(report)," seg=%d",pdus[j].size()); + } + // Add filler, if any. Unused RLC data field filled with 0x2b as per 04.60 10.4.16 + int fillsize = payloadsize - block->mPayload.size(); + if (GPRSDebug) sprintf(report+strlen(report)," fill=%d",fillsize); + if (fillsize) block->mPayload.appendFill(0x2b,fillsize); + } + LOGWATCHF("%s\n",report); + + // The TFI is not set yet! TFI will be set by send1Frame just before transmit. + //block->mTFI = mtTFI; + return block; +} +#endif + +// Is this the final unacked block? +// Note that it may not be the last block in mSt.VB +// because we may be resending some previous block. +//bool RLCDownEngine::isLastUABlock() +//{ +// if (mtUnAckMode) { +// return mSt.VS == (mSt.TxQNum-1); +// } +// int cnt = 0; // count of unacked blocks. +// for (unsigned i = mSt.VA; i < mSt.TxQNum; i++) { +// if (!mSt.VB[i]) { cnt++; } // Are we still waiting for an ack for this block? +// if (cnt > 1) { return false; } +// } +// assert(cnt == 1); +// return true; +//} + +// How many blocks to go, but dont bother to count more than 5. +// Now that we manufacture downlink rlc blocks on demand, this is difficult to calculate, +// so just return a guess. +//unsigned RLCDownEngine::blocksToGo() +//{ +// unsigned i, cnt = 0; // count of unacked blocks remaining. +// for (i = mSt.VS; deltaSN(i,mSt.TxQNum)<0; incSN(i)) { +// if (!mSt.VB[i]) { cnt++; } // Are we still waiting for an ack for this block? +// if (cnt > 5) break; +// } +//#if FAST_TBF +// if (cnt < 5) { +// // Estimate number of blocks from remaining data. +// int payloadsize = mtPayloadSize(); +// cnt += mDownPDU.size() / payloadsize; +// if (cnt > 5) return cnt; +// if (configGetNumQ("GPRS.SinglePduMode",0)) {return cnt;} +// if (mtMS->msDownlinkQueue.size()) { +// // Just assume its a bunch of data. +// return 6; +// } +// } +//#endif +// return cnt; +//} + + +// See if the TBF in this RLCEngine has some RLC data or control messages to send. +// Return TRUE if we sent something on the downlink. +bool RLCDownEngine::engineService(PDCHL1Downlink *down) +{ + //TBFState::type state = mtGetState(); + //stalled(); // Are we stalled? Sets mDownStalled. + //if (state == TBFState::DataFinal || (WaitForStall && mDownStalled)) { + // The expectedack time is set in send1DataFrame. + // The RRBP ACK is optional, so mtExpectedAckBSN may not yet + // be valid, but mtMsgPending handles that. + //if (mtMsgPending()) { return false; } + //} + + // Logic restructured and this code moved below... + //if (mAllAcked && ! dataAvail()) { return false; } + + // Note: When an acknack is received, mSt.VS is set back to the first + // negatively acknowledged block, so we advanceVS() after using VS, not before. + // After final block sent, VS is left == TxNum. + bool advanced = true; + RLCDownlinkDataBlock *block = getBlock(mSt.VS,down->TN()); + + if (block == NULL) { + // This happens when data is exhausted, in which case VS == TxNum. + // Only send the final block on the primary channel. + if (!getTBF()->isPrimary(down)) {return false;} + + if (mAllAcked) { + // All current TBFs have been sent and acknowledged. + // This case does not occur if we are doing one TBF at a time, + // so we dont need to check for that case specially. + assert(dlPersistentMode()); + //if (dlPersistentMode()) + if (mtDownKeepAliveTimer.elapsed() > (int)gL2MAC.macDownlinkKeepAlive) { + // Start a new keep-alive on its way. + mtMS->sgsnSendKeepAlive(); + mtDownKeepAliveTimer.setNow(); + } + return false; // Nothing to do. + } + + // After sending all unacknowledged blocks we just send the final block + // over and over. Alternatively we could start over at VA. + // If so, it should be done only if no other TBFs have something more + // important to do. + + assert(mDownFinished); + // Back up one to get the final block to resend. + assert(mSt.VS == mSt.TxQNum); + block = getBlock(addSN(mSt.VS,-1),down->TN()); + advanced = false; + assert(block); + } + + if (block->mIdle) { + assert(dlPersistentMode()); + if (!mDownFinished && mtDownPersistTimer.expired()) { + // Time to kill the tbf. + block->mFBI = true; + mDownFinished = true; + } + } else if (dlPersistentMode()) { + // We are sending a non-idle block. + if (gL2MAC.macDownlinkKeepAlive) mtDownKeepAliveTimer.setNow(); + if (gL2MAC.macDownlinkPersist) mtDownPersistTimer.setNow(); + } + + // TODO: + // 44.060 9.3.1 says that after all pending unacked blocks have been transmitted, + // you can start over again. + + + // Astonishingly, we have to resend the final block every time we + // send any acknowledged blocks. + // You can not just set the FBI indicator in the last block sent. + // I am not sure if the MS will not re-ack the final block, so do not + // reset its ack indicator, just retransmit it. + if (block->mFBI) + { + if (!getTBF()->isPrimary(down)) {return false;} + // We need an acknowledgment for the final block, even in unacknowledged mode. + // We are allowed to have three RRBPs pending at a time, total for the MS, which includes + // both uplink and downlink TBFs, so we will only have one at a time + // for each of the two possible uplink and downlink tbfs, + // so wait if we already have an earlier RRBP out. + if (mtMsgPending()) { return false; } + //mtMsgReset(); + if (! down->send1DataFrame(this, block, 2, MsgTransDataFinal,&mtN3105)) { return false; } + // This is not needed: + //mtMsgSetWait(); // Dont resend again until reservation passed. + if (mtUnAckMode) { + // We're done. We send it once and forget it. + // TODO: unacknowledged mode - are we supposed to wait for the RRBP reply? + mtFinishSuccess(); // We just sent the last block. + } else { + // This mandated timer is dumb. This time period is no different than + // the rest of the downlink in that we are + // still counting unanswered RRBPs, and will detect + // a non-responsive MS that way. + mtMS->msT3191.set(); + } + } else { + assert(mtGetState() == TBFState::DataTransmit); + int rrbpflag = 0; + if (mtUnAckMode) { + // 9.3.3.5: For unack mode do not have to set RRBP except in final block. + rrbpflag = 0; // redundant assignment. + } else if (mDownStalled) { + if (!getTBF()->isPrimary(down)) {return false;} + // Every block we send will include a reservation. + // If there is only one ms, we should probably go ahead and send + // other blocks too, so the behavior should depend on the utilization, + // and also on how big this TBF is, ie, we should have another variable + // that tracks when we start a stall resend, send all those blocks, + // then possibly wait for the reservation. + if (mtMsgPending()) { return false; } + rrbpflag = 1; + } else if (getTBF()->isPrimary(down)) { + // Only send RRBP reservations on the primary channel. + + //static const int RrbpGuardBlocks = 5; + // The mNumDownPerAckNack and the RrbpGuardBlocks must be chosen to prevent a stall, + // meaning the sum has to be less than 64; not a problem. + if (++mNumDownBlocksSinceAckNack >= mNumDownPerAckNack) { + rrbpflag=1; + } + // TODO: This test just does not work with persistent mode, + // because it prevents us from sending the rrbp in the last block; + // this test depended on that case being handled by the + // if (mFBI) branch above. + // This blocksRemaining test is only an efficiency issue, + // so get rid of it for now: + //int blocksRemaining = blocksToGo(); + //if (blocksRemaining <= RrbpGuardBlocks) { rrbpflag = 0; } + // In this case we dont need to wait for any reservation. + if (rrbpflag && mtMsgPending()) { + rrbpflag = 0; // Dont do two RRBPs at once. + } + } + + bool result = down->send1DataFrame(this,block,rrbpflag,MsgTransTransmit,&mtN3105); + assert(result); // always succeeds. +#if FAST_TBF + if (advanced) { + incSN(mSt.VS); // Skip block we just sent. + advanceVS(); + } +#else + if (!mDownStalled) { + incSN(mSt.VS); // Skip block we just sent. + advanceVS(); + } +#endif + } + + mTotalBlocksSent++; + return true; +} + +float RLCDownEngine::engineDesiredUtilization() +{ + // Very approximately, stalled downlink TBF wants to retry every few blocks. + if (stalled()) { return 0.2; } + return 1.0; // Transmitting downlink TBF wants the entire bandwidth. +} + +// Make sure the blocks are deleted. +RLCDownEngine::~RLCDownEngine() +{ + unsigned i; + for (i = 0; i < mSNS; i++) { // overkill, but safe. + if (mSt.TxQ[i]) { delete mSt.TxQ[i]; mSt.TxQ[i] = NULL; } + } +#if INTERNAL_SGSN==0 + if (mBSSGDlMsg) { delete mBSSGDlMsg; } +#endif + if (mDownlinkPdu) { delete mDownlinkPdu; } + mDownlinkPdu = 0; // extreme paranoia +} + +RLCUpEngine::RLCUpEngine(MSInfo *wms,int wOctetCount) : + TBF(wms,RLCDir::Up), + mUpPDU(0), + mBytesPending(wOctetCount) +{ + mtUpState = RlcUpTransmit; + memset(&mSt,0,sizeof(mSt)); + mStartUsfGrants = wms->msNumDataUSFGrants; + // Use the same criteria for persistent mode as for extended uplink. + // Can only use this if the phone supports geran feature package I? + mUpPersistentMode = mtMS->msCanUseExtendedUplink(); +} + +RLCUpEngine::~RLCUpEngine() +{ + unsigned i; + for (i = 0; i < mSNS; i++) { // overkill, but safe. + if (mSt.RxQ[i]) { delete mSt.RxQ[i]; mSt.RxQ[i] = NULL; } + } + if (mUpPDU) { delete mUpPDU; } +} + +void RLCDownEngine::engineDump(std::ostream &os) const +{ + os < +#include + +#include "GPRSInternal.h" +#if INTERNAL_SGSN==0 +#include "BSSGMessages.h" +#endif +#include "TBF.h" +#define FAST_TBF 1 // Use aggregated downlink TBFs. + +namespace GPRS { + +class PDTCHL1FEC; + +// Maximum LLC PDU size is 1560 bytes. (GSM04.60 sec9.1.12) Bytes over are discarded in RLC. +// In unacknowledged mode, LLC-PDUs delivered in the order received, with 0-bits for missing blocks. +// The minimum payload size (using CS-1) is 20 bytes (see RLCPayloadSize) +// Therefore, a single PDU may take 78 blocks. +const int RLC_PDU_MAX_LEN = 1560; + + +//typedef std::list LLCSegmentList; + +class RLCEngineBase +{ + public: + // WS => number of blocks sent simultaneously, awaiting ack, before blocking. + // SNS => must be double WS, by definition. + static const unsigned mWS = 64; // Window Size + static const unsigned mSNS = 128; // Sequence Number Space + + /**@name SN arithmetic */ + int deltaSN(unsigned sn1, unsigned sn2) const; + int deltaSNS(unsigned sn1, unsigned sn2) const; + bool deltaEQ(unsigned sn1, unsigned sn2) const; + unsigned addSN(int sn1, int sn2) const; // Allow negative numbers. + void incSN(unsigned &psn); + virtual void engineDump(std::ostream &os) const = 0; + string engineDumpStr() const { std::ostringstream os; engineDump(os); return os.str(); } +}; + +enum RlcUpState { + RlcUpTransmit, + RlcUpQuiescent, + RlcUpPersistFinal, + RlcUpFinished, +}; + +// (pat) This is an RLC endpoint as per GSM 04.60 9.1 +// It sits in layer 2. +class RLCUpEngine : public TBF, public RLCEngineBase +{ + protected: + /**@name RLC state variables, from GSM 04.60 9. */ + // We depend on the MS not to send us blocks with BSN > VQ+mWS, and inform + // the MS of our concept of VQ in the acknack we send. + // However, the only way we know the acknack is received may be that the MS + // sends more blocks. + struct { + unsigned VR; ///< highest BSN received + 1 modulo wSNS; + // 0 <= VR <= wSNS-1; + unsigned VQ; ///< lowest BSN not yet received (window base) + // In acknowledged mode, receive window is + // defined by: VQ <= BSN <= VQ + mWS + bool VN[mSNS]; ///< receive status of previous RLC data blocks + RLCUplinkDataBlock *RxQ[mSNS]; ///< assembly queue for inbound RLC data blocks + //unsigned RBSN; ///< BSN of incoming blocks. + } mSt; + //@} + +#if INTERNAL_SGSN + ByteVector *mUpPDU; // The PDU being assembled. +#else + BSSG::BSSGMsgULUnitData *mUpPDU; // The PDU being assembled. +#endif + Bool_z mIncompletePDU; // Special case: If set, the PDU did not finish in this TBF; + // MS needs another TBF to send the rest of it. + // Dont think we need this if using unlimited dynamic mode uplink. + Bool_z mUpStalled; // MS is stalled, copied from each uplink block header. + + static const unsigned mNumUpPerAckNack = 10; + UInt_z mNumUpBlocksSinceAckNack; // Number of blocks sent since the last acknack + UInt_z mTotalBlocksReceived; + UInt_z mUniqueBlocksReceived; + int mBytesPending; +#if UPLINK_PERSIST + RLCBSN_t mDataPersistFinalEndBSN; // When DataPersistFinal mode started. + bool mUpPersistentMode; + GprsTimer mtUpKeepAliveTimer; // Time to next keep alive. + GprsTimer mtUpPersistTimer; // How long TBF persists while idle. +#endif + + // The MS keeps a running total of USF slots granted to the MS. + // The difference between the current value and the starting value divided + // by the number of unique blocks received is a measure of the loss ratio, + // although that breaks down if there are multiple simultaneous uplink TBFs. + unsigned mStartUsfGrants; + //unsigned mNumServiceSlotsSkipped; // Reality check disaster recovery. + RlcUpState mtUpState; // Set after all blocks have been received. + + public: + RLCUpEngine(MSInfo *wms,int wOctetCount); + ~RLCUpEngine(); + + void addUpPDU(BitVector&); + void sendPDU(); + bool stalled() const { return mUpStalled; } +#if UPLINK_PERSIST + bool ulPersistentMode(); + bool sendNonFinalAckNack(PDCHL1Downlink *down); +#endif + + void engineRecvDataBlock(RLCUplinkDataBlock* block, int tn); + void engineUpAdvanceWindow(); + bool engineService(PDCHL1Downlink *down); // Thats right: we pass the downlink. + float engineDesiredUtilization(); + void engineDump(std::ostream &os) const; + void engineGetStats(unsigned *pSlotsTotal, unsigned *pSlotsUsed, unsigned *pGrants) const { + *pSlotsTotal = mTotalBlocksReceived; + *pSlotsUsed = mUniqueBlocksReceived; + *pGrants = mtMS->msNumDataUSFGrants - mStartUsfGrants; + } + int engineGetBytesPending() { return mBytesPending; } + RLCMsgPacketUplinkAckNack * engineUpAckNack(); + TBF *getTBF() { return dynamic_cast(this);} +}; + +class RLCDownEngine : public TBF, public RLCEngineBase +{ + public: + + /**@name RLC state variables, from GSM 04.60 9. */ + //@{ + struct { + unsigned VS; ///< BSN of next RLC data block for tx + // After receipt of acknack message, VS is set back to VA. + unsigned VA; ///< BSN of oldest un-acked RLC data block + bool VB[mSNS]; ///< ack status of pending RLC data blocks (true = acked) + RLCDownlinkDataBlock *TxQ[mSNS]; ///< unacked RLC data blocks saved for re-tx + //int sendTime[mSNS]; ///= mSt.TxQNum; } + unsigned engineDownPDUSize() const { return mDownPDU.size(); } + + RLCDownlinkDataBlock *getBlock(unsigned vs,int tn); + void engineWriteHighSide(SGSN::GprsSgsnDownlinkPdu *dlmsg); // TODO: get rid of this. + bool dlPersistentMode(); // Are we using persistent TBF mode? + bool dataAvail(); // Is more downlink data available? + RLCDownlinkDataBlock* engineFillBlock(unsigned bsn,int tn); + void engineRecvAckNack(const RLCMsgPacketDownlinkAckNack*); + bool engineService(PDCHL1Downlink *down); + float engineDesiredUtilization(); + void engineDump(std::ostream &os) const; + void engineGetStats(unsigned *pSlotsTotal, unsigned *pSlotsUsed, unsigned *pGrants) const { + // We dont care about the control blocks sent, only the data. + //*pSlotsTotal = mTotalBlocksSent; // Number of blocks we have sent. + *pSlotsTotal = mTotalDataBlocksSent; // Number of blocks we have sent. + *pSlotsUsed = mUniqueDataBlocksSent; // Number of blocks MS has acknowledged. + *pGrants = 0; // Inapplicable to downlink. + } + int engineGetBytesPending() { return 0; } // TODO, but it is a negligible amount + //bool isLastUABlock(); + //unsigned blocksToGo(); + bool resendNeeded(int bsn); + void advanceVS(); + void advanceVA(); + TBF *getTBF() { return dynamic_cast(this);} +}; + + + +// This incorporates both an up and down engine, but only one of them is used. +//class RLCEngine : +// public RLCUpEngine, +// public RLCDownEngine +//{ +// public: +// RLCEngine(TBF *); +// ~RLCEngine(); +//}; + + +}; // namespace GPRS + + + +#endif diff --git a/GPRS/RLCHdr.h b/GPRS/RLCHdr.h new file mode 100644 index 00000000..a92beea8 --- /dev/null +++ b/GPRS/RLCHdr.h @@ -0,0 +1,481 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef RLCHDR_H +#define RLCHDR_H + +#include +#include "GPRSRLC.h" +#include "ScalarTypes.h" +#include "Defines.h" +//#include +#include +#include "ByteVector.h" +#include "MsgBase.h" +#include "GPRSInternal.h" +#include "MemoryLeak.h" +#define CASENAME(x) case x: return #x; + + +namespace GPRS { +extern unsigned RLCPayloadSizeInBytes[4]; +extern unsigned RLCBlockSizeInBits[4]; + +class MACPayloadType // It is two bits. GSM04.60sec10.4.7 +{ + public: + enum type { + RLCData=0, + RLCControl=1, // The RLC/MAC block does NOT include the optional Octets. + RLCControlExt=2, // The RLC/MAC block DOES include the optional Octets. + // Used only in downlink direction. + // Value 3 is reserved. + }; + static const char *name(int val) { + switch ((type)val) { + CASENAME(RLCData) + CASENAME(RLCControl) + CASENAME(RLCControlExt) + } + return "unrecognized MACPayloadType"; + } +}; + + +// RLC/MAC control message may be sent in an RLC/MAC control block, which is always +// encoded with CS-1, so length is 176 bits (22 octets). +// Some MAC messages are also sent on PBCCH, PCCCH or PACCH. +// Note that no fields in any MAC or RLC header exceed one byte, +// so there are no network ordering problems, and we can simply use C structs +// to define the bit packing. + +// (pat) Data blocks defined in GSM04.60 sec 10.3. + +struct MACDownlinkHeader // 8 bits. See GSM04.60sec10.3.1 +{ + // From GSM 04.60 10.4: + // RRBP - expected frame delay for ACK + // SP - If 1 we expect an ack and RRBP means something. + // USF - User state flag for shared channels. + protected: + //MACPayloadType::type mPayloadType:2; + Field_z<2> mPayloadType; + Field_z<2> mRRBP; // RRBP: Relative Reserved Block Period. See GSM04.60sec10.4.5 + // It specifies 3,4,5 or 6 block delay for ACK/NACK (to give the + // MS time to decode the block.) + public: + Field_z<1> mSP; // Supplementary/Polling Bit - indicates whether RRBP is valid. + Field_z<3> mUSF; // For uplink dynamic allocation method. + // indicates owner of the next uplink radio block in the same timeslot. + // Except on PCCCH, where a value of 0x111 indicates next uplink radio + // block reserved for PRACH. + unsigned lengthBits() { return 8; } // Size of the MAC header in bits. + + void writeMACHeader(MsgCommon& dest) const; + + void init(MACPayloadType::type wPayloadType) { mPayloadType = wPayloadType; } + void setRRBP(int rrbp) { mRRBP = rrbp; mSP = 1; } + bool isControlMsg() { return mPayloadType != MACPayloadType::RLCData; } + bool isMacUnused() { return mSP == 0 && mUSF == 0; } +}; +#if RLCHDR_IMPLEMENTATION + void MACDownlinkHeader::writeMACHeader(MsgCommon& dest) const { + //dest.WRITE_ITEM(mPayloadType); + dest.writeField(mPayloadType,2,"PayLoadType",MACPayloadType::name); + dest.WRITE_ITEM(mRRBP); + dest.WRITE_ITEM(mSP); + dest.WRITE_ITEM(mUSF); + } +#endif + +struct MACUplinkHeader // 8 bits. See GSM04.60sec10.3.2 +{ + public: + // Note: countdown value and SI are used only for uplink data blocks; + // for uplink control blocks, CountdownValue and SI are unused, always 0. + // Octet 1 (and only): + MACPayloadType::type mPayloadType:2; + Field<4> mCountDownValue; // GSM04.60 9.3.1. + // 15 until close to the end, then countdown. Last block 0. + // Once MS starts countdown, it wont interrupt the + // transfer for higher priority TBFs. + // Update: MSs dont do the countdown, they send 15 until + // the next-to-last block, then send 0. + Field<1> mSI; // SI: Stall Indicator + Field<1> mR; // Retry bit, sent by MS if it had to try more than once to get this through. + + unsigned lengthBits() { return 8; } // Size of the MAC header in bits. + int parseMAC(const BitVector&src); + // Since this is an uplink header, we only need to write it for testing purposes, eg, text(). + void writeMACHeader(MsgCommon&dst) const; + void text(std::ostream&os) const; + bool isFinal() { return mCountDownValue == 0; } +}; +#if RLCHDR_IMPLEMENTATION + // It is one byte. + int MACUplinkHeader::parseMAC(const BitVector&src) { + size_t rp = 0; + mPayloadType = (MACPayloadType::type) src.readField(rp,2); + mCountDownValue = src.readField(rp,4); + mSI = src.readField(rp,1); + mR = src.readField(rp,1); + return 8; + } + + // Since this is an uplink header, we only need to write it for testing purposes, eg, text(). + void MACUplinkHeader::writeMACHeader(MsgCommon&dst) const { + // This is special cased so we get the name of the payload type in the text. + dst.writeField(mPayloadType,2,"PayLoadType",MACPayloadType::name); + /*** + std::ostream *os = dst.getStream(); // returned os is non-null only if caller is text(). + if (os) { + *os << "mPayloadType=(" << MACPayloadType::name(mPayloadType) << ")"; + } else { + dst.writeField(mPayloadType,2); + } + ***/ + dst.WRITE_ITEM(mCountDownValue); + dst.WRITE_ITEM(mSI); + dst.WRITE_ITEM(mR); + } + void MACUplinkHeader::text(std::ostream&os) const { + MsgCommonText dst(os); + writeMACHeader(dst); + //os << "mPayloadType=(" << MACPayloadType::name(mPayloadType) << ")"; + //os << RN_WRITE_TEXT(mCountDownValue); + //os << RN_WRITE_TEXT(mSI); + //os << RN_WRITE_TEXT(mR); + } +#endif + +struct RadData { + bool mValid; + float mRSSI; + float mTimingError; + RadData() { mValid = false; } + RadData(float wRSSI, float wTimingError) : mValid(true),mRSSI(wRSSI),mTimingError(wTimingError) {} +}; + + +// An incoming block straight from the decoder. +struct RLCRawBlock { + RLCBSN_t mBSN; // The BSN corresponding to the GSM FN of the first received burst. + RadData mRD; + BitVector mData; + MACUplinkHeader mmac; + ChannelCodingType mUpCC; + RLCRawBlock(int wfn, const BitVector &wData,float wRSSI, float wTimgingError,ChannelCodingType cc); + ~RLCRawBlock() { RN_MEMCHKDEL(RLCRawBlock) } +}; +#if RLCHDR_IMPLEMENTATION + RLCRawBlock::RLCRawBlock(int wbsn, const BitVector &wData, + float wRSSI, float wTimingError, ChannelCodingType cc) + { + RN_MEMCHKNEW(RLCRawBlock) + mData.clone(wData); // Explicit clone. + mBSN = wbsn; + mmac.parseMAC(mData); // Pull the MAC header out of the BitVector. + mUpCC = cc; + assert(mData.isOwner()); + mRD = RadData(wRSSI,wTimingError); + } +#endif + +// There may be multiple RLC_sub_blocks for multiple PDUs in one RLC/MAC block. +struct RLCSubBlockHeader +{ + static int makeoctet(unsigned length, unsigned mbit, unsigned ebit) { + return (length << 2) | (mbit << 1) | ebit; + } + // unsigned mLengthIndicator:6; // length of PDU with block, but see GSM04.60sec10.4.14 + // If the LLC PDU does not fit in a single RLC block, then there is no + // length indicator byte for the full RLC blocks, and the rest of the RLC block is PDU data. + // (Works because the E bit in the RLC header tells us if there is a length indicator.) + // Otherwise (ie, for the final RLC segment of each PDU), there is a length indicator, + // and there may be additional PDUs tacked on after the end of this PDU, specified by M bit. + // The description of length indicator in 10.4.14 is confusing. + // In English: you need the length-indicator byte if: + // (a) The PDU does not fill the RLC block, indicated by the LI field, or + // (b) This is the last segment of a PDU and there are more PDUs following + // in the same TBF, indicated by M=1. + // The "singular" case mentioned in 10.4.14 occurs when you need the + // length-indicator for (b) but not for (a). In this singular case only, + // you set the LI (length) field to 0, god only knows why, and presumably M=0, + // in the next-to-last segment, then presumably you put the last byte of + // the PDU into the next RLC block with a LI=1 and M=1. + // If the PDU is incomplete (for uplink blocks only, which presumably means + // the allocation ran out before the PDU data) then the final RLC block + // would be full so you normally wouldn't need an LI Byte, but to mark this fact + // you must reduce the last segment size by one to make room for an LI Byte with LI=0, + // and presumably M=0, which is distinguishable from the "singular case" above + // by the CountDown field reaching 0. + // unsigned mM:1; // More bit; if set there is another sub-block after this one. + // unsigned mE:1; // Extension bit, see GSM04.60 table 10.4.13.1. + // With M bit, indicates if there is more PDU data in this RLC block. + // M+E = 0+0: reserved; + // M+E = 0+1: no more data after the current LLC PDU segment. + // M+E = 1+0: new LLC PDU after current LLC PDU, with extension octet. + // M+E = 1+1: new LLC PDU after current LLC PDU, fills rest of RLC block. +}; + + +// We are not using this, because we are not doing contention resolution required +// for single phase uplink. +struct RLCSubBlockTLLI +{ + RLCSubBlockHeader hdr; // 1 byte. + unsigned char mTLLI[4]; // 4 bytes containing a TLLI. + struct { + unsigned mPFI:7; + unsigned mE:1; + } b5; +}; + + +struct RLCDownlinkDataBlockHeader // GSM04.60sec10.2.1 + : public MACDownlinkHeader // 1 byte MAC header +{ + // From GSM 04.60 10.4: + // Octet 1: + // Use of Power Reduction field described in 5.08 10.2.2. We dont need it. + Field_z<2> mPR; // Power Reduction. 0 means no power reduction, 1,2 mean some, 3 means 'not usable.' + Field_z<5> mTFI; // TFI - temp flow ID that this block is in + Field_z<1> mFBI; // Final Block Indicator - last block of this TBF + + // Octet 2: + Field_z<7> mBSN; // Block Sequence Number, modulo 128 + // If E bit is one, followed directly by RLC data. + // If E bit is zero, followed by zero or more RLC_Data_Sub_Block. + Field_z<1> mE; // End of extensions bit. + + // Note: unused RLC data field filled with 0x2b as per 04.60 10.4.16 + + RLCDownlinkDataBlockHeader() { + MACDownlinkHeader::init(MACPayloadType::RLCData); + } + void writeRLCHeader(MsgCommon& dest) const; + void write(BitVector&dst) const; + void text(std::ostream&os) const; + + //void setTFI(unsigned wTFI) { b1.mTFI = wTFI; } + //void setFBI(bool wFBI) { b1.mFBI = wFBI; } + //void setBSN(unsigned wBSN) { b2.mBSN = wBSN; } + //void setE(bool wE) { b2.mE = wE; } + +}; +#if RLCHDR_IMPLEMENTATION + void RLCDownlinkDataBlockHeader::writeRLCHeader(MsgCommon& dest) const { + dest.WRITE_ITEM(mPR); + dest.WRITE_ITEM(mTFI); + dest.WRITE_ITEM(mFBI); + dest.WRITE_ITEM(mBSN); + dest.WRITE_ITEM(mE); + } + + void RLCDownlinkDataBlockHeader::write(BitVector&dst) const { + MsgCommonWrite mcw(dst); + MACDownlinkHeader::writeMACHeader(mcw); + RLCDownlinkDataBlockHeader::writeRLCHeader(mcw); + } + void RLCDownlinkDataBlockHeader::text(std::ostream&os) const { + MsgCommonText dst(os); + writeMACHeader(dst); + writeRLCHeader(dst); + } +#endif + + +// GSM04.60sec10.2.2 +struct RLCUplinkDataBlockHeader +{ + // Octet 0 is the MAC header. + MACUplinkHeader mmac; + // Octet 1: RLC Header (starts at bit 8) + Field<1> mSpare; + Field<1> mPI; // PFI Indicator bit; If set, optional PFI is present in data. + // PFI identifies a Packet Flow Context defined GSM24.008, + // and mentioned in GSP04.60 table 11.2.6.2 + // Range will not support these. + Field<5> mTFI; // TFI - temp flow ID that this block is in + Field<1> mTI; // TLLI indicator. If set, optional TLLI is present in dataa. + // TLLI must be send by MS during a one phase access, because + // the network does not know TLLI. It is not needed for two phase access. + // Octet 2: + Field<7> mBSN; // Block Sequence Number, modulo 128 + Field<1> mE; // Extension bit: 0 indicates next word is length indicator, + // 1 means whole block is data. + + public: + + size_t parseRLCHeader(const RLCRawBlock *src); + // Since this is an uplink header, we only need to write it for testing purposes. + void writeRLCHeader(MsgCommon&dst) const; + void write(BitVector&dst) const; // Only needed for testing. + void text(std::ostream&os) const; +}; +#if RLCHDR_IMPLEMENTATION + size_t RLCUplinkDataBlockHeader::parseRLCHeader(const RLCRawBlock *src) { + mmac = src->mmac; + size_t rp = mmac.lengthBits(); + rp++; // skip spare bit. + mPI = src->mData.readField(rp,1); + mTFI = src->mData.readField(rp,5); + mTI = src->mData.readField(rp,1); + mBSN = src->mData.readField(rp,7); + mE = src->mData.readField(rp,1); + return rp; + } + // Since this is an uplink header, we only need to write it for testing purposes. + void RLCUplinkDataBlockHeader::writeRLCHeader(MsgCommon&dst) const { + dst.WRITE_ITEM(mSpare); + dst.WRITE_ITEM(mPI); + dst.WRITE_ITEM(mTFI); + dst.WRITE_ITEM(mTI); + dst.WRITE_ITEM(mBSN); + dst.WRITE_ITEM(mE); + } + void RLCUplinkDataBlockHeader::write(BitVector&dst) const { // Only needed for testing. + MsgCommonWrite mcw(dst); + mmac.writeMACHeader(mcw); + writeRLCHeader(mcw); + } + void RLCUplinkDataBlockHeader::text(std::ostream&os) const { + MsgCommonText dst(os); + mmac.writeMACHeader(dst); + writeRLCHeader(dst); + } +#endif + +class RLCUplinkDataSegment : public BitVector { + public: + RLCUplinkDataSegment(const BitVector&wPayload) : + BitVector(NULL,(char*)wPayload.begin(),(char*)wPayload.end()) {} + + // access to potentially multiple data fields + // GSM04.60 sec 10.4.13 and 10.4.14 + size_t LIByteLI(size_t lp=0) const { return peekField(lp,6); } + bool LIByteM(size_t lp=0) const { return peekField(lp+6,1); } + bool LIByteE(size_t lp=0) const { return peekField(lp+7,1); } +}; + +class RLCUplinkDataBlock + : public RLCUplinkDataBlockHeader +{ + BitVector mData; + public: + static const unsigned mHeaderSizeBits = 24; + ChannelCodingType mUpCC; // We dont use this - debugging info only. + + // Convert a BitVector into an RLC data block. + // We simply take ownership of the BitVector memory. + // The default destructor will destroy the BitVector when delete is called on us. + RLCUplinkDataBlock(RLCRawBlock* wSrc); + ~RLCUplinkDataBlock() { RN_MEMCHKDEL(RLCUplinkDataBlock) } + + // Return subset of the BitVector that is payload. + BitVector getPayload() { return mData.tail(mHeaderSizeBits); } + void text(std::ostream&os); +}; +#if RLCHDR_IMPLEMENTATION + RLCUplinkDataBlock::RLCUplinkDataBlock(RLCRawBlock* wSrc) + { + RN_MEMCHKNEW(RLCUplinkDataBlock) + mData = wSrc->mData; + size_t tmp = parseRLCHeader(wSrc); + mUpCC = wSrc->mUpCC; + assert(tmp == mHeaderSizeBits); + } + void RLCUplinkDataBlock::text(std::ostream&os) { + os << "RLCUplinkDataBlock=("; + RLCUplinkDataBlockHeader::text(os); + os << "\npayload:"; + // Write out the data as bytes. + BitVector payload(getPayload()); + payload.hex(os); + /*** + int i, size=payload.size(); char buf[10]; + for (i=0; i < size-8; i+=8) { + sprintf(buf," %02x",(int)payload.peekField(i,8)); + os << buf; + } + ***/ + os << ")"; + } +#endif + +/** GSM 04.60 10.2.1 */ +class RLCDownlinkDataBlock + : public RLCDownlinkDataBlockHeader, public Text2Str +{ + public: + // The mPayload does not own any allocated storage; it points into the mPDU of the TBF. + // We are not currently putting multiple PDUs in a TBF, so this is ok. + ByteVector mPayload; // max size is 52, may be smaller. + bool mIdle; // If true, block contains only a keepalive. + ChannelCodingType mChannelCoding; + + int getPayloadSize() const { // In bytes. + return RLCPayloadSizeInBytes[mChannelCoding]; + } + + int headerSizeBytes() { return 3; } + + RLCDownlinkDataBlock(ChannelCodingType wCC) : mIdle(0), mChannelCoding(wCC) {} + + // Convert the Downlink Data Block into a BitVector. + // We do this right before sending it down to the encoder. + BitVector getBitVector() const; + void text(std::ostream&os, bool includePayload) const; + void text(std::ostream&os) const { text(os,true); } // Default value doesnt work. Gotta love that C++. + + // /** Construct the block and write data into it. */ + // DownlinkRLCDataBlock( + // unsigned RRBP, bool SP, unsigned USF, + // bool PR, unsigned TFI, bool FBI, + // unsigned BSN, + // const BitVector& data); + +}; +#if RLCHDR_IMPLEMENTATION + BitVector RLCDownlinkDataBlock::getBitVector() const + { + // Add 3 bytes for mac and rlc headers. + BitVector result(8 *(3+mPayload.size())); + RLCDownlinkDataBlockHeader::write(result); + BitVector resultpayload(result.tail(3*8)); + resultpayload.unpack(mPayload.begin()); // unpack mPayload into resultpayload + return result; + } + void RLCDownlinkDataBlock::text(std::ostream&os, bool includePayload) const { + os << "RLCDownlinkDataBlock=("; + RLCDownlinkDataBlockHeader::text(os); + os << LOGVAR2("CCoding",mChannelCoding) < +#include "Defines.h" +//#include "GSMCommon.h" +#include +#include +#define RLCHDR_IMPLEMENTATION 1 +#include "RLCHdr.h" +#include "TBF.h" +#define RLCMESSAGES_IMPLEMENTATION 1 +#include "RLCMessages.h" +#include "MAC.h" +#include "FEC.h" + + +namespace GPRS { + +const char *RLCUplinkMessage::name(MessageType type) +{ + static char buf[50]; + switch (type) { + CASENAME(PacketCellChangeFailure) + CASENAME(PacketControlAcknowledgement) + CASENAME(PacketDownlinkAckNack) + CASENAME(PacketUplinkDummyControlBlock) + CASENAME(PacketMeasurementReport) + CASENAME(PacketEnhancedMeasurementReport) + CASENAME(PacketResourceRequest) + CASENAME(PacketMobileTBFStatus) + CASENAME(PacketPSIStatus) + CASENAME(EGPRSPacketDownlinkAckNack) + CASENAME(PacketPause) + CASENAME(AdditionalMSRadioAccessCapabilities) + default: + sprintf(buf,"RLCUplinkMessageType %d (unknown)",(int)type); + return buf; + } +} + +const char *RLCDownlinkMessage::name(MessageType type) +{ + static char buf[50]; + switch (type) { + CASENAME(PacketAccessReject ) + CASENAME(PacketCellChangeOrder) + CASENAME(PacketDownlinkAssignment) + CASENAME(PacketMeasurementOrder ) + CASENAME(PacketPagingRequest ) + CASENAME(PacketPDCHRelease) + CASENAME(PacketPollingRequest) + CASENAME(PacketPowerControlTimingAdvance) + CASENAME(PacketPRACHParameters) + CASENAME(PacketQueueingNotification) + CASENAME(PacketTimeslotReconfigure) + CASENAME(PacketTBFRelease) + CASENAME(PacketUplinkAckNack) + CASENAME(PacketUplinkAssignment) + CASENAME(PacketDownlinkDummyControlBlock) + CASENAME(PSI1) + CASENAME(PSI2) + CASENAME(PSI3) + CASENAME(PSI3bis) + CASENAME(PSI4) + CASENAME(PSI5) + CASENAME(PSI6) + CASENAME(PSI7) + CASENAME(PSI8) + CASENAME(PSI13) + CASENAME(PSI14) + CASENAME(PSI3ter) + CASENAME(PSI3quater) + CASENAME(PSI15) + default: + sprintf(buf,"RLCDownlinkMessageType %d (unknown)",(int)type); + return buf; + } +} + +MSInfo *RLCMsgPacketResourceRequest::getMS(PDCHL1FEC *chan, bool create) +{ + // If MS is identified by a TLLI, the msg is not specifically associated with a TBF. + if (mTLLIPresent) { + MSInfo *ms = gL2MAC.macFindMSByTlli(mTLLI, create); + return ms; + } else { + RLCDirType dir = mGTFI.mIsDownlinkTFI ? RLCDir::Down : RLCDir::Up; + TBF *tbf = chan->getTFITBF(mGTFI.mGTFI,dir); + if (tbf) return tbf->mtMS; + } + return NULL; +} + +void RLCMsgPacketUplinkAssignmentDynamicAllocationElt::setFrom(TBF *tbf,MultislotSymmetry sym) +{ + MSInfo *ms = tbf->mtMS; + setAlpha(ms->msGetAlpha()); + PDCHL1Uplink *up; + RN_FOR_ALL(PDCHL1UplinkList_t,ms->msPCHUps,up) { + int tn = up->TN(); + setUSF(tn,ms->msUSFs[tn]); + setGamma(tn,ms->msGetGamma()); + } + if (ms->isExtendedDynamic()) { + mExtendedDynamicAllocation = true; + } + + //if (sym == MultislotSymmetric && isExtendedDynamic()) { + // // This sounds odd, but we need to use the downlink timeslots to program + // // the uplink timeslots so that they will be symmetric. + // // If they are assymetric, the smaller array is always valid in both directions. + // PDCHL1Downlink *down; + // RN_FOR_ALL(PDCHL1DownlinkList_t,ms->msPCHDowns,down) { + // int tn = down->TN(); + // setUSF(tn,ms->msUSFs[tn]); + // setGamma(tn,ms->msGetGamma()); + // } + //} else { + // PDCHL1Uplink *up; + // RN_FOR_ALL(PDCHL1UplinkList_t,ms->msPCHUps,up) { + // int tn = up->TN(); + // setUSF(tn,ms->msUSFs[tn]); + // setGamma(tn,ms->msGetGamma()); + // } + //} +} + +void RLCMsgPowerControlParametersIE::setFrom(TBF *tbf) +{ + MSInfo *ms = tbf->mtMS; + setAlpha(ms->msGetAlpha()); + PDCHL1Downlink *down; + RN_FOR_ALL(PDCHL1DownlinkList_t,ms->msPCHDowns,down) { + int tn = down->TN(); + setGamma(tn,ms->msGetGamma()); + } +} + + +/** GSM 04.60 11.2 */ +RLCUplinkMessage* RLCUplinkMessageParse(RLCRawBlock *src) +{ + RLCUplinkMessage *result = NULL; + + unsigned mMessageType = src->mData.peekField(8,6); + + // Kyle reported that OpenBTS crashes parsing the RLCMsgMSRACapabilityValuePartIE. + // If you look at the message that is in, if the RACap is trashed, the message is unusable. + // So lets catch errors way up at this level, and ignore them on error. + // In case of error, we are probably permanently losing the memory associated with the messsage, oh well. + try { + switch (mMessageType) { + case RLCUplinkMessage::PacketControlAcknowledgement: + result = new RLCMsgPacketControlAcknowledgement(src); + break; + case RLCUplinkMessage::PacketDownlinkAckNack: + // Thats right: DownlinkAckNack is an uplink message. + result = new RLCMsgPacketDownlinkAckNack(src); + break; + case RLCUplinkMessage::PacketUplinkDummyControlBlock: + result = new RLCMsgPacketUplinkDummyControlBlock(src); + break; + case RLCUplinkMessage::PacketResourceRequest: + result = new RLCMsgPacketResourceRequest(src); + break; + // case PacketMobileTBFStatus: + // case AdditionalMSRadioAccessCapabilities: + default: + GLOG(INFO) << "unsupported RLC uplink message, type=" << mMessageType; + //result = new RLCMsgPacketUplinkDummyControlBlock(src); + return NULL; + } + return result; + } catch (ByteVectorError) { + GLOG(ERR) << "Parse Error: Premature end of message, type=" + < +#include +#include "Defines.h" +#include "BitVector.h" +#include "Logger.h" +#include "GSMCommon.h" +//#include "GSMTransfer.h" +//#include "GSMTDMA.h" +//#include +#include "MsgBase.h" +#include "RLCHdr.h" +#include "TBF.h" +#include "MemoryLeak.h" + + +namespace GPRS { +class TBF; + + +/** GSM 04.60 11.2 */ +class RLCMessage : public Text2Str { + public: + virtual void text(std::ostream&) const = 0; + RLCMessage() { RN_MEMCHKNEW(RLCMessage) } + // The virtual keyword on a destructor indicates that both the base destructor (this one) + // and the derived class destructor are both called. Otherwise they aren't. It is foo bar. + virtual ~RLCMessage() { RN_MEMCHKDEL(RLCMessage) } +}; +std::ostream& operator<<(std::ostream& os, const RLCMessage *msg); +#if RLCMESSAGES_IMPLEMENTATION +std::ostream& operator<<(std::ostream& os, const RLCMessage *msg) +{ + msg->text(os); + return os; +} +#endif + +class RLCMsgUplinkIE +{ + public: + virtual void parseElement(const BitVector &src, size_t &rp) = 0; + virtual void writeBody(MsgCommon&dst) const = 0; + // The indivual IE can overwrite textElement is MsgCommon does not work very well for it. + virtual void textElement(std::ostream& os) const { + MsgCommonText dst(os); + writeBody(dst); + } +}; + +class RLCMsgDownlinkIE +{ + public: + virtual void writeBody(MsgCommon&dst) const = 0; + + void writeOptional01(MsgCommon &dst, bool control) const { + if (control) { + dst.write1(); + writeBody(dst); + } else { + dst.write0(); + } + } +}; + +// The RLCDownlink and RLCUplink Messagesc are RLC Control messages - +// they are not L3 Messages as defined in GSM 04.18. +// These messages are used at the RLC/MAC layer to control the PDCH channels +// assigned for packet (GPRS) use. Their primary use is to transfer +// L3 Messages and user data between the SGSN and the MS. +// Mobility Management routines reside entirely inside the SGSN. +class RLCDownlinkMessage : + public MACDownlinkHeader, + public RLCMessage +{ + public: + + /** Message Type GSM 04.06 11.2.0.2 */ + // The ones we will implement marked with <<< + // Message marked PCCCH only cannot be implemented now. + // From sec 11.1.1.1: if the high bit of a downlink control message type is 1, + // then it is a distribution message to all MS, otherwise it is a non-distribution message. + // Note that the downlink message content is byte aligned (6 bit MessageType + 2 bit PageMode) + // Also note that the uplink message content is not byte aligned. + // From sec 11.1.1.2: Non-distribution messages may have distribution contents, + // followed by an Address Information (to identify a single MS) followed by + // non-distribution contents. + // NOTE: The unused bits at the end of the control message must be packed out + // as per GSM04.60sec11: one 0 bit, followed by 0 bits to get to a byte boundary, + // followed by byte 0x2b forever. + enum MessageType { + /*100001 */ PacketAccessReject = 0x21, // PACCH or PCCCH <<<< + /*000001 */ PacketCellChangeOrder = 0x1, // PCCCH or PACCH + /*000010 */ PacketDownlinkAssignment = 0x2, // PCCCH or PACCH <<<< acknowledged + /*000011 */ PacketMeasurementOrder = 0x3, // PCCCH or PACCH + + // PagingRequest is only sent on PACCH to initiate an RR connection, so skip for now: + // Note: You can send still GSM paging messages on CCCH when MS is in packet-idle mode. + /*100010 */ PacketPagingRequest = 0x22, // PCCCH or PACCH + /*100011 */ PacketPDCHRelease = 0x23, // PACCH + /*000100 */ PacketPollingRequest = 0x4, // PACCH or PCCH <<<< not used implicitly acknowldged + /*000101 */ PacketPowerControlTimingAdvance = 0x5, // PACCH + /*100100 */ PacketPRACHParameters = 0x24, // PCCCH only + /*000110 */ PacketQueueingNotification = 0x6, // PCCCH only + /*000111 */ PacketTimeslotReconfigure = 0x7, // PACCH + /*001000 */ PacketTBFRelease = 0x8, // PACCH <<< acknowledged + /*001001 */ PacketUplinkAckNack = 0x9, // PACCH <<< + // The acknowledgement to Packet Uplink Assignment is not strictly needed + // because we will know as soon as the MS starts sending blocks. + /*001010 */ PacketUplinkAssignment = 0xa, // PCCCH or PACCH <<< acknowledged + /*100101 */ PacketDownlinkDummyControlBlock = 0x25, // PCCCH or PACCH + /*110001 */ PSI1 = 0x31, + /*110010 */ PSI2 = 0x32, + /*110011 */ PSI3 = 0x33, + /*110100 */ PSI3bis = 0x34, + /*110101 */ PSI4 = 0x35, + /*110110 */ PSI5 = 0x36, + /*110000 */ PSI6 = 0x30, + /*111000 */ PSI7 = 0x38, + /*111001 */ PSI8 = 0x39, + /*110111 */ PSI13 = 0x37, + /*111010 */ PSI14 = 0x3a, + /*111100 */ PSI3ter = 0x3c, + /*111101 */ PSI3quater = 0x3d, + /*111110 */ PSI15 = 0x3e + }; + static const char *name(MessageType mtype); + + // There are optional additional RLC Control Block header fields here, but + // we will not use them, so they are not even mentioned here. + + MessageType mMessageType; // 6 bits + unsigned mPageMode; // 2 bits + + TBF *mTBF; // If this message is associated with a tbf, here it is. + + RLCDownlinkMessage(MessageType mtype, bool wRequiresAck, TBF *wtbf) : + mMessageType(mtype), + mPageMode(0), + mTBF(wtbf) + { + MACDownlinkHeader::init(MACPayloadType::RLCControl); + } + + + // Not all the messages have setTLLI(). + virtual void setTLLI(int) {}; + // Not all the messages have setTimingAdvance(). + virtual void setTimingAdvance(int) {}; + + // writeBody defined in each message class to write the message body starting after PageMode. + virtual void writeBody(MsgCommon&dst) const = 0; + void writeHeader(MsgCommon &dst) const; + + /** Serialize this message into a BitVector. The caller is responsible for deleting the memory. + */ + void write(BitVector&vec) const; + void text(std::ostream &os) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCDownlinkMessage::writeHeader(MsgCommon& dst) const { + MACDownlinkHeader::writeMACHeader(dst); + dst.WRITE_FIELD(mMessageType,6); + dst.WRITE_FIELD(mPageMode,2); + } + void RLCDownlinkMessage::text(std::ostream &os) const { + MsgCommonText dst(os); + os << name(mMessageType) << ":"; + writeHeader(dst); + writeBody(dst); + } + void RLCDownlinkMessage::write(BitVector&vec) const { + MsgCommonWrite dst(vec); + writeHeader(dst); + writeBody(dst); + // GSM04.60 sec 11: + // The padding bits may be the 'null' string. Otherwise, the padding + // bits starts with bit '0', followed by 'spare padding'. + if (dst.wp & 0x7) { dst.write0(); } // one optional 0 bit + while (dst.wp & 0x7) { dst.writeL(); } // pad to a byte boundary. + while (dst.wp < RLCBlockSizeInBits[0]) { dst.writeField(0x2b,8); } + } +#endif + + + +/** GSM 04.60 11.2 */ +class RLCUplinkMessage : public RLCMessage +{ + public: + // GSM04.60 sec 11.2 + enum MessageType { + /* 000000 */ PacketCellChangeFailure = 0x0, + /* 000001 */ PacketControlAcknowledgement = 0x1, + /* 000010 */ PacketDownlinkAckNack = 0x2, + /* 000011 */ PacketUplinkDummyControlBlock = 0x3, + /* 000100 */ PacketMeasurementReport = 0x4, + /* 001010 */ PacketEnhancedMeasurementReport = 0xa, + /* 000101 */ PacketResourceRequest = 0x5, + /* 000110 */ PacketMobileTBFStatus = 0x6, + /* 000111 */ PacketPSIStatus = 0x7, + /* 001000 */ EGPRSPacketDownlinkAckNack = 0x8, + /* 001001 */ PacketPause = 0x9, + /* 001011 */ AdditionalMSRadioAccessCapabilities = 0xb + }; + static const char *name(MessageType type); + MACUplinkHeader mmac; // 8 bits + MessageType mMessageType; // 6 bits + + + // Parse/text just the body. These must be defined in each descendent class + //virtual void textBody(std::ostream&os) const = 0; + + virtual int getTFI() { return -1; } + virtual MSInfo *getMS(PDCHL1FEC *chan, bool create) { assert(0); } + virtual MSInfo *getTBF(PDCHL1FEC *chan) { assert(0); } + + virtual void parseBody(const BitVector &src, size_t &rp) = 0; + virtual void writeBody(MsgCommon&dst) const = 0; + void parse(const RLCRawBlock *src); + void writeHeader(MsgCommon &dst) const; + + /** Serialize this message into a BitVector. The caller is responsible for deleting the memory. + */ + void write(BitVector&vec) const; + void text(std::ostream &os) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCUplinkMessage::parse(const RLCRawBlock*src) { + // The MACUplinkHeader is explict (mmac) instead of a base class + // to make this line of code more obvious: + mmac = src->mmac; // Parsed previously. + size_t rp = mmac.lengthBits(); + mMessageType = (MessageType) src->mData.readField(rp,6); + parseBody(src->mData,rp); + } + + void RLCUplinkMessage::writeHeader(MsgCommon&dst) const { + mmac.writeMACHeader(dst); + dst.WRITE_FIELD(mMessageType,6); + } + + void RLCUplinkMessage::text(std::ostream &os) const { + MsgCommonText dst(os); + os << name(mMessageType) << ":"; + mmac.text(os); // or: mmac.writeMACHeader(dst); + dst.WRITE_FIELD(mMessageType,6); + writeBody(dst); + } + void RLCUplinkMessage::write(BitVector&vec) const { + MsgCommonWrite dst(vec); + writeHeader(dst); + writeBody(dst); + // GSM04.60 sec 11: For RLC messages: one 0 followed by byte-aligned 0x2b + if (dst.wp & 0x7) { dst.write0(); } // one optional 0 bit + while (dst.wp & 0x7) { dst.writeL(); } // pad to a byte boundary. + while (dst.wp < RLCBlockSizeInBits[0]) { dst.writeField(0x2b,8); } + } +#endif + +// (pat) This is for downlink power parameters, as per GSM04.60 sec 11.2.29. +// We will probably never use this. +//struct L3IADownlinkPowerOptionIE { +// int mPowerOption; // 1 if P0, PWR_CTRL_MODE, PR_MODE should be included. +// unsigned mP0:4; +// unsigned mBTSPwrCtrlMode:1; +// unsigned mPRMode:1; +// void writePower(L3Frame &dst, int whichDesignationMethod) { +// if (mPowerOption) { +// whichDesignationMethod ? dst.writeH() : dst.writeField(1,1); +// dst.writeField(P0,4); +// dst.writeField(mBTSPwrCtrlMode,1); +// dst.writeField(mPRMode,1); +// } else { +// whichDesignationMethod ? dst.writeL() : dst.writeField(0,1); +// } +// } +// L3AssignmentPowerOptionIE::L3AssignmentPowerOptionIE() { memset(this,0,sizeof(*this)); } +//}; + + +// GSM04.60 12.3 +// Note this IE is both uplink and downlink. +struct RLCMsgPacketAckNackDescriptionIE : public RLCMsgUplinkIE, public RLCMsgDownlinkIE +{ + Field<1> mFinalAckIndication; + Field<7> mSSN; // "Starting Sequence Number" except it is not. + // It is actually the ending sequence number. + static const int mbitmapsize = 64; + bool mBitMap[mbitmapsize]; + + void parseElement(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketAckNackDescriptionIE::parseElement(const BitVector &src, size_t &rp) + { + mFinalAckIndication = src.readField(rp,1); + mSSN = src.readField(rp,7); + for (int i = 0; i < mbitmapsize; i++) { + mBitMap[i] = src.readField(rp,1); + } + } + void RLCMsgPacketAckNackDescriptionIE::writeBody(MsgCommon&dst) const + { + dst.WRITE_ITEM(mFinalAckIndication); + dst.WRITE_ITEM(mSSN); + dst.writeBitMap((bool*)mBitMap,mbitmapsize,"Bitmap"); + } +#endif + +// GSM04.60 12.7 +struct RLCMsgChannelRequestDescriptionIE : public RLCMsgUplinkIE +{ + Field<4> mPeakThroughputClass; // See 3GPP 24.008 + Field<2> mRadioPriority; // See 11.2.5 0 =highest, 3=lowest + Field<1> mRLCMode; // 0 == acknowledged, 1 == unacknowleged mode. + Field<1> mLLCPDUType; // 0 == LLC PDU is SACK or ACK[acknowledged mode], 1 == its not + Field<16> mRLCOctetCount; // Number of octets MS wants to transfer, or 0 if unspecified. + + void parseElement(const BitVector &src, size_t &rp); + //void textElement(std::ostream&) const; + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgChannelRequestDescriptionIE::parseElement(const BitVector &src, size_t &rp) + { + mPeakThroughputClass = src.readField(rp,4); + mRadioPriority = src.readField(rp,2); + mRLCMode = src.readField(rp,1); + mLLCPDUType = src.readField(rp,1); + mRLCOctetCount = src.readField(rp,16); + } + void RLCMsgChannelRequestDescriptionIE::writeBody(MsgCommon&dst) const { + dst.WRITE_ITEM(mPeakThroughputClass); + dst.WRITE_ITEM(mRadioPriority); + dst.WRITE_ITEM(mRLCMode); + dst.WRITE_ITEM(mLLCPDUType); + dst.WRITE_ITEM(mRLCOctetCount); + } + + /* + void RLCMsgChannelRequestDescriptionIE::textElement(std::ostream&os) const + { + os << " ChannelRequest=("; + RN_WRITE_TEXT(mPeakThroughputClass); + RN_WRITE_TEXT(mRadioPriority); + RN_WRITE_TEXT(mRLCMode); + RN_WRITE_TEXT(mLLCPDUType); + RN_WRITE_TEXT(mRLCOctetCount); + os << ")"; + } + */ +#endif + +struct MSRACap_s : public RLCMsgUplinkIE +{ + Field<4> mAccessTechnologyType; // The caller parses and sets this. + bool mA5BitsPresent; + Field<3> mRFPowerCapability; + Field<7> mA5Bits; + Field<1> mESInd; + Field<1> mPS; + Field<1> mVGCS; + Field<1> mVBS; + bool mMultiSlotCapPresent; + bool mHSCDMultiSlotClassPresent; + Field<5> mHSCDMultiSlotClass; + bool mGPRSMultiSlotClassPresent; + Field<5> mGPRSMultiSlotClass; + Field<1> mGPRSExtendedDynamicAllocationCapability; + bool mSMSPresent; + Field<4> mSMS_VALUE; Field<4> mSM_VALUE; + // There is much more, but we dont keep it. + + void parseElement(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; + //void textElement(std::ostream&) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void MSRACap_s::parseElement(const BitVector &src, size_t &rp) + { + mRFPowerCapability = src.readField(rp,3); + if ((mA5BitsPresent = src.readField(rp,1))) { + mA5Bits = src.readField(rp,7); + } + mESInd = src.readField(rp,1); + mPS = src.readField(rp,1); + mVGCS = src.readField(rp,1); + mVBS = src.readField(rp,1); + if ((mMultiSlotCapPresent = src.readField(rp,1))) { + // Multislot Capability Struct: + if ((mHSCDMultiSlotClassPresent = src.readField(rp,1))) { + mHSCDMultiSlotClass = src.readField(rp,5); + } + if ((mGPRSMultiSlotClassPresent = src.readField(rp,1))) { + mGPRSMultiSlotClass = src.readField(rp,5); + mGPRSExtendedDynamicAllocationCapability = src.readField(rp,1); + } + if ((mSMSPresent = src.readField(rp,1))) { + mSMS_VALUE = src.readField(rp,4); + mSM_VALUE = src.readField(rp,4); + } + } else { + mGPRSMultiSlotClassPresent = 0; + } + } + void MSRACap_s::writeBody(MsgCommon&dst) const { + dst.WRITE_ITEM(mAccessTechnologyType); + dst.WRITE_ITEM(mRFPowerCapability); + if (dst.write01(mA5BitsPresent)) { dst.WRITE_ITEM(mA5Bits); } + dst.WRITE_ITEM(mESInd); + dst.WRITE_ITEM(mVGCS); + dst.WRITE_ITEM(mVBS); + if (mMultiSlotCapPresent) { + if (dst.write01(mHSCDMultiSlotClassPresent)) dst.WRITE_ITEM(mHSCDMultiSlotClass); + if (dst.write01(mGPRSMultiSlotClassPresent)) { + dst.WRITE_ITEM(mGPRSMultiSlotClass); + dst.WRITE_ITEM(mGPRSExtendedDynamicAllocationCapability); + } + if (dst.write01(mSMSPresent)) { + dst.WRITE_ITEM(mSMS_VALUE); + dst.WRITE_ITEM(mSM_VALUE); + } + } + // More stuff ignored. + } + + /* + void MSRACap_s::textElement(std::ostream&os) const { + RN_WRITE_TEXT(mAccessTechnologyType); + RN_WRITE_TEXT(mRFPowerCapability); + RN_WRITE_OPT_TEXT(mA5Bits,mA5BitsPresent); + RN_WRITE_TEXT(mESInd); + RN_WRITE_TEXT(mPS); + RN_WRITE_TEXT(mVGCS); + RN_WRITE_TEXT(mVBS); + RN_WRITE_OPT_TEXT(mGPRSMultiSlotClass,mGPRSMultiSlotClassPresent); + } + */ +#endif + +// GSM24.008sec10.5.5.12a +struct RLCMsgMSRACapabilityValuePartIE : public RLCMsgUplinkIE +{ + // There can be many of these. + // The first one is all we are interested in. + // We will map all the other caps into the second struct, which we wont even use. + MSRACap_s MSRACap[2]; + int mNumMSRACap; + + void parseElement(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; + //void textElement(std::ostream&) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgMSRACapabilityValuePartIE::parseElement(const BitVector &src, size_t &rp) + { + mNumMSRACap = 0; + do { + int AccessTechnologyType = src.readField(rp,4); + unsigned len = src.readField(rp,7); // Length in bits of the rest of this access cap. + // Kyle at L3 reported that this was crashing in the src.readField below, + // which means this struct is invalid or not being parsed properly. + // Lets check, although it actually needs to be much bigger than this + // because this is embedded inside a large message. + if (len + 1 >= src.size()) { + LOG(INFO) << "Invalid RA Capability Value Part struct"< mIsDownlinkTFI; // 1 for downlink TFI, 0 for uplink TFI + Field_z<5> mGTFI; // This is NOT a TFI assignment; it is used + // to identify the MS which receives this message. + + void setTFI(bool wIsDownlinkTFI,int wTFI) { + // TBF uninitialized TFI is -1; it is an error if that value finds its way here. + assert(wTFI >= 0 && wTFI < 32); + mIsDownlinkTFI = wIsDownlinkTFI; mGTFI = wTFI; + } + void parseElement(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgGlobalTFIIE::parseElement(const BitVector &src, size_t &rp) { + mIsDownlinkTFI = src.readField(rp,1); + mGTFI = src.readField(rp,5); + } + void RLCMsgGlobalTFIIE::writeBody(MsgCommon &dst) const { + dst.WRITE_ITEM(mIsDownlinkTFI); + dst.WRITE_ITEM(mGTFI); + } +#endif + +// 3GPP 44-060 11.2.6 +// See 45.008 for definitions of these things. +struct ChannelQualityReportIE : public RLCMsgUplinkIE +{ + + // 45.008 10.2.3: "C Value is the enormalized received signal level at the MS + // as defined in 10.2.3.1". + // During Packet Transfer mode (which we are for a DownlinkAckNack) + // C Value and SIGN_VAR (variance of C Value) are measured from BCCH and + // subsequently used to determine MS output power as per 10.2.1. + Field<6> mCValue; + // RXQUAL is derived from decoder BEP [Bit Error Probability] + Field<3> mRXQual; // From decoder, low is better, meaningless for CS-4 encoding. + // SIGN_VAR from 0 to 64 encoded as: 0 to 15.75dB in 0.25dB steps. + Field<6> mSignVar; + // 45.008 10.3: I_LEVEL is measured interference level on each channel. + Bool_z mHaveILevel[8]; + Field<4> mILevel[8]; // For each timeslot. + void parseElement(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void ChannelQualityReportIE::parseElement(const BitVector &src, size_t &rp) { + mCValue = src.readField(rp,6); + mRXQual = src.readField(rp,3); + mSignVar = src.readField(rp,6); + for (int i = 0; i <= 7; i++) { + if ((mHaveILevel[i] = src.readField(rp,1))) { + mILevel[i] = src.readField(rp,4); + } + } + } + void ChannelQualityReportIE::writeBody(MsgCommon&dst) const { + dst.WRITE_ITEM(mCValue); + dst.WRITE_ITEM(mRXQual); + dst.WRITE_ITEM(mSignVar); + for (int i = 0; i <= 7; i++) { + if (mHaveILevel[i]) { dst.WRITE_ITEM(mILevel[i]); } + } + } +#endif + +/** GSM04.60 11.2.6 From MS to network. */ +struct RLCMsgPacketDownlinkAckNack : public RLCUplinkMessage +{ + Field<5> mTFI; + RLCMsgPacketAckNackDescriptionIE mAND; + bool mHaveChannelRequest; + RLCMsgChannelRequestDescriptionIE mCRD; + ChannelQualityReportIE mCQR; + + // We can ignore the rest of the message. + // struct ChannelQualityReport CQR; + // unsigned PFI:7 // (we wont use it) + + RLCMsgPacketDownlinkAckNack(const RLCRawBlock *src) { parse(src); } + + int getTFI() { return mTFI; } + + void parseBody(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; + //void textBody(std::ostream&) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketDownlinkAckNack::parseBody(const BitVector &src, size_t &rp) + { + mTFI = src.readField(rp,5); + mAND.parseElement(src,rp); + mHaveChannelRequest = src.readField(rp,1); + if (mHaveChannelRequest) { mCRD.parseElement(src,rp); } + mCQR.parseElement(src,rp); + } + void RLCMsgPacketDownlinkAckNack::writeBody(MsgCommon&dst) const { + dst.WRITE_ITEM(mTFI); + mAND.writeBody(dst); + if (dst.write01(mHaveChannelRequest)) { mCRD.writeBody(dst); } + mCQR.writeBody(dst); + } + + /* + void RLCMsgPacketDownlinkAckNack::textBody(std::ostream&os) const + { + RN_WRITE_TEXT(mTFI); + mAND.textElement(os); + if (mHaveChannelRequest) { mCRD.textElement(os); } + } + */ +#endif + +//static uint64_t readOptField(BitVector&src,size_t&rp,int bits,bool&option) { +// if ((option = src.readField(rp,1))) { +// return src.readField(rp,bits); +// } +//} + + +/** GSM04.60 11.2.6 From MS to network. */ +struct RLCMsgPacketControlAcknowledgement : public RLCUplinkMessage +{ + Field<32> mTLLI; // Yes, this really is not byte aligned in the message. + Field<2> mCtrlAck; // Which segments of the RLC/MAC control message were received? + + RLCMsgPacketControlAcknowledgement(const RLCRawBlock *src) { parse(src); } + void parseBody(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; + //void textBody(std::ostream&) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketControlAcknowledgement::parseBody(const BitVector&src, size_t&rp) + { + mTLLI = src.readField(rp,32); + mCtrlAck = src.readField(rp,2); + } + void RLCMsgPacketControlAcknowledgement::writeBody(MsgCommon&dst) const { + dst.writeField(mTLLI,32,"TLLI",tohex); + dst.WRITE_ITEM(mCtrlAck); + } + + /* + void RLCMsgPacketControlAcknowledgement::textBody(std::ostream&os) const + { + RN_WRITE_TEXT(mTLLI); + RN_WRITE_TEXT(mCtrlAck); + } + */ +#endif + +/** GSM04.60 11.2.16 From MS to network. */ +struct RLCMsgPacketResourceRequest : public RLCUplinkMessage +{ + bool mAccessTypePresent; // 0=two phase, 1=page response, 2=cell update, 3=mm procedure + Field<2> mAccessType; + RLCMsgGlobalTFIIE mGTFI; + bool mTLLIPresent; // 1 for TLLI present, 0 for TFI present. + Field<32> mTLLI; + bool mMSRadioAccessCapability2Present; + RLCMsgMSRACapabilityValuePartIE mMSRACap; + RLCMsgChannelRequestDescriptionIE mCRD; + bool mChangeMarkPresent; + Field<2> mChangeMark; + Field<5> mCValue; + bool mSignVarPresent; + Field<6> mSignVar; // Not present for two phase access. + bool mILevelPresent[8]; + Field<4> mILevelTN[8]; + bool mExtensionsPresent; + + RLCMsgPacketResourceRequest(const RLCRawBlock *src) { parse(src); } + + // Return the MSInfo identified by the contents of this message. + MSInfo *getMS(PDCHL1FEC *chan, bool create); + + void parseBody(const BitVector &src, size_t &rp); + void writeBody(MsgCommon&dst) const; + //void textBody(std::ostream&) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketResourceRequest::parseBody(const BitVector &src, size_t &rp) + { + if ((mAccessTypePresent = src.readField(rp,1))) { mAccessType = src.readField(rp,2); } + if ((mTLLIPresent = src.readField(rp,1))) { + mTLLI = src.readField(rp,32); + } else { + mGTFI.parseElement(src,rp); + } + + if ((mMSRadioAccessCapability2Present = src.readField(rp,1))) { + mMSRACap.parseElement(src,rp); + } + + mCRD.parseElement(src,rp); // We will use this. + if ((mChangeMarkPresent = src.readField(rp,1))) { mChangeMark = src.readField(rp,2); } + mCValue = src.readField(rp,6); + if ((mSignVarPresent = src.readField(rp,1))) { mSignVar = src.readField(rp,2); } + for (int i = 0; i < 8; i++) { + mILevelTN[i] = (mILevelPresent[i] = src.readField(rp,1)) ? src.readField(rp,4) : -1; + } + mExtensionsPresent = rp < src.size() && src.readField(rp,1); + // But ignore the exensions. + } + void RLCMsgPacketResourceRequest::writeBody(MsgCommon&dst) const { + if (dst.write01(mAccessTypePresent)) { dst.WRITE_ITEM(mAccessType); } + if (dst.write01(mTLLIPresent)) { + dst.writeField(mTLLI,32,"TLLI",tohex); + } else { + mGTFI.writeBody(dst); + } + if (dst.write01(mMSRadioAccessCapability2Present)) { + mMSRACap.writeBody(dst); + } + mCRD.writeBody(dst); + if (dst.write01(mChangeMarkPresent)) { dst.WRITE_ITEM(mChangeMark); } + dst.WRITE_ITEM(mCValue); + if (dst.write01(mSignVarPresent)) { dst.WRITE_ITEM(mSignVar); } + for (int i = 0; i < 8; i++) { + if (dst.write01(mILevelPresent[i])) dst.WRITE_ITEM(mILevelTN[i]); + } + dst.writeField(mExtensionsPresent,1,"mExtensionsPresent"); + } + + /* + void RLCMsgPacketResourceRequest::textBody(std::ostream&os) const { + RN_WRITE_OPT_TEXT(mAccessType,mAccessTypePresent); + RN_WRITE_OPT_TEXT(mTLLI,mTLLIPresent); + RN_WRITE_OPT_TEXT(mIsDownlinkTFI,!mTLLIPresent); + RN_WRITE_OPT_TEXT(mTFI,!mTLLIPresent); + if (mMSRadioAccessCapability2Present) { mMSRACap.textElement(os); } + mCRD.textElement(os); + RN_WRITE_OPT_TEXT(mChangeMark,mChangeMarkPresent); + RN_WRITE_TEXT(mCValue); + RN_WRITE_OPT_TEXT(mSignVar,mSignVarPresent); + for (int i = 0; i < 8; i++) { + RN_WRITE_OPT_TEXT(mILevelTN[i],mILevelTN[i] >= 0); + } + } + */ +#endif + +/** GSM04.60 11.2.8b From MS to network. */ +struct RLCMsgPacketUplinkDummyControlBlock : public RLCUplinkMessage +{ + Field<32> mTLLI; + + RLCMsgPacketUplinkDummyControlBlock(const RLCRawBlock*src) { parse(src); } + + void parseBody(const BitVector &src, size_t &rp); + void textBody(std::ostream&) const; + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketUplinkDummyControlBlock::parseBody(const BitVector &src, size_t &rp) + { + mTLLI = src.readField(rp,32); + } + void RLCMsgPacketUplinkDummyControlBlock::writeBody(MsgCommon&dst) const { + dst.writeField(mTLLI,32,"TLLI",tohex); + } + + /* + void RLCMsgPacketUplinkDummyControlBlock::textBody(std::ostream&os) const + { + RN_WRITE_TEXT(mTLLI); + }; + */ +#endif + + +// GSM04.60 12.12 +struct RLCMsgPacketTimingAdvanceIE : public RLCMsgDownlinkIE +{ + // Timing Advance defined in GSM05.10 + Field_z<1> mHaveTimingAdvanceValue; + Field_z<6> mTimingAdvanceValue; // New timing advance if present. + // We dont use the following: + // Timeslot number defined in GSM05.10 + Field_z<1> mHaveContinuousTiming; // Controls next two fields: + Field_z<4> mTimingAdvanceIndex; // Used for continuous timing advance. + Field_z<3> mTimingAdvanceTimeslotNumber; // Timeslot that gets the continuous timing advance. + + void setTimingAdvance(int ta) { mHaveTimingAdvanceValue=1; mTimingAdvanceValue=ta; } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketTimingAdvanceIE::writeBody(MsgCommon &dst) const { + if (dst.write01(mHaveTimingAdvanceValue)) { + dst.WRITE_ITEM(mTimingAdvanceValue); + } + if (dst.write01(mHaveContinuousTiming)) { + dst.WRITE_ITEM(mTimingAdvanceIndex); + dst.WRITE_ITEM(mTimingAdvanceTimeslotNumber); + } + } +#endif + +// GSM04.60 12.12a - same as TimingAdvanceIE but allows separate +// values for uplink and downlink. +struct RLCMsgPacketGlobalTimingAdvanceIE : public RLCMsgDownlinkIE +{ + // Timing Advance defined in GSM05.10 + Bool_z mHaveTimingAdvanceValue; + Field_z<6> mTimingAdvanceValue; // New timing advance if present. + // Timeslot number defined in GSM05.10 + // We dont use the following: + Bool_z mHaveUplinkContinuousTiming; // Controls next two fields: + Field_z<4> mUplinkTimingAdvanceIndex; // Used for continuous timing advance. + Field_z<3> mUplinkTimingAdvanceTimeslotNumber; // Timeslot that gets the continuous timing advance. + Bool_z mHaveDownlinkContinuousTiming; // Controls next two fields: + Field_z<4> mDownlinkTimingAdvanceIndex; // Used for continuous timing advance. + Field_z<3> mDownlinkTimingAdvanceTimeslotNumber; // Timeslot that gets the continuous timing advance. + + void setTimingAdvance(int ta) { mHaveTimingAdvanceValue=1; mTimingAdvanceValue=ta; } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketGlobalTimingAdvanceIE::writeBody(MsgCommon &dst) const { + if (dst.write01(mHaveTimingAdvanceValue)) { + dst.WRITE_ITEM(mTimingAdvanceValue); + } + if (dst.write01(mHaveUplinkContinuousTiming)) { + dst.WRITE_ITEM(mUplinkTimingAdvanceIndex); + dst.WRITE_ITEM(mUplinkTimingAdvanceTimeslotNumber); + } + if (dst.write01(mHaveDownlinkContinuousTiming)) { + dst.WRITE_ITEM(mDownlinkTimingAdvanceIndex); + dst.WRITE_ITEM(mDownlinkTimingAdvanceTimeslotNumber); + } + } +#endif + +// GSM04.60 12.13 +struct RLCMsgPowerControlParametersIE : public RLCMsgDownlinkIE +{ + Field_z<4> mAlpha; // 4 bits. + Bool_z mHaveTN[8]; + Field_z<5> mGammaTN[8]; // 5 bits each + void setAlpha(int alpha) { mAlpha = alpha; } + void setGamma(int timeslot, int gamma) { mGammaTN[timeslot] = gamma; mHaveTN[timeslot] = true; } + void setFrom(TBF *tbf); + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPowerControlParametersIE::writeBody(MsgCommon &dst) const { + dst.WRITE_ITEM(mAlpha); + for (int i=0; i<8; i++) { + if (dst.write01(mHaveTN[i])) dst.WRITE_ITEM(mGammaTN[i]); + } + } +#endif + +// This is not an official Informaion Element, but is commonly used in several messages. +// Used to identify to the MS targetted for a downlink message. +// The MS may be identified by TLLI, uplink TFI, or downlink TFI. +// In some messages, can also use TQI or Packet Request Reference, +// but we dont use those. +// This is not an official "Information Element", but it is used the +// same way in many downlink messages, since they all have to target an MS. +struct RLCMsgGlobalTFIorTLLIElt : public RLCMsgDownlinkIE +{ + Bool_z mHaveGlobalTFI; + // Global TFI includes the following two fields: + RLCMsgGlobalTFIIE mGlobalTFI; // This is NOT a TFI assignment; it is used + // to identify the MS which receives this message. + Bool_z mHaveTLLI; + Field_z<32> mTLLI; + + void setTLLI(int tlli) { + mHaveGlobalTFI=0; mHaveTLLI=1; mTLLI=tlli; + } + void setGlobalTFI(bool wIsDownlinkTFI,int wTFI) { + mHaveGlobalTFI = 1; mGlobalTFI.setTFI(wIsDownlinkTFI,wTFI); + } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgGlobalTFIorTLLIElt::writeBody(MsgCommon &dst) const { + if (mHaveGlobalTFI) { + dst.write0(); + mGlobalTFI.writeBody(dst); + } else if (mHaveTLLI) { + dst.write1(); + dst.write0(); + dst.writeField(mTLLI,32,"TLLI",tohex); + } else { + // Could be TQI or Packet Request Reference + assert(0); + } + } +#endif + +// GSM04.60 12.21 +class RLCMsgStartingFrameNumberIE : public RLCMsgDownlinkIE +{ + public: + Field_z<1> mAbsoluteOrRelative; // 0 => absolute, 1 => relative + Field_z<16> mAbsoluteFrameNumber; + Field_z<13> mRelativeFrameNumber; + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgStartingFrameNumberIE::writeBody(MsgCommon &dst) const { + //dst.writeField(mAbsoluteOrRelative,1); + dst.WRITE_ITEM(mAbsoluteOrRelative); + if (mAbsoluteOrRelative) { + dst.WRITE_ITEM(mRelativeFrameNumber); + } else { + dst.WRITE_ITEM(mAbsoluteFrameNumber); + } + } +#endif + +// GSM04.60 11.2.29 +// Also used in Timeslot Reconfigure message 44.060 11.2.31 +class RLCMsgPacketUplinkAssignmentDynamicAllocationElt : public RLCMsgDownlinkIE +{ + public: + Field_z<1> mExtendedDynamicAllocation; // Needed if number up > number down + // P0, PR_MODE unused. + Field_z<1> mUSFGranularity; // Use 0 for USF specified per-block. + + // In the TimeslotReconfigure message, this is never present, always one-bit 0: + Bool_z mUplinkTFIAssignmentPresent; + Field_z<5> mUplinkTFIAssignment; + + //Bool_z mRLCDataBlocksGrantedPresent; + //Field_z<8> mRLCDataBlocksGranted; + + Field_z<1> mTBFStartingTimePresent; + RLCMsgStartingFrameNumberIE mTBFStartingTime; + + // There are two types of Timeslot allocation, with and without power control params. + Bool_z mHavePower; // Set if gamma supplied for any timeslot. + Field_z<4> mAlpha; // 4 bits. + Bool_z mHaveTN[8]; + Field_z<3> mUSFTN[8]; // 3 bits each. + Field_z<5> mGammaTN[8]; // 5 bits each + + void setUplinkTFI(int tfi) { mUplinkTFIAssignmentPresent=1; mUplinkTFIAssignment=tfi; } + void setUSF(int timeslot, int usf) { mUSFTN[timeslot] = usf; mHaveTN[timeslot] = true; } + void setGamma(int timeslot, int gamma) { mGammaTN[timeslot] = gamma; mHavePower = true; } + void setAlpha(int alpha) { mAlpha = alpha; mHavePower = true; } + void setFrom(TBF *tbf,MultislotSymmetry); + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketUplinkAssignmentDynamicAllocationElt::writeBody(MsgCommon &dst) const { + dst.WRITE_ITEM(mExtendedDynamicAllocation); + dst.write0(); // No P0, PR_MODE. + dst.WRITE_ITEM(mUSFGranularity); + //dst.WRITE_OPT_FIELD01(mUplinkTFIAssignment,5,mUplinkTFIAssignmentPresent); + dst.WRITE_OPT_ITEM01(mUplinkTFIAssignment,mUplinkTFIAssignmentPresent); + // 44.060 removed the RLC Data Blocks Granted field, just inserts a 0 bit here. + // dst.WRITE_OPT_ITEM01(mRLCDataBlocksGranted,mRLCDataBlocksGrantedPresent); + dst.write0(); // RLC Data Blocks Granted removed from 44.060 11.2.29 + //dst.writeField(mTBFStartingTimePresent,1); + dst.WRITE_ITEM(mTBFStartingTimePresent); + if (mTBFStartingTimePresent) { + mTBFStartingTime.writeBody(dst); + } + + char name[14]; // Make the output pretty. + strcpy(name,"USF_TN?"); + char *end = name+strlen(name)-1; + if (dst.write01(mHavePower)) { + dst.WRITE_ITEM(mAlpha); + for (int i=0; i<8; i++) { + if (dst.write01(mHaveTN[i])) { + *end = '0' + i; + dst.writeField(mUSFTN[i],3,name); + dst.writeField(mGammaTN[i],5,"Gamma"); + } + } + } else { + for (int i=0; i<8; i++) { + if (dst.write01(mHaveTN[i])) { + *end = '0' + i; + dst.writeField(mUSFTN[i],3,name); + } + } + } + } +#endif + + +/** GSM04.60 11.2.29 From network to MS */ +class RLCMsgPacketUplinkAssignment : public RLCDownlinkMessage +{ + public: + //Bool_z mPersistenceLevelPresent; + //Field_z<4> mPersistenceLevel[4]; // This is for PRACH control, so we wont use it. + + // mTQI // We wont use it. + // mPacketRequestReference // We wont use it. + RLCMsgGlobalTFIorTLLIElt mMSID; + void setTLLI(int tlli) { mMSID.setTLLI(tlli); } + + // Currently we only support CS-1 for uplink. + ChannelCodingType mChannelCodingCommand; // MS to use which: CS-1, etc. + + // For one phase access, the MS must send the TLLI in an RLC Data Block early on. + // TLLIBlockChannelCoding controls the coding of that block. We are not using it. + Field_z<1> mTLLIBlockChannelCoding; // 0 MS to use CS-1 for TLLI block, 1 to use mChannelCoding. + RLCMsgPacketTimingAdvanceIE mPacketTimingAdvance; + void setTimingAdvance(int ta) { mPacketTimingAdvance.setTimingAdvance(ta); } + + // FreqParams specifies an ARFCN, TSC (Training Sequence Code) frequency hopping, etc. + // FrequenceParameters; // We dont use it. + + // Finally, the allocation itself: We only support dynamic on uplink for now. + Field_z<2> mAllocationType; // 1 => dynamic, 2 => single block, 3 => fixed + + RLCMsgPacketUplinkAssignmentDynamicAllocationElt mDynamicAllocation; + RLCMsgPacketUplinkAssignmentDynamicAllocationElt *setDynamicAllocation() { + mAllocationType = 1; + return &mDynamicAllocation; + } + + RLCMsgPacketUplinkAssignment(TBF *tbf) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketUplinkAssignment,true,tbf), + mChannelCodingCommand(tbf->mtChannelCoding()) + //mChannelCodingCommand(ChannelCodingCS1) + { + // Blackberry was using CS-1 when commanded to use CS-4, and it shouldnt matter + // what we put here, so try 1. Didnt help, back to 0 + mTLLIBlockChannelCoding = 0; + } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketUplinkAssignment::writeBody(MsgCommon &dst) const { + dst.write0(); // No persistence Level field. + // MSID Could also be TQI or Packet Request Reference in this message; we dont support them. + mMSID.writeBody(dst); + dst.write0(); // Message escape, means more stuff follows. + dst.WRITE_FIELD(mChannelCodingCommand,2); + dst.WRITE_ITEM(mTLLIBlockChannelCoding); + mPacketTimingAdvance.writeBody(dst); + dst.write0(); // No frequency parameters. + dst.WRITE_ITEM(mAllocationType); + switch (mAllocationType) { + case 0: // reserved. + assert(0); + case 1: // dynamic allocation + mDynamicAllocation.writeBody(dst); + break; + case 2: // single block allocation + assert(0); + case 3: // fixed allocation + assert(0); + } + dst.write0(); // One more 0 required to mark no Packet Extended Timing Advance. + } +#endif + +/** GSM 04.60 11.2.7 From network to MS */ +// Unlike an uplink assignment there is no ChannelCoding in the downlink assignment +// because it is passed in the qbits in each radio block. +// See: PDCHL1Downlink::transmit(BitVector *mI, const int *qbits) +class RLCMsgPacketDownlinkAssignment : public RLCDownlinkMessage +{ + public: + + //Bool_z mPersistenceLevelPresent; + //Field_z mPersistenceLevel[4]; // This is for PRACH control, so we wont use it. + + RLCMsgGlobalTFIorTLLIElt mMSID; + void setTLLI(int tlli) { mMSID.setTLLI(tlli); } + + Field_z<2> mMACMode; // 0 == dynamic, 2 = fixed. Not sure why this is here. + Field_z<1> mRLCMode; // 0 == acknowledged, 1 == unacknowleged mode. + Field_z<1> mControlAck; // Set if establishing a new downlink TBF and T3192 is running. + // We only use this message to change an existing downlink, + // so always 0. Not sure how it could ever be 1. + Field_z<8> mTimeslotAllocation; + RLCMsgPacketTimingAdvanceIE mPacketTimingAdvance; + void setTimingAdvance(int ta) { mPacketTimingAdvance.setTimingAdvance(ta); } + // No P0, BTS_PWR_CTRL_MODE, PR_MODE. + // No Frequence Parameters + Bool_z mHaveDownlinkTFIAssignment; + Field_z<5> mDownlinkTFIAssignment; // Assigned TFI for this downlink. + Bool_z mHavePowerControlParameters; + RLCMsgPowerControlParametersIE mPowerControlParameters; + // No TBF Starting Time. + // No Measurement Mapping. + + RLCMsgPacketDownlinkAssignment(TBF *tbf, bool isNewAssignment); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + RLCMsgPacketDownlinkAssignment::RLCMsgPacketDownlinkAssignment(TBF *tbf,bool isNewAssignment) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketDownlinkAssignment,true,tbf) + { + mTimeslotAllocation = tbf->mtMS->msGetDownlinkTimeslots(MultislotSymmetric); + // We dont track T3192, we track T3193, which is (almost) the same thing. + mControlAck = isNewAssignment ? tbf->mtMS->msT3193.active() : 0; + // 5-13-2012 I tried setting this to true all the time, and it seemed to make no difference + // on the Blackberry, in particular, did not get rid of the cause=3105 retries. + // 5-23-2012: After a 3105 failure we retry with a new TBF. That tbf was established + // with controlack=false for some reason, and based on its first downlinkacknack, it clearly + // took over from the previous TBF rather than establishing a new one. + // So I am setting this to true permanently. + // 6-18: The above is all fixed. The Blackberry ignores ControlAck + // but other phones do not, so it must be set properly. + + assert(tbf->mtTFI >= 0); + mHaveDownlinkTFIAssignment = true; + mDownlinkTFIAssignment = tbf->mtTFI; + mHavePowerControlParameters = true; + mPowerControlParameters.setFrom(tbf); + } + + void RLCMsgPacketDownlinkAssignment::writeBody(MsgCommon &dst) const { + dst.write0(); // No persistence level yet. + mMSID.writeBody(dst); // MS identified by either uplink or downlink TFI. + dst.write0(); // Message escape, means more stuff follows. + // Next comes MAC_MODE, dynamic, fixed, etc. + // This is mostly meaningless for a downlink, and I dont know why it is here. + // Except if assigned fixed slots, the MS can do other stuff when its not busy + // listening to us. + dst.WRITE_ITEM(mMACMode); + dst.WRITE_ITEM(mRLCMode); + dst.WRITE_ITEM(mControlAck); + dst.WRITE_ITEM(mTimeslotAllocation); + mPacketTimingAdvance.writeBody(dst); + dst.write0(); // No P0, PR_MODE. + dst.write0(); // No Frequency Parameters. + if (dst.write01(mHaveDownlinkTFIAssignment)) { + dst.WRITE_ITEM(mDownlinkTFIAssignment); + } + mPowerControlParameters.writeOptional01(dst,mHavePowerControlParameters); + dst.write0(); // No TBF Starting time. + dst.write0(); // No Measurement mapping. + dst.write0(); // End of message (no EGPRS, etc, stuff.) + } +#endif + +// 44.060 11.2.31 +// This message does not include a TLLI so it can not be used to initiate +// an uplink or downlink assignment. This message includes tfi assignment +// elements, but they are only to change the tfi of an existing TBF. +// The normal uplink and downlink assignment messages can only allocate PDCH +// symmetrically, ie, 2-up/2-down, and we must use this message to change +// to any other multislot mode. +class RLCMsgPacketTimeslotReconfigure : public RLCDownlinkMessage +{ + public: + RLCMsgGlobalTFIIE mGlobalTFI; // This is NOT a TFI assignment; it is used + // to identify the MS which receives this message. + + // This IE has timing advance for both uplink and downlink. + // We dont need it so unimplemented, just use three '0' bits instead. + // GLOBAL_PACKET_TIMING_ADVANCE + + // Elements for downlink modification: + Field_z<1> mDownlinkRlcMode; // 0 == acknowledged, 1 == unacknowleged mode. + // Always acknowledged for us. + Field_z<1> mControlAck; // Set if T3192 is running in MS. + // DOWNLINK_TFI_ASSIGNMENT unused + // UPLINK_TFI_ASSIGNMENT unused + Field_z<8> mDownlinkTimeslotAllocation; + + // FrequenceParameters; // We dont use it. + + // Elements for an uplink modification: + ChannelCodingType mChannelCodingCommand; + RLCMsgPacketUplinkAssignmentDynamicAllocationElt mDynamicAllocation; + RLCMsgPacketUplinkAssignmentDynamicAllocationElt *setDynamicAllocation() { + //mAllocationType = 1; + return &mDynamicAllocation; + } + + RLCMsgPacketTimeslotReconfigure(TBF *tbf); + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + RLCMsgPacketTimeslotReconfigure::RLCMsgPacketTimeslotReconfigure(TBF *tbf) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketTimeslotReconfigure,true,tbf) + { + mGlobalTFI.setTFI(tbf->mtDir == RLCDir::Down,tbf->mtTFI); + mDownlinkTimeslotAllocation = tbf->mtMS->msGetDownlinkTimeslots(MultislotFull); + mChannelCodingCommand = tbf->mtChannelCoding(); + // Must not call mDynamicAllocationsetUplinkTFI() + mDynamicAllocation.setFrom(tbf,MultislotFull); + } + void RLCMsgPacketTimeslotReconfigure::writeBody(MsgCommon &dst) const + { + // PAGE_MODE is handled by the parent class + dst.write0(); // unlabeled extra 0 bit. + mGlobalTFI.writeBody(dst); + dst.write0(); // message escape: not EGPRS + dst.WRITE_FIELD(mChannelCodingCommand,2); + dst.writeField(0,3); // Skip Global Packet Timing Advance + dst.WRITE_ITEM(mDownlinkRlcMode); + dst.WRITE_ITEM(mControlAck); + dst.write0(); // no downlink tfi assignment + dst.write0(); // no uplink tfi assignment + dst.WRITE_ITEM(mDownlinkTimeslotAllocation); + dst.write0(); // no frequence parameters + dst.write0(); // just an extra 0 bit. + mDynamicAllocation.writeBody(dst); + dst.write0(); // Marks end of message, no extensions. + } +#endif + +/** GSM 04.60 11.2.1 From network to MS */ +class RLCMsgPacketAccessReject : public RLCDownlinkMessage +{ + public: + Field_z<32> mTLLI; + // GlobalTFI mGlobalTFI; We dont use. + Bool_z mHaveWaitIndication; + Field_z<8> mWaitIndication; + Field_z<1> mWaitIndicationSize; + + RLCMsgPacketAccessReject(TBF *tbf) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketAccessReject,false,NULL), + mTLLI(tbf->mtMS->msTlli) + { } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketAccessReject::writeBody(MsgCommon &dst) const { + dst.write1(); // Start of reject struct. + dst.write0(); // Indicate we have a TLLI + dst.writeField(mTLLI,32,"TLLI",tohex); + if (dst.write01(mHaveWaitIndication)) { + dst.WRITE_ITEM(mWaitIndication); + dst.WRITE_ITEM(mWaitIndicationSize); + } + dst.write0(); // No more reject structs. + } +#endif + +/** GSM 04.60 11.2.26 From network to MS */ +class RLCMsgPacketTBFRelease : public RLCDownlinkMessage +{ + public: + RLCMsgGlobalTFIIE mGlobalTFI; + Field_z<1> mUplinkRelease; + Field_z<1> mDownlinkRelease; + Field_z<4> mTBF_RELEASE_CAUSE; + RLCMsgPacketTBFRelease(TBF *tbf); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + RLCMsgPacketTBFRelease::RLCMsgPacketTBFRelease(TBF *tbf) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketTBFRelease,true,tbf) + { + assert(tbf->mtTFI != -1); // TBF has not been released yet. + mGlobalTFI.mGTFI = tbf->mtTFI; + if (tbf->mtDir == RLCDir::Down) { + mGlobalTFI.mIsDownlinkTFI = true; + mDownlinkRelease = true; + } else { + mGlobalTFI.mIsDownlinkTFI = false; + mUplinkRelease = true; + } + mTBF_RELEASE_CAUSE = 2; // Abnormal release, the only reason we will ever do this. + } + void RLCMsgPacketTBFRelease::writeBody(MsgCommon &dst) const { + dst.write0(); // Just an extra 0 in the spec. + mGlobalTFI.writeBody(dst); + dst.WRITE_ITEM(mUplinkRelease); + dst.WRITE_ITEM(mDownlinkRelease); + dst.WRITE_ITEM(mTBF_RELEASE_CAUSE); + } +#endif + + +/** GSM 04.60 11.2.28 From network to MS */ +// Note that description in the GSM documentation is indented out of sync +// with the options; be careful reading it. +class RLCMsgPacketUplinkAckNack : public RLCDownlinkMessage +{ + private: + Field_z<5> mUplinkTFI; + ChannelCodingType mChannelCodingCommand; + RLCMsgPacketAckNackDescriptionIE mAND; + // 12.12b: Allows larger timing advance values. We wont use. + Bool_z mPacketExtendedTimingAdvancePresent; + Field_z<2> mPacketExtendedTimingAdvance; + // 4.60 9.2.3.4 Describes TBF_EST use. + // For dynamic uplink (which we use) the MS may return either a ControlAcknowledgement + // or a PacketResourceRequest in the RRBP reservation to this message. + Field<1> mTBFEst; // If true, MS may request a new uplink TBF in the response Packet Control Acknowledgment. + // It helps avoid raches in the case where there is an + // uplink and no downlink. + + // The contention resolution TLLI is needed for one-phase uplink access, for which + // the network does not know the TLLI when it does the uplink resource assignment, + // and has to get it from the uplink data; this specifies that the necessary + // TLLI is inside this block somewhere. + // We arent using this. + //bool mHaveContentionResolutionTLLI; + //unsigned mContentionResolutionTLLI; + + // We dont need to supply timing advance or power control in this message, + // because we supplied these in the assignment message. + Bool_z mHavePacketTimingAdvance; + RLCMsgPacketTimingAdvanceIE mPacketTimingAdvance; + void setTimingAdvance(int ta) { + mHavePacketTimingAdvance = true; + mPacketTimingAdvance.setTimingAdvance(ta); + } + + Bool_z mHavePowerControlParameters; + RLCMsgPowerControlParametersIE mPowerControlParameters; + + public: + RLCMsgPacketUplinkAckNack(TBF *wtbf, const RLCMsgPacketAckNackDescriptionIE& wAND); + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + RLCMsgPacketUplinkAckNack::RLCMsgPacketUplinkAckNack( + TBF *wtbf, const RLCMsgPacketAckNackDescriptionIE& wAND) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketUplinkAckNack,false,wtbf), + mUplinkTFI(wtbf->mtTFI), + mChannelCodingCommand(wtbf->mtChannelCoding()), + mAND(wAND), + // 6-26-2012: set TBF_EST true by default. + mTBFEst(gConfig.getBool("GPRS.TBF.EST")) // Allow MS to request another uplink assignment instead of Packet Control Acknowledgment in RRBP response. + { + assert(wtbf->mtDir == RLCDir::Up); + setTimingAdvance(wtbf->mtMS->msGetTA()); + } + void RLCMsgPacketUplinkAckNack::writeBody(MsgCommon &dst) const { + dst.write0(); // two extra 0 after PAGE_MODE. + dst.write0(); + dst.WRITE_ITEM(mUplinkTFI); + dst.write0(); // message escape - indicates normal (non-EGPRS) message. + dst.WRITE_FIELD(mChannelCodingCommand,2); + mAND.writeBody(dst); + dst.write0(); // no CONTENTION_RESOLUTION_TLLI + if (dst.write01(mHavePacketTimingAdvance)) { + mPacketTimingAdvance.writeBody(dst); + } + if (dst.write01(mHavePowerControlParameters)) { + mPowerControlParameters.writeBody(dst); + } + dst.write0(); // no Extension Bits. + dst.write0(); // no Fixed Allocation Parameters. + // Note that the indentation in the documentation here is misleading. + dst.write1(); // Enable additions for R99 + if (dst.write01(mPacketExtendedTimingAdvance)) { + dst.WRITE_ITEM(mPacketExtendedTimingAdvance); + } + dst.WRITE_ITEM(mTBFEst); + } +#endif + +// GSM04.60 11.2.8 From network to MS +struct RLCMsgPacketDownlinkDummyControlBlock : public RLCDownlinkMessage +{ + //Field_z<1> mPersistenceLevelPresent; + //Field_z<4> mPersistenceLevel[4]; // This is for PRACH control, so we wont use it. + + RLCMsgPacketDownlinkDummyControlBlock() + : RLCDownlinkMessage(RLCDownlinkMessage::PacketDownlinkDummyControlBlock,false,NULL) + { RN_MEMCHKNEW(RLCMsgPacketDownlinkDummyControlBlock) } + + ~RLCMsgPacketDownlinkDummyControlBlock() { RN_MEMCHKDEL(RLCMsgPacketDownlinkDummyControlBlock) } + + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketDownlinkDummyControlBlock::writeBody(MsgCommon &dst) const { + dst.write0(); // No Persistence Level + } +#endif + +///** A factory function for uplink messages */ +//RLCUplinkMessage *RLCUplinkFactory(RLCUplinkMessage::MTI); + +///** Generic RLC uplink parser */ +extern RLCUplinkMessage* RLCUplinkMessageParse(RLCRawBlock *src); + + +/** GSM 04.60 11.2.13 From network to MS */ +// Note that description in the GSM documentation is indented out of sync +// with the options; be careful reading it. +class RLCMsgPacketPowerControlTimingAdvance : public RLCDownlinkMessage +{ + // The MS may be identified by GlobalTFI, TQI, or PacketRequestReference, + // but we only use GlobalTFI, so I left the other methods out: + Bool_z mHaveGlobalTFI; // Better be true. + RLCMsgGlobalTFIIE mGlobalTFI; // 12.10 + // Global Power Control Parameters not implemented: + // Bool_z mHaveGlobalPowerControlParameters; + // RLCMsgGlobalPowerControlParametersIE mPowerControlParameters; // 12.9 + Bool_z mHavePowerControlParameters; + RLCMsgPowerControlParametersIE mPowerControlParameters; // 12.9 + Bool_z mHaveGlobalTimingAdvance; + RLCMsgPacketGlobalTimingAdvanceIE mGlobalTimingAdvance; // 12.12a + + // 12.12b: Allows larger timing advance values. + Bool_z mPacketExtendedTimingAdvancePresent; + Field_z<2> mPacketExtendedTimingAdvance; + public: + RLCMsgPacketPowerControlTimingAdvance(TBF *wtbf) + : RLCDownlinkMessage(RLCDownlinkMessage::PacketPowerControlTimingAdvance,false,wtbf) + { + assert(wtbf->mtTFI >= 0); + setGlobalTFI(wtbf->mtDir == RLCDir::Down,wtbf->mtTFI); + setTimingAdvance(wtbf->mtMS->msGetTA()); + } + + void setTimingAdvance(int ta) { + mHaveGlobalTimingAdvance = true; + mGlobalTimingAdvance.setTimingAdvance(ta); + } + void setGlobalTFI(bool wIsDownlinkTFI,int wTFI) { + mHaveGlobalTFI = 1; mGlobalTFI.setTFI(wIsDownlinkTFI,wTFI); + } + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketPowerControlTimingAdvance::writeBody(MsgCommon &dst) const { + // page_mode is written by RLCDownlinkMessage writeHeader() + if (mHaveGlobalTFI) { + dst.write0(); + mGlobalTFI.writeBody(dst); + } else { + assert(0); // Other MS ID methods not supported. + } + dst.write0(); // message escape. + dst.write0(); // No Global Power Parameters. + if (mHaveGlobalTimingAdvance && mHavePowerControlParameters) { + dst.write0(); + mGlobalTimingAdvance.writeBody(dst); + mPowerControlParameters.writeBody(dst); + } else { + dst.write1(); + if (mHaveGlobalTimingAdvance) { + dst.write0(); + mGlobalTimingAdvance.writeBody(dst); + } else { + dst.write1(); + mPowerControlParameters.writeBody(dst); + } + } + // The following is confusing in the spec because + // the indentation is misleading. + // Look carefully and note that there is no ending + // curly brace on the line " null | 0 bit" + if (dst.write01(mPacketExtendedTimingAdvancePresent)) { + if (dst.write01(mPacketExtendedTimingAdvancePresent)) { + dst.WRITE_ITEM(mPacketExtendedTimingAdvance); + } + } + } +#endif + +// GSM04.60 11.2.9b. +// When you set the NETWORK_CONTROL_ORDER to NC2 (network does cell reselection) +// then the MS sends measurement reports at the reporting periods defined below. +// These messages clog up our very limited ACCH. +// This message is one of the ways to change the reporting period. +// I'm not sure there is any other way for us. +class RLCMsgPacketMeasurementOrder : public RLCDownlinkMessage +{ + RLCMsgGlobalTFIorTLLIElt mMSID; + void setTLLI(int tlli) { mMSID.setTLLI(tlli); } + void setGlobalTFI(bool wIsDownlinkTFI,int wTFI) { + mMSID.setGlobalTFI(wIsDownlinkTFI,wTFI); + } + + Field_z<3> mPMOIndex; + Field_z<3> mPMOCount; + Bool_z mHaveNCMeasurementParameters; + Field<2> mNetworkControlOrder; + Field<3> mNCNonDrxPeriod; // Non-drx period after MS sends a measurement. + // Reporting period is documented in PSI5 + // It is a 3 bit code. Larger values mean longer times. + // Min is 0.48 sec and max is 61.44 sec. + Field<3> mNCReportingPeriodI; // GPRS default is 7 == 61.44 sec. + Field<3> mNCReportingPeriodT; // GPRS default is 3 == 3.84 sec. + + RLCMsgPacketMeasurementOrder(TBF*wtbf) : + RLCDownlinkMessage(RLCDownlinkMessage::PacketMeasurementOrder,false,wtbf), + mNetworkControlOrder(2), + mNCNonDrxPeriod(0), + mNCReportingPeriodI(7), // Max these out + mNCReportingPeriodT(7) + {} + void writeBody(MsgCommon&dst) const; +}; +#if RLCMESSAGES_IMPLEMENTATION + void RLCMsgPacketMeasurementOrder::writeBody(MsgCommon &dst) const { + // page_mode is written by RLCDownlinkMessage writeHeader() + mMSID.writeBody(dst); // MS identified by either uplink or downlink TFI. + dst.WRITE_ITEM(mPMOIndex); + dst.WRITE_ITEM(mPMOCount); + if (dst.write01(mHaveNCMeasurementParameters)) { + dst.WRITE_ITEM(mNetworkControlOrder); + dst.write1(); // We always include this optional section. + dst.WRITE_ITEM(mNCNonDrxPeriod); + dst.WRITE_ITEM(mNCReportingPeriodI); + dst.WRITE_ITEM(mNCReportingPeriodT); + dst.write0(); // No NC_FREQUENCY_LIST. + } + dst.write0(); // No EXT Measurement Parameters. + dst.write0(); // No optional stuff follows. + } +#endif + + +}; // namespace GPRS +#endif diff --git a/GPRS/RList.h b/GPRS/RList.h new file mode 100644 index 00000000..b3e6cf1d --- /dev/null +++ b/GPRS/RList.h @@ -0,0 +1,214 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef RLIST_H +#define RLIST_H +#include +#include "GPRSInternal.h" // For devassert + +// A list with random access too. +template +class RList : public std::list { + typedef typename std::list::iterator itr_t; + typedef typename std::list type_t; + public: + T operator[](unsigned ind) { + unsigned i = 0; + for (itr_t itr = type_t::begin(); itr != type_t::end(); itr++) { + if (i++ == ind) return *itr; + } + return 0; // Since we use 0 to mean unknown, this class can not be used + // if this is a valid value of T. Could throw an error instead, + // and complicate things for the caller. + } + + bool find(T element) { + for (itr_t itr = type_t::begin(); itr != type_t::end(); itr++) { + if (*itr == element) { + return true; + } + } + return false; + } + + //bool find_safely(T element) { return find(element); } + //void push_back_safely(T&val) { assert(! find(val)); std::list::push_back(val); } + //void remove_safely(T&val) { std::list::remove(val); } +}; + +// Like RList but add an internal Mutex so it can auto-lock for multi-threads using RlistIteratorThreadSafe. +template +class RListThreadSafe : RList { + Mutex mListLock; + bool find_safely(T element) { + ScopedLock lock(mListLock); + return find(element); + } + + void push_back_safely(T&val) { + ScopedLock lock(mListLock); + assert(! find(val)); + std::list::push_back(val); + } + void remove_safely(T&val) { + ScopedLock lock(mListLock); + std::list::remove(val); + } +}; + +// Like ScopedLock but creates a scoped iterator especially useful in a for statement. +// Use like this: given a list and a mutex to protect access to the list: +// class T; list mylist; Mutex mymutex; for (ScopedIterator >(mylist,mymutex); next(var);) +template > +class ScopedIterator { + ListType &mPList; + Mutex& mPMutex; + public: + typename ListType::iterator mNextItr, mEndp; + void siInit() { + mPMutex.lock(); + mNextItr = mPList.begin(); + mEndp = mPList.end(); + } + ScopedIterator(ListType &wPList, Mutex &wPMutex) : mPList(wPList), mPMutex(wPMutex) { siInit(); } + // Yes you can use this in a const method function and yes we are changing the Mutex and yes it is ok so use a const_cast. + // ScopedIterator(ListType const &wPList) : mPList(const_cast(wPList)), mPMutex(mPList.mListLock) { siInit(); } + ~ScopedIterator() { mPMutex.unlock(); } + + // This is meant to be used as the test statement in a for or while loop. + // We always point the iterator at the next element, so that + // deletion of the current element by the caller is permitted. + // We also store the end element when the iteration starts, so new + // elements pushed onto the back of the list during the iteration are ignored. + bool next(T &var) { + if (mNextItr == mEndp) return false; + var = *mNextItr++; + return true; + } +}; + +// An iterator to be used in for loops. +template +class RListIterator +{ + typedef RList ListType; + ListType &mPList; + public: + typename ListType::iterator mItr, mNextItr, mEndp; + bool mFinished; + void siInit() { + mNextItr = mPList.begin(); + mItr = mEndp = mPList.end(); + mFinished = (mNextItr == mEndp); + } + RListIterator(ListType &wPList) : mPList(wPList) { siInit(); } + RListIterator(ListType const &wPList) : mPList(const_cast(wPList)) { siInit(); } + bool next(T &var) { + if (mFinished) { return false; } + mItr = mNextItr; + var = *mNextItr++; + mFinished = (mNextItr == mEndp); // We check now in case caller deletes end(). + return true; + } + // Erase the current element. + void erase() { + devassert(mItr != mEndp); + mPList.erase(mItr); + mItr = mEndp; // To indicate we have already erased it. + } + bool next(T &var, typename ListType::iterator &itr) { // Return a regular old iterator to the user. + bool result = next(var); + itr = mItr; + return result; + } +}; + +// A ScopedIterator wrapper specifically for RList, to be used in for loops. +// The RList has the mutex built-in. +template +class RListIteratorThreadSafe : public ScopedIterator +{ public: + typedef RList ListType; + RListIteratorThreadSafe(ListType &wPList) : ScopedIterator(wPList,wPList.mListLock) {} + // Yes you can use this in a const method and yes the Mutex is non-const but yes it is ok so use a const_cast. + RListIteratorThreadSafe(ListType const &wPList) : ScopedIterator(const_cast(wPList),const_cast(wPList).mListLock) {} +}; + + +// Assumes the list is an RList which has an internal mutex. +#define RN_RLIST_FOR_ALL_THREAD_SAFE(type,list,var) \ + for (RListIteratorThreadSafe itr(list); itr.next(var); ) + +/* +// This macro requires the caller to advance itr if the var is deleted. +//#define RN_FOR_ALL_WITH_ITR(type,list,var,itr) \ +// for (type::iterator itr = list.begin(); \ +// itr == list.end() ? 0 : ((var=*itr),1); \ +// itr++) +*/ + +// This macro allows deletion of the current var from the list being iterated, +// because itr is advanced to the next position at the beginning of the loop, +// and list iterators are defined as keeping their position even if elements are deleted. +#define RN_FOR_ALL(type,list,var) \ + for (type::iterator itr = (list).begin(); \ + itr == (list).end() ? 0 : ((var=*itr++),1);) + + +// Geez, the language sure botched this... +#define RN_FOR_ALL_CONST(type,list,var) \ + for (type::const_iterator itr = (list).begin(); \ + itr == (list).end() ? 0 : ((var=*itr++),1);) + +/* not used +#define RN_FOR_ALL_REVERSED(type,list,var) \ + for (type::reverse_iterator var##_itr = (list).rbegin(); \ + var##_itr == (list).rend() ? 0 : ((var=*var##_itr),1); \ + var##_itr++) +*/ + + +#if 0 +// For your edification, these are the old functions. + +// Does the list contain the element? Return TRUE if so. +template +bool findlistrev(std::list list, T element, typename std::list::reverse_iterator *result = 0) { + for (typename std::list::reverse_iterator itr = list.rbegin(); itr != list.rend(); itr++) { + if (*itr == element) { + if (result) *result = itr; + return true; + } + } + return false; + // This did not work? + // return std::find(msPCHDowns.begin(),msPCHDowns.end(),(const PDCHL1Downlink*)down) != msPCHDowns.end(); +} + +template +bool findlist(std::list list, T element, typename std::list::iterator *result = 0) { + for (typename std::list::iterator itr = list.begin(); itr != list.end(); itr++) { + if (*itr == element) { + if (result) *result = itr; + return true; + } + } + return false; + // This did not work? + // return std::find(msPCHDowns.begin(),msPCHDowns.end(),(const PDCHL1Downlink*)down) != msPCHDowns.end(); +} +#endif +#endif diff --git a/GPRS/ScalarTypes.h b/GPRS/ScalarTypes.h new file mode 100644 index 00000000..077d889a --- /dev/null +++ b/GPRS/ScalarTypes.h @@ -0,0 +1,136 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef SCALARTYPES_H +#define SCALARTYPES_H +#include // For size_t +#include +//#include "GSMCommon.h" // Was included for Z100Timer + +// We dont bother to define *= /= etc.; you'll have to convert: a*=b; to: a=a*b; +#define _INITIALIZED_SCALAR_BASE_FUNCS(Classname,Basetype,Init) \ + Classname() : value(Init) {} \ + Classname(Basetype wvalue) { value = wvalue; } /* Can set from basetype. */ \ + operator Basetype(void) const { return value; } /* Converts from basetype. */ \ + Basetype operator=(Basetype wvalue) { return value = wvalue; } \ + Basetype* operator&() { return &value; } + +#define _INITIALIZED_SCALAR_ARITH_FUNCS(Basetype) \ + Basetype operator++() { return ++value; } \ + Basetype operator++(int) { return value++; } \ + Basetype operator--() { return --value; } \ + Basetype operator--(int) { return value--; } \ + Basetype operator+=(Basetype wvalue) { return value = value + wvalue; } \ + Basetype operator-=(Basetype wvalue) { return value = value - wvalue; } + +#define _INITIALIZED_SCALAR_FUNCS(Classname,Basetype,Init) \ + _INITIALIZED_SCALAR_BASE_FUNCS(Classname,Basetype,Init) \ + _INITIALIZED_SCALAR_ARITH_FUNCS(Basetype) + + +#define _DECLARE_SCALAR_TYPE(Classname_i,Classname_z,Basetype) \ + template \ + struct Classname_i { \ + Basetype value; \ + _INITIALIZED_SCALAR_FUNCS(Classname_i,Basetype,Init) \ + }; \ + typedef Classname_i<0> Classname_z; + + +// Usage: +// Where 'classname' is one of the types listed below, then: +// classname_z specifies a zero initialized type; +// classname_i initializes the type to the specified value. +// We also define Float_z. +_DECLARE_SCALAR_TYPE(Int_i, Int_z, int) +_DECLARE_SCALAR_TYPE(Char_i, Char_z, signed char) +_DECLARE_SCALAR_TYPE(Int16_i, Int16_z, int16_t) +_DECLARE_SCALAR_TYPE(Int32_i, Int32_z, int32_t) +_DECLARE_SCALAR_TYPE(UInt_i, UInt_z, unsigned) +_DECLARE_SCALAR_TYPE(UChar_i, UChar_z, unsigned char) +_DECLARE_SCALAR_TYPE(UInt16_i, UInt16_z, uint16_t) +_DECLARE_SCALAR_TYPE(UInt32_i, UInt32_z, uint32_t) +_DECLARE_SCALAR_TYPE(Size_t_i, Size_t_z, size_t) + +// Bool is special because it cannot accept some arithmetic funcs +//_DECLARE_SCALAR_TYPE(Bool_i, Bool_z, bool) +template +struct Bool_i { + bool value; + _INITIALIZED_SCALAR_BASE_FUNCS(Bool_i,bool,Init) +}; +typedef Bool_i<0> Bool_z; + +// float is special, because C++ does not permit the template initalization: +struct Float_z { + float value; + _INITIALIZED_SCALAR_FUNCS(Float_z,float,0) +}; +struct Double_z { + double value; + _INITIALIZED_SCALAR_FUNCS(Double_z,double,0) +}; + + +class ItemWithValueAndWidth { + public: + virtual unsigned getValue() const = 0; + virtual unsigned getWidth() const = 0; +}; + +// A Range Networks Field with a specified width. +// See RLCMessages.h for examples. +template +class Field_i : public ItemWithValueAndWidth +{ + public: + unsigned value; + _INITIALIZED_SCALAR_FUNCS(Field_i,unsigned,Init) + unsigned getWidth() const { return Width; } + unsigned getValue() const { return value; } +}; + +// Synonym for Field_i, but no way to do it. +template +class Field_z : public ItemWithValueAndWidth +{ + public: + unsigned value; + _INITIALIZED_SCALAR_FUNCS(Field_z,unsigned,Init) + unsigned getWidth() const { return Width; } + unsigned getValue() const { return value; } +}; + +// This is an uninitialized field. +template +class Field : public ItemWithValueAndWidth +{ + public: + unsigned value; + _INITIALIZED_SCALAR_FUNCS(Field,unsigned,Init) + unsigned getWidth() const { return Width; } + unsigned getValue() const { return value; } +}; + + +// A Z100Timer with an initial value specified. +//template +//class Z100Timer_i : public GSM::Z100Timer { +// public: +// Z100Timer_i() : GSM::Z100Timer(Init) {} +//}; + +#endif diff --git a/GPRS/TBF.cpp b/GPRS/TBF.cpp new file mode 100644 index 00000000..59c19035 --- /dev/null +++ b/GPRS/TBF.cpp @@ -0,0 +1,1445 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#define TBF_IMPLEMENTATION 1 +#include "MSInfo.h" +#include "TBF.h" +#include "FEC.h" +#include "RLCMessages.h" +#include "BSSG.h" +#include "RLCEngine.h" + +namespace GPRS { + +typedef SGSN::GprsSgsnDownlinkPdu DownlinkQPdu; + +static bool SendExtraTA = 0;// DEBUG: Send an extra TA message + // after an Immediate Assignment just to prove it is working. + // 4-24-2012, turned it back off, does not help CS-4 bug. + +static bool T3168Behavior = 1; // Dont send downlink assignments while t3168 running. + +static int configTbfRetry() { + return gConfig.getNum("GPRS.TBF.Retry"); +} + +bool MsgTransaction::mtMsgPending(MsgTransactionType mttype) +{ + // The multiple tests here are overkill. + return mtMsgExpectedBits.isSet(mttype) && + mtExpectedAckBSN[mttype].valid() && + mtExpectedAckBSN[mttype] + BSNLagTime >= gBSNNext; +} + +// Is any message pending? +bool MsgTransaction::mtMsgPending() +{ + // We are not 'waiting' until the expectedAckBSN is valid, because: + // 1. the first time through the TBF state machine's logic, + // it tests this function before the msgAckTime has been set. + // 2. In RLCEngine.cpp, we wait on an optional RRBP reservation, + // so we dont want to wait forever if the first one does not get + // its reservation and expectedAckBSN is still invalid. + + //GPRSLOG(1) << "mtMsgPending:" <mtGetState() == TBFState::DataFinal) { + // In this state this uplink TBF has already completed and we have sent the + // uplinkAckNack message. We now set the TBF_EST field in that message + // so the MS may respond to the RRBP reservation with a PacketResourceRequest. + // Doesn't really matter whether that is the case or not - in this state, + // go ahead and start a new uplink TBF. + } else if (activeTbf->isTransmitting()) { + // When the MS requests a second TBF when one is already running, + // it means it has a new PDU with a different priority or QoS (throughput.) + // See 44.060 8.1.1.1.2 and 9.5. + // We are required to reissue the TBF assignment within timer T3168. + // Originally I just sent a new PacketUplinkAssignment, but that does not always, + // we have to end the TBF and restart it. + // Maybe if the new priority/QoS is lower, then just reissuing a new + // PacketUplinkAssignment on the current TBF would work, but I'm just + // going to reissue the TBF. + // This code waits until the current PDU finishes and then terminates the transfer, + // which implies that we expect the MS to comply by setting the uplink MAC header Countdown Value to 0 + // within the T3168 grace period. + // FIXME: Temporarily provide a way to disable this in case it has a bug: + if (gConfig.getBool("GPRS.Reassign.Enable")) { + activeTbf->mtPerformReassign = true; + } + return NULL; + } else { + // The activeTbf is in the midst of being issued a new assignment. + if (tlli != activeTbf->mtTlli) { + // The new uplink tbf has a different tlli. + // This case happens only for the AttachComplete message when it is + // sent by the MS in an uplink tbf requested in the downlinkacknack + // for the AttachAccept message. So check for this special case: + if (tlli != ms->msTlli && tlli != ms->msOldTlli) { + // Should not happen because the internal SGSN provides + // the newTlli as an alias with the AttachAccept message. + GLOG(ERR) << "uplink TBF request"<mtTlli = tlli; + } else { + // Just ignore this duplicate uplink request. + // TODO: We can infer the MS mode if this arrives on RACH. + GPRSLOG(1) << "MS denied second uplink TBF" << activeTbf; + } + return NULL; + } + } + + TBF *tbf = (TBF*) new RLCUpEngine(ms,mCRD.mRLCOctetCount); + tbf->mtTlli = tlli; + tbf->mtUnAckMode = mCRD.mRLCMode; + if (mCRD.mLLCPDUType == 0) { // 0 == LLC PDU is SACK or ACK, 1 == its not + GLOG(ERR) << "Uplink PDU LLC SACK request"<mtSetState(TBFState::DataReadyToConnect); + return tbf; +} + +ChannelCodingType TBF::mtChannelCoding() const +{ // Return 0 - 3 for CS-1 or CS-4 for data transfer. + assert(mtChannelCodingMax >= ChannelCodingCS1 && mtChannelCodingMax <= ChannelCodingCS4); + ChannelCodingType result; + if (mtChannelCodingMax == ChannelCodingCS1) { + result = ChannelCodingCS1; // Locked to lowest speed. No need to query the MS RSSI. + } else { + ChannelCodingType dynamicCS = mtMS->msGetChannelCoding(mtDir); + result = min(mtChannelCodingMax,dynamicCS); + } + mtMS->msChannelCoding.addPoint((int)result); + return result; +} + + +TBF::TBF(MSInfo *wms, RLCDirType wdir) + : mtState(TBFState::Unused), mtDebugId(++Stats.countTBF), mtMS(wms), mtDir(wdir), mtTFI(-1) +{ + gReports.incr("GPRS.TBF"); + RN_MEMCHKNEW(TBF) + mtChannelCodingMax = ChannelCodingMax; // This may be changed by caller. + mtCCMin = mtCCMax = (ChannelCodingType)-1; + gL2MAC.macAddTBF(this); + mtMS->msAddTBF(this); + // Reset these counters to avoid killing the TBF before it has a chance to do anything: + mtMS->msTalkUpTime.setNow(); + mtMS->msTalkDownTime.setNow(); + mtMS->msCountTbfs++; + // Reset these counts just to validate them. + //mtPersistLastUse.setNow(); + //mtPersistKeepAlive.setNow(); +} + +TBF::~TBF() { RN_MEMCHKDEL(TBF) } // housekeeping handled by mtDelete() + +const char *TBF::tbfid(bool verbose) +{ + static char buf[30]; + int n1 = sprintf(buf, "%c%d", (mtDir==RLCDir::Up ? 'U' : 'D'), mtDebugId); + if (verbose) { + // Just add the seconds part of the time string, which looks like "HH:MM:SS.T". + const char *ts = strrchr(timestr().c_str(),':'); + if (ts) { n1 += sprintf(buf+n1," %s",ts+1); } + // Add the TLLI. + //sprintf(buf+n1," MS#%d,%x",mtMS->msDebugId,mtMS->msTlli); + sprintf(buf+n1," MS#%d",mtMS->msDebugId); + } + return (const char *)buf; +} + +//void TBF::setRadData(RadData &wRD) +//{ +// mtMS->msSetRadData(wRD); +// if (wRD.mRSSI < mtLowRSSI) { mtLowRSSI = wRD.mRSSI; } +//} + +void TBF::mtDelete(bool forever) +{ + devassert(! mtAttached); + //mtDetach(); // Did this already, but be safe and call again. + mtMS->msForgetTBF(this); + gL2MAC.macForgetTBF(this,forever); // TBF destruction happens in here. +} + +void MsgTransaction::text(std::ostream &os) const +{ + os << LOGHEX(mtMsgExpectedBits); + os << LOGHEX(mtMsgAckBits); + for (int i = 0; i < MsgTransMax; i++) { + if (mtExpectedAckBSN[i].valid() && mtExpectedAckBSN[i] + BSNLagTime >= gBSNNext) { + os << " mtExpectedAckBSN["<msPacch; + //if (mtDir == RLCDir::Up && pacch) { + // tn = pacch->TN(); + // usf = (tn >= 0) ? (int)mtMS->msUSFs[tn] : -1; + //} + // os << LOGVAR(pacch) << LOGVAR(usf); + os << this // Dumps the operator<< value, which is sprintf(TBF#%d,mtDebugId) + << LOGVAR(mtMS) + //<< " mtDir=" << RLCDir::name(mtDir) + << LOGVAR(mtDir) + << "\n"; + + os << "\t"; mtMS->msDumpChannels(os); os << "\n"; + if (0) { + PDCHL1Uplink *up; PDCHL1Downlink *down; + os << "\t"; + RN_FOR_ALL(PDCHL1DownlinkList_t,mtMS->msPCHDowns,down) { + os << format(" down%d:%d",down->ARFCN(),down->TN()); + } + RN_FOR_ALL(PDCHL1UplinkList_t,mtMS->msPCHUps,up) { + int tn = up->TN(); + os << format(" up%d:%d usf=%d",up->ARFCN(),tn,(int)mtMS->msUSFs[tn]); + } + } + + os << "\t" + << LOGVAR2("mtState=",TBFState::name(mtState)) + << LOGVAR(mtAttached) + << LOGVAR(mtTFI) + << LOGHEX(mtTlli); + if (mtDir == RLCDir::Down) { os <<" size="<msN3101) os << LOGVAR2("N3101",mtMS->msN3101); + if (mtN3103) os << LOGVAR2("N3103",mtN3103); + if (mtN3105) os << LOGVAR2("N3105",mtN3105); + if (mtDeadTime.valid()) os << LOGVAR2("deadTime",- mtDeadTime.elapsed()); + if (mtMS->msT3193.active()) { os<msT3193.remaining()); } + if (mtMS->msT3168.active()) { os<msT3168.remaining()); } + if (mtDescription.size()) os <<" descr="<msDumpCommon(os); + mtMS->dumpSignalQuality(os); + + os << "\t"; engineDump(os); + unsigned total, unique, grants; + engineGetStats(&total,&unique,&grants); + // Note that this statistic is off by the small number of blocks + // that have been sent but not yet acknowledged. + os << "\n\t blocks:" << LOGVAR(total) << LOGVAR(unique) << LOGVAR(grants); + + //float efficiency = 1.0 * slotsused / slotstotal; + // os << LOGVAR(efficiency); + //os << "\n"; + return os.str(); +} + +static void tbfDumpAll() +{ + if (GPRSDebug & 2) { + GPRSLOG(2) <<"TBF DUMP:\n"; + TBF *tbf; + RN_MAC_FOR_ALL_TBF(tbf) { GPRSLOG(2) << tbf->tbfDump(true) << "\n"; } + } +} + +// Can never have too many const in a language, thats what I always say. +RLCDownEngine *TBF::getDownEngine() +{ + return (mtDir == RLCDir::Down) ? dynamic_cast(this) : NULL; +} +//RLCDownEngine const *TBF::getDownEngine() const +//{ +// return (mtDir == RLCDir::Down) ? dynamic_cast(this) : NULL; +//} + + +// Release resources for this TBF: TFI, and if we were the last user of this USF, +// release that as well. +// Note this code is overkill at the moment, because there is only +// one tfi list shared among all channels. +// This does not release the channel assignment. +void TBF::mtDetach() +{ + if (mtAttached) { + mtAttached = false; // Must do this before calling msCleanUSFs() + // Set the state to the transitory Deleting state, so that the callers + // who wade through the TBF lists will ignore this one now. + mtSetState(TBFState::Deleting); + mtFreeTFI(); + if (mtDir == RLCDir::Up) { mtMS->msCleanUSFs(); } + } +} + +// TODO: The TBF needs to detach and then re-attach. +void TBF::mtDeReattach() +{ + if (mtAttached) { + mtAttached = false; // Must do this before calling msCleanUSFs() + mtSetState(TBFState::DataReadyToConnect); + mtFreeTFI(); + if (mtDir == RLCDir::Up) { mtMS->msCleanUSFs(); } + } +} + +uint32_t TBF::mtGetTlli() +{ + uint32_t tlli = mtTlli; + if (gFixConvertForeignTLLI) { + if ((tlli & 0xc0000000) == 0x80000000) { // Is it a foreign tlli? + tlli |= TLLI_LOCAL_BIT; + GPRSLOG(1) << "*** Converting foreign tlli to local:"<setTimingAdvance(ms->msGetTA()); + + // This will set mtExpectedAckBSN if the message is sent. + // If the MS gets this message, it will send Packet Control Acknowledgment + // in the block specified by RRBP. + if (os) { + msg->text(*os); + delete msg; + return true; + } else { + GPRSLOG(1) << "GPRS sendReassignment "<downlink()-> + send1MsgFrame(tbf,msg,2,MsgTransReassign,&tbf->mtReassignCounter); + } +} + +static bool sendAssignmentPacch( + PDCHL1FEC *pacch, // The PACCH channel. + TBF *tbf, + bool isNewAssignment, // If false, it is a reassignment. + MsgTransactionType msgstate, + std::ostream *os) // for testing - if set, print out the message instead of sending it. +{ + MSInfo *ms = tbf->mtMS; + // Send assignment message on PACCH. + // A reassignment message is identical to an assignment message except + // for the ControlAck bit, however, now we use timeslot reconfigure + // for reassignments. + RLCDownlinkMessage *msg; + if (tbf->mtDir == RLCDir::Up) { + RLCMsgPacketUplinkAssignment *ulmsg = new RLCMsgPacketUplinkAssignment(tbf); + + RLCMsgPacketUplinkAssignmentDynamicAllocationElt *dynelt; + dynelt = ulmsg->setDynamicAllocation(); + dynelt->setUplinkTFI(tbf->mtTFI); + dynelt->setFrom(tbf,MultislotSymmetric); + msg = ulmsg; + } else { + RLCMsgPacketDownlinkAssignment *dlmsg = + new RLCMsgPacketDownlinkAssignment(tbf,isNewAssignment); + msg = dlmsg; + } + + // todo: stop timers for ms? + // todo: start timers? + + msg->setTimingAdvance(ms->msGetTA()); + msg->setTLLI(tbf->mtGetTlli()); + + // This will set mtExpectedAckBSN if the message is sent. + // If the MS gets this message, it will send Packet Control Acknowledgment + // in the block specified by RRBP. + if (os) { + msg->text(*os); + delete msg; + return true; + } else { + unsigned *pcounter = NULL; + switch (msgstate) { + case MsgTransAssign1: + case MsgTransAssign2: + GPRSLOG(1) << "GPRS sendAssignment "<mtAssignCounter; + break; + case MsgTransReassign: + GPRSLOG(1) << "GPRS sendReassignment "<mtReassignCounter; + break; + default: devassert(0); + } + tbfDumpAll(); + // This will increment the counter if the message is really sent. + devassert(pcounter); + return pacch->downlink()->send1MsgFrame(tbf,msg,2,msgstate,pcounter); + } +} + +void sendAssignmentCcch( + PDCHL1FEC *pacch, // The PACCH channel. + TBF *tbf, + std::ostream *os) // for testing - if set, print out the message instead of sending it. +{ + static GSM::CCCHLogicalChannel *ourAGCH = 0; + MSInfo *ms = tbf->mtMS; + // Send a message on CCCH. + // TODO FIXME: + // GSM 44.60 5.5.1.5 Describes the conditions under which DRX should be applied. + // The MS sends the DRX info to the SGSN in the Attach message. + // None of that implemented yet. + bool drxmode = false; + + // BUG: The returned AGCH time is not monotonically increasing. The bug traces all + // the way back down that the getAGCH call is not, in fact, returning the next one. + // I am guessing that the choice of CCCH has a bug that does not really + // return the next CCCH slot because of the way the idle frames are handled. + // Anyway, this screws up sendAssignment; see todo.txt. + // To try to fix this, we will use just one AGCH, ever, to try to get our + // messages delivered in order. + + // TODO: We need to use ourAGCH for unattached MS, + // but we could select a different AGCH for every attached MS. + if (ourAGCH == 0) { ourAGCH = gBTS.getAGCH(); } + + // Make a reservation for the poll response. + RLCBSN_t resBSN = 0; + // TODO: Fix this. The DRX mode is sent to us by the MS I think in the + // initial attach message which went to the SGSN. + if (gFixDRX && tbf->mtCcchAssignCounter >= (unsigned)gFixDRX) { + // We may have lost contact with the MS because it is in DRX mode. + // Send the message on all 3 AGCH channels twice - we need + // to saturate both 51 multi-frames because there + // are 6 paging channels. And we just hope no other + // AGCH messages are in the way. + // We need to increase the reservation to make sure it is beyond + // the CCCH in the second 51 multiframe. + drxmode = true; + } + GSM::CCCHLogicalChannel *currentAGCH = drxmode ? gBTS.getPCH(0) : ourAGCH; + + resBSN = pacch->makeCCCHReservation(currentAGCH,RLCBlockReservation::ForPoll,tbf,NULL,drxmode,MsgTransAssign1); + if (! resBSN.valid()) { + // We will try again later. + return /*false*/; // We did not use the packet channel downlink. + } + // TODO: mtSetAckExpected function should be moved into the makeReservation code. + // We have to wait for the time whether we sent the poll or not. + tbf->mtSetAckExpected(resBSN,MsgTransAssign1); + + // The RequestReference is not used for this type of downlink assignment, + // which contains TLLI instead; we have to set RequestReference to a number that + // cannot possibly be confused with any valid value, ie, somewhere in the future, + // at the time this is sent. + RLCBSN_t impossibleBSN = resBSN + 1000; + GSM::Time impossible(BSN2FrameNumber(impossibleBSN),0); // TN not used. + // The downlink bit documentation is goofed up in GSM 4.18: They clarified it + // in GSM 44.018 sec 10.5.2.25b: This bit is 1 only for downlink TBFs. + L3ImmediateAssignment amsg( + L3RequestReference(0,impossible), + pacch->packetChannelDescription(), + GSM::L3TimingAdvance(ms->msGetTA()), + true,tbf->mtDir == RLCDir::Down); // tbf, downlink + + L3IAPacketAssignment *pa = amsg.packetAssign(); + // DEBUG: I tried taking out power: + // 12-16: Put power back in: + pa->setPacketPowerOptions(GetPowerAlpha(),GetPowerGamma()); + if (gFixIAUsePoll) { + pa->setPacketPollTime(resBSN.FN()); + } + if (tbf->mtDir == RLCDir::Up) { + // This assignment type is only for 1-phase access, which we do not support. + // An uplink immediate assignment can only be in response to a RACH + // and is identified by the request reference. Instead we send a + // one-block uplink assignment to get a packet resource request, + // then do the uplink TBF on PACCH based on that. + devassert(0); + // There is only one USF (ie, no multi-slot). + //int usf = ms->msUSFs[pacch->uplink()->TN()]; + int usf = ms->msUSFs[pacch->TN()]; + devassert(USF_VALID(usf)); + pa->setPacketUplinkAssignDynamic(tbf->mtTFI, tbf->mtChannelCoding(), usf); + } else { + pa->setPacketDownlinkAssign(tbf->mtGetTlli(), tbf->mtTFI, + tbf->mtChannelCoding(), tbf->mtUnAckMode, 1); + } + + if (os) { + amsg.text(*os); + } else { + GPRSLOG(1) << "GPRS sendAssignment "<load()) << amsg; + LOGWATCHF("%s CCCH load=%d\n", tbf->tbfid(1), currentAGCH->load()); + tbfDumpAll(); + tbf->mtAssignCounter++; + + // FIXME TODO: + if (drxmode) { + // Send the message on a paging channel. + // This code assumes the paging channel setup described + // in the class L3ControlChannelDescription. + // This code should be replaced with real paging channels. + // Our BS_PA_MFRMS is 2, so send the paging message twice to make + // sure it is sent in all (2) available paging 51-multiframes. + currentAGCH->send(amsg); + currentAGCH->send(amsg); + // DEBUG: try just plastering these messages out there. + //for (int ii = 0; ii < 6; ii++) { + // GSM::CCCHLogicalChannel *anyagch = gBTS.getAGCH(); + // anyagch->send(amsg); + //} + } else { + currentAGCH->send(amsg); // send() takes care of converting it to a BitVector. + } + tbf->mtCcchAssignCounter++; + } +} + +// Return true if we send a block on the downlink. +// NOTE: The L3ImmediateAssignment message can not assign multislot assignments. +// NOTE: If MS T3204 (GSM04.08) 1 second, started when MS sends RACH, expires before +// receiving ImmediateAssignment, MS aborts packet access procedure. +bool sendAssignment( + PDCHL1FEC *pacch, // The PACCH channel. + TBF *tbf, + std::ostream *os) // for testing - if set, print out the message instead of sending it. +{ + MSInfo *ms = tbf->mtMS; + + // This did not help the cause=3105 errors. My idea was that the packet downlink assignment + // is sent in the block immediately following the previous one, and I thought maybe there + // Should we send the message on CCCH or PACCH? + // It depends on which channel the MS is listening to now. + // In short: if there are any active TBFs, or T3168 is running, or T3193 is running, + // the MS is on PACCH. + // The spec is all muddled about this; there is no clear state machine + // showing what the MS is doing, rather, there are separate uplink and downlink + // timers that get started in various modes, and it is not really even obvious + // whether those timers should be started depending on other modes. + bool onccch = (ms->getRROperatingMode() == RROperatingMode::PacketIdle); + + // We have maintained the T3193 and T3168 timers exclusively for this moment! + // T3168 is for uplink requests initiated by the MS. + // T3192/3193 are for a new network initiated downlink after completion of previous downlink. + if (ms->msT3193.expired()) { ms->msT3193.reset(); } + if (ms->msT3168.expired()) { + ms->msT3168.reset(); + if (tbf->mtDir == RLCDir::Up) { + tbf->mtCancel(MSStopCause::T3168,TbfNoRetry); + return false; + } + } + if (ms->msT3193.active()) { onccch = false; } + if (ms->msT3168.active()) { + if (T3168Behavior) { + // If T3168 is running, the MS supposedly ignores downlink assignments. + // GSM04.60 7.1.3.1, and I quote: + // "At sending of the PACKET RESOURCE REQUEST message, + // the mobile station shall start timer T3168. Further more, + // the mobile station shall not respond to PACKET DOWNLINK ASSIGNMENT messages − + // but may acknowledge such messages if they contain a valid RRBP field − + // while timer T3168 is running." GSM44.60 says something different. + if (tbf->mtDir == RLCDir::Down) { return false; } // wait until later. + } + onccch = false; + } + + // This is a total hack. After several attempts, just ignore + // whether we think it should be on ccch or not, and alternate back and forth. + // This only works for downlink assignments, which include TLLI. + // The uplink immediate assignment on CCCH is only for one-phase access + // after a RACH, and we dont use that. + tbf->mtAssignmentOnCCCH = false; // For uplink TBF, always false. + if (tbf->mtDir == RLCDir::Down) { + if (tbf->mtAssignCounter < 4) { + tbf->mtAssignmentOnCCCH = onccch; + } else { + tbf->mtAssignmentOnCCCH = ! tbf->mtAssignmentOnCCCH; + } + } + + if (GPRSDebug & 1) { + GPRSLOG(1) <send1MsgFrame(tbf,msg,2,MsgTransTA,NULL); +} + +bool MSInfo::msCanUseDownlinkTn(unsigned tn) +{ + PDCHL1Downlink *down; + RN_FOR_ALL(PDCHL1DownlinkList_t,msPCHDowns,down) { + if (down->TN() == tn) {return true;} + } + return false; +} + +bool MSInfo::msCanUseUplinkTn(unsigned tn) +{ + PDCHL1Uplink *up; + RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) { + if (up->TN() == tn) {return true;} + } + return false; +} + +bool TBF::mtAllocateUSF() +{ + // TODO WAY LATER: Is there a deadlock condition possible if multiple multislot MS + // are in contention over USFs in multiple channels? + // I dont think so because we run in one thread and so they cannot become codependent. + PDCHL1Uplink *up; + RN_FOR_ALL(PDCHL1UplinkList_t,mtMS->msPCHUps,up) { + // Is this channel bidirectional? Otherwise the MS cannot use the USF. + if (!mtMS->msCanUseDownlinkTn(up->TN())) {continue;} + int usf = up->mchParent->allocateUSF(mtMS); + // This only allocates a new USF if one is not already allocated for this MS. + if (usf < 0) { + // Failure. But we will keep the TFIs and USFs we have so far and try again later. + GLOG(INFO) << "USF congestion on uplink"; + return false; + } + mtMS->msUSFs[up->TN()] = usf; // It might already be assigned, or maybe new. + } + return true; +} + + +// Set the TFI in the channels in use by the MS that will be used for this TBF transaction. +// The newvalue must be == tbf to set it, or == NULL to reset it. +static +void propagateTFI(TBF*tbf,int tfi,TBF*newvalue) +{ + if (tbf->mtDir == RLCDir::Up) { + PDCHL1Uplink *up; + RN_FOR_ALL(PDCHL1UplinkList_t,tbf->mtMS->msPCHUps,up) { + up->setTFITBF(tfi,RLCDir::Up,newvalue); + } + } else { + PDCHL1Downlink *down; + RN_FOR_ALL(PDCHL1DownlinkList_t,tbf->mtMS->msPCHDowns,down) { + down->setTFITBF(tfi,RLCDir::Down,newvalue); + } + } +} + +void TBF::mtFreeTFI() +{ + if (mtTFI >= 0) { + propagateTFI(this,mtTFI,NULL); // Reset tfi in all channels + mtTFI = -1; + } +} + +bool TBF::mtAllocateTFI() +{ + if (mtTFI < 0) { + devassert(mtMS->msPCHDowns.size() && mtMS->msPCHUps.size()); + PDCHL1FEC* pdch = mtMS->msPCHDowns.front()->parent(); + int tfi = pdch->mchTFIs->findFreeTFI(mtDir); + if (tfi < 0) { + GLOG(INFO) << "TFI congestion on "<msAssignChannels()) return false; + if (! mtAllocateTFI()) return false; + if (mtDir == RLCDir::Up && ! mtAllocateUSF()) return false; + + mtAttached = true; + mtSetState(TBFState::DataWaiting1); + return true; +} + +void TBF::mtSetState(TBFState::type wstate) +{ + if (mtState != wstate) { + //mtMsgReset(); // May be starting a new transaction message state. + mtState = wstate; + tbfDumpAll(); + + if (mtState == TBFState::Dead) { + // TODO: We may be able to reduce the dead time by the length of time + // the MS has not responded and we have not sent it any messages. + // This is how long the TBF will be dead, ie, reserving its resources. + int timerval = mtDir == RLCDir::Up ? gL2MAC.macT3169Value : gL2MAC.macT3195Value; + mtDeadTime.setFuture(timerval); + } + + if (mtState == TBFState::DataTransmit) { + LOGWATCHF("%s Start%s bytes=%d down/up=%d/%d\n", tbfid(1), + mtAssignmentOnCCCH ? " CCCH" : "", + engineGetBytesPending(), mtMS->msPCHDowns.size(),mtMS->msPCHUps.size()); + // FIXME: + // There is some housekeeping to do. + // If a previous uplink TBF died for this MS, and then the MS was issued + // a new uplink channel assignment, it may still have the the old USFs reserved + // for the old channel assignment. + // We should free those now, but it is just an efficiency issue. + } else if (mtState == TBFState::Finished) { + LOGWATCHF("%s Fin\n", tbfid(1)); + } else if (mtState == TBFState::Dead) { + LOGWATCHF("%s Dead\n", tbfid(1)); + } + } +} + +// Stop all the active TBFs associated with this MS in the specified dir. +// They enter the dead state, which means their resources cannot be reused +// until the timeout expires. +void MSInfo::msStop(RLCDir::type dir, MSStopCause::type cause, TbfCancelMode cmode, + unsigned howlong) // howlong in msecs to disable MS - unused now. +{ + TBF *tbf; + RN_MS_FOR_ALL_TBF(this,tbf) { + if (dir != RLCDir::Either && tbf->mtDir != dir) continue; + // 6-26-2012: Changing this test from isTransmitting to isActive. + //if (tbf->isTransmitting()) + if (tbf->isActive()) { + tbf->mtCancel(cause,cmode); + } + } +#if 0 + //if (!msTxxxx.active()) { + // GPRSLOG(1) << this <<" STOPPED" <mtGetState() == TBFState::Dead) { tbf->mtCancel(); } + } + msTxxxx.reset(); + msT3191.reset(); + msT3193.reset(); // Should have expired already, but be sure. + //msMode = RROperatingMode::PacketIdle; +} +#endif + +static RLCDownEngine *createDownlinkTbf(MSInfo *ms, DownlinkQPdu *dlmsg, bool isRetry, ChannelCodingType codingMax) +{ + ms->msStalled = 0; + GPRSLOG(2) << "<---- downlink PDU"; + RLCDownEngine *engine = new RLCDownEngine(ms); + // Wrong! Removed 4-24 : ms->msT3193.reset(); + // At this point the RLCEngine takes charge of the dlmsg memory. + TBF *tbf = engine->getTBF(); + tbf->mtTlli = dlmsg->mTlli; // Dont think mtTlli is used in a downlink TBF. + tbf->mtChannelCodingMax = codingMax; + //tbf->mtIsRetry = isRetry; + engine->engineWriteHighSide(dlmsg); + engine->mtSetState(TBFState::DataReadyToConnect); + return engine; +} + +// Service this MS, called from the service loop every RLCBSN time. +// Counters and Timers defined in GSM04.60 sec 13. +void MSInfo::msService() +{ + // After the last downlink TBF, the MS waits until this timer expires + // before going to PacketIdle mode. + if (msT3193.expired()) { + msT3193.reset(); + // If there are no uplink TBFs going, the MS has dropped to PacketIdle mode. + //if (! msCountActiveTBF(RLCDir::Up)) { + // msMode = RROperatingMode::PacketIdle; + //} + } + + if (msIsSuspended()) {return;} + + if (msTBFs.size()) { + msIdleCounter = 0; + } else { + if (++msIdleCounter > gL2MAC.macMSIdleMax) { + msDelete(); + return; + } + } + + // If MS running, check for counter expiration and stop if necessary. + // =========== From GSM04.60 sec 13: ========== + // N3101: + // When the network after setting USF, receives a valid data block from the mobile station, it will + // reset counter N3101. The network will increment counter N3101 for each USF for which no data + // is received. N3101max shall be greater than 8. + // N3103: + // N3103 is reset when transmitting the final PACKET UPLINK ACK/NACK message within a TBF + // (final ack indicator set to 1). If the network does not receive the PACKET CONTROL + // ACKNOWLEDGEMENT message in the scheduled block, it shall increment counter N3103 and + // retransmit the PACKET UPLINK ACK/NACK message. If counter N3103 exceeds its limit, the + // network shall start timer T3169. + // N3105: + // When the network after sending a RRBP field in the downlink RLC data block , receives a valid + // RLC/MAC control message from the mobile station, it will reset counter N3105. The network will + // increment counter N3105 for each allocated data block for which no RLC/MAC control message + // is received. The value of N3105max is network dependent. + // =========================================== + + // (pat) Having multiple timers here is overkill in the spec; + // they all do the same thing and will probably have the same value: + // they wait to make sure the MS is in PacketIdle mode before releasing resources. + + // When N3101 or N3103 counter expires, timeout using T3169. + // 7-5: I want to count RLC block periods, not USFs, so multiply by the number + // of uplink channels in use. + if (msN3101 * min(1,(int)msPCHUps.size()) > gL2MAC.macN3101Max) { + msStop(RLCDir::Up,MSStopCause::N3101,TbfRetryAfterWait,gL2MAC.macT3169Value); + } + // TODO: + //if (msN3103 > gL2MAC.macN3103Max) { + // msStop(MSStopCause::N3103,gL2MAC.macT3169Value); + //} + + // 12-22: I am taking this timer out for now, because it needs to be in the TBF, + // not the MS. We will detect timeout here using RRBPs. If you want to put it back in, + // move it to class TBF. + //if (msT3191.expired()) { + // Spec says when T3191 expires (5 secs) can release resources, + // but we will wait another second. + // msStop(MSStopCause::T3191,1000); + //} + + // If MS is stopped, check for timer expiry and restart if necessary. + //if (msTxxxx.expired()) { msRestart(); } + + // If there is a downlink message and this MS does not have any downlink TBFs running, + // create a new TBF. + while (msDownlinkQueue.size()) { + // We will not start a new downlink TBF as long as there is any kind + // of downlink TBF. + // Formerly, (if 0==StallOnlyForActiveTBF) we also stalled for dead TBFS + // (indicating the MS is probably unreachable) but that tended to kill off + // active TBFs when any one died for mysterious reasons, so I turned it off. + // 6-24-2012 UPDATE: I am going to reset StallOnlyForActive because we dont + // have bugs and we now use dead tbfs to legitimately block downlinks until expiry. + bool stallOnlyForActiveTBF = configGetNumQ("GPRS.TBF.StallOnlyForActive",0); + TBF *blockingtbf; + if (! msCountTBF2(RLCDir::Down,stallOnlyForActiveTBF?TbfMActive:TbfMAny,&blockingtbf)) { + DownlinkQPdu *dlmsg = msDownlinkQueue.read(); + // Because the message is queued for this MS, it means the tlli + // is equal to either msTlli or msOldTlli. The SGSN tells us which + // one to use. Make sure it is the current one. + // The tlli is changed on the next message after an attach procedure. + msChangeTlli(dlmsg->mTlli); + createDownlinkTbf(this,dlmsg,false,ChannelCodingMax); + } else { + // This code just prints a nice message: + devassert(blockingtbf); + // stalltype is 1 for stalled by active, 2 for stalled by dead tbf. + unsigned stalltype = blockingtbf->isActive() ? 1 : 2; + if (stalltype != msStalled) { + GPRSLOG(2) << this <<" msDownlinkQueue stalled by " + <<(stalltype==1 ? "active:" : "dead:") << blockingtbf; + msStalled = stalltype; + } + } + break; + } + + // If the MS has a delayed request an uplink TBF, start it up. + // TODO: If the Q is small, try flow control? + + // FIXME: no longer necessary? + //if (msUplinkRequest) { + // TBF *tbf = TBF::newUpTBF(ms,msrmsg->mCRD); + //if (ms->msCountActiveTBF(RLCDir::Up, &activeTbf) >= 1) + //} + + // Over-riding TBF killer. + // In the spec there is no such timer; instead there are timers for individual + // states from the spec, but that does not include all the individual substates + // we may go through, like reassignment. If any of those has a bug the TBF + // state machine may hang forever. This would usually prevent that. + // I am defaulting this timer to 6 secs which is longer than any other. + if (msTBFs.size()) { + // TODO: Should be TBF.NonResponsivve. + int timerVal = gConfig.getNum("GPRS.Timers.MS.NonResponsive"); // value of 0 disables. + if (timerVal > 0 && msTalkUpTime.elapsed() > timerVal) { + msStop(RLCDir::Either,MSStopCause::NonResponsive,TbfNoRetry,gL2MAC.macT3169Value); + } + } + + if (((int)gBSNNext % 24) == 0) msTrafficMetric = msTrafficMetric / 2; +} + +// The TBF ends either in mtFinishSuccess or mtCancel. +void TBF::mtFinishSuccess() +{ + mtMS->msT3191.reset(); + //GPRSLOG(1) << "@@@ok" << this <<" dir="<tbfDump(false)<msAddConnectTime(now.delta(mtStartTime)); // This includes the time it took the TBF to get its assignment through. + + // When a downlink TBF stops, set T3193, which measures how long the MS camps on the line. + // Note that this timer runs even if there are intervening uplink TBFs. + if (mtDir == RLCDir::Down) { + mtMS->msT3193.set(); + } else { + // If the last uplink TBF ends and the T3193 is not running, + // MS immediately enters PacketIdle mode. + //int anyactive = msCountActiveTBF(RLCDir::Either); + //if (!anyactive) { + // mtMS->msMode = RROperatingMode::PacketIdle; + //} + } +} + +// If the TBF never even started, the previous dlmsg will not have been pulled +// off of the queue yet. +void TBF::mtRetry() +{ + int retrycoding; + bool retry = (mtDir == RLCDir::Down) && (retrycoding = configTbfRetry()); + if (mtMS->msDeprecated) { + // No retry for MSInfo that has been replaced by some other TLLI. + // We check again because deprecated may have changed between the time this TBF + // was first attempted and when we get here. + retry = false; + } + if (retry) { + // Retry the last packet with a possibly slower codec: + ChannelCodingType chCoding = (ChannelCodingType) RN_BOUND((retrycoding-1),ChannelCodingCS1,ChannelCodingCS4); + RLCDownEngine *oldengine = getDownEngine(); + // TODO: We would like to retry all the PDUs that were not acknowledged, + // but right now we only keep the last. + // The mDownlinkPdu might be NULL if the engine never started, ie, MS didnt get the assignment. + DownlinkQPdu *dlmsg = NULL; + if (oldengine->mDownlinkPdu) { + //dlmsg = new DownlinkQPdu(*oldengine->mDownlinkPdu); + // retrychannel=1 implies ChannelCodingCS1 + dlmsg = oldengine->mDownlinkPdu; + oldengine->mDownlinkPdu = NULL; + } else { + dlmsg = mtMS->msDownlinkQueue.readNoBlock(); + } + if (dlmsg) { // Not possible to be NULL, but be safe. + if (dlmsg->mDlTime.elapsed() < gConfig.getNum("GPRS.TBF.Expire")) { + createDownlinkTbf(mtMS, dlmsg, true, chCoding); + } else { + // Too old. Give up. + delete dlmsg; + } + } + // The old tbf and engine will be deleted momentarily. + } + mtSetState(TBFState::Dead); +} + +// Kill the TBF with prejudice, either because it timed out, +// or for reasons beyond its purview, like we are shutting down GPRS. +// Same actions in either case. +void TBF::mtCancel(MSStopCause::type cause, + TbfCancelMode release) // When to release and whether to retry. +{ + // Clear out any reservation, in case the reservation does try to notify + // this tbf, which will no longer exist. This is probably overkill, + // because either the reservations were answered and cleaned up and they + // weren't, in which case this turned into a dead tbf, + // and we keep dead tbfs around for 5 seconds, and their reservations + // should be long passed over. + // NO DONT DO THIS!!! The thing may actually respond at that time. + // PDCHL1Downlink *down; + //if (mtExpectedAckBSN.valid()) { + // RN_FOR_ALL(PDCHL1DownlinkList_t,mtMS->msPCHDowns,down) { + // down->parent()->clearReservation(mtExpectedAckBSN,this); + // } + //} + + // Keep separate statistics for TBF that never got a connection. + switch (mtState) { + default: + mtMS->msCountTbfNoConnect++; + break; + case TBFState::DataTransmit: + case TBFState::DataFinal: + case TBFState::Finished: + { Timeval now; + mtMS->msAddConnectTime(now.delta(mtStartTime)); // This includes the time it took the TBF to get its assignment through. + mtMS->msCountTbfFail++; + break; + } + } + + if (mtDir == RLCDir::Up) { + mtMS->msFailUSFs(); // Cant use these USFs for a different MS for 5 seconds. + } + if (mtMS->msDeprecated) { release = TbfNoRetry; } + + std::string ss = tbfDump(true); + bool retry = false, needRelease = false; + const char *what = ""; + switch (release) { + case TbfRetryInapplicable: + case TbfNoRetry: + what = "@@@failed tbf"; + break; + case TbfRetryAfterRelease: + needRelease = (mtDir == RLCDir::Down) && isTransmitting(); + what = "@@@release tbf"; + goto retryafterwait; + case TbfRetryAfterWait: + what = "@@@failed tbf"; + retryafterwait: + retry = (mtDir == RLCDir::Down) && ! mtMS->msDeprecated && configTbfRetry(); + break; + } + GLOG(NOTICE) << timestr() << what <msT3191.reset(); + + // If it is a shutdown cause or an error during starting up the TBF, we kill it immediately. + // If the TBF was in a transmit mode, we need to send a PacketTBFRelease message. + // I think we are supposed to be able to set mControlAck in the PacketDownlinkAssignment + // but the Blackberry does not implement that properly, probably because the documentation is unclear. + // Update: other phones do implement it properly, ie, if the controlack bit is set, + // the assignment creates a new TBF. + // We only send the TBFRelease message in transmit mode, even though in DataWaiting modes the TBF is + // already started, because the reason we are sending it at all is so that a new TBF does + // not try to use RLC acknowledged mode to retrieve blocks from the previous (released) tbf. + // So it only matters if we have started transmitting blocks. + // Update: I am just going to send this in all transmitting modes, because we should + // do it for DataReassign or DataFinal also, and we rarely even use the DataWaiting2 mode. + + // If we were in state TbfRelease then we set kill time in the previous call + // to mtCancel and we dont need to add any additional time to mtKillTime. + // That is the only way mtDeadTime can be valid on entry to this function. + //if (mtGetState() != TBFState::TbfRelease) { + //mtKillTime = gBSNNext.addTime(gL2MAC.macT3169Value); + //} + + mtCause = cause; + mtSetState(needRelease ? TBFState::TbfRelease : TBFState::Dead); + + if (retry && release == TbfRetryAfterWait) { + mtRetry(); + } +} + +// Handle the few cases for a TBF that does not have channels +// assigned to its MS yet. +void TBF::mtServiceUnattached() +{ + // Dead tbfs are attached, so dont test this flag. + //if (mtAttached) return; + switch (mtState) { + case TBFState::Unused: + // This state may occur legally during testing. + GLOG(ERR) << "GPRS found TBF with uninitialized state\n"; + // Fix it so we wont see this message again. + mtCancel(MSStopCause::CauseUnknown, TbfNoRetry); + //mtState = TBFState::Dead; + return; + case TBFState::DataReadyToConnect: + if (mtAttach()) { + mtSetState(TBFState::DataWaiting1); + } + return; + case TBFState::Deleting: + casedeleting: + if (mtMsgPending()) { + // Wait until the response must have been received, + // to make sure it gets delivered to the right TBF. + // This is overkill - whoever put this in Deleting state already did this. + return; + } + mtDelete(); // Cleans up and deletes. + return; + case TBFState::Dead: + // A dead TBF is normally still "attached", ie, hanging onto its resources + // to prevent someone else from using them, until its killtime expires. + // But the MS may lose its channel assignment (eg, due to RACH) + // so this case may not be handled by the attached tbf code. + devassert(mtDeadTime.valid()); + if (mtDeadTime.expired()) { mtDetach(); goto casedeleting; } + return; + default:return; + } +} + +// Generic check for non-reponsive ms before sending another message. +// If we have sent the same message more than a specified number of times, +// consider the TBF dead. +// There is no specific mention of some of these timeout conditions in the spec, +// so just use reasonable values. +// Previously I stopped the whole MS for these cases, but sometimes the MS +// simply refuses to respond to one particular TBF, so instead, just kill the +// specific TBF that is non-responsive. +//bool TBF::mtNonResponsive() +//{ +// if (mtSendTries > gConfig.getNum("GPRS.Counters.Misc",10)) { +// mtCancel(MSStopCause::Misc); +// return true; +// } +// if (mtN3105 > gL2MAC.macN3105Max) { +// mtCancel(MSStopCause::N3105); +// return true; +// } +// return false; +//} + +bool TBF::isPrimary(PDCHL1Downlink *down) +{ + return (down->parent() == mtMS->msPacch); +} + +bool TBF::wantsMultislot() +{ + if (mtDir == RLCDir::Down) { + return mtMS->msPCHDowns.size() >= 2; + } else { + return mtMS->msPCHUps.size() >= 2; + } +} + +// If we get a response to TbfRelease, try to restart the TBF. +// Return true if we sent something on the downlink. +// We depend on setState resetting the msgAck flag. +bool TBF::mtSendTbfRelease(PDCHL1Downlink *down) +{ + if (mtMsgPending()) { return false; } // Wait for message in progress. + if (mtGotAck(MsgTransTbfRelease,true)) { + mtDetach(); + //mtSetState(TBFState::Dead); // redundant, done inside mtRetry() + mtRetry(); + return false; + } + if ((int)mtTbfReleaseCounter > gConfig.getNum("GPRS.Counters.TbfRelease")) { + mtCancel(MSStopCause::ReleaseCounter,TbfRetryAfterWait); + return false; + } + RLCMsgPacketTBFRelease *rmsg = new RLCMsgPacketTBFRelease(this); + return down->send1MsgFrame(this,rmsg,2,MsgTransTbfRelease,&mtTbfReleaseCounter); +} + +// See if the TBF can send anything on this downlink, and return true if it sent a block. +bool TBF::mtServiceDownlink(PDCHL1Downlink *down) +{ + // Only send messages on PACCH. + while (1) { + mac_debug(); + switch (mtState) { + case TBFState::Unused: + // This state may occur legally during testing. + GLOG(ERR) << "GPRS found TBF with uninialized state\n"; + mtCancel(MSStopCause::CauseUnknown, TbfNoRetry); + //mtState = TBFState::Dead; // Fix it so we wont see this message again. + return false; + case TBFState::DataReadyToConnect: + if (mtAttach()) { + mtSetState(TBFState::DataWaiting1); + continue; + } + return false; + case TBFState::DataWaiting1: // Waiting for ACK to assignment + if (! isPrimary(down)) { return false; } + // A non-responsive MS is detected by too many mtAssignCounter. + if (mtGotAck(MsgTransAssign1,true)) { + if (mtDir == RLCDir::Up) { + // If the MS Rached us then this timer is running; + // must reset it when the MS receives the assignment. + mtMS->msT3168.reset(); + } + mtSetState(TBFState::DataWaiting2); + continue; + } else if (! mtMsgPending()) { + // DEBUG: Try sending extra TA messages. + //if (mtAssignCounter > 6 && !mtTASent && sendTA(down,this)) { mtTASent=1; return true; } + + if ((int)mtAssignCounter > gConfig.getNum("GPRS.Counters.Assign")) { + mtCancel(MSStopCause::AssignCounter,TbfNoRetry); + return false; + } + if (mtAssignmentOnCCCH && ! gFixIAUsePoll) { + // We will never get a response since we didnt poll. + // The second time through this loop, // just go to the next state. + // The ExpectedAckBSN is valid even though we are not polling because we are + // using it basically as a timer to wait until the message is sent on AGCH. + //if (mtExpectedAckBSN.valid()) + if (mtMsgPending()) { + mtSetState(TBFState::DataWaiting2); + continue; + } + } + bool result = sendAssignment(down->parent(),this,NULL); + // else we wait in this state for the poll result + return result; + } + return false; + + case TBFState::DataWaiting2: + if (! isPrimary(down)) { return false; } + if (mtAssignmentOnCCCH) { + if (mtDir == RLCDir::Down) { + // See GSM4.60 7.2.2.1 + // We do not have to send a timing advance message + // because we included a starting time in the + // assignment message, but it is useful for debugging + // to see if the MS responds. + if (SendExtraTA && ! sendTA(down,this)) { return false; } + mtSetState(TBFState::DataTransmit); + return true; // We sent a message. + } + } + mtSetState(TBFState::DataTransmit); + continue; + + case TBFState::DataReassign: + devassert(0); +#if 0 + // Wait for existing messages to clear. + if (mtMsgPending()) {return false;} + // Did we get the ack to the reassignment? + if (mtGotAck(MsgTransReassign,true)) { + mtSetState(TBFState::DataTransmit); + continue; + } + if ((int)mtReassignCounter > gConfig.getNum("GPRS.Counters.Reassign")) { + mtCancel(MSStopCause::ReassignCounter,TbfRetryAfterWait); + return false; + } + // Send the reassignment. + // This is currently used only in the case where an uplink TBF + // wants to change its priority. + devassert(tbf->mtDir == RLCDir::Up); + if (sendTimeslotReconfigure(down->parent(),this,NULL)) { return true; } + // TODO: While we are waiting for the above, we could be sending + // blocks or setting USFs on the old channels, but for now + // we will not send any more blocks until the reassign occurs. + return false; // We did not use the downlink. +#endif + case TBFState::DataTransmit: + if (mtAssignmentOnCCCH) { + if (mtGotAck(MsgTransAssign2,true)) { + // Woo hoo! We are multislot now. + mtAssignmentOnCCCH = false; + // Turn off the reassign too, since we just did an additional reassign. + mtPerformReassign = false; + // And fall through to service the TBF on this channel. + } else { + // If the assignment was sent on CCCH we can not set up multislot, + // so we will use PACCH exclusively for the nonce. + if (! isPrimary(down)) { return false; } + + if (wantsMultislot()) { + // We would like to be multislot, but we are not yet. + // A multislot assignment can not be included in + // the L3ImmediateAssignment message on CCCH. + // So to do multislot we will need to send two messages + // the initial assignment and then another + // on PACCH to get into multislot mode. + if (! mtMsgPending()) { + // Send a second assignment to set up multislot. + if (sendAssignmentPacch(down->parent(),this,false,MsgTransAssign2,NULL)) { return true; } + } + } + } + } + +#if OLD_REASSIGN + if (mtPerformReassign) { + // We are sending a reassign because the MS requested a new priority for this TBF. + if (mtGotAck(MsgTransReassign,true)) { + mtPerformReassign = false; + // And fall through to service the TBF on this channel. + } else { + if (! mtMsgPending()) { + if ((int)mtReassignCounter > gConfig.getNum("GPRS.Counters.Reassign")) { + // The iphone is not answering these. It may be because we are only allowed + // to have 3 RRBPs out at a time, but whatever, dont kill the TBF for this, + // just stop sending the messages. If the MS wants + //mtCancel(MSStopCause::ReassignCounter,TbfRetryAfterWait); + // return false; + mtPerformReassign = false; + } else { + if (sendAssignmentPacch(down->parent(),this,false,MsgTransReassign,NULL)) { return true; } + } + } + // Otherwise fall through to utilize the channel normally. + } + } +#endif + + // Nonresponsive uplink is detected by N3101 (too many unanswered USF) + // in the msService routine, or N3103 in DataFinal mode. + // Nonresponsive downlink is detected by N3105 (no answer to RRBP data block), + // or when finished by T3191 expiry with no downlinkacknack received from MS. + // When this overflows the final reservation is still outstanding. + // TODO: fix this minor problem. + if (mtN3105 > gL2MAC.macN3105Max) { + if (mtAssignmentOnCCCH && !gFixIAUsePoll) { + // This error indicates the assignment failed. + // Go back and try it again. + mtSetState(TBFState::DataWaiting1); + continue; + } + mtCancel(MSStopCause::N3105,TbfRetryAfterRelease); + return false; + } + return engineService(down); + + case TBFState::DataFinal: + if (mtAssignmentOnCCCH && ! isPrimary(down)) { return false; } + // Nonresponsive uplink detected by N3103 (no answer to RRBP in final acknack). + // Not applicable to downlink. + if (mtN3103 > gL2MAC.macN3103Max) { + mtCancel(MSStopCause::N3103,TbfRetryInapplicable); + return false; + } + // For a downlink, we have to wait for the acknack from the MS. + // For an uplink, we have to wait for the RRBP response + // to the acknack that we sent the MS. + // The engineService routine takes care of this. + return engineService(down); + + //case TBFState::TbfRelease1: + // // Wait for any existing reservations to clear first. + // if (gBSNNext >= mtKillTime) { mtSetState(TBFState::Dead); continue; } + // if (! mtMsgPending()) { mtSetState(TBFState::TbfRelease2); continue; } + // return false; + + // Currently TbfRelease is used only when killing a TBF. + // mtSendTbfRelease will retry the TBF if possible. + case TBFState::TbfRelease: + if (! isPrimary(down)) { return false; } + // This extra check for killtime is no longer needed because + // we check in the msService loop. + //if (gBSNNext >= mtKillTime) { mtSetState(TBFState::Dead); continue; } + // Send the TBF Release message. + return mtSendTbfRelease(down); + + case TBFState::Finished: + // Hang around in this state until we are sure + // the MS has stopped talking to us. + if (! mtMsgPending()) { mtDetach(); } + return false; + + case TBFState::Dead: + // A dead TBF is still attached, ie, holding onto USF and TFI + // resources, but the MS may or may not have channel assignments, + // so this case must appear both here and in mtServiceUnattached. + devassert(mtDeadTime.valid()); + if (mtDeadTime.expired()) { mtDetach(); } + return false; + + case TBFState::Deleting: + return false; + } + } +} + +}; // namespace diff --git a/GPRS/TBF.h b/GPRS/TBF.h new file mode 100644 index 00000000..de7ac12a --- /dev/null +++ b/GPRS/TBF.h @@ -0,0 +1,522 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef TBF_H +#define TBF_H + +//#include +//#include + +#include "GPRSInternal.h" +#include "GPRSRLC.h" +#include "RLCHdr.h" +//#include "RList.h" +//#include "BSSG.h" +#include "Utils.h" +#include "MSInfo.h" + +namespace GPRS { +class MSInfo; +struct RLCMsgChannelRequestDescriptionIE; + +// TBF - Temporary Block Flow. +// This class is responsible for doing the work of moving data to/from the MS, +// and all the directly involved messages and acknowledgements. +// The TBF class is the main class, but always includes a MsgTransaction class, +// and is itself encapsulated in either an RLCEngineUp or RLCEngineDown class. +// These did not need to be separate classes, it was just a convenient way to +// encapsulate the different functionalities clearly. + +// A major design decision was how to share the channel resource. +// We share the channels because an MS with a bad connection may require +// multiple block resends, and may have up to 5 second timeouts mulitple times. +// We allow only one downlink and one uplink TBF per MS. +// But all TBFs of all connected MS, both uplink and downlink, run simultaneously +// and are serviced in round-robin fashion, so short TBFs dont get hung behind hogs, +// and the system should degrade gracefully under load. +// +// Rather than using queues for messages and data blocks, these classes generate +// all the messages and data blocks on demand. At each RLC Block time, and for +// each GPRS channel, we look for a message or data block to send on that channel +// by calling a TBF service routine the downlink PDCH service routine. +// If the TBF has something to send on the channel at that time, it does so, +// and notifies the calling service routine that it should stop looking for a block to send. +// +// Why do we use on-demand instead of queues? Several reasons: +// o The data to be sent may be influenced by uplink blocks up until the time it is sent. +// For example, we may need to resend a previous block, based on an intervening +// acknack message from the MS. +// o If there is a block to be sent that needs an RRBP reservation and can't get it (because +// the RRBP has an extremely limited reservation range and all may be in use), it surrenders +// its service time to some TBF that can use the downlink without a reservation. +// o Assignment messages dont even know if they are going out on the PDCH or on the +// CCCH queue until their transmit time arrives, at which time they consult the T3192/T3193 +// timers to find out. +// By generating all data and messages blocks on demand, we also ensure maximum utilization +// of the downlink resource regardless of the load. +// Note that TBFs may be traveling on multiple channels for multislot MS. + +// The TBF defines a little state machine, which marks the progress of the TBF. +// This has nothing to do with the RACH responder, which happens before a TBF ever gets started. +// The RACH repsponder grants only single-block uplink reservations, which we promptly +// forget about (except reserving that timeslot), and service only if a message +// actually arrives in the granted uplink block, at which time a TBF is created. +// And if we dont get the message, nobody ever knows; +// it is the responsibility of the MS to run a timer and try RACHing again. +// +// Uplink for MS in PacketIdle: +// o MS sends RACH +// o BTS sends ImmediateAssignment of single block on CCCH. +// no timers started; if MS does not receive it, oh well... +// o MS sends PacketResourceRequest on PDCH, then listens to PDCH for period T3168. +// note that MS ignores downlink assignment during this period. +// label1: +// o BTS allocates uplink TBF in state DataReadyToConnect, waits for internal resources. +// label2: +// o BTS sends PacketUplinkAssignment with poll for ControlAcknowledgment, +// TBF moves to state DataWaiting1. Note there is no timer - the ControlAcknowledgement +// is scheduled for a particular RLCBSN. +// TODO: If T3168 expired before reaching this state, dont bother. +// o MS sends ControlAcknowledgement, TBF moves to DataTransmit state. +// or: MS does not send response - if T3168 or retries not expired goto label2 +// o MS starts sending TBF data. +// Uplink for MS in PacketTransfer mode. +// o MS sends PacketResourceRequest on PDCH. It can use an RRBP poll for this purpose. +// I still havent figured out if we can let the MS do this during T3192 wait. +// Unclear if MS starts T3168, but we certainly dont. +// BTS allows multiple simultaneous uplinks, so just goto label1; +// Not sure about special case where we are still waiting for response +// from first PacketUplinkAssignment. +// Downlink when MS in PacketIdle: +// This can only happen if we have heard from the MS recently so we +// already have a TLLI to identify it. It occurs when the SGSN sends a downlink TBF +// but the MS has timed out and is back in PacketIdle mode, listening to CCCH. +// This is distinct from a Paging Request which is initiated by the SGSN +// when it is not sure if the MS is listening to this BTS or not. +// label3: +// o BTS sends ImmediateAssignment downlink message with poll. +// we dont need to start timer T3141 because we poll. +// o MS sends ControlAcknowledgement, TBF moves to DataTransmit state. +// or: MS does not send response - if retries not expired goto label3 +// o BTS starts sending TBF data; TBF in DataTransmit state. +// Downlink for MS in PackeTransfer mode or T3192 wait period. +// Can happen if MS is doing an uplink TBF or if MS camped on PDCH during +// T3192 period after downlink completes. +// label4: +// o BTS sends PacketDownlinkAssignment message with RRBP poll. +// o MS sends ControlAcknowledgement or other message in response to poll, +// TBF moves to DataTransmit state. +// or: MS does not send response - if retries not expired goto label4 +// +class TBFState +{ + public: + enum type { + Unused, // 0 Reserved. + + DataReadyToConnect, + // Waiting to call tbf->mtAttach...() successfully. + // It is waiting on resources like TFI or USF. + // We (currently) only allow one downlink TBF per MS, although the MS can send multiple + // simultaneous uplink TBFs, and nothing we can do about that. + // When it connects (gets the resources reserved), + // it calls MsgTransaction->sendMsg, which enqueues the message + // (for either PACCH or CCCH), increments mSendTrys. + // TBF state changes to DataWaiting1. + + // If the MS is in packet-idle mode, need to send the message on CCCH, + // otherwise on PACCH. See MSMode. + + DataWaiting1, + // Waiting on MsgTransaction for MS to respond to uplink/downlink message, + // Reply is in the form of RRBP granted PacketControlAcknowledgement. + // If it is a multislot assignment that went on CCCH, we need yet + // another MsgTransaction to do the timeslot reconfigure, so go to DataWaiting2, + // otherwise go directly to DataTransmit. + // (Because the CCCH Immediate Assignment supports only single-slot.) + // Otherwise go directly to DataWaiting2. + + DataWaiting2, + // Send the Packet Timing Advance required for downlink immediate assignment on CCCH. + // Multislot TODO + // Send the Multislot assignment. + // Reply is in the form of RRBP granted PacketControlAcknowledgement. + // On reply, go to state DataTransmit. + // if gBSNNext <= mtExpectedAckBSN: + // We are waiting for the MS to send the response. Do nothing. + // else: + // if the MS did not set mAckYet (by sending us a message, + // probably Packet Control Acknowledgment): + // if too many mSendTrys, give up, goto stateDead. + // else resend the message and stay in this state. + // else the MS did set mAck: + // If the message is: + // Packet TBF release, kill this TBF. + // FinalAckNack: All uplink data successful, kill this TBF. + // Otherwise it is data up/down: activate the TBF, goto stateActive + + DataReassign, + // Not currently used. + + DataTransmit, + // MS and BTS are in PacketTransfer Mode + // A data transfer TBF is trying to do its thing using the RLCEngine. + // If it is a packet uplink assignment, set some timer, and we start setting USF + // in downlink blocks to allow the MS to send us uplink blocks. + // If it is a packet downlink assignment, start some timer and the serviceloop + // will start sending downlink blocks when there is nothing else to do. + // If it is packet TBF release, destroy this TBF. + // Periodically the RLCEngine will send AckNack blocks in unacknowledged mode. + // (Only the final AckNack message is sent in acknowledged mode.) + + DataFinal, + // Only used for uplink TBFs now. + // We have received all the uplink data, but we are waiting + // for the MS to respond to the uplinkacknack message. + // It wont stop sending data until it gets it, so we make sure. + + TbfRelease, + // TBF is undergoing PacketTBFRelease procedure as the result of an abnormal termination. + // We send the PacketTBFRelease message and then wait in this state until we get the acknowledgement. + // When the get the response we will retry the TBF. + + Finished, + // TBF is completely finished, but we keep it around until + // all its reservations expire before detaching. + + Dead, + // Similar to Finished, but this TBF is dead because + // we lost contact with the MS or some timer expired. + // It is *not* detached yet, ie, it hangs on to its resources. + // We cannot reuse the resources for 5 (config parameter) seconds. + // We could also act preemptively to send TBF destruction messages, which if answered + // would allow us to get rid of the TBF, not that we care that much. + Deleting + // This state is entered via mtDetach(). + // This resources for this TBF have been (or are in the midst of being) released. + // We use this ephemeral state to make deletion simpler. + }; + static const char *name(int value); +}; +std::ostream& operator<<(std::ostream& os, const TBFState::type &type); + +#if TBF_IMPLEMENTATION +const char *TBFState::name(int value) +{ + switch ((type)value) { + case Unused: return "TBFState::Unused"; + case DataReadyToConnect: return "TBFState::DataReadyToConnect"; + case DataWaiting1: return "TBFState::DataWaiting1"; + case DataWaiting2: return "TBFState::DataWaiting2"; + case DataReassign: return "TBFState::DataReassign"; + case DataTransmit: return "TBFState::DataTransmit"; + //case DataStalled: return "TBFState:DataStalled"; + case TbfRelease: return "TBFState::TbfRelease"; + case Finished: return "TBFState::Finished"; + case DataFinal: return "TBFState::DataFinal"; + case Dead: return "TBFState::Dead"; + case Deleting: return "TBFState::Deleting"; + } + return "TBFState undefined!"; // Makes gcc happy +} +std::ostream& operator<<(std::ostream& os, const TBFState::type &type) +{ + os << TBFState::name(type); + return os; +} +#endif + +// These are the message transaction types. +// Each TBFState only uses one type of message transaction, so we could use +// the TBFState as the message transaction type, but the code is clearly +// if the message types are seprate from the TBF states. +// When we change state there may be outstanding messages that belong to the previous state, +// especially on error conditions. +// Most of these states are used only by uplink or downlink tbfs but not both; +// the inapplicable states are never used. +enum MsgTransactionType { + MsgTransNone, // Means nothing pending. + MsgTransAssign1, // For ack to first assignment msg (on ccch or pacch.) + MsgTransAssign2, // For ack to optional second assignment msg (always on pacch.) + MsgTransReassign, // For ack to reassignment if required. + MsgTransDataFinal, // For ack to final transmitted block. Used for both up and downlink. + // In downlink, the block with the FBI indicator. N3103 in uplink, N3105 in downlink + MsgTransTransmit, // For acknack message during DataTransmit mode. Used for both uplink and downlink + // In downlink N3105, in uplink for the non-final-ack. + // There are two different transaction states for Assign messages because we may send two: + // if the first is on CCCH and we want multislot mode, we have to send a second + // assignment on pacch. + MsgTransTA, // For ack to Timing Advance msg. + MsgTransTbfRelease, // For ack to TbfRelease msg. + MsgTransMax // Not a transaction - indicates number of Transaction Types. +}; + +// This facility is used only for messages belonging to a TBF, which includes RRBP reservations +// and the Poll reservation for an assignment message sent on AGCH. +// It is not used for RACH polls, since we dont know who they belong to, and would be +// meaningless anyway because they do not correspond to a TBF somewhere changing states. +// We only track one outstanding reservation at a time for each TBF. +// Generally we send a message and then wait for a PacketControlAcknowledgement. +class MsgTransaction +{ private: + BitSet mtMsgAckBits; // Type of acks received. + BitSet mtMsgExpectedBits; // The types messages we are waiting for. + + public: + RLCBSN_t mtExpectedAckBSN[MsgTransMax]; // When we expect to get the acknowledgement from the MS. + // This is supposed to be in the MS, but I moved it to the TBF because + // some TBFs become non-responsive individually while others are still moving, + // so we dont want to kill off the MS if a single TBF dies. + UInt_z mtN3105; // Counts RRBP data reservations that MS ignores. + UInt_z mtN3103; // Counts final downlinkacknacks RRBP that MS ignores. + UInt_z mtAssignCounter; // Count Assignment Messages. + UInt_z mtReassignCounter; // Count reassigment messages. + UInt_z mtCcchAssignCounter; // Number of assignments sent on CCCH. + UInt_z mtTbfReleaseCounter; + + // Wait for the next message. + void mtMsgSetWait(MsgTransactionType mttype) { + devassert(mtExpectedAckBSN[mttype].valid()); + mtMsgAckBits.clearBit(mttype); + mtMsgExpectedBits.setBit(mttype); + GPRSLOG(4) << "mtMsgSetWait"< mtTFI; // Assigned TFI, or -1. + + // There is some question about whether we need ACKs (PacketControlAcknowledgement) at all. + // For packet downlink, without the ACK we would not notice until the MS + // did not respond to RRBPs long enough that we figured it out. + // For packet uplink, without the ACK we could look at responses from this MS, + // and if they are bad for awhile, restart this, but we cant really be sure + // that the old TFI is released first. + // So the ACKs make the state machine much safer in both cases. + + + Bool_z mtUnAckMode; // a.k.a. RLCMode. If 1, send in unacknowledged mode. + // Note: as of 6-2012 unacknowledged mode is not implemented. + Bool_z mtAttached; // Flag for mtAttach()/mtDetach() status. + Bool_z mtAssignmentOnCCCH; // Set if assignment was sent on CCCH. + Bool_z mtPerformReassign; // Reissue an uplink TBF assignment to 'change priority' geesh. + //Bool_z mtIsRetry; // Is this our second attempt to send this TBF? + Bool_z mtTASent; // For debugging, true if we sent an extra Timing Adv message. + GprsTimer mtDeadTime; // When a dead TBF can finally release resources. + MSStopCause::type mtCause; // Why the TBF died. + //Float_z mtLowRSSI; // Save the lowest RSSI seen for reporting purposes. + uint32_t mtTlli; // The tlli of an uplink TBF. It is != mtMS->msTlli only + // in the special case of a second AttachRequest occuring + // after the TLLI reassignment procedure. + + // Persistence timers, used for both uplink and downlink. + //GprsTimer mtKeepAliveTimer; // Time to next keep alive. + GprsTimer mtPersistTimer; // How long TBF persists while idle. + + std::string mtDescription; // For error reporting, what was in this TBF? + + Timeval mtStartTime; // For reporting. default init is to current time. + + + // Statistics for flow control, needed only in downlink direction. + //TODO: RLCBSN_t mtStartTime; // When we started sending it. + + TBF(MSInfo *wms, RLCDirType wdir); + // The virtual keyword tells C++ to call the derived destructors too. + // Otherwise it may not. It is foo bar. + virtual ~TBF(); + + // Can this TBF use the specified downlink? + // It depends on the MS allocated channels. + bool canUseDownlink(PDCHL1Downlink*down) { return mtMS->canUseDownlink(down); } + + // Can this TBF use the specified uplink? + bool canUseUplink(PDCHL1Uplink*up) { return mtMS->canUseUplink(up); } + + void mtSetState(TBFState::type wstate); + TBFState::type mtGetState() { return mtState; } + + // These are the states that count toward an MS RROperatingMode being + // in PacketTransfer mode instead of PacketIdle mode. + // We leave DataWaiting1 out. + // First of all, we call this when we are doing a sendAssignment + // and the tbf on whose behalf we are inquiring is in DataWaiting1, + // so we get stuck here. + // Second of all, we dont really know if the MS is listening + // or not in DataWaiting1 mode because we have not heard back from + // it after the sendAssignment. Maybe we need yet another mode. + bool isTransmitting() { + switch (mtState) { + case TBFState::DataWaiting2: + case TBFState::DataTransmit: + case TBFState::DataReassign: + //case TBFState::DataStalled: + case TBFState::DataFinal: + return true; + default: + return false; + } + } + // Used to determine if there is already a TBF running so we should not start another. + // It is very important to include DataReadyToConnect because those indicate + // that an assignment is already in progress, and we dont want to start another. + bool isActive() { // The TBF is trying to do something. + switch (mtState) { + case TBFState::DataReadyToConnect: + case TBFState::DataWaiting1: + case TBFState::DataWaiting2: + case TBFState::DataTransmit: + case TBFState::DataReassign: + //case TBFState::DataStalled: + case TBFState::DataFinal: + case TBFState::TbfRelease: // Counts until it is acknowledged as released. + return true; + case TBFState::Dead: + case TBFState::Finished: // TBF may still wait for res, but we dont count it. + case TBFState::Deleting: + case TBFState::Unused: + return false; + } + return false; // Unreached, but makes gcc happy. + } + bool mtServiceDownlink(PDCHL1Downlink *down); + bool mtSendTbfRelease(PDCHL1Downlink *down); + void mtServiceUnattached(); + void mtCancel(MSStopCause::type cause, TbfCancelMode release); + void mtCancel(MsgTransactionType cause, TbfCancelMode release) { + // Canceled due to expiry of MsgTransactionType timer. + mtCancel((MSStopCause::type) cause, release); + } + void mtRetry(); + void mtFinishSuccess(); + std::string tbfDump(bool verbose) const; + + // For downlink we specify the channelcoding in the qbits of every block, + // so we can change channelcoding dynamically between CS-1 and CS-4. + // For uplink, the BTS specifies the encoding the MS will use in both + // the uplink assignment and in every uplinkacknack message. + // The ChannelCodingMax is used for retries to throttle back to a more secure codec. + ChannelCodingType mtChannelCodingMax; // The max channel coding (0-3) allowed for this TBF. + ChannelCodingType mtCCMin, mtCCMax; // Saved for reporting purposes. + ChannelCodingType mtChannelCoding() const; // Return 0 - 3 for CS-1 or CS-4 for data transfer. + + // Note that this TBF is a base class of either an RLCEngineUp or RLCEngineDown, + // depending on the TBF direction. + // These functions are defined in RLCEngineUp and RLCEngineDown: + virtual bool engineService(PDCHL1Downlink *down) = 0; + virtual unsigned engineDownPDUSize() const {return 0;} + virtual void engineRecvDataBlock(RLCUplinkDataBlock* block, int tn) {} + virtual void engineRecvAckNack(const RLCMsgPacketDownlinkAckNack *msg) {} + virtual float engineDesiredUtilization() = 0; + virtual void engineGetStats(unsigned *pSlotsTotal, unsigned *pSlotsUsed, unsigned *pGrants) const = 0; + virtual int engineGetBytesPending() = 0; + virtual void engineDump(std::ostream &os) const = 0; + virtual bool stalled() const = 0; + //RLCDownEngine const* getDownEngine() const; // Cant use this to change anything in RLCDownEngine + RLCDownEngine * getDownEngine(); + static TBF *newUpTBF(MSInfo *ms,RLCMsgChannelRequestDescriptionIE &mCRD, uint32_t tlli, bool onRach); + + // In a multislot configuration one of the slots is the primary slots and is + // used exclusively for all messages, and all other channels are used only for data. + // The primary slot must have both uplink and downlink timeslots assigned, + // and is currently identical to msPacch. It is also the first channel in the + // downlink list. + bool isPrimary(PDCHL1Downlink *down); + bool wantsMultislot(); // Does this tbf want to be multislot? + + // Attach to a channel. Return true if we succeeded. + bool mtAttach(); + bool mtNonResponsive(); // Is this TBF non responsive? + // Detach from the channel. Release our resources. + void mtDetach(); // Internal use only - call mtCancel or mtFinishSuccess + void mtDeReattach(); // Internal use only - call mtCancel or mtFinishSuccess + // If forever, do not move to expired list, just kill it. + void mtDelete(bool forever=0); // Internal use only - call mtCancel or mtFinishSuccess + //void setRadData(RadData &wRD); + //void talkedUp() { mtMS->talkedUp(); } + void talkedDown() { mtMS->talkedDown(); } + uint32_t mtGetTlli(); + const char *tbfid(bool verbose); + + private: + bool mtAllocateTFI(); + bool mtAllocateUSF(); + void mtFreeTFI(); +}; +extern unsigned gTBFDebugId; + +std::ostream& operator<<(std::ostream& os, const TBF*tbf); +#if TBF_IMPLEMENTATION +std::ostream& operator<<(std::ostream& os, const TBF*tbf) +{ + if (tbf) { + os << " TBF#" << tbf->mtDebugId <<" "; + } else { + os << " TBF(null ptr) "; + } + return os; +} +#endif + +extern bool sendAssignment(PDCHL1FEC *pacch,TBF *tbf, std::ostream *os); + +} // namespace GPRS +#endif diff --git a/GPRS/makefile.pat b/GPRS/makefile.pat new file mode 100644 index 00000000..93226ef7 --- /dev/null +++ b/GPRS/makefile.pat @@ -0,0 +1,138 @@ +HDR=BSSG.h BSSGMessages.h ByteVector.h FEC.h GPRSExport.h GPRSInternal.h \ + GPRSTDMA.h MAC.h MsgBase.h GPRSRLC.h RLCEngine.h RLCHdr.h RLCMessages.h RList.h \ + ScalarTypes.h TBF.h MSInfo.h +SRC1= ByteVector.cpp \ + MSInfo.cpp TBF.cpp FEC.cpp RLCEngine.cpp RLC.cpp MAC.cpp \ + BSSG.cpp BSSGMessages.cpp GPRSCLI.cpp \ + RLCMessages.cpp RLCEngine.cpp MsgBase.cpp +# Compile the most recently modified ones first. +SRC=$(shell ls -t $(SRC1)) +#CSRC= iputils.c + +INCLUDE= -I. -I.. -I../SGSNGGSN -I../CommonLibs -I../Control -I../GPRS -I../GSM -I../SIP -I../SMS -I../TRXManager -I../Globals -I../CLI -I../HLR -I../SR -I../sqlite3 + +ODIR=.libs + +GPRSOBJ= $(SRC:%.cpp=$(ODIR)/%.o) +COBJ= $(CSRC:%.c=$(ODIR)/%.o) +OBJ= $(COBJ) $(GPRSOBJ) + + +default: a +#default: Makefile.am a + +# 'all' is the target made by ../Makefile +all: + make -f Makefile + +more: + (clear && make lib && cd ../apps && make) 2>&1 | more + +a: .ALWAYS + #make lib && (cd ..; make) + make -f Makefile && (cd ..; make) + + +g: $(GGSNOBJ) +g2: miniggsn.o iputils.o + +# The at-sign makes it not echo the program, so you can do: make sql > gprs.sql +gprs.sql: .ALWAYS + @awk '/BEGINCONFIG/,/ENDCONFIG/ { \ + if (/BEGINCONFIG/||/ENDCONFIG/) next; \ + sub("^[^/]*//",""); \ + commas=$$0; gsub("[^,]*","",commas); \ + if (length(commas) < 4) print "syntax error in",FILENAME,":",$$0 >"/dev/tty"; \ + print "INSERT INTO \"CONFIG\" VALUES(" $$0 ");" \ + }' *.cpp > gprs.sql + +test1: test1.cpp Makefile libGPRS.a + g++ $(INCLUDE) -o test1 test1.cpp libGPRS.a ../CommonLibs/.libs/libcommon.a + +crc: crc24.c + gcc -o crc crc24.c + +test2: test1.cpp Makefile libGPRS.a + g++ $(INCLUDE) -o test1 test1.cpp libGPRS.a ../CommonLibs/.libs/libcommon.a ../GSM/.libs/libGSM.a + +testbv: ByteVector.cpp ByteVector.h makefile + g++ $(INCLUDE) -g -o testbv -DTEST=1 ByteVector.cpp ../CommonLibs/.libs/libcommon.a + +lib: $(OBJ) + ar cru $(ODIR)/libGPRS.a $(OBJ) + touch libGPRS.la + +#.cpp.o: +$(ODIR)/%.o: %.cpp + -mkdir $(ODIR) 2>/dev/null + g++ -O0 -DHAVE_CONFIG_H $(INCLUDE) -Wall -g -c -o $(ODIR)/$*.o $*.cpp + +$(ODIR)/%.o: %.c + -mkdir $(ODIR) 2>/dev/null + g++ -O0 -DHAVE_CONFIG_H $(INCLUDE) -Wall -g -c -o $(ODIR)/$*.o $*.c + + + + +# g++ -DHAVE_CONFIG_H -I. -I. -I.. -I../CommonLibs -I../Control -I../GPRS -I../GSM -I../SIP -I../SMS -I../TRXManager -I../Globals -I../CLI -I../HLR -I../SR -I../sqlite3 -Wall -O3 -g -O2 -MT RadioResource.lo -MD -MP -MF ".deps/RadioResource.Tpo" -c -o RadioResource.lo RadioResource.cpp; \ + then mv -f ".deps/RadioResource.Tpo" ".deps/RadioResource.Plo"; else rm -f ".deps/RadioResource.Tpo"; exit 1; fi + +$(OBJ):$(HDR) +$(ODIR)/miniggsn.o $(ODIR)/iputils.o: miniggsn.h Ggsn.h + +svnadd: + svn add $(HDR) $(SRC) + +clean: + /bin/rm $(ODIR)/* + +commit: + svn commit $(HDR) $(SRC) + +pinghttp: pinghttp.c + gcc -DSTANDALONE=1 -o pinghttp pinghttp.c + +# Need a short name for DOS file system. +SMALLFILES= GPRS/*.[hc]* GSM/*.[hc]* CLI/*.[hc]* \ + CommonLibs/*.[hc]* Control/*.[hc]* TRXManager/*.[hc]* +small: + cd .. && tar -czvf GPRS_backup_`date +%m-%d`.tgz $(SMALLFILES) \ + --no-recursion +backup: + cd .. && tar -czvf GPRS_full_`date +%m-%d`.tgz */* \ + --exclude .svn --exclude .deps --exclude .libs --exclude 'sqlite*' \ + --exclude '*o' --exclude '*.asn' --exclude '*cache*' --exclude 'Trans*' \ + --exclude OpenBTS --exclude *Test --exclude bk* + +ctags tags: .ALWAYS + cd ..; sh PAT.ctags + +.ALWAYS: + + +# Evidently the makefile autogenerator doesnt work, because David complains +# every time he tries to make this directory. So lets just write out the +# # automake makefile generator file to try to make him happy. +# This is pretty dumb, making an auto-make makefile from a makefile. +# Rebuild it whenever this makefile changes: +Makefile.am: makefile + @: Start with the copyright: + @sed -n '1,/^$$/p' < ../Makefile.am > Makefile.am + @awk >> Makefile.am '\ + BEGIN { \ + print "include $$(top_srcdir)/Makefile.common\n"; \ + print "AM_CPPFLAGS = $$(STD_DEFINES_AND_INCLUDES)\n"; \ + print "#AM_CXXFLAGS = -O2 -g\n"; \ + print "noinst_LTLIBRARIES = libGPRS.la\n"; \ + src="$(SRC)"; gsub(" +"," \\\n\t",src); \ + hdr="$(HDR)"; gsub(" +"," \\\n\t",hdr); \ + print "\nlibGPRS_la_SOURCES = \\"; print "\t" src; \ + print "\nnoinst_HEADERS = \\"; print "\t" hdr; \ + }' + + +#============================================== +# These are the lines that modified the existing file, but I decided to just overwrite: +# /libGPRS_la_SOURCES/,/^$$/ { next } +# /noinst_HEADERS/,/^$$/ { next } +#{print} diff --git a/GPRS/makefile.tests b/GPRS/makefile.tests new file mode 100644 index 00000000..57df6e5a --- /dev/null +++ b/GPRS/makefile.tests @@ -0,0 +1,60 @@ +HDR=BSSG.h BSSGMessages.h ByteVector.h FEC.h GPRSExport.h GPRSInternal.h \ + GPRSTDMA.h MAC.h MsgBase.h RLCEngine.h RLCHdr.h RLCMessages.h RList.h \ + ScalarTypes.h TBF.h Transfer.h Utils.h +#HDR= Utils.h BSSG.h BSSGMessages.h FEC.h GPRSExport.h GPRSInternal.h \ +# GPRSTDMA.h MAC.h RLCEngine.h RLCHdr.h RLCMessages.h TBF.h Transfer.h Utils.h BaseTypes.h +SRC= GPRSConfig.cpp TBF.cpp RLCMessages.cpp BSSG.cpp BSSGMessages.cpp ByteVector.cpp FEC.cpp MAC.cpp MsgBase.cpp \ + RLCEngine.cpp Transfer.cpp Utils.cpp + +INCLUDE= -I. -I.. -I../CommonLibs -I../Control -I../GPRS -I../GSM -I../SIP -I../SMS -I../TRXManager -I../Globals -I../CLI -I../HLR -I../SR -I../sqlite3 + +ODIR=.libs + +OBJ= $(SRC:%.cpp=$(ODIR)/%.o) + + +all: lib + +test1: test1.cpp Makefile libGPRS.a + g++ $(INCLUDE) -o test1 test1.cpp libGPRS.a ../CommonLibs/.libs/libcommon.a + +test2: test1.cpp Makefile libGPRS.a + g++ $(INCLUDE) -o test1 test1.cpp libGPRS.a ../CommonLibs/.libs/libcommon.a ../GSM/.libs/libGSM.a + +lib: $(OBJ) + ar cru $(ODIR)/libGPRS.a $(OBJ) + touch libGPRS.la + +#.cpp.o: +$(ODIR)/%.o: %.cpp + -mkdir o 2>/dev/null + g++ -DHAVE_CONFIG_H -I. -I. -I.. -I../CommonLibs -I../Control -I../GPRS -I../GSM -I../SIP -I../SMS -I../TRXManager -I../Globals -I../CLI -I../HLR -I../SR -I../sqlite3 -Wall -g -c -o $(ODIR)/$*.o $*.cpp + + + + +# g++ -DHAVE_CONFIG_H -I. -I. -I.. -I../CommonLibs -I../Control -I../GPRS -I../GSM -I../SIP -I../SMS -I../TRXManager -I../Globals -I../CLI -I../HLR -I../SR -I../sqlite3 -Wall -O3 -g -O2 -MT RadioResource.lo -MD -MP -MF ".deps/RadioResource.Tpo" -c -o RadioResource.lo RadioResource.cpp; \ + then mv -f ".deps/RadioResource.Tpo" ".deps/RadioResource.Plo"; else rm -f ".deps/RadioResource.Tpo"; exit 1; fi + +$(OBJ):$(HDR) + +svnadd: + svn add $(HDR) $(SRC) + +clean: + /bin/rm $(ODIR)/* + +commit: + svn commit $(HDR) $(SRC) + +ping: ping.o + gcc -o ping ping.c + +pinghttp: pinghttp.c iputils.c makefile.tests + gcc -DSTANDALONE=1 -o pinghttp $(CFLAGS) pinghttp.c iputils.c + + +tags: .ALWAYS + cd ..; sh PAT.ctags + +.ALWAYS: diff --git a/GPRS/notes.txt b/GPRS/notes.txt new file mode 100644 index 00000000..b5a68c28 --- /dev/null +++ b/GPRS/notes.txt @@ -0,0 +1,151 @@ +7-13, uplink.persist=3000, 1 channel: speed 11.7 latency 1.8 + uplink.persist=3000 3-down/2-up: speed 46.2 latency 1.3 + uplink.persist=3000 4-down/1-up: speed 41.5 latency 1.4 + again uplink.persist=3000 4-down/1-up: speed 27.8.5 latency 1.1 + again: uplink.persist=3000 4-down/1-up: speed 38.1 latency 1.3 + +Fri Jul 13 04:52:16 PDT 2012 +7-11: Multitech modem 4-up/1-down. This is after fixing the downlink USF bug. + These are with T3192Code=2 (1500ms) + downlink speed from mobilespeedtest.com: 22Kbps + In these there is downlink traffic on the same shared PACCH: + uplink ftp: total 1023652 bytes in 324s = 25Kbps + Raw (from looking at a single tbf, 3062B in 0.9s) 27Kbps + U412 96.5-93 4624b = 11Kbps. Yuck. + uplink ftp 50025 in 15s = 26.7Kbps. + for 3-up/2-down: uplink ftp 50025 in 30s = 13Kbps. + for 2-up/2-down: uplink ftp 50025 in 18s. second test: 18s = 22.2Kbps + I checked the log, and we are definitely getting blocks on all 4 channels. + For the 3/2 test that was slow, it looks like the phone is having to RACH in to + start nearly every uplink. For 4/1 the downlinks are so slow that they are still + running when the uplink ends. + These are with T3192Code=0 (500ms) + + +7-5: 4 channel multislot, T3192Code=0, got 32.8 Kbps. +7-5: 4 channel multislot, T3192Code=3, got 25, and cause=105 errors (TBF killed by RACH). + I suspect setting T3192 too high can force the MS to RACH for an uplink TBF. + + +7-4-2012: + Tried ftp. + download 6:40 + upload: 20 seconds "to calculate the time" + then sent a 0 byte file. Gotta love that. +6-27-2012: + With new channel allocator that guarantees 3-down/2-up assignments: + mobilespeedtest.com: 25Kbps, latency 2.2 + Had to set GPRS.ChannelCodingControl.RSSI to -50 to get it though + Setting GPRS.Codecs.Uplink,Downlink to 4 got 25.6Kbps + Setting T3192Code to 3 gives 29.4Kbps +6-21-2012: + iphone 001690000000002 + iphone speedtest.com 20.6KBps, latency 2.6s 100K in 39s. + The phone sent XID type 5 (set U and UI frame size) to 1520 + and XID type 11 (L3 params): LLC:LLC XID xidtype=11 xidlen=3 value=256; LLC:LLC Sending XID command:03FB1605F02F000100 + After power cycling the bts, + this phone does not accept the DetachRequest or PdpContextReject ImplicitlyDetached thing. + if you manually request the operator again with the phone, it sends a RoutingAreaRequest with a new TLLI + and after receiving the RoutingAreaUpdateReject does an immediate new attach. + -- + This phone lost connection temporarily in log: iphone/1 around 07:54:46. + An uplink TBF#127 was (correctly) cancelled, but I subsequently saw many 'received uplink data block after expiration' + How is that possible? + -- + After power cycling the bts, the carrier selection reported ATT and T-MOBILE only; tried 3 times. + It did not do anything to gprs, so I think this is a normal registration failure. + Then when power cycled, it did an immediate gprs-attach. + When I did the "Carrier Selection" again, it showed only 1001, not ATT and T-MOBILE. + + Samsung GT-I5500 Baseband version: I5500NEJP1 + +Put gprs tests on the wiki. + +6-18-2012: Davids galaxy error log: + res 303714: TBF#170 uplinkacknack - received + res 303715: TBF#171 downlinkassignment - unanswered + res 303735: TBF#171 immediate downlinkassignment - received + res 305577 TBF#177 sendass PACCH - unans (at 583) + res 305585 TBF#177 sendass PACCH - unans (at 591) + res 305593 TBF#177 sendass PACCH - unans (at 599) + at 5598 res 305601 TBF#177 sendass PACCH - unans (at 607) + at 5606 res 305618 TBF#177 sendass imm on TN1. 617 PDCH#51:1 has no usf. + at 5623 res 305636 TBF#177 packetuplinkacknack + +6-13-2012: + After RLC modification to send only negatively acknowledged blocks, + now at 22.2Kbps, latency=2.0, 100K in 36.1s. + +6-11-2012: + After fixing nstuck bug, setting nstuck to 1000, fixing the negatively acknowledged resend bugs: + 15.6Kbps, 19.8Kbps, 19.3Kbps + +With 2-down/2-up I typically get 19Kbps. +Tried setting Poll1 to 30, got 11-13Kbps on two different tests +with 8 total cause=3 errors. +Restarted with Poll1 at 10 (default) got 19Kbps with one cause=3. +then 10Kbps with one cause=3. + +Restarted, removing the the needy USF fix entirely, +setting Multislot.Max.Downlink/Uplink =1/1 in sql and turning on GPRS.WATCH. + 18.5Kbps +Turned off GPRS.WATCH 14.7Kbps with 2x3105 retries +Turned on GPRS.WATCH 19.7Kbps, did not see any retries. +Turned off GPRS.WATCH 17.5, no retries, then 16.7 with 1x3105 +Turned on GPRS.WATCH 19.3Kbps, 16.3Kbps +Turned off GPRS.WATCH 12.6Kpbs, 17.6Kbps (no retries), 17.2Kbps. +I think it may depend on how long you wait between re-running the test, +ie, maybe there are still old tcp packets coming that bash the current test. + +6-8-2012 GPRS-Beta-4 3-down/2-up with needy USF turned off: + Your speed: 18.4 + Your latency: 1.8s + Transferred 100KB in 43.5 +Changing GPRS.USFMode (needy USF) didnt change a thing. +with 2-down/2-up: 13.9Kbps, 2.2s, 100KB in 57.3s, and it had alot of retries. +again: 13.5Kbps, 2.3s, 100KB in 60s + +6-7-2012 with 3-down/2-up working multislot: + Your speed: 19.8Kbps + Your latency: 1.8s + Transferred 100KB in 40.3 + +6-3-2012 with 2-down/2-up working multislot: + Your speed: 20.78Kbps + Your latency: 1.8s + Transferred 100KB in 38.5s + +mobilespeedtest.com: +6-3-2012 with 2-down/1-up buggy multislot: + Your speed: 18.29Kbps + Your latency: 2.86 + Transferred 100KB in 43.74s + + +5-22-2012, with continuous tbf, but the speed test stops for maybe 5 seconds right in the middle -why? + + Your speed: 7.08Kbps + Your latency: 2.702 + Transferred 100KB in 113s + + + google.com - 8 secs + pats home page (www.menu1.org/pat) - 15 secs + cnn.com - 35-40 secs (note that this page is not invariant.) + wikipedia dinosaur 1:52 + +After bug fix, but it still hung half way through, but the hang looked correct (saw fbi) so whats up? +Your speed: 8.08Kbps +Your latency: 2.6 +Transferred 100KB in 99s + +After setting RLCMsgPacketDownlinkAssignment->mControlAck +Your speed: 11.17Kbps +Your latency: 2.8 +Transferred 100KB in 71 +wikipedia dinosaur 1:40 + +After fixing the mSP bug: + wikipedia dinosaur 1:35 + +============ diff --git a/GPRS/pat.txt b/GPRS/pat.txt new file mode 100644 index 00000000..af51f03b --- /dev/null +++ b/GPRS/pat.txt @@ -0,0 +1,788 @@ +Kyles Blackberry is Blackberry 9700 + +Speed tests: + From mobileSpeedTest.com: + Original: + Your speed: 5.166 Kbps + Your latency: 3.057 seconds + Transferred 100KB in 154.86 seconds. + With ganged TBFs, TBF wrap-around disabled, GPRS.Counters.N3105=3, GPRS.TBF.Retry=4, + GPRS.SinglePduMode=0. The thing is still getting alot of 3105 errors. + And in general it gets cause=1 too, although not during this test, but before. + Your speed: 6.984 Kbps + Your latency: 2.09 seconds + Transferred 100KB in 114.55 seconds. + Another try: + Your speed: 7.539 Kbps + Your latency: 2.583 seconds + Transferred 100KB in 106.11 seconds. + Then I tried allowing TBF wrap-around and it went down! + Your speed: 6.383 Kbps + Your latency: 2.34 seconds + Transferred 100KB in 125.34 seconds. + + +Got 11 retries during the test, + +Note: GSM04.08 (L3 Procedures) and GSM04.18 (L3 messages) replaced by 44.18 + 24.08 + 4.08 has a state machine picture in 4.1.2, and GMM states a little after. + 24.007 7.1.1 has lots of state machines including same state machine picture, + but I dont think they are useful. + 3.64 6.2 has the DTM state machine picture. + + 23.060 6.1 has a description of GMM states (PMM_whatever) for 3G-SGSN. + + 24.007 11.2.4 - Lists IEI formats so you can tell how to skip an unrecognized IEI. + + 3.03 2.6 - how to encode TLLI. + +Measurement Reports: + 45.008 10.1.4: in Network Control Order 2 (the one I have been using) if the + MS detects a downlink signalling failure or random access failure + (as defined in 44.018/44.060) the MS will perform autonomous cell reselection. + This may have been what was happening when the MS would stop listening + to the BTS for 2 second straight. + +RA Update - for GPRS. +LA Update - for CS calls. +Combined RA/LA update permitted at SGSN. + +DRX mode and paging groups covered in GSM05.02. + MS tells DRX mode to SGSN in the GPRS Attach or RA Update messages. + Just what does the sgsn think it is going to do with it? + +GPRS: + Jean Samuel - French guy funding Russians to develop GPRS. + +The BCS Block Check Sequence shown in GSM03.64 6.6.4 figure 20 +is just the 40 bit checksum added by transmit(). + +PDP Context Activiation: Getting IP address: 24.008 6.1.2 + +GSM 4.64 - LLC layer. + +Note from man 7 tcp: + tcp_frto (integer; default: 0; since Linux 2.4.21/2.6) + Enables F-RTO, an enhanced recovery algorithm for TCP retransmission time- + outs (RTOs). It is particularly beneficial in wireless environments where + packet loss is typically due to random radio interference rather than inter- + mediate router congestion. See RFC 4138 for more details. + + This file can have one of the following values: + + 0 Disabled. + + 1 The basic version F-RTO algorithm is enabled. + + 2 Enable SACK-enhanced F-RTO if flow uses SACK. The basic version can be + used also when SACK is in use though in that case scenario(s) exists + where F-RTO interacts badly with the packet counting of the SACK-enabled + TCP flow. + + Before Linux 2.6.22, this parameter was a Boolean value, supporting just + values 0 and 1 above. + +RadioResource.cpp: AccessGrantResponder() +serviceLoop() +TCHFACCHL1Encoder::dispatch() - does TCH data pushing. +mDemuxTable in TRXManager.cpp, calls writeLowSideRx in a L1Decoder descendent class. +mGPRSFEC + +Maximum LLC PDU size is 1560 bytes. (GSM04.60 sec9.1.12) Bytes over are discarded in RLC. +In unacknowledged mode, LLC-PDUs delivered in the order received, with 0-bits for missing blocks. +The minimum payload size (using CS-1) is 20 bytes (see RLCPayloadSize) +Therefore, a single PDU may take 78 blocks. + +There are 1-N downstream L1 physical channels. +Each is connected to an L1FEC. + +An N-PDU is on the network side of SGMS +A PDU (aka NS-PDU) is on the downstream side of SGMS, after SNDCP +A Segment is a part of a PDU for transmission in an RLC Radio Block. + +All MAC control functions come from the SGMS via BSSGP. +The RLC/MAC is entirely oblivious of PDU content, just passes it to SGMS. +The RLC reports downlink packet loss information to SGMS as +a Bucket Leak Rate, per BSS (aka BVC on the BSSGP interface), and per MS. + +Notes: + Notes: The SAPMux class defines writeHighSide and writeLowSide + An encoder class defines only writeHighSide + A decoder class defines only writeLowSide. + + MAC MODE: + Dont understand. For downlink allocation it is: + Dynamic allocation, Extended Dynamic allocation, (arent these inapplicable?) + Fixed allocation full duplex, Fixed allocation half duplex. + + TBF Mode: + Packet Uplink Assignment, Packet Downlink Assignment, Immediate Assignment + TFI goes with TBF. TFI is unique only within a PDCH. + For Multislot, TFI is unique in all PDCH of multislot. + + RLC Mode: + Acknowledged or Unacknowledged. (See GSM04.60 11.2.7 Packet Downlink Assignment) + +Upstream: + BSSGSP + Definitions: + BVCI - BSSGP Virtual Connection ID, 0 = signaling, 1 = PTM (point-to-multipoint) + It corresponds to a cell, and can be used instead of routing area id + at operators discretion. + NSEI+BVCI + NSE - Network Service Entity. There is one or more NSE inside the BSS for signaling. + NSEI - Network Service Entity Id. + I think these correspond to BSS. + --- + NS-VC - Virtual Connection + --- + LSP - Link Selector Parameter, something used only inside the BSS or SGSN, + and not transmitted, to uniquely identify NS-VC. + We wont use it. + QoS Profile: specifies transmission mode (acknowledged, etc), bit rate, other stuff. + Messages: + DL-UNITDATA + Includes PDU type (DL-UNITDATA), TLLI, QoS Profile, PDU Lifetime, PDU. + optional: IMSI, oldTLLI, PFI (Packet Flow Identifier), etc. + Does NOT include LSP, BVCI, NSEI + UL-UNITDATA + Includes PDU type (UL-UNITDATA), TLLI, BVCI, Cell Identifier, PDU. + Does NOT include LSP, BVCI, NSEI. + GMM-PAGING-PS/GMM-PAGING-CS (for packet or voice) + Includes PDU type (PAGING-PS), QoS Profile, P-TMSI IMSI. + Note: If TLLI is specified and already exists within a Radio Context in BSS + [because MS has communicated previously] it is used. + + BVCI Location Area Routing Area BSSArea Indication + Optional: P-TMSI, BVCI, Location area, Routing area. + GMM-RA-CAPABILITY, GMM-RA-CAPABILITY-UPDATE + Astonishingly, the BSS asks the SGSN for this info. + GMM-RADIO-STATUS + GMM-SUSPEND + GMM-RESUME + NM-FLUSH + NM-LLC-DISCARDED + NM-FLOW-CONTROL-BVC + NM-FLOW-CONTROL-MS + NM-STATUS + NM-BVC-BLOCK/NM-BVC-UNBLOCK + NM-BVC-RESET + NM-TRACE + PFM-... + +Downstream: + RACH/AGCH for paging. + PDUs via RLC + + +SGSN Uplink: +// +// OpenBSC endpoint for NS layer packets: libgp/gprs_ns.c:read_nsip_msg +// (in OpenBSC, NSIP means NS over IP), which eventually calls: +// libgp/gprs_ns.c:gprs_ns_rcvmsg(), in which BSS is identified by three ways: +// 1. First it tries using the IP address of the BSS to identify the BSS. +// 2. If unrecognized, the NSEI specified in the NS_RESET command is used. +// 3. If no NS_RESET ever received, sets NSEI to 0xfffe and proceeds; this works +// if there is only one BSS, ever, as in our case. +// NS-UNITDATA packets (the only data type) are sent to gprs_ns_rx_unitdata(), +// which somehow calls sgsn_ns_cb() (a callback function) +// which calls libgp/bprs_bssgp.c:gprs_bssgp_rcvmsg(bci and nsei as params) +// which looks up the BTS using the NSEI, then uses: +// switch (BVCI) { +// case BVCI_SIGNALLING: gprs_bssgp_rx_sign() +// case PVCI_PTM: // throw an error +// default: gprs_bssgp_rx_ptp(). +// gprs_bssgp_rx_ptp() is the main BSSGP function, does this: +// switch (pdu_type): +// case BSSGP_PDUT_UL_UNITDATA: bssgp_rx_ul_ud(), +// which calls bssgp_rx_ul_ud(), which calls gprs_llc_rcvmsg() +// case BSSGP_PDUT_FLOW_CONTROL_BVC: bssgp_rx_fc_bvc(); +// default: // throw an error +// +// gprs/gprs_llc.c:gprs_llc_rcvmsg() is the main entry point. +// extracts TLLI, and forwards message based on SAPI to: +// case GPRS_SAPI_GMM: gsm0408_gprs_rcvmsg() +// case GPRS_SAPI_SNDCP3/5/9/11: sndcp_llunitdata_ind(), +// which assembles NS-PDUs as per SNDCP, then sends the complete N-PDU to: +// sgsn_rx_sndcp_ud_ind(), which looks up the MM context by RAI+TLLI, +// then looks up the PDP context by NSAPI+MM context, +// counts bytes sent, then calls gtp_data_req(gsn, pdp->lib,npdu,npdu_len) + +SGSN Downlink: +Packets come in on a tunnel, may have different header sizes depending on version. +Gtp lib decapsulates them, gtp_decaps0(),gtp_decaps1(), gets the header, +which may indicate PDP creation, update, etc, or "GTP_PDU" type, which calls: +gtp/gtp.c: gtp_gtpu_ind() which calls (via callback table, to get out of gtp lib) +sgsn_libgtp.c: cb_data_ind(struct pdp_t*lib,void *packet,unsigned len) +The pdp has an MM context, which has the nsei+bvci+tlli. +If mm_state is GMM_REGISTERED_SUSPENDED, it calls gprs_bssgp_tx_paging(), +(and apparently drops the incoming NPDU on the floor) +else if mm_state is GMM_REGISTERED_NORMAL it just sends the packet to: +sndcp_unitdata_req() +Eventually it calls: +int gprs_bssgp_tx_dl_ud(struct msgb *msg, struct sgsn_mm_ctx *mmctx) + which calls: gprs_ns_sendmsg(bssgp_nsi, msg); + where bssgp_nsi is a global var. +int gprs_ns_sendmsg(struct gprs_ns_inst *nsi, struct msgb *msg) + does: { ... nsvc = nsvc_by_nsei(nsi, msgb_nsei(msg)); } + then calls gprs_ns_tx(nsvc,msg) +which calls nsip_sendmsg(nsvc,msg) +which if the encapsulation is udp calls gprs_ns_tx(nsvc,msg) (else *_frgre_*something() +which calls nsip_sendmsg(nsvc,msg) which uses: + struct gprs_ns_inst *nsi = nsvc->nsi; + struct sockaddr_in *daddr = &nsvc->ip.bts_addr; + rc = sendto(nsi->nsip.fd.fd, msg->data, msg->len, 0, + (struct sockaddr *)daddr, sizeof(*daddr)); + + +In osmocom/openbsc/openbsc/src/gprs +I modified the sgsn Makefile to remove -lgtp, and took out all the gtp references +execpt pdp stuff, so we can move that file to sgsn and stop linking with libgtp. +Notes: the cb_conf callback creates the pdp context; +to replace, I must call create_pdp_conf(), which calls gsm48_tx_gsm_act_pdp_acc() to +send an acknowledgment to the MS. +The eua is struct pdp_t is the IP address. First byte is IETF, second +GPRS Messages in GSM04.08 also GSM44.18 + +the opensgsn accepts flow control messages but ignores them + +Wikipediate/GPRS_Core_Network says: +GTP-U is used for user-data in spearated unnels for each PDP context. +GTP-C for control: setup of PDP contexts, etc. +GTP-C on UDP port 2123 and GTP-U port 2125 +GTP version zero supports both on one generic header, can be used with UDP or TCP on port 3386. +But port 3386 is also dedicated to the charging service, specifically: "GSM/UMTS CDR logging protocol". +GSM 20.060 - Routing! Finally! + +sndcp: +/* Request transmission of a SN-PDU over specified LLC Entity + SAPI */ +sndcp_unitdata_req() +Note that sndcp header compression is optional, so I suspect opensgsn doesnt bother, not sure. + + +GTP and GGSN: +See 29.060 for GTP, and 27.060 for an example. +29.060 7.3.2 create pdp context response: + +PPP is not normally used on the GGSN +PPP support was added to allow an MS to use PPP to go all the way to a network endpoint. +See cisco document: http://www.cisco.com/en/US/docs/ios/12_3/12_3y/12_3yq/ggsn_5_2/configuration/guide/ggsnppp.html + +And I quote: +If the MS requests a dynamic PDP address with the PDP Type IPv4, IPv6 or +IPv4v6 and a dynamic PDP address is allowed, then the End User Address +information element shall be included and the PDP Address field in the End +User Address information element shall contain the dynamic PDP Address(es) +allocated by the GGSN. If the MS requests a static PDP address with +the PDP Type IPv4, IPv6 or IPv4v6, or a PDP address is specified with +PDP Type PPP, then the End User Address information element shall be +included and the PDP Address field shall not be included. + + +ephemeral ports: +cat /proc/sys/net/ipv4/ip_local_port_range + + + /* actually send the N-PDU to the SGSN core code, which then + * hands it off to the correct GTP tunnel + GGSN via gtp_data_req() */ +return sgsn_rx_sndcp_ud_ind(&sne->ra_id, lle->llme->tlli, sne->nsapi, msg, npdu_len, npdu) + +// Wrap BSSGP packets in yet another layer per 44.018 +// OpenBSC endpoint: libgp/gprs_ns.c:read_nsip_msg (NSIP == NS over IP), which eventually calls: +struct NSLayer { +}; + +// Talk to BSSGSP (or whomever) +struct BSSGPLayer { + send and recv L2 messages. + Talk to the socket. +}; + + + +class MACB { + // Just one of these. + For each PCH: + TFI table - points to MACD for each TFI for both uplink and downlink. + // Which MS is using which Block locations currently. + // This could just be bit mask, which is set when TBF allocated + // and reset when TBF unallocated. If you really need to know + // who owns a slot, you could run through the TFI table. + Uplink_Block_Assignment[12]; + Downlink_Block_Assignment[12]; + IMSI to MACD table + PTMSI to MACD table + TLLI to MACD table + + // Routine to assign blocks to MACDs. +}; + +GPRSChannel pickChannel() { +} + + +class MACD { // aka Radio Context. + // One per IMSI or TLLI, which means one per MS. + // We will keep these around until the MS detaches, or they get really old. + TLLI mtlli; // Not known when MS first attaches. + IMSI mimsi; + // State of MS: packet-idle, packet-transfer. + // Points to in-process TBFs; there could be multiple ones because a single + // block. + // 1 or more PCH assigned to MS. + // NO, the SGMS does this: Incoming message may be control or data, routed to UplinkTBF. +}; + +class GPRSChannel { + GPRSL12Uplink *uplink; + GPRSL12Downlink *downlink; +}; + +class GPRSL12Uplink { // aka PCH + // downstream attaches to a single Physical channel in L1FEC. + // Incoming messages are routed to MACD based on TFI. + List mReservations; // Radio blocks that have been reserved for some purpose, + // eg, single block grants requested by RACH + // Return an available RB on this uplink. + RBN reserveOneBlock() { + } +}; + +class GPRSL12Downlink { // aka PCH + // One of these for each PCH (physical channel), attached to L1FEC. + // Accepts Radio Blocks from anybody. +}; + +class AGCHResponder { + // This class queues GPRS responses that must be sent via AGCH. + // These are: +}; + +class GPRSRachManager - not needed, just use some functions. +{ + // Receives Packet Channel Request on RACH. (from Control:AccessGrantResponder()) + // Routes to MACD. + // Note that SGMS knows nothing about this yet - the MS will use its newly + // allocated channel to send a PDU that goes to SGMS. +}; + +// A TBF can be a single block packet access, or one or two phase multi-block access, +// although this class does not do the phases, it just handles a single TBF transaction, +// then disappars. +class DownlinkTBF { + // Contains the downlink RLCEngine. + PDU *data[0..n] // 1 or more PDUs to send. + GPRSL12Downlink mpch[4]; // Up to 4 physical channel + int PDUPriority + MACD *my_ms; // Used to get TLLI. + int TFI; // 0..7, assigned when transaction starts. +}; + +// For open-ended dynamic uplink, which uses USF, the MS will continue to monitor +// the PDCH for its USF value until it receives ... or until T3180 expires: 5 seconds. +// If MS finishes, it sends its final block then immediately enters packet-idle mode +// unless there is a downlink in progress. +class UplinkTBF { + // Contains the uplink RLCEngine. + // Assembles the PDUs and sends them to BSSGP. +}; + + + + +========================================================================= + +// IMMEDIATE ASSIGNMENT FORMAT: GSM04.08sec9.1.18 +// Notes: We will not use: +// Immediate_Assignment_Extended for RR only and addresses 2 mobiles simultaneously. +// "RR Packet Uplink/Downlink Assignment" is part of the "Packet Assignment" +// command that is sent on DCCH of an MS with an ongoing RR channel. +struct { + uint l2_pseudo_length:8; + uint RR_protocol_discriminator:4; // Not sure, see 04.18sec10.2, GSM24.07 + uint skip_indicator:4; // 0 == dont ignore this message. + uint message_type:8; // 0x3f == IMMEDIATE_ASSIGNMENT, + uint page_mode:4; + uint dedicated_mode_or_TBF:4; // sec10.5.2.25b + // bits are: + // unused:1; + // TMA:1; 0 == no meaning ??, 1 == first of two message TBF assignment. + // downlink:1; 0 == RR, 1 == TBF. + // TD:1; 0 == no meaning ??, 1 == there is something in the rest octets + // for a TBF, it is the Packet Uplink Assignment. + uint channel_description:24; // for RR, ignored for TBF. + // GSM04.08sec10.5.2.25a + uint packet_channel_description:24; // for TBF + struct { + uint channel_type:5; // unused, must be 1; + uint TN:3; // Timeslot Number. + uint TSC:3; // Training Sequence Code; GSN.05.02 + uint union_encode_bits:2; // set to 0x00 to indicate no frequency hopping. + uint spare:1; // set to 0 + uint ARFCN:10; + }; + uint request_reference:24; // Identifies the MS that sent the RACH. + // use: L3RequestReference foo(RA,GSM::Time&when); + struct { + uint RA:8; // The 8-bit RACH message sent by the MS. + // The T? fields encode the FN module 42432 in which RACH was received. + // Note: FN modulo 42432 == (51X((T3-T2) mod 26) + T3 + 51*26*T1prime + uint T1prime:5; // FN (div 1326) mod 32; + uint T3highpart:3 // FN mod 51, in 6 bits; + uint T3lowpart:3; + uint T2:5; // FN mod 26. + }; + uint timing_advance:8; + mobile allocation:8; // encoded as Format LV? length 1-9 bytes. + // length indicator set to 0 unless there is frequency hopping. + starting_time:24; // optional, encoded as Format TV with IEI = 0x7c; + // Sec 9.1.18.2 says: starting_time ignored for TBF. + IA_rest_octets[] // For RR, may be Frequency Params + // For TBF, may be Packet Uplink Assignment, Packet Downlink Assignment, + // or Second Part Packet Assignment sec10.5.2.16 + // Packet Uplink Assignment Message rest_octets: + struct IA_Rest_Octets { // 10.5.2.16 + union1:2; // Must be HH for a TBF + uint type1; // 0 => Packet Uplink/downlink Assignment, + // 1 => Second Part Assignment. + uint type2: // 0 => packet_uplink_assignment, 1 => Packet_Downlink_Assignment, + + Packet_Uplink_Assignment { + uint union_altype:1: // 0 => single_block_allocation, 1 => Fixed or Dynamic Allocation; + // (But note: polling indicates a Fixed Allocation for just one block.) + if (union_altype == 1) { + uint TFI_Assignment:5; + uint Polling:1; // 1 => MS shall send a Packet Control Acknowledgement in the + // uplink block specified by TBF Starting TIme. + uint union_fixed_indicator:1; + if (union_fixed_indicator == 0) { + // USF stuff, we dont need yet. + } else { // union_fixed_indicator == 1 + Allocation_Bitmap_Length:5; + Allocation_Bitmap // variable sized. + union_p0_indicator:1; // 1 => P0 present; + if (union_p0_indicator == 1) { + P0:4; BTS_PWR_CTRL_MODE:1; PR_MODE:1; + } + } + Channel_Coding_Command:2; + TLLI_Block_Channel_Coding:1; + { 0 | 1 ALPHA:4; } + GAMMA:5 + { 0 | 1 Timing_Advance_Index:4} + { 0 | 1 TBF_Starting_time:16 } + } else { // union_altype == 0 + // Single Block Allocation + { 0 | 1 ALPHA:4; } + GAMMA:5 + uint Note1_bits:2; // Must be 0x1; + TBF_Starting_time:16; + {L | H + P0:4; BTS_PWR_CTRL_MODE:1; PR_MODE:1; + } + } + }; + }; +}; + +========================================================================= + +Mobile Originated Packet Transfer: + Described in GSM03.64sec6.6.4.7, and GSM44.018sec3. + Note: GSM44.018 talks about RR establishment, which is a voice call, in sec 3.2,3.3 and 3.4. + Voice mode is "idle mode" or "dedicated mode" + GPRS is in "packet idle mode" or "packet transfer mode". + Note that an MS in packet idle mode will receive pages on PACCH, but the Page may specify + establishment of a GSM RR connection, ie a voice call. + Section 3.5 (and only sec 3.5) talks about packet mode. + + PACCH = Packet Associated Control Channel. + + RACH -> Packet Channel Request (requests one block, may request one or two phase mode) + GSM04.18sec9.1.8 - Channel Request word encoding. + GSM04.18sec3.3.1.1.1 Channel request procedure. and GSM24.07? + This message contains only: + Establishment Cause (1 byte) + Random Reference (There is none in this case, + because Establishment Cause takes the whole byte.) + (Mobile may send M+1 of these) + Which phase will be requested documented in 44.018 3.5.2.1.2, but I dont think we care. + Establishment cause is one or two phase "packet access". + The request may specify: + - for user data, unacknowledged mode, MS requests single block, and attempts two-phase; + - for user data, acknowledged mode, MS requests either one-phase access + or a single block packet access. + - for page response, etc, MS requests a one phase access. + Note: Network may change one-phase request into single-block access, which forces + the MS to perform two-phase access [if it wants to send multiple packets]. + fy, for GSM: After sending M+1, MS starts T3126, and if no answer, gives up. + MS enters packet transfer mode, and listens to all of BCCH and CCCH in its timeslot. + + GPRS: After sending max number of Channel Request, MS starts T3146, + after which is sends a Random Access Failure and looks for another cell. + + Response may be any of: + AGCH <- IMMEDIATE ASSIGNMENT, which either contains Packet Uplink Assignment, + or bit in "Dedicated mode or TBF" element saying it is 2 part, + in which case the real IMMEDIATE ASSIGNMENT is sent within 2 multiframes. + The MS will then respond to IMMEDIATE ASSIGNMENT, (and then can try to do its + uplink using the packet channel.) + AGCH <- REJECT, which lets the MS look for another cell. + PAGCH <- Packet Queuing Notification. (optional, used if heavy traffic, + but only applicable on PCCCH, not CCCH.) + + Medium Access Modes: + Fixed, Dynamic (uses USF), Extended Dynamic, or Exclusive Allocation. + + For two-phase access, need an additional set of transfers before starting: + PACCH -> Packet Resource Request (optional, used for Fixed or Exclusive Allocation) + PACCH <- Packet Uplink Assignment (optional, used for Fixed or Exclusive Allocation) + For Fixed Allocation, Packet Uplink Assignment specfies start frame, slot, and + bitmap of blocks assigned to MS. + + For one-phase access: + I think one-phase access also works for a single unacknowledged uplink block? + For Dynamic Allocation, USF (Uplink State Flag) in download block tells MS + when they can use the upload blocks. + +Unacknowledged Mode Uplink Data Transfer: + Fixed Allocation Uplink 4.60sec8.1.1.3: + Initiation by MS sending PACKET RESOURCE REQUEST, using Mobile Originated blah above, + or during downlink transfer, can also send PACKET DOWNLINK ACK/NAC with + essentially the same info. + Closed-ended TBF: + PACKET RESOURCE REQUEST includes non-zero RLC_OCTET_COUNT, which is number + of bytes + RLC block length but less LLC boundaries, which the network must add in. + Network sends PACKET UPLINK ASSIGNMENT with enough blocks to handle it. + Premature end is possible by FINAL_ALLOCATION indication in a fixed allocation + assignment message. + Open-ended TBF: + At the beginning of the uplink allocation the MS may request to continue the TBF + by transmitting another PACKET RESOURCE REQUEST or PACKET DOWNLINK ACK/NACK, + with the number RLC data otets ready to transmit in the RLC_OCTET_COUNT. + [in the Channel Request Description IE.] + Alternatively, can request open-ended TBF with first PACKET RESOURCE REQUEST + RLC_OCTET_COUNT == 0. + Network sends another PACKET UPLINK ASSIGNMENT with a new fixed allocation, + or PACKET ACCESS REJECT. PACKET UPLINK ASSIGNMENT may also set the FINAL_ALLOCATION bit + to stop the open-ended transfer after this final assignment. + + PDTCH (assigned slot and block numbers) -> Data + ... + PACCH <- Packet Uplink Ack/Nack (includes the successfully received block mask, but it is not used except to determine channel quality.) + No Packet Control Acknowledgment is sent.. + +Notes Uplink Data Transfer, either acknolwedged or unacknowledged: + Network can send a downlink assignment or timeslot reconfig (needed for multislot) + while uplink is running. + If MS in half duplex mode it has to wait for uplink to end before + starting a downlink. + Note: Half Duplex Mode is a Medium Access Mode described in GSM04.60sec8.1.0, + specified in MAC_MODE parameter in downlink assignment, or + weirdly in uplink assignment. + + +Holding the line open: + By this I mean, keeping the MS listening to PDCH so you dont have to send + another control block on CCCh. + Downlink: you cant hold the TBF open, because the RLC Data Block has a TFI bit + that indicates the end of the transfer. I looked a little at sending 0 sized blocks, + but became confused. However, after downlink, the MS stays on PDCH + until T3192 expires, for both Acknowledged (sec 9.3.2.6) and Unacknowledged mode, (sec 9.3.3.5.) + (Note: T3191 is something to do with how long the MS has to respond + to the final block sent downlink.) + Accodring to GSM04.60 table 13.1, you can continue to send down the final block + of the transaction over and over to keep T3192 reset. + This would work for acknowledged mode, because the network is just pretending + it did not receive the final block, but I am not sure how this would work in + unacknowledged mode, where each block is only sent once. + + Uplink: See 04.60 sec 9.3.3.3. After the final uplink block, network sends + Packet Uplink Ack/Nack (even for unacknowledged mode) with "Final Ack" == 1 + and an RRBP field, which indicates an uplink slot the MS uses to send + a new PACKET RESOURCE REQUEST if it wants to send more, or PACKET CONTROL ACKNOLWEDGEMENT + and unless there is a downlink TBF in progress, immediately enters packet-idle mode. + If the Packet Control Acknowledgement is a RACH, there is also a way to encode + CTRL_ACK to specify the MS wants a new uplink TBF. The Packet Control Acknowledgement + documentation for CTRL_ACK is full of special cases because you would only use + that if it is a RACH, otherwise the MS would send a Packet Resource Request. + Options: + o Sec 7.1 lists all the ways an uplink TBF can be established, and there is no way + for the MS to do this during the T3192 period, when there is no TBF in either + direction but the MS is still waiting out T3192 for a new downlink TBF. + If you want to keep the MS camped on PDCH, they expect you to use PCCCH. + o The network could delay sending the final uplinkAck/Nack up to 5 sec, but that might + prevent the MS from starting a new uplink until then. + o The network could send an AckNack requesting retransmission just to keep the MS + on the line, but that risks having an outright failure reported by the MS to upper + layers if it cannot get the block through on the resend, and at the very least, + reporting incorrect QoS parameters. + o Near the end of T3192 (after downlink ends) send an unsolicited single-block uplink. + This is undocumented, so it might not work. + o This idea does not work (because MS must respond to an RRBP, which it will not get): + The network could send a new downlink message before T3192 expires, + and then just never send any downlink (let the timer expire) and send + Packet Polling Requests to the MS. The 51-multiframe is a 1/4 sec. + o Maybe network could send a dummy control block with an RRBP field. + + For Fixed uplink, the MS may request more uplink via: + bits in the "Packet Downlink Ack/Nack" block sent + upwards during an existing downlink, or in the first packet of an existing uplink. + +Notes: The "GPRS Cell Options" information element (GSM04.60 table 12.4.2) is +sent in PSI1, PSI13, PSI14 (PACCH only), and SI13. +Note that both PSI1 and PSI13 can be sent on PAACH as well as PCCCH. + +It specifies the values for T3168: range 0-0.5sec and T3192: range 0-1.5sec. + +The RRBP field in acknowledged download mode specifies yet another +single block packet the MS can send upwards, for Ack/Nack or any other purpose +the MS desires. + +The first block of a downlink must arrive within T3190, which is 5sec. +The MS will stay camped on the downlink until T3190 expires, which is 5sec. + + + + +Acknowledged Mode Uplink Data Transfer: + (Note: TBF survives until all blocks acknowledged.) + + Fixed Allocation Transfer: + PDTCH (assigned slot and block numbers) -> Data + ... + PACCH <- Packet Uplink Ack/Nack + (or unsolicited PACKET UPLINK ASSIGNMENT or TIMESLOT RECONFIG) + PACCH -> Packet Control Acknowledgment. + + Dynamic Allocation Transfer: + PDTCH -> Data + ... + PACCH <- Packet Uplink Ack/Nack + PDTCH -> Data (new data or possibly retries) + .. + PDTCH -> Last Data Block. + + PACCH <- Packet Uplink Assignment + PACCH -> Packet Control Acknowledgment. + +Downlink Transfer: + PACKET DOWNLINK ASSIGNMENT or TIMESLOT RECONFIG contains TBF start time. + If a second downlink assignment or timeslot reconfig arrives while downlink + in progress, MS waits until TBF start time, then switches to new one. + If the downlink assignment does not include a TBF start time, the MS reacts + within reaction time specified in GSM05.10. + If the gap between blocks addressed to itself ever exceeds T3190, MS aborts + as per sec 8.7.1 + Network sends PACKET TBF RELEASE to end downlink prematurely. + If no on-going uplink TBF, puts the MS in packet idle mode. + Note: It looks like you can keep the "line open" by letting + +Network Originated Downlink: GSM4.08 sec3.5.3 + CCCH <- IMMEDIATE ASSIGNMENT with a Packet Downlink Assignment, + IMMEDIATE ASSIGNMENT format in GSN04.18sec9.1.18 + Note: The network must use this only when MS is in packet-idle mode. + The IMMEDIATE ASSIGNMENT contains: + packet channel description, initial timing advance, and packet downlink assignment, + which contains: TLLI, TFI, RLC mode, power control, polling bit, + timing advance valid flag, + and optionally: TAI (timing advance index) and/or TBF start time. + optional: MS -> PDCH If polling bit is 1, MS waits for TBF start time and + sends PACKET CONTROL ACKNOWLEDGMENT first. + Note: in this case TBF start time applies both to download time and upload time. + Then network starts sending the packets. + Note: 4.08 sec 3.5.3.2 says you can also send a single RLC/MAC control message the same way, + but you dont have to specify TFI. + +Network Originated Page: + The GMM can use this to find out the Routing Area of the MS. + For GPRS see 44.018sec3.3.2.1.1 "Paging initiation using paging subchannel on CCCH" + PCH of CCCH <- Paging Request, using P-TMSI or IMSI. + CCCH Paging Request format in GSM04.18sec9.1.22 and following. + If IMSI, page request may be for RR (voice) or packet, depending on + "Packet Page Indication" field. + RACH -> Channel Request + + Mobile then does a Mobile Originated Packet Transfer, and sends a packet paging response + (GSM03.64) to the network, which is an LLC frame and we dont need to know anything + more about it, because it is interpreted inside the SGSN. + Note: The SGSN sets the mobility management mode to "Ready", but the MS is usually + still in packet-idle-mode until the SGSN initiates a downlink packet transfer. + + CCCH [AGCH?] <- Packet Uplink Assignment (or Immediate Assignment) + + For two-phase access: + PACCH -> Packet Resource Request (optional, used for Fixed or Exclusive Allocation) + PACCH <- Packed Uplink Assignment (optional, used for Fixed or Exclusive Allocation) + + PDTCH -> Packet Paging Response (LLC frame) + Now the MM (Mobility management) state of the MS is Ready. + + When MS is in Ready state: + If Packet uplink in progress: + PACCH <- Packet Downlink Assignment Message, specifies PDCH to be used. + else: + CCCH <- Immediate Assignment Message, specifies PDCH to be used. + -> Packet Control Acknowledgement (if requested by network, used for timing advance) + PDCH <- Data, indicated by TFI + All the modes same as for uplink. + + + +========================================================================= + +GSM MODE (old, I aborted this): + RACH -> Packet Channel Request (requests one block, may request one or two phase mode) + (Mobile may send M+1 of these) + Includes establishment cause which may be "answer to paging", or + "procedures that can be completed with SDCCH" + GPRS Channel Request Purposes are in 44.018 3.5.2.1.2 + GSM: After sending M+1, MS starts T3126, and if no answer, gives up. + GPRS: After sending max number of Channel Request, MS starts T3146, + after which is sends a Random Access Failure and looks for another cell. + + Response may be any of: + PAGCH <- Packet Queuing Notification. (optional, used if heavy traffic, + only applicable on PCCCH, not CCCH) + AGCH <- Packet Uplink Assignment??? + AGCH <- REJECT, which lets the MS look for another cell. + AGCH <- IMMEDIATE ASSIGNMENT, which either contains Packet Downlink Assignment, + or bit saying it is 2 part, in which case the real IMMEDIATE ASSIGNMENT + is sent within 2 multiframes. + The MS will then respond to IMMEDIATE ASSIGNMENT, and then can try to do its + uplink using the packet channel. + On CCCH, may send either IMMEDIATE ASSIGNMENT,must use Immediate Assignment. + Class A or B may receive <- Paging Request, which upon which the MS may abort the packet attempt. +Network Originated Transfer: + If MS is in Standby state: + PCH <- Paging Request + RACH -> Channel Request + Mobile then does a Mobile Originated Packet Transfer, and sends a Packet Paging Response + to the network, which is an LLC frame. + CCCH [AGCH?] <- Packet Uplink Assignment (or Immediate Assignment) + + For two-phase access: + PACCH -> Packet Resource Request (optional, used for Fixed or Exclusive Allocation) + PACCH <- Packed Uplink Assignment (optional, used for Fixed or Exclusive Allocation) + + PDTCH -> Packet Paging Response (LLC frame) + Now the MM (Mobility management) state of the MS is Ready. + + When MS is in Ready state: + If Packet uplink in progress: + PACCH <- Packet Downlink Assignment Message, specifies PDCH to be used. + else: + CCCH <- Immediate Assignment Message, specifies PDCH to be used. + -> Packet Control Acknowledgement (if requested by network, used for timing advance) + PDCH <- Data, indicated by TFI + All the modes same as for uplink. + + +============================================================================ +Radio stuff: +GSMConfig.cpp: +TCHFACCHLogicalChannel *GSMConfig::getTCH() +getChan(mTCHPool) + +L1Encoder, L2Decoder are ok +L1FEC is referenced by parent. diff --git a/GPRS/pinghttp.c b/GPRS/pinghttp.c new file mode 100644 index 00000000..73112412 --- /dev/null +++ b/GPRS/pinghttp.c @@ -0,0 +1,926 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//#include +//#include +//#include +//#include + +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include // pat added for make_sock + +#include // pat added for gethostbyname +#include // pat added for IPv4 iphdr +#include // pat added for tcphdr +#include // pat added for udphdr + +#include // pat added. +#include // pat added. +#include // pat added. +#include // pat added +#include // pat added +#include // pat added. +#include +#include +//#include "miniggsn.h" + +#ifndef MG_CON_DEFINED +// MiniGGSN IP connections. One of these structs for each PDP context. +// Each PDP context will be mapped to a new IP address. +// There should be a pointer to this struct in the sgsn_pdp_ctx, +// but I am trying to keep all changes local for now. +typedef struct mg_con_s { + struct sgsn_pdp_ctx *mg_pctx; // Points back to the PDP context using this connection. + uint32_t mg_ip; // The IP address used for this connection, in network order. + int inited; +} mg_con_t; + +#endif + +// From iputils.h: +char *ip_ntoa(int32_t ip, char *buf); +char *ip_sockaddr2a(void * /*struct sockaddr * */ sap,char *buf); +int ip_add_addr(char *ifname, int32_t ipaddr, int maskbits); +unsigned int ip_checksum(void *ptr, unsigned len, void *dummyhdr); +void ip_hdr_dump(unsigned char *packet, const char *msg); +int ip_tun_open(char *tname, char *addrstr); +void ip_init(); + + +// These options are used only for standalone testing. +struct options_s { + int bind; + int printall; // Print entire tcp response. + int raw_bind; + char *from; + int v; + int use_tunnel; + int repeat; +}; +struct options_s options; + +#if STANDALONE +char *tun_if_name = "mstun"; +int tun_fd = -1; +struct options_s options; +static char *mg_base_ip_str = "192.168.99.1"; // This did not raw bind. +static char *mg_base_ip_route = "192.168.99.0/24"; +#define MGERROR(...) {printf("error:"); printf(__VA_ARGS__);} +#define MGINFO(...) {printf(__VA_ARGS__);} +#else +char *miniggsn_rcv_npdu(struct osmo_fd *bfd, int *error, int *plen) { return 0; } +int miniggsn_snd_npdu(struct sgsn_pdp_ctx *pctx,unsigned char *npdu, unsigned len) { return 0; } +int miniggsn_snd_npdu_by_mgc(mg_con_t *mgp,char *npdu, unsigned len) { return 0; } +void miniggsn_delete_pdp_ctx(struct sgsn_pdp_ctx *pctx) {} +struct sgsn_pdp_ctx *miniggsn_create_pdp_conf(struct pdp_t *pdp,struct sgsn_pdp_ctx *pctx) { return 0;} +void miniggsn_init(); +#endif + +#if STANDALONE + +// ip address is in network order; return as dotted string. +char *ip_ntoa(int32_t ip, char *buf) +{ + static char sbuf[30]; + if (buf == NULL) { buf = sbuf;} + ip = ntohl(ip); + sprintf(buf,"%d.%d.%d.%d",(ip>>24)&0xff,(ip>>16)&0xff,(ip>>8)&0xff,ip&0xff); + return buf; +} +// IP standard checksum, see wikipedia "IPv4 Header" +// len is in bytes, will normally be 20 == sizeof(struct iphdr). +unsigned int ip_checksum(void *ptr, unsigned len, void *dummyhdr) +{ + //if (len != 20) printf("WARNING: unexpected header length in ip_checksum\n"); + uint32_t i, sum = 0; + uint16_t *pp = (uint16_t*)ptr; + while (len > 1) { sum += *pp++; len -= 2; } + if (len == 1) { + uint16_t foo = 0; + unsigned char *cp = (unsigned char*)&foo; + *cp = *(unsigned char *)pp; + sum += foo; + } + if (dummyhdr) { // For TCP and UDP the dummy header is 3 words = 6 shorts. + pp = (uint16_t*)dummyhdr; + for (i = 0; i < 6; i++) { sum += pp[i]; } + } + //printf("intermediate sum=0x%x\n",sum); + // Convert from 2s complement to 1s complement: + //sum = ((sum >> 16)&0xffff) + (sum & 0xffff); /* add hi 16 to low 16 */ + sum = ((sum >> 16)) + (sum & 0xffff); /* add hi 16 to low 16 */ + sum += (sum >> 16); /* add carry */ + //printf("intermediate sum16=0x%x result=0x%x\n",sum,~sum); + return 0xffff & ~sum; +} + +void ip_hdr_dump(unsigned char *packet, const char *msg) +{ + struct iptcp { // This is only accurate if no ip options specified. + struct iphdr ip; + struct tcphdr tcp; + }; + struct iptcp *pp = (struct iptcp*)packet; + char nbuf[100]; + printf("%s: ",msg); + printf("%d bytes protocol=%d saddr=%s daddr=%s version=%d ihl=%d tos=%d id=%d\n", + ntohs(pp->ip.tot_len),pp->ip.protocol,ip_ntoa(pp->ip.saddr,nbuf),ip_ntoa(pp->ip.daddr,NULL), + pp->ip.version,pp->ip.ihl,pp->ip.tos, ntohs(pp->ip.id)); + printf("\tcheck=%d computed=%d frag=%d ttl=%d\n", + pp->ip.check,ip_checksum(packet,sizeof(struct iphdr),NULL), + ntohs(pp->ip.frag_off),pp->ip.ttl); + printf("\ttcp SYN=%d ACK=%d FIN=%d RES=%d sport=%u dport=%u\n", + pp->tcp.syn,pp->tcp.ack,pp->tcp.fin,pp->tcp.rst, + ntohs(pp->tcp.source),ntohs(pp->tcp.dest)); + printf("\t\tseq=%u ackseq=%u window=%u check=%u\n", + ntohl(pp->tcp.seq),ntohl(pp->tcp.ack_seq),htons(pp->tcp.window),htons(pp->tcp.check)); +} +char *ip_sockaddr2a(void * /*struct sockaddr * */ sap,char *buf) +{ + static char sbuf[100]; + if (buf == NULL) { buf = sbuf;} + struct sockaddr_in *to = (struct sockaddr_in*)sap; + sprintf(buf,"proto=%d%s port=%d ip=%s", + ntohs(to->sin_family), + to->sin_family == AF_INET ? "(AF_INET)" : "", + ntohs(to->sin_port), + ip_ntoa(to->sin_addr.s_addr,NULL)); + return buf; +} +// Run the command. +// This is not ip specific, but used to call linux "ip" and "route" commands. +// If commands starts with '|', capture stdout and return the file descriptor to read it. +int runcmd(const char *path, ...) +{ + int pipefd[2]; + int dopipe = 0; + if (*path == '|') { + path++; + dopipe = 1; + if (pipe(pipefd) == -1) { + MGERROR("could not create pipe: %s",strerror(errno)); + return -1; + } + } + int pid = fork(); + if (pid == -1) { + MGERROR("could not fork: %s",strerror(errno)); + return -1; + } + if (pid) { // This is the parent; wait for child to exit, then return childs status. + int status; + if (dopipe) { + close(pipefd[1]); // Close unused write end of pipe. + } + waitpid(pid,&status,0); + return dopipe ? pipefd[0] : status; // Return read end of pipe, if piped. + } + // This is the child process. + if (dopipe) { + close(pipefd[0]); // Close unused read end of pipe. + dup2(pipefd[1],1); // Capture stdout to pipe. + close(pipefd[1]); // Close now redundant fd. + } + + // Gather args into argc,argv; + int argc = 0; char *argv[100]; + va_list ap; + va_start(ap, path); + do { + argv[argc] = va_arg(ap,char*); + } while (argv[argc++]); + argv[argc] = NULL; + va_end(ap); + + // Print them out. + // But dont print if piped, because it goes into the pipe! + if (! dopipe) { + char buf[208], *bp = buf, *ep = &buf[200]; + int i; + for (i = 0; argv[i]; i++) { + int len = strlen(argv[i]); + if (bp + len > ep) { strcpy(bp,"..."); break; } + strcpy(bp,argv[i]); + bp += len; + *bp++ = ' '; + *bp = 0; + } + buf[200] = 0; + MGINFO("%s",buf); + } + + // exec them. + execv(path,argv); + exit(2); // Just in case. + return 0; // This is never used. +} +// Return an array of ip addresses, terminated by a -1 address. +uint32_t *ip_findmyaddr() +{ +#define maxaddrs 5 + static uint32_t addrs[maxaddrs+1]; + int n = 0; + int fd = runcmd("|/bin/hostname","hostname","-I", NULL); + if (fd < 0) { + failed: + addrs[0] = (unsigned) -1; // converts to all 1s + return addrs; + } + //printf("ip_findmyaddr fd=%d\n",fd); + const int bufsize = 2000; + char buf[bufsize]; + int size = read(fd,buf,bufsize); + if (size < 0) { goto failed; } + buf[bufsize-1] = 0; + if (size < bufsize) { buf[size] = 0; } + char *cp = buf; + //printf("ip_findmyaddr buf=%s\n",buf); + while (n < maxaddrs) { + char *ep = strchr(cp,'\n'); + if (ep) *ep = 0; + if (strlen(cp)) { + uint32_t addr = inet_addr(cp); + //printf("ip_findmyaddr cp=%s = 0x%x\n",cp,addr); + if (addr != 0 && addr != (unsigned)-1) { + addrs[n++] = inet_addr(cp); + } + } + if (!ep) { + addrs[n] = (unsigned) -1; // terminate the list. + return addrs; + } + cp = ep+1; + } + addrs[maxaddrs] = (unsigned) -1; // converts to all 1s + return addrs; +} +int ip_tun_open(char *tname, char *addrstr) { assert(0); } +#endif + + +typedef struct { + int tot_len; + struct iphdr *ip; // Pointer to ip header in buf ( == buf). + struct tcphdr *tcp; // Pointer to tcp header in buf, if it is tcp. + struct udphdr *udp; // Pointer to udp header in buf, if it is udp. + char *payload; // Pointer to data in buf. + char storage[0]; // Storage for packet data, if this packet_t was malloced. +} packet_t; + +// Return -1 on error +static int ip_findaddr(char *spec, struct sockaddr *addr, char *hostname) +{ + struct sockaddr_in *to = (struct sockaddr_in*)addr; + memset(addr, 0, sizeof(struct sockaddr)); + to->sin_family = AF_INET; + if (inet_aton(spec,&to->sin_addr)) { + if (hostname) strcpy(hostname,spec); + } else { + //alternate: getaddrinfo() + struct hostent *hp = gethostbyname(spec); // func is deprecated. + if (hp) { + to->sin_family = hp->h_addrtype; + memcpy(&to->sin_addr,hp->h_addr, hp->h_length); + if (hostname) strcpy(hostname,hp->h_name); + } else { + return -1; // failure + } + } + return 0; // ok +} + +// Add an ip header to the packet and return in malloced memory. +// ipsrc,ipdst,srcport,dstport in network order. +// payload may be NULL to just send the header with flags. +packet_t * +ip_add_hdr2(int protocol, uint32_t ipsrc, uint32_t ipdst, + char *payload, struct tcphdr *tcpin, unsigned ipid) +{ + int hdrsize = sizeof(struct iphdr); + int paylen = payload ? strlen(payload) : 0; + struct tcphdr *tcp; + struct udphdr *udp; + switch (protocol) { + case IPPROTO_TCP: hdrsize += sizeof(struct tcphdr); break; + case IPPROTO_UDP: hdrsize += sizeof(struct udphdr); break; + default: break; // Assume must ip header. + } + struct { // dummy ip header, including just part of the IP header, added to tcp checksum. + uint32_t saddr, daddr; + uint8_t zeros; + uint8_t protocol; + uint16_t tcp_len; // This is in network order! + } tcpdummyhdr; + + packet_t *result = malloc(sizeof(packet_t) + hdrsize + paylen + 3); + struct iphdr *packet_header = (struct iphdr*)result->storage; + result->ip = packet_header; + result->tcp = (struct tcphdr*)((char*)result->storage + sizeof(struct iphdr)); + result->udp = (struct udphdr*)((char*)result->storage + sizeof(struct iphdr)); + result->payload = result->storage + hdrsize; + result->tot_len = hdrsize + paylen; + //while (result->tot_len & 0x3) { result->storage[result->tot_len++] = 0; } + memset(packet_header,0,hdrsize); + if (payload) memcpy(result->payload,payload,paylen); + + packet_header->ihl = 5; + packet_header->version = 4; + // tos, id, frag_off == 0 + packet_header->ttl = 64; + packet_header->id = ipid; + packet_header->protocol = protocol; + packet_header->saddr = ipsrc; + packet_header->daddr = ipdst; + packet_header->tot_len = htons(result->tot_len); + packet_header->check = ip_checksum(packet_header,sizeof(*packet_header),NULL); + // Double check: + /* + if (0 != ip_checksum(packet_header,sizeof(*packet_header),NULL)) { + printf("WARNING: computed checksum invalid: check=%d computed=%d\n", + packet_header->check,ip_checksum(packet_header,sizeof(*packet_header),NULL)); + } + */ + + switch (protocol) { + case IPPROTO_UDP: + assert(0); // unimplemented. + udp = result->udp; + udp->len = htons(paylen + sizeof(*udp)); + //udp->source = srcport; + //udp->dest = dstport; + // Checksum includes udp header plus IP pseudo-header! + // But checksum is optional, so just set to 0. + udp->check = 0; + break; + case IPPROTO_TCP: + tcp = result->tcp; + memcpy(tcp,tcpin,sizeof(struct tcphdr)); + tcp->doff = 5; // tcp header size in words. + tcp->window = htons(0x1111); + // create the dummy header. + tcpdummyhdr.saddr = packet_header->saddr; + tcpdummyhdr.daddr = packet_header->daddr; + tcpdummyhdr.zeros = 0; + tcpdummyhdr.protocol = protocol; + // This length excludes the length of the ip header. + unsigned tcplen = result->tot_len - sizeof(struct iphdr); + tcpdummyhdr.tcp_len = htons(tcplen); + tcp->check = ip_checksum(tcp,tcplen,&tcpdummyhdr); + break; + // successful options were: + } + + + return result; +} + +static void pinghttpPrintReply(char *result,int len,char*hostname,int recvcnt) +{ + if (len < 0) { printf("unexpected result len: %d\n",len); return; } + int plen = len; // how much to print + if (0 == recvcnt) { + printf("pinghttp: %s replies %d bytes:\n--->",hostname,len); + if (!options.printall) { + // Prune the response a little. + // Just print the first line. + char *c1 = memchr(result,'\n',len); + if (c1 && c1-result < len) plen = c1-result; + printf("%.*s\n",plen,result); + } + } + if (options.printall) { + printf("%.*s\n",plen,result); + } +} + +int pinghttpsend(packet_t *packet,mg_con_t *mgp,int sfd,struct sockaddr *toaddr) +{ + int rc; + usleep(100000); + if (options.v) ip_hdr_dump(packet->storage,"sending:"); + //struct iphdr *pip = (struct iphdr*)packet->s; + //printf("packet->len = %d tot_len=%d\n",packet->tot_len,ntohs(pip->tot_len)); + if (options.use_tunnel) { + rc = write(tun_fd,packet->storage,packet->tot_len); + } else if (mgp) { +#ifndef STANDALONE + rc = miniggsn_snd_npdu_by_mgc(mgp,packet->storage,packet->tot_len); +#endif + } else { + // Note: when I left the non-raw socket connected above and fell through + // to this code, it looked like the raw socket may not receive on this port until + // the connected socket is closed, not sure. + rc = sendto(sfd,packet->storage,packet->tot_len,0,toaddr,sizeof(struct sockaddr)); + } + if (rc < 0) { + printf("sendto failed: %s\n",strerror(errno)); + return 2; + } + return 0; +} + + + +// Ping using http port 80, see if anything is there. +// It is not a real 'ping', which uses ICMP protocol. +// This implements a subset of the TCP handshaking protocol. +// We assume there is only data packet (the httpget message) and we assume +// all packets are delivered, ie, no retries. +// It runs the protocol all the way to the final closure (FIN) acknowledgements, +// to make sure the connection is completely closed. Otherwise you can only +// run one of these tests to any host until that host times our connection out. +int pinghttp(char *whoto,char *whofrom,mg_con_t *mgp) +{ + struct sockaddr toaddr; + struct sockaddr_in *to = (struct sockaddr_in*) &toaddr; + uint16_t dstport = htons(80); // Connect to HTTP port 80. + char hostname[300]; + int one = 1; + //char nbuf[100]; + const int rbufsize = 1500; + char rbuf[rbufsize+1]; + int sfd = -1; // socket fd + int raw_mode = mgp || options.use_tunnel || whofrom; + int rc; + + if (ip_findaddr(whoto,&toaddr,hostname)) { + printf("pinghttp: unknown to host %s\n", whoto); + return 2; + } + to->sin_port = dstport; + if (toaddr.sa_family == AF_INET) { + printf("pinghost %s AF_INET addr %s\n",hostname,inet_ntoa(to->sin_addr)); + } + + char httpget[300]; + //sprintf(httpget, "GET /index.html HTTP/1.1\r\nHost: %s\r\n\r\n",hostname); + sprintf(httpget, "GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n"); + + //sprintf(httpget, "GET /index.html HTTP/1.1\r\nHost: %s\r\n\r\n\r\n",inet_ntoa(to->sin_addr)); + //sprintf(httpget, "GET /index.html\r\n\r\n"); + //sprintf(httpget, "GET /index.html HTTP/1.0\r\n\r\n\r\n"); + + if (! raw_mode) { + // Simple pinghttp version uses a regular socket and connect. + // The kernel handles IP headers and TCP handshakes. + // Send the httpget message, print any reply. + sfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sfd < 0) { printf("pinghttp: socket() failed: %s\n",strerror(errno)); return 2; } + + if (connect(sfd,&toaddr,sizeof(struct sockaddr))) { + close(sfd); + printf("pinghttp: connect() to %s failed: %s\n",hostname,strerror(errno)); + return 2; + } + + if (0) { + // Out of interest, get the srcport assigned by connect above. + struct sockaddr sockname; socklen_t socknamelen = sizeof(struct sockaddr); + getsockname(sfd,&sockname,&socknamelen); + // Add one to the port, just hope it is free. + //srcport = htons(ntohs(((struct sockaddr_in*)&sockname)->sin_port) + 10); + } + + if (options.v) printf("pinghttp to %s send %s\n",hostname,httpget); + rc = send(sfd,httpget,strlen(httpget),0); + if (rc < 0) { printf("pinghttp: send failed: %s\n",strerror(errno)); return 2; } + rc = recv(sfd,rbuf,rbufsize,0); + if (rc <= 0 || rc > rbufsize) { + printf("pinghttp: recv failed rc=%d error:%s\n",rc,strerror(errno)); return 2; + } + pinghttpPrintReply(rbuf,rc,hostname,0); + return 0; + } + + // We are going to use raw mode. + // We will add the IP headers to the packet, and handle TCP handshake ourselves. + + uint32_t dstip = to->sin_addr.s_addr; + uint32_t srcip = 0; + packet_t reply; + uint16_t srcport = htons(4000 + (time(NULL) & 0xfff)); //made up + + if (whofrom) { + //srcip = inet_addr(whofrom); // only allows IP numbers. + //if (srcip == INADDR_NONE) { + // printf("pinghttp: Cannot find specified from address: %s\n",whofrom); + // return 2; + //} + struct sockaddr fromaddr; + char fromhostname[300]; + if (ip_findaddr(whofrom,&fromaddr,fromhostname)) { + printf("pinghttp: unknown from host %s\n", whofrom); + return 2; + } + srcip = ((struct sockaddr_in*)&fromaddr)->sin_addr.s_addr; // already swizzled. + } else if (mgp) { + srcip = mgp->mg_ip; + } else { + assert(0); + } + + + + if (whofrom && !(mgp || options.use_tunnel)) { + // open raw socket + // If it going through nat, we should be able to pick nearly any port, + // and the nat will change it if it conflicts. + // NOTE: When you use RAW sockets, the kernel tcp driver will become confused + // by this tcp traffic and insert packets into the stream to goof it up. + // To prevent that use: + // iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP + // + sfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); + if (sfd < 0) { printf("cannot open raw socket\n"); return 2;} + setsockopt(sfd,SOL_SOCKET,SO_REUSEADDR,&one,sizeof(one)); + setsockopt(sfd,IPPROTO_IP,IP_HDRINCL,&one,sizeof(one)); + + // Binding is unnecessary to simply read/write the socket. + if (options.bind) { + struct sockaddr_in bindto; + memset(&bindto,0,sizeof(bindto)); + bindto.sin_family = AF_INET; + bindto.sin_port = 0; + bindto.sin_addr.s_addr = srcip; + if (bind(sfd,(struct sockaddr*)&bindto,sizeof(bindto))) { + printf("bindto %s failed\n",ip_sockaddr2a(&bindto,NULL)); + } + + // Try using bindtodevice on the receive rfd. +#if 0 + char *dummy = "foo"; + rfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); + if (rfd < 0) { printf("cannot open raw socket\n"); return 2;} + setsockopt(rfd,SOL_SOCKET,SO_REUSEADDR,&one,sizeof(one)); + setsockopt(rfd,IPPROTO_IP,IP_HDRINCL,&one,sizeof(one)); + if (setsockopt(rfd,SOL_SOCKET,SO_BINDTODEVICE,dummy,strlen(dummy)+1)) { + printf("BINDTODEVICE failed\n"); + } +#endif + } + } + int rfd = sfd; + + // seqloc is our sequence number. seqrem is the servers sequence number. + //unsigned seqloc = 2580167164u; // got from a successful TCP session. + //unsigned seqloc = 1580160000u + (rand() & 0xffff); + unsigned startloc = time(NULL)*9;// + (rand() & 0xffff); + unsigned seqloc = startloc; + //unsigned seqloc_syn = 0; // seqloc of SYN sent. + unsigned seqloc_fin = 0; // seqloc of FIN sent. + unsigned seqrem = 0; + unsigned seg_ack = 0; + int sendid = 1; + unsigned ipid = 0xfff & (time(NULL) * 9); + struct tcphdr tcpb; + + // States from RFC 793 page 22 + enum { + tcp_start, + tcp_syn_sent, + tcp_syn_received, + tcp_established, + tcp_fin_wait1, + tcp_fin_wait2, + tcp_close_wait, + tcp_closing, + tcp_last_ack, + tcp_time_wait, + tcp_closed, + tcp_data_finished, // State added to initiate close connect on our side. + } state = tcp_start; + char *state_name[20]; + state_name[tcp_start] = "tcp_start"; + state_name[tcp_syn_sent] = "tcp_syn_sent"; + state_name[tcp_syn_received] = "tcp_syn_received"; + state_name[tcp_established] = "tcp_established"; + state_name[tcp_fin_wait1] = "tcp_fin_wait1"; + state_name[tcp_fin_wait2] = "tcp_fin_wait2"; + state_name[tcp_close_wait] = "tcp_close_wait"; + state_name[tcp_closing] = "tcp_closing"; + state_name[tcp_last_ack] = "tcp_last_ack"; + state_name[tcp_time_wait] = "tcp_time_wait"; + state_name[tcp_closed] = "tcp_closed"; + state_name[tcp_data_finished] = "tcp_data_finished"; + + reply.tcp = 0; // unnecessary, makes gcc happy. + + enum { SYN = 1, FIN = 2 }; + + int tcpsend(int flags, char *payload) { + memset(&tcpb,0,sizeof(tcpb)); + tcpb.source = srcport; + tcpb.dest = dstport; + tcpb.seq = htonl(seqloc); + tcpb.ack_seq = htonl(seqrem); // probably not right. + + int next_seqloc = seqloc; + packet_t *packet; + if (flags & SYN) { + tcpb.syn = 1; + next_seqloc++; + //seqloc_syn = next_seqloc; + } else { + tcpb.ack = 1; + } + if (flags & FIN) { + tcpb.fin = 1; + next_seqloc++; + seqloc_fin = next_seqloc; + } + if (payload) { + tcpb.psh = 1; + next_seqloc += strlen(payload); + // the packet id for you. How kind of it. + if (options.use_tunnel && !sendid) { + sendid = 1; + ipid = (time(NULL) & 0xfff) + (rand() & 0xfff); + } + } + packet = ip_add_hdr2(IPPROTO_TCP, srcip, dstip, payload,&tcpb,htons(ipid)); + if (pinghttpsend(packet,mgp,sfd,&toaddr)) return 1; + if (sendid) { ipid++; } + seqloc = next_seqloc; + return 0; + } // tcpsend + + + int tries = 0; + int recvcnt = 0; + while (tries++ < 20) { + int prevstate = state; + + switch (state) { + case tcp_start: + state = tcp_syn_sent; + tcpsend(SYN,NULL); + break; + case tcp_syn_sent: + if (reply.tcp->syn) { + // OK, now gotta send back an ack without data first. + // We dont inc seqrem here - that is now done after reply below. + // seqrem++; // First byte is number 1, not number 0. + if (reply.tcp->ack) { + // If you dont send this first naked ACK, the host responds with another SYN. + tcpsend(0,NULL); + // If you dont send the data now, host times out waiting for something to do. + state = tcp_established; + goto send_data; + } else { + // We have to wait for an ACK. + state = tcp_syn_received; + tcpsend(0,NULL); + } + } else { + // We were waiting for a SYN but did not get it. Oops. + printf("in tcp state syn_sent expecting SYN but did not get it?\n"); + // This happens if a previous connection is still open. + // go listen some more + } + break; + case tcp_syn_received: + if (reply.tcp->ack) { + //seqrem++; // First byte is number 1, not number 0. + state = tcp_established; + tcpsend(0,httpget); + } else { + printf("int tcp state syn_received expecting ACK but did not get it?\n"); + // go listen some more + } + break; + case tcp_established: + if (reply.tcp->fin) { + state = tcp_close_wait; + tcpsend(FIN,NULL); + if (seg_ack <= startloc+1) { + printf("remote connection closed before we could send data\n"); + } + } else if (seg_ack <= startloc+1) { + send_data: + tcpsend(0,httpget); // Now send or resend the data; + } else { + // We only have one packet to send, so if we got it then we are done. + // Advance seqloc + //seqloc = seg_ack; moved to after reply. + state = tcp_fin_wait1; + tcpsend(FIN,NULL); + } + break; + case tcp_data_finished: + state = tcp_fin_wait1; + tcpsend(FIN,NULL); + break; + case tcp_fin_wait1: + // We sent a FIN, now we have to wait for a FIN or ACK back. + if (reply.tcp->fin) { + state = tcp_closing; + tcpsend(0,NULL); + } else { + assert(seqloc_fin); + if (seg_ack > seqloc_fin) { + state = tcp_fin_wait2; + } + } + //printf("fin_wait1 end=%d next=%s\n",seg_ack>=seqloc_fin, state_name[state]); + break; + case tcp_fin_wait2: + // We are waiting for a FIN. + if (reply.tcp->fin) { + state = tcp_time_wait; + } else { + printf("tcp in fin_wait2 expected FIN but did not get it\n"); + } + tcpsend(0,NULL); + break; + case tcp_close_wait: + state = tcp_last_ack; + tcpsend(FIN,NULL); + break; + case tcp_last_ack: + if (reply.tcp->ack) { + state = tcp_closed; + } + break; + case tcp_closing: + // Waiting for ACK of FIN, but who cares? We are done. + if (reply.tcp->ack) { + state = tcp_time_wait; + } + state = tcp_time_wait; + tcpsend(0,NULL); + break; + default: + assert(0); // unhandled state + } + + if (state == tcp_time_wait || state == tcp_closed || state == tcp_closing) break; + + wait_again: + // Wait for a reply... + if (options.use_tunnel) { + // The read only returns what is available, not full rc. + rc = read(tun_fd,rbuf,rbufsize); + } else if (mgp) { +#ifndef STANDALONE + char *msg = miniggsn_rcv_npdu(&mgp->mg_tcpfd, &error, &plen); +#endif + rc = -1; + //if (msg == NULL) { rc = -1; } + } else { + struct sockaddr recvaddr; socklen_t recvaddrlen = sizeof(struct sockaddr); + // Note: For a connected socket, the recvaddr is given garbage. + rc = recvfrom(rfd,rbuf,rbufsize,0,&recvaddr,&recvaddrlen); + printf("Reply %d bytes from socket: %s\n",rc,ip_sockaddr2a(&recvaddr,NULL)); + } + + if (rc < 0) { printf("pinghttp: recv error: %s\n",strerror(errno)); return 2; } + if (rc == 0) { + printf("received 0 length packet - means shutdown\n"); + break; + } + + // Reply received. + // Both headers are variable size, ipdr size in ->ihl, tcp in ->doff. + reply.ip = (struct iphdr*)rbuf; + reply.tcp = (struct tcphdr*) (rbuf + 4 * reply.ip->ihl); + int hdrsize = 4*reply.ip->ihl + 4*reply.tcp->doff; + if (rc < hdrsize) { + printf("bytes received %d less than header size %d?\n",rc,hdrsize); + continue; + } + if (reply.tcp->dest != srcport) { + printf("Message to port %d (not port %d) ignored\n",ntohs(reply.tcp->dest),ntohs(srcport)); + goto wait_again; + } + + // Process the reply: + + reply.payload = rbuf + hdrsize; + int payloadlen = rc - hdrsize; + int seg_len = payloadlen + (reply.tcp->syn ? 1 : 0) + (reply.tcp->fin ? 1 : 0); + seqrem = ntohl(reply.tcp->seq) + seg_len; + if (reply.tcp->ack) { + seg_ack = ntohl(reply.tcp->ack_seq); + seqloc = seg_ack; + } + if (options.v) printf("state=%s received %d bytes %s%s next state=%s\n", + state_name[prevstate],rc-hdrsize, + reply.tcp->syn?"SYN":"", reply.tcp->fin?" FIN":"", + state_name[state]); + if (options.v) ip_hdr_dump(rbuf,"received:"); + + if (payloadlen) { + pinghttpPrintReply(reply.payload,payloadlen,hostname,recvcnt++); + } + } // whileloop + if (sfd >= 0) close(sfd); + return 0; +} + +#if STANDALONE +struct options_s options = {0}; + +int main(int argc, char *argv[]) +{ + int xflag = 0; // Extended test. + //options.from = "192.168.99.2"; + uint32_t *addrs = ip_findmyaddr(); + char mebuf[30]; + options.from = ip_ntoa(addrs[0],mebuf); // Hope this is the right one. + +#if STANDALONE +#else + ip_init(); +#endif + //miniggsn_init(); + next_option: + argc--; argv++; + while (argc && argv[0][0] == '-') { + if (0==strcmp(*argv,"-v")) { options.v=1; goto next_option; } + if (0==strcmp(*argv,"-x")) { xflag=1; goto next_option; } + if (0==strcmp(*argv,"-r")) { options.repeat=1; goto next_option; } + if (0==strcmp(*argv,"-b")) { options.bind=1; goto next_option; } + if (0==strcmp(*argv,"-a")) { options.printall=1; goto next_option; } + if (0==strcmp(*argv,"-t")) { options.use_tunnel=1; goto next_option; } + if (0==strcmp(*argv,"-T")) { options.use_tunnel=1; argc--;argv++; + if (argc <= 0) { printf("expected arg to -t"); return 2; } + mg_base_ip_route = *argv; + goto next_option; + } + if (0==strcmp(*argv,"-f")) { + argc--; argv++; + if (argc <= 0) { printf("expected arg to -f"); return 2; } + options.from = *argv; + goto next_option; + } + printf("Unrecognized option: %s\n",*argv); exit(2); + } + if (argc <= 0) { + printf("syntax: pinghttp [-v] [-a] [-t] [-x] [-T tun_addr] [-f from_ip] hostname\n"); + printf(" -v: verbose dump of http traffic in and out\n"); + printf(" -a: print entire http response\n"); + printf(" -t: use a tunnel (instead of a raw socket)\n"); + printf(" -x: emulate miniggsn; only one of -t or -x may be specified\n"); + printf(" -T : set the tunnel address, default -T %s\n",mg_base_ip_route); + printf(" -f : set the from address, default -f %s\n",mg_base_ip_str); + printf(" Note: if specified the -f address should be in address range specified by -T,\n"); + printf(" for example: 192.168.99.2\n"); + printf(" hostname: call the http server at this host. Dont use google.com\n"); + exit(2); + } + + if (geteuid() != 0) { + printf("Warning: you should be root to run this!\n"); fflush(stdout); + } + + if (options.use_tunnel) { + if (!options.from) { printf("need -f addr\n"); exit(2); } + tun_fd = ip_tun_open(tun_if_name,mg_base_ip_route); + if (tun_fd < 0) { + printf("Could not open %s\n",tun_if_name); + exit(2); + } + } + + int stat = 0; + if (xflag) { + // Emulate some MS allocating some IP addresses. + mg_con_t cons[4]; // Up to four simulated outgoing phone connections. + int32_t base_ip = inet_addr(mg_base_ip_str); + if (base_ip == INADDR_NONE) { + printf("miniggsn: Cannot grok specified base ip address: %s\n",mg_base_ip_str); + exit(2); + } + printf("base_ip=0x%x=%s\n",base_ip,ip_ntoa(base_ip,NULL)); + base_ip = ntohl(base_ip); + + int i; + for (i=0; i < 4; i++) { + memset(&cons[i],0,sizeof(mg_con_t)); + cons[i].mg_ip = htonl(base_ip + 1 + i); + } + // Try sending a raw ping packet. + stat = pinghttp(argv[0],NULL,&cons[0]); + } else { + stat = pinghttp(argv[0],options.from,0); + } + if (stat == 0) { printf("http ping %s success\n",argv[0]); } + return stat; +} +#endif diff --git a/GPRS/todo.txt b/GPRS/todo.txt new file mode 100644 index 00000000..394bd5be --- /dev/null +++ b/GPRS/todo.txt @@ -0,0 +1,356 @@ +Memory Leak: +With GPRS running continuously: + Startup, around 7:46:50 + 4 0 30359 30357 20 0 40388 4396 wait_f Sl+ pts/0 0:01 /home/pat/RangeNetworks/src/range/software/private/openbts/trunk/apps/OpenBTSGprs + + Tue Aug 14 09:01:29 PDT 2012: + 4 0 24590 10656 20 0 46276 9796 wait_f Sl+ pts/0 16:08 /home/pat/RangeNetworks/src/range/software/private/openbts/trunk/apps/OpenBTSGprs + + Tue Aug 14 09:01:40 PDT 2012: + 4 0 24590 10656 20 0 46276 9796 wait_f Sl+ pts/0 16:13 /home/pat/RangeNetworks/src/range/software/private/openbts/trunk/apps/OpenBTSGprs + + Tue Aug 14 11:49:04 PDT 2012: + 4 0 24590 10656 20 0 56260 20064 wait_f Sl+ pts/0 53:05 /home/pat/RangeNetworks/src/range/software/private/openbts/trunk/apps/OpenBTSGprs +168 minutes, 9984 KB change = 1K/sec + +Without GPRS running: + Tue Aug 14 13:47:14 PDT 2012 + 4 0 30359 30357 20 0 40388 4396 wait_f Sl+ pts/0 0:17 /home/pat/RangeNetworks/src/range/software/private/openbts/trunk/apps/OpenBTSGprs + Tue Aug 14 16:26:28 PDT 2012 + 4 0 30359 30357 20 0 41412 4400 wait_f Sl+ pts/0 4:45 /home/pat/RangeNetworks/src/range/software/private/openb + +o IPHONE: + Screws up royally. + Unlike all the other phones, when you power cycle the bts or the iphone, it does not do a DCCHController registration, + but it will register for gprs. + This is with GPRS.Timer.T3212=0 + Even with GPRS.Enable=0, it will usually refuse to register. + I tried changing the LAC code, still did not register, power cycled the phone, it finally did a DCCH registration. + Next I set GPRS.Enable=1 and T3212=60, phone never ever registered, even after power cycling. What gives? + Tried changing the LAC code and rebooting BTS again... manual selection still does not work. + Power cycled the phone... Finally, it registered DCCH and then GPRS. Worked fine. + Power cycled the bts, it got gprs service without registering on DCCH. + +o Add reporting statistic: IPs in use, available, timingout. +o Someone sometimes uses the TFI in the global tfi after the TBF has ended. +o Try changing the decay power params. + +o Add more statistics: + number of failed/successful tbfs. Use a better word than 'failed'. + number of unanswered/answered RRBPs. + total tbf connect time, as well as bytes up/down. + +o The FIXME in mtSetState regarding removing unneeded USF reservations. +o The SGSN.Timer.RAUpdate is a GprsTimer - the IE description (24.008 10.5.7.3) says there + is a special 'units' value to deactivate the timer - implement it. test it. + update: setting the RAUpdate timer to 0 worked for the Multitech modem - disabled RAUpdate. +o The timeslot reconfig message does not work, so I took it out. +o The Blackberry sends packets with a return address that does not match its assignment, + and we dont catch that error. +o The 3101 failure is sometimes due to a GPRS suspension request. +o Blackberry does not like RAupdate, maybe tmsi status is incorrect. +Received RAUpdateComplete MS#3,TLLI=c0087003,8008e001 imsi=310260520943554 + 08:31:26.6:SGSN:Received GMMStatus: 98=Message_type_not_compatible_with_the_protocol_state MS#3,TLLI=c0087003,8008e001 imsi=310260520943554 + + +o GPRS Resumption IE must be included in the GSM CHANNEL RELEASE message, whereever that is. + 44.018 3.4.13.1.1, IE described 10.5.2.14c + GPRS Suspend is in 44.018 9.1.13b + Update: It is not critical because the MS is supposed to restart GPRS on its own initiative by sending + an RAUpdate. + +o Since the IP addresses are semi-static, we may want a 'new' flag to the shell script +to indicate if the PdpAllocate is a duplicate. + +o Why am I getting this from Sgsn.cpp, after enabling TLLI reassignment: + LOG(WARNING) << "no corresponding MS for TLLI " << mMsHandle; + +o macAddChannel/macFreeChannel are no longer useful. + +o If an uplink fails (eg cause=3101) the downlink will probably fail soon. + Should wind it down so we dont lose so many TBFs when the downlink is cancelled. + +o If the MS becomes non-responsive (eg, misses RRBP) stop sending downlink blocks ahead + until we hear from it. This would avoid losing so many TBFs if it gets cancelled. + Currently I think we just keep sending ahead until we stall. +o If there are communication problems, try sending RLC blocks with an RRBP in CS-1. + +o 6-19-2012, from talk with David: + - Someday we may want to set GPRS priorities in the HLR so some phones + have better GPRS service than others, but probably not limit the bandwidth + since if it is available and no one has higher priority, just use it. + - Allocate the first GPRS channel for unregistered phones in ARFCN 0. + - Allocate other gprs channels dynamically from the end, even + if they end up in ARFCN 1. + - He also suggested the channel allocator should walk through and look + for sets of adjacent channels first. +I think fixed: o Looks like multislot 3-down/1-up fails. + +o Use a single PACCH for all unregistered TLLIs. + +o Get rid of engineWriteHighSide() + +o Extended downlink TBF - 44.60 9.3.1a: Send an LLC UI Dummy command to keep the line open. +That is because there is no way to specify that the entire RLC block is filler +(unlike UMTS where they fixed this); the start is assumed to be the first TBF. +Naturally, no 'dummy command' is defined in the LLC spec, but I think they maybe +they mean a NULL UI frame. + +o Implement DRX mode paging. + +== STATE MACHINE PROBLEMS == + o For uplink TBF, the modem sends the first uplink data blocks + before it sends the RRBP reply; tbf is still in state DataWaiting1, + make sure this works, and also it should automatically move + the tbf to DataTransmit. + +o Need high and low priority engine service routines, so if there is nothing else + happening on the radio link, can resend old blocks again, as well as blocks + that have not been positively acked. + +o Multitech modem does not like a detachRequest: + 58.5:SGSN:Received ActivatePDPContextRequest TransactionId=0 mNSapi=5 mLlcSapi=3 mRequestType=0 mPdpAddress=ByteVector(size=2 data: 01 21) mQoS=ByteVector(size=3 data: 00 00 00) mApName=ByteVector(size=9 data: 08 69 6e 74 65 72 6e 65 74) mPco=ByteVector(size=20 data: 80 80 21 10 01 08 00 10 81 06 00 00 00 00 83 06 00 00 00 00) MS#1,TLLI=c00ef001 + 58.5:SGSN:Sending ActivatePDPContextReject TransactionId=0 mCause=101=Message_not_compatible_with_the_protocol_state MS#1,TLLI=c00ef001 frame=ByteVector(size=3 data: 8a 43 65) + 58.5:SGSN:Sending GMMStatus mCause=10=Implicitly_detached MS#1,TLLI=c00ef001 frame=ByteVector(size=3 data: 08 20 0a) + 58.5:SGSN:Sending DetachRequest mDetachType=1 mForceToStandby=0 mGmmCause=10=Implicitly_detached MS#1,TLLI=c00ef001 frame=ByteVector(size=6 data: 08 05 01 25 00 0a) + 59.9:SGSN:Received GMMStatus: 96=Invalid_mandatory_information MS#1,TLLI=c00ef001 + +o Check for thread problems in SGSN. Maybe getMS calls need to be changed + to something that return a result instead of a pointer to an MSInfo that + might disappear. + +o If the BSN runs backwards in an uplink, it is time to send an uplinkacknack. + +Tickets: +== RESERVED TFI BUG == + o BUG: If you send an assignment on CCCH, you cannot reuse an existing TFI + for a while. 44.060 9.3.2.6 + I worked around by round-robin allocating TFIs and it worked great. + o Implement T3191; part of this. + +== AGCH QUEUE REWORK == + This encompasses ticket 69: Implement proper paging groups. + o Either add ability to cancel messages, or add a call-back to create messages on demand. + o Get rid of the extraneous AGCH delay. + o Neither the returned AGCH, nor the getNextMsgSendTime(), are monotonically increasing. + Example: A downlink assignment is sent on CCCH, but a subsequent rach causes + a new single-block uplink assignment at an earlier time. The MS is now sitting on PACCH, + and does not respond to the downlink assignment on CCCH, which is then repeated on PACCH. + However, this downlink TBF fails: the blackberry will send the control ack for + the downlink assignment but then does not respond subsequently. + o DRX mode workaround is really hacked in TBF.cpp. Instead of sending CCCH in the + correct paging group, if the MS stops responding I send the same message + on all 6 (count-em) CCCH paging channels. + o Get the DRX param from the Attach Request message - it has a non-drx period timer + after transfer state. 25.008 10.5.5.6, but DRX mode described GSM05.02 and GSM3.64 sec 6.5.10. + +== RA-Update problems == + o user data transmission is suspended during RAupdate, + so try setting the RAupdate timers to infinity. + - suspended at what level? The message goes to L3, so do the TBFs really get suspended? + o Why is Blackberry sending new raupdate-requests about 100 secs in? + +o During a downlink TBF, make sure we send updated timing advance to the MS - in what message? + +o Handle GPRS suspend, look at MobilityManagement.cpp:IMSIDetachController + +o They are changing the network color codes on the fly now, so make sure + they get that integrated into GPRS after merging gprs. + +o When you change the GSM beacon (like turning gprs on/off) we may need to set a + classmark somewhere so phones update. + +o I saw it send an l3immediateassignment while there was an uplink tbf in progress! + When? Is this still possible? + Yes. The messages cross in transit. + +== TESTING == +o Test nokia gprs attach with thai sim card. - dump is in RangeNetworks/ticket_ms_does_not_work +o Test nokia gprs attach with range sim card - did not work at all with old SGSN. +o Retest Samsung MS with correct OpenRegistration in sql and grab OpenBTS log. + +o Test the transceiver code from the Handover features branch, which + has the GPRS filler patch that David integrated, and also has some kind + of bug to log a problem where there are multiple transactions with the same + timestamp and ARFCN. This could conceivably be one of the + bugs that I am seeing? + +== ENHANCEMENTS == + +o Get rid of the extra polls in the state machines. +o RLC unacknowledged mode. +o Do CS-4 to CS-1 fallback more intelligently. + o After retry, fall back to CS-1 until timer expires. +o Add CS-3. + +RLC Down Engine: + o RLCDownEngine: create blocks on the fly, so that we can change CS-1/CS-4 on the fly. + o Done. Also allow new PDUs to be tacked onto an existing TBF. + o Done. RLCDownEngine: allow BSN to wrap-around. + o 3GPP 44.060 has new info on what RLCEngine should send when data exhausted. + + +o Slow down the adaptive delay in TransceiverRAD1. + +o recvReservation - harden to check the TBF recipient before setting its msg ack. + We probably dont want to accept any data blocks ever, but check users. + Update: The MS sends a data block sometimes, and it seems to be ok. + +o The RadData is probably saved in the wrong place - it is not related to any particular channel, + so it should be in some global place. Or maybe not, because the single-block assignment + is associated with a specific channel. + Also: + o Only change timing advance by 1 unit at a time. + +o Why aren't I seeing measurement reports? + +o Other TODO: + Dual Transfer Mode? + Implement RA Capabilities & QofS? What would we do - the service is pathetic no matter what. + Paging not needed unless we implement dual transfer mode. + +o Catch 'stuck' rlc and fix. - done + +o After killing a tbf, send a PacketTbfRelease and either wait for response or 5 seconds. - done + Although is that necessary? The new tbf just takes up where the old left off, which is ok. + +o Fixed: Got a core-dump; third mReservations is corrupted, 0 vptr: + +o FIXED: I downloaded dinosaur, switched to tmobile, downloaded dinosaur, switched back to Range, downloaded dinosaur, +and I saw this: + It did a new AttachRequest + then ActivatePdpContext, goti message: SGSN Duplicate PdpContextRequest + then: sndcp: packet too old +vvvvvv + SGSNReceived ActivatePDPContextRequest TransactionId=0 mNSapi=5 mLlcSapi=3 mRequestType=0 mPdpAddress=ByteVector(size=2 data: 01 21) mQoS=ByteVector(size=3 data: 00 00 1f) mApName=ByteVector(size=15 data: 0a 62 6c 61 63 6b 62 65 72 72 79 03 6e 65 74) mPco=ByteVector(size=47 data: 80 80 21 0a 01 9d 00 0a 81 06 00 00 00 00 80 21 0a 01 9e 00 0a 83 06 00 00 00 00 c0 23 11 01 9f 00 11 03 72 69 6d 08 70 61 73 73 77 6f 72 64) MS#2,TLLI=c00ba001 imsi=31026052094355 + 924.5:SGSNDuplicate PdpContextRequest + 924.5:SGSNSending ActivatePDPContextAccept TransactionId=0 mLlcSapi=3 mPdpAddress=ByteVector(size=6 data: 01 21 c0 a8 63 01) mQoS=ByteVector(size=12 data: 63 92 12 72 99 10 10 43 ff ff ff 00) mRadioPriority=2 mPco=ByteVector(size=47 data: 80 80 21 0a 02 8e 00 0a 81 06 4b 4b 4b 4b 80 21 0a 02 8f 00 0a 83 06 4b 4b 4c 4c c0 23 11 01 90 00 11 03 72 69 6d 08 70 61 73 73 77 6f 72 64) MS#2,TLLI=c00ba001 imsi=31026052094355 frame=ByteVector(size=10 data: 8a 42 03 0c 63 92 12 72 99 10) + 926.3:LLCllcWriteLowSide sapi=3 + 926.3:LLCUI::llcProcess + 926.3:SNDCPuplink packet pdunum=0 segnum=0 size=488 header=ByteVector(size=20 data: 65 00 00 00 45 00 01 e8 f4 0f 00 00 80 11 38 5b c0 a8 63 01) + 926.3:SNDCPflush num=0 sp->mSegCount=1 + 926.3:SNDCPpdpWriteLowSide packetlen=488 + 926.3:ggsn: writing proto=udp 488 byte packet from 192.168.99.1 to 206.51.26.189 at 17:48:23.2 + 928.9:LLCllcWriteLowSide sapi=3 + 928.9:LLCUI::llcProcess + 928.9:SNDCPuplink packet pdunum=0 segnum=0 size=48 header=ByteVector(size=20 data: 66 00 00 00 45 00 00 30 f4 10 00 00 80 06 b7 cf c0 a8 63 02) + 928.9:LLCSNDCP packet too old, discarded (number=0,current=521) + 930.8:LLCllcWriteLowSide sapi=3 + 930.8:LLCUI::llcProcess + 930.8:SNDCPuplink packet pdunum=1 segnum=0 size=488 header=ByteVector(size=20 data: 65 00 00 01 45 00 01 e8 f4 11 00 00 80 11 38 59 c0 a8 63 01) + 930.8:SNDCPflush num=1 sp->mSegCount=1 + 930.8:SNDCPpdpWriteLowSide packetlen=488 +^^^^^^^^ + +o done: Dump the MNC/MCC the phone roams in. Only dump the phone caps once. + +o Extended uplink TBF mode 44.060 9.3.1b - allows uplink to ride out temporary inactive periods. +o The control-ack bit says whether we are establishing a new downlink TBF!! +Update: This does not work on the blackberry. I resorted to sending a TbfRelease message. +o Fixed: A stalled uplink never completed. +o Done: Try removing extra unasked-for USF in findNeedy +o This was caused by the same bug: after it stalled no acknacks were ever sent again. + The 06-08 TBF#180 sends the same blocks eg: BSN=(1) multiple times. +o Done: Fix the "CHAP" message in sgsn. +o Done: Fix the "Unable to allocate channel" message. +o Fixed: The STUCK should not count it as stuck if we received some new blocks, + even if SSN did not change - if there is an outage there may be many blocks + between SSN-64 and SSN to send, so SSN may not change for quite a while. +o Fixed: mUSF was not inited to 0 on blocks that were resent. + Not sure exactly, but this possibly resulted in errors: + Radio Block Data Block with unrecognized tfi + Uplink Data Block with unknown tfi + ERROR: Received reservation in RLC data block + After this fix, I could no longer generate the above errors, nor did I + see the long pauses in downlink reception. + +o Done: implemented LLC Change TLLI procedure + I think the LlcEngine should be in the GmmInfo, or at least we need to implement + change TLLI procedures. + +o Same as above: The LLC spec says the state machine does not change when TLLI reassigned. + But I thought I saw somewhere else that it does. Which is it? + Since we dont use ABM, it would only affect the unacknowledged sequence number. + +o Probably fixed by adding GLOG: + Why does LOG(ERR)<< in TBF.cpp (example @@@fail) not work if you + use the default Log.Level, but works if you add: + INSERT INTO "CONFIG" VALUES('Log.Level.TBF.cpp','DEBUG',0,0,'debug'); + +o Fixed by mAltTlli, but has it been tested? + When we change tlli, when we check for an existing TBF, we have to check + both MSInfo structs. The SGSN knows the IMSI for every TLLI, so it should + ship down the assigned TLLI on AttachComplete. + +o Maybe fixed by resetting mSP and mUSF: + Saw mSP being set in an outgoing data block but without a reservation?? + +o I think I fixed this by adding the TbfRelease procedue: + BUG: The MS will send packet resource request using an expired TFI. + I think we need to keep the tfis reserved until the T3312, or until explicitly reused. + And if reused, must be for the same MS, or an in-flight packet resource request would be invalid. + +o I think this is fixed by a combination of many bug fixes: + == cause=3105 error == + Might be T3168 problem GSM04.60 7.1.3.1 and TBF.cpp:sendAssignemnt() + o Try resending the assignment. + +o Done: add ms imsi to ggsn.log + +o Done, for uplink initiated by downlink acknack: + == Multiple uplink TBF == + o Fix the multiple uplink TBF stuff - these will be initiated using the RRBP + reservations, not come in on a RACH. + +o Done: Accept a new request in the PacketDownlinkAckNack. Here is one from the blackberry: + INFO GPRS,1,412353: TBF#91PacketDownlinkAckNack: PayLoadType=RLCControl mCountDownValue=(0) + mSI=(0) mR=(0) mMessageType=2 mTFI=(1) mFinalAckIndication=(1) mSSN=(2) Bitmap=(0000000000000003) + mPeakThroughputClass=(4) mRadioPriority=(1) mRLCMode=(0) mLLCPDUType=(1) mRLCOctetCount=(1345) + mSt.TxQNum=2 mSt.VA=2 mSt.VS=1 + INFO GPRS,1,412353:@@@ok TBF#91 mtMS= MS#2 TLLI=c0009001 usf=0 mtDir=RLCDir::Down mtState==TBFState:DataTransmit + mtAttached=1 mtTFI=1 mtMsgAck=1 mtExpectedAckBSN=412350 size=54 + mtChannelCoding=3 mtUnAckMode=0 OnCCCH=1 mtAssignCounter=1 descr=user pdu 18:52:31.9 + +o NO: The way I am doing it now is fine: + Need to assign a permanent PACCH. Need to move the MS off it? + MS->BTS RACH + MS<-BTS single block + MS->BTS resource request + MS<-BTS uplink/downlink assignment sent to old channel may assign new ch. + +o mControlAck fixed in trunk, but not in GPRS3 yet. +o Done: 6-18: Re-merge the controlAck fix from the trunk. +o Done 6-23: Clean up processUplinkResourceRequest: + when a RACH comes in cancel the downlink TBF. + improve uplink handling as well. + Add msUplinkRequest for delayed uplink request. +o Done 6-23 Add a count of number of reassignments, and possibly other states, to prevent lockup. + Added GPRS.Timers.MS.NonResponsive +o Done 6-26: Allow uplink request in final uplink acknack using TBF_EST. +o Done 6-27: Add an over-riding non-responsive MS timer. +o Fixed 7-4: TBFs are not dying properly - why not? Because the MS lost + the channel assignment and so Dead TBF was not serviced. +o Who cares: windows ftp says: 500 I won't open a connection to 192.168.99.1 (only to 24.20.208.209) + (The latter is comcast, probably my static IP.) +o The Pittsburgh logs have @@@ messages with no antecedent messages. Why? + Because David was turning the debug level on and off on the console. +o Fixed! If the multitech modem is attached and bts is power cycled, cant get it back. + I send a DetachRequest and a GMMStatus, and the modem sends back invalid mandatory information. + Try sending each message separately to figure out which one it doesnt like. + Try taking out the Gmm "Cause". +o Done 8-2, Pittsburgh problem: retest the InterThreadQueue fix. +o Fixed: 8-2, Pittsburgh problem: If multiple modems share the channel, does not work. +Fixed o A 1-down/2-up config asserted in findNeedyUsf here: assert(ms->msCanUseUplinkTn(tn)); +Done o Send power params in downlink assignment Probably need to put them there first. +FIXED: o Possibly the L3ImmediateAssignment for downlink is not working. +Completely redone: o sendReassignment has to restart the persistent uplink TBF. +Worked around this: o multitech modem says it is not extended uplink capable? +DONE o Can increase 3101 now, and multiply it by the number of uplink channels. +Fixed o The initial assignment is being multislot, and then I am doing a reassignment? +DONE o Put the power reports (eg: CX) from the MS somewhere in the CLI so David can see it in the field. +FIXED: o The blackberry uses a local tlli in the packetcontrolacknowledgment, see /OpenBTS/8-10/*.log Causes TBF failure. +FIXED: o David complained about the ggsn.log. Make sure everything in there is in the real log. + It is, but you have to set LOG.Level to INFO go see it. +OK o I tried setting GGSN.MS.IP.MaxCount to 4. After issuing 3 IPs, it denied the fourth (off by one) + The iphone reports: Could not acviate cellular network, Samsung says No Connection. + I think that is right, because they are reserved for a few minutes. diff --git a/GSM/AppInfTest.cpp b/GSM/AppInfTest.cpp new file mode 100644 index 00000000..829427d1 --- /dev/null +++ b/GSM/AppInfTest.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include + +// Load configuration from a file. +ConfigurationTable gConfig("OpenBTS.config"); + +int main() +{ + GSM::L3ApplicationInformation ai(); + static const char init_request_msbased_gps[4] = {'@', '\x01', 'x', '\xa8'}; // pre encoded PER for the following XER: + static std::vector request_msbased_gps(init_request_msbased_gps, + init_request_msbased_gps + sizeof(init_request_msbased_gps)); + GSM::L3ApplicationInformation ai2(request_msbased_gps); + + GSM::L3Frame f(ai2); + std::cout << f; + + return 0; +} + diff --git a/GSM/GSM610Tables.cpp b/GSM/GSM610Tables.cpp index fe1933c9..8cee7791 100644 --- a/GSM/GSM610Tables.cpp +++ b/GSM/GSM610Tables.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/GSM610Tables.h b/GSM/GSM610Tables.h index 33030f96..4f634d08 100644 --- a/GSM/GSM610Tables.h +++ b/GSM/GSM610Tables.h @@ -1,21 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. +* * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/GSMCommon.cpp b/GSM/GSMCommon.cpp index 6b45e2a6..fa02108a 100644 --- a/GSM/GSMCommon.cpp +++ b/GSM/GSMCommon.cpp @@ -1,29 +1,22 @@ /* * Copyright 2008 Free Software Foundation, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2013 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +#include #include "GSMCommon.h" using namespace GSM; @@ -47,6 +40,9 @@ const char* GSM::CallStateString(GSM::CallState state) case ReleaseRequest: return "release-request"; case SMSDelivering: return "SMS-delivery"; case SMSSubmitting: return "SMS-submission"; + case HandoverInbound: return "HANDOVER Inbound"; + case HandoverProgress: return "HANDOVER Progress"; + case HandoverOutbound: return "HANDOVER Outbound"; case BusyReject: return "Busy Reject"; default: return NULL; } @@ -56,7 +52,7 @@ ostream& GSM::operator<<(ostream& os, GSM::CallState state) { const char* str = GSM::CallStateString(state); if (str) os << str; - else os << "?" << state << "?"; + else os << "?" << ((int)state) << "?"; return os; } @@ -131,17 +127,17 @@ unsigned GSM::uplinkFreqKHz(GSMBand band, unsigned ARFCN) { switch (band) { case GSM850: - assert((ARFCN<252)&&(ARFCN>129)); + assert((ARFCN>=128)&&(ARFCN<=251)); return 824200+200*(ARFCN-128); case EGSM900: if (ARFCN<=124) return 890000+200*ARFCN; - assert((ARFCN>974)&&(ARFCN<1024)); + assert((ARFCN>=975)&&(ARFCN<=1023)); return 890000+200*(ARFCN-1024); case DCS1800: - assert((ARFCN>511)&&(ARFCN<886)); + assert((ARFCN>=512)&&(ARFCN<=885)); return 1710200+200*(ARFCN-512); case PCS1900: - assert((ARFCN>511)&&(ARFCN<811)); + assert((ARFCN>=512)&&(ARFCN<=810)); return 1850200+200*(ARFCN-512); default: assert(0); @@ -241,6 +237,19 @@ int32_t Clock::FN() const return currentFN; } +double Clock::systime(const GSM::Time& when) const +{ + ScopedLock lock(mLock); + const double slotMicroseconds = (48.0 / 13e6) * 156.25; + const double frameMicroseconds = slotMicroseconds * 8.0; + int32_t elapsedFrames = when.FN() - mBaseFN; + if (elapsedFrames<0) elapsedFrames += gHyperframe; + double elapsedUSec = elapsedFrames * frameMicroseconds + when.TN() * slotMicroseconds; + double baseSeconds = mBaseTime.sec() + mBaseTime.usec()*1e-6; + double st = baseSeconds + 1e-6*elapsedUSec; + return st; +} + void Clock::wait(const Time& when) const { @@ -320,7 +329,13 @@ ostream& GSM::operator<<(ostream& os, TypeAndOffset tao) case SDCCH_8_5: os << "SDCCH/8-5"; break; case SDCCH_8_6: os << "SDCCH/8-6"; break; case SDCCH_8_7: os << "SDCCH/8-7"; break; - case TDMA_BEACON: os << "(beacon)"; break; + case TDMA_BEACON: os << "BCH"; break; + case TDMA_BEACON_BCCH: os << "BCCH"; break; + case TDMA_BEACON_CCCH: os << "CCCH"; break; + case TDMA_PDCH: os << "PDCH"; break; + case TDMA_PACCH: os << "PACCH"; break; + case TDMA_PTCCH: os << "PTCCH"; break; + case TDMA_PDIDLE: os << "PDIDLE"; break; default: os << "?" << (int)tao << "?"; } return os; @@ -343,8 +358,14 @@ ostream& GSM::operator<<(ostream& os, ChannelType val) case AnyTCHType: os << "any TCH"; break; case LoopbackFullType: os << "Loopback Full"; break; case LoopbackHalfType: os << "Loopback Half"; break; + case PDTCHCS1Type: os << "PDTCHCS1"; break; + case PDTCHCS2Type: os << "PDTCHCS2"; break; + case PDTCHCS3Type: os << "PDTCHCS3"; break; + case PDTCHCS4Type: os << "PDTCHCS4"; break; + case PSingleBlock1PhaseType: os << "GPRS_SingleBlock1Phase"; break; + case PSingleBlock2PhaseType: os << "GPRS_SingleBlock2Phase"; break; case AnyDCCHType: os << "any DCCH"; break; - default: os << "?" << (int)val << "?"; + default: os << "?" << (int)val << "?"; break; } return os; } diff --git a/GSM/GSMCommon.h b/GSM/GSMCommon.h index 164319dc..2baf842a 100644 --- a/GSM/GSMCommon.h +++ b/GSM/GSMCommon.h @@ -1,25 +1,19 @@ /**@file Common-use GSM declarations, most from the GSM 04.xx and 05.xx series. */ /* -* Copyright 2008-2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -28,6 +22,7 @@ #ifndef GSMCOMMON_H #define GSMCOMMON_H +#include "Defines.h" #include #include #include @@ -38,8 +33,6 @@ #include - - namespace GSM { /**@namespace GSM This namespace covers L1 FEC, L2 and L3 message translation. */ @@ -82,7 +75,6 @@ std::ostream& operator<<(std::ostream& os, CallState state); /** A base class for GSM exceptions. */ - class GSMError {}; /** Duration ofa GSM frame, in microseconds. */ @@ -221,6 +213,7 @@ enum ChannelType { CCCHType, ///< common control, a combination of several sub-types RACHType, ///< random access SACCHType, ///< slow associated control (acutally dedicated, but...) + CBCHType, ///< cell broadcast channel //@} ///@name Dedicated control channels (DCCHs). //@{ @@ -232,6 +225,19 @@ enum ChannelType { TCHFType, ///< full-rate traffic TCHHType, ///< half-rate traffic AnyTCHType, ///< any TCH type + //@{ + //@name Packet channels for GPRS. + PDTCHCS1Type, + PDTCHCS2Type, + PDTCHCS3Type, + PDTCHCS4Type, + //@} + //@{ + //@name Packet CHANNEL REQUEST responses + // These are used only as return value from decodeChannelNeeded(), and do not correspond + // to any logical channels. + PSingleBlock1PhaseType, + PSingleBlock2PhaseType, //@} ///@name Special internal channel types. //@{ @@ -240,6 +246,7 @@ enum ChannelType { AnyDCCHType, ///< any dedicated control channel UndefinedCHType, ///< undefined //@} + //@} }; @@ -274,7 +281,12 @@ enum TypeAndOffset { /// Some extra ones for our internal use. TDMA_BEACON_BCCH=253, TDMA_BEACON_CCCH=252, - TDMA_BEACON=255 + TDMA_BEACON=255, + //TDMA_PDTCHF, // packet data traffic logical channel, full speed. + TDMA_PDCH, // packet data channel, inclusive + TDMA_PACCH, // packet control channel, shared with data but distinguished in MAC header. + TDMA_PTCCH, // packet data timing advance logical channel + TDMA_PDIDLE // Handles the packet channel idle frames. }; std::ostream& operator<<(std::ostream& os, TypeAndOffset); @@ -297,7 +309,7 @@ enum L3PD { L3PDSS2PD=0x04, L3MobilityManagementPD=0x05, L3RadioResourcePD=0x06, - L3MobilityManagementGPRSPD=0x08, + L3GPRSMobilityManagementPD=0x08, L3SMSPD=0x09, L3GPRSSessionManagementPD=0x0a, L3NonCallSSPD=0x0b, @@ -328,6 +340,7 @@ extern const unsigned RACHWaitSParam[]; /**@name Modulus operations for frame numbers. */ //@{ /** The GSM hyperframe is largest time period in the GSM system, GSM 05.02 4.3.3. */ +// It is 2715648 const uint32_t gHyperframe = 2048UL * 26UL * 51UL; /** Get a clock difference, within the modulus, v1-v2. */ @@ -547,6 +560,9 @@ class Clock { /** Block until the clock passes a given time. */ void wait(const Time&) const; + + /** Return the system time associated with a given timestamp. */ + double systime(const Time&) const; }; diff --git a/GSM/GSMConfig.cpp b/GSM/GSMConfig.cpp index 736713ee..7616ce97 100644 --- a/GSM/GSMConfig.cpp +++ b/GSM/GSMConfig.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -26,21 +16,27 @@ #include "GSMConfig.h" #include "GSMTransfer.h" #include "GSMLogicalChannel.h" +#include "GPRSExport.h" #include #include +#include #include #include + using namespace std; using namespace GSM; GSMConfig::GSMConfig() - : + :mCBCH(NULL), mSI5Frame(UNIT_DATA),mSI6Frame(UNIT_DATA), - mStartTime(::time(NULL)) + mSI1(NULL),mSI2(NULL),mSI3(NULL),mSI4(NULL), + mSI5(NULL),mSI6(NULL), + mStartTime(::time(NULL)), + mChangemark(0) { } @@ -48,7 +44,7 @@ void GSMConfig::init() { mBand = (GSMBand)gConfig.getNum("GSM.Radio.Band"); mT3122 = gConfig.getNum("GSM.Timer.T3122Min"); - regenerateBeacon(); + regenerateBeacon(); } void GSMConfig::start() @@ -56,6 +52,12 @@ void GSMConfig::start() mPowerManager.start(); // Do not call this until the paging channels are installed. mPager.start(); + // If requested, start gprs to allocate channels at startup. + // Otherwise, channels are allocated on demand, if possible. + if (GPRS::configGprsChannelsMin() > 0) { + // Start gprs. + GPRS::gprsStart(); + } // Do not call this until AGCHs are installed. mAccessGrantThread.start(Control::AccessGrantServiceLoop,NULL); } @@ -65,10 +67,13 @@ void GSMConfig::start() void GSMConfig::regenerateBeacon() { + // FIXME -- Need to implement BCCH_CHANGE_MARK + gReports.incr("OpenBTS.GSM.RR.BeaconRegenerated"); + mChangemark++; // Update everything from the configuration. - LOG(NOTICE) << "regenerating system information messages"; + LOG(NOTICE) << "regenerating system information messages, changemark " << mChangemark; // BSIC components mNCC = gConfig.getNum("GSM.Identity.BSIC.NCC"); @@ -79,61 +84,100 @@ void GSMConfig::regenerateBeacon() // MCC/MNC/LAC mLAI = L3LocationAreaIdentity(); + std::vector neighbors = gNeighborTable.ARFCNList(); + // if the neighbor list is emtpy, put ourselves on it + if (neighbors.size()==0) neighbors.push_back(gConfig.getNum("GSM.Radio.C0")); + // Now regenerate all of the system information messages. // SI1 - L3SystemInformationType1 SI1; - LOG(INFO) << SI1; + L3SystemInformationType1 *SI1 = new L3SystemInformationType1; + if (mSI1) delete mSI1; + mSI1 = SI1; + LOG(INFO) << *SI1; L3Frame SI1L3(UNIT_DATA); - SI1.write(SI1L3); + SI1->write(SI1L3); L2Header SI1Header(L2Length(SI1L3.L2Length())); mSI1Frame = L2Frame(SI1Header,SI1L3); LOG(DEBUG) << "mSI1Frame " << mSI1Frame; // SI2 - L3SystemInformationType2 SI2; - LOG(INFO) << SI2; + L3SystemInformationType2 *SI2 = new L3SystemInformationType2(neighbors); + if (mSI2) delete mSI2; + mSI2 = SI2; + LOG(INFO) << *SI2; L3Frame SI2L3(UNIT_DATA); - SI2.write(SI2L3); + SI2->write(SI2L3); L2Header SI2Header(L2Length(SI2L3.L2Length())); mSI2Frame = L2Frame(SI2Header,SI2L3); LOG(DEBUG) << "mSI2Frame " << mSI2Frame; // SI3 - L3SystemInformationType3 SI3; - LOG(INFO) << SI3; + L3SystemInformationType3 *SI3 = new L3SystemInformationType3; + if (mSI3) delete mSI3; + mSI3 = SI3; + LOG(INFO) << *SI3; L3Frame SI3L3(UNIT_DATA); - SI3.write(SI3L3); + SI3->write(SI3L3); L2Header SI3Header(L2Length(SI3L3.L2Length())); - mSI3Frame = L2Frame(SI3Header,SI3L3); + mSI3Frame = L2Frame(SI3Header,SI3L3,true); LOG(DEBUG) << "mSI3Frame " << mSI3Frame; // SI4 - L3SystemInformationType4 SI4; + L3SystemInformationType4 *SI4 = new L3SystemInformationType4; + if (mSI4) delete mSI4; + mSI4 = SI4; + LOG(INFO) << *SI4; LOG(INFO) << SI4; L3Frame SI4L3(UNIT_DATA); - SI4.write(SI4L3); + SI4->write(SI4L3); + //printf("SI4 bodylength=%d l2len=%d\n",SI4.l2BodyLength(),SI4L3.L2Length()); + //printf("SI4L3.size=%d\n",SI4L3.size()); L2Header SI4Header(L2Length(SI4L3.L2Length())); - mSI4Frame = L2Frame(SI4Header,SI4L3); + mSI4Frame = L2Frame(SI4Header,SI4L3,true); LOG(DEBUG) << "mSI4Frame " << mSI4Frame; +#if GPRS_PAT | GPRS_TEST + // SI13. pat added 8-2011 to advertise GPRS support. + L3SystemInformationType13 *SI13 = new L3SystemInformationType13; + LOG(INFO) << *SI13; + L3Frame SI13L3(UNIT_DATA); + //printf("start=%d\n",SI13L3.size()); + SI13->write(SI13L3); + //printf("end=%d\n",SI13L3.size()); + //printf("SI13 bodylength=%d l2len=%d\n",SI13.l2BodyLength(),SI13L3.L2Length()); + //printf("SI13L3.size=%d\n",SI13L3.size()); + L2Header SI13Header(L2Length(SI13L3.L2Length())); + mSI13Frame = L2Frame(SI13Header,SI13L3,true); + LOG(DEBUG) << "mSI13Frame " << mSI13Frame; +#endif + // SI5 - L3SystemInformationType5 SI5; - LOG(INFO) << SI5; - SI5.write(mSI5Frame); - LOG(DEBUG) << "mSI5Frame " << mSI5Frame; + regenerateSI5(); // SI6 - L3SystemInformationType6 SI6; - LOG(INFO) << SI6; - SI6.write(mSI6Frame); + L3SystemInformationType6 *SI6 = new L3SystemInformationType6; + if (mSI6) delete mSI6; + mSI6 = SI6; + LOG(INFO) << *SI6; + SI6->write(mSI6Frame); LOG(DEBUG) "mSI6Frame " << mSI6Frame; } - - +void GSMConfig::regenerateSI5() +{ + std::vector neighbors = gNeighborTable.ARFCNList(); + // if the neighbor list is emtpy, put ourselves on it + if (neighbors.size()==0) neighbors.push_back(gConfig.getNum("GSM.Radio.C0")); + L3SystemInformationType5 *SI5 = new L3SystemInformationType5(neighbors); + if (mSI5) delete mSI5; + mSI5 = SI5; + LOG(INFO) << *SI5; + SI5->write(mSI5Frame); + LOG(DEBUG) << "mSI5Frame " << mSI5Frame; +} CCCHLogicalChannel* GSMConfig::minimumLoad(CCCHList &chanList) { @@ -158,43 +202,190 @@ CCCHLogicalChannel* GSMConfig::minimumLoad(CCCHList &chanList) -template ChanType* getChan(vector& chanList) +template ChanType* getChan(vector& chanList, bool forGprs) { const unsigned sz = chanList.size(); + LOG(DEBUG) << "sz=" << sz; if (sz==0) return NULL; - // Start the search from a random point in the list. - //unsigned pos = random() % sz; - // HACK -- Try in-order allocation for debugging. - for (unsigned i=0; irecyclable()) return chan; - //pos = (pos+1) % sz; + // (pat) Dont randomize for GPRS! GPRS requires that channels are returned + // in order for the initial channels allocated on C0. + // We shouldnt randomize at all because we want RR TCH to come from the + // front of the list and GPRS from the back. + unsigned pos = 0; + const char *configRandomize = "GSM.Channels.Randomize"; + if (gConfig.defines(configRandomize)) { + if (forGprs) { + // If the parameter is 'required', the gConfig.remove fails, but dont print a zillion messages. + static bool once = true; + if (once) { + LOG(ALERT) << "Config parameter '" << configRandomize << "' is incompatible with GPRS, removed"; + once = false; + } + gConfig.remove(configRandomize); + } else { + pos = random() % sz; + } + } + for (unsigned i=0; iinUseByGPRS() && chan->recyclable()) return chan; } return NULL; } +// Are the two channels adjacent? +template +bool testAdjacent(ChanType *ch1, ChanType *ch2) +{ + return (ch1->CN() == ch2->CN() && ch1->TN() == ch2->TN()-1); +} + +// Return the goodness of this possible match of gprs channels. +// Higher numbers are gooder. +template +int testGoodness(vector& chanList, int lo, int hi) +{ + int goodness = 0; + if (lo > 0) { + ChanType *ch1 = chanList[lo-1]; // ch1 is below to ch lo. + if (testAdjacent(ch1,chanList[lo])) { + // The best match is adjacent to other gprs channels. + if (ch1->inUseByGPRS()) { goodness += 2; } + // The next best is an empty adjacent channel. + else if (ch1->recyclable()) { goodness += 1; } + } + } + if (hi < (int)chanList.size()-1) { + ChanType *ch2 = chanList[hi+1]; // ch2 is above ch hi + if (testAdjacent(ch2,chanList[hi])) { + if (ch2->inUseByGPRS()) { goodness += 2; } + else if (ch2->recyclable()) { goodness += 1; } + } + } + return goodness; +} + +// (pat) 6-20-2012: To increase the likelihood that GPRS channels will be adjacent, +// GSM RR channels will be allocated from the front of the channel list +// and GPRS from the end. +// This function allocates a group of channels for gprs. +// Look for the largest group of adjacent channels <= groupSize. +// Give preference to channels that are adjacent to channels already +// allocated for gprs, or to empty channels. +// Give second preference to groups near the end of the channel list. +// Return the allocated channels in the array pointed to by results and +// return number of channels found. +template +static unsigned getChanGroup(vector& chanList, ChanType **results) +{ + const unsigned sz = chanList.size(); + if (sz==0) return 0; + + const bool backwards = true; // Currently we always search backwards. + // To search forwards, dont forget to invert besti,bestn below + ChanType *prevFreeCh = NULL; // unneeded initialization + int curN = 0; // current number of adjacent free channels. + int bestI=0, bestN=0; // best match + int bestGoodness = 0; // goodness of best match + for (unsigned i=0; iinUseByGPRS()) { continue; } + if (! chan->recyclable()) { continue; } + if (bestN == 0) { + bestI = i; + curN = bestN = 1; + bestGoodness = testGoodness(chanList,bestI,bestN); + } else { + if (testAdjacent(chan,prevFreeCh)) { + curN++; // chan is adjacent to prevCh. + int curGoodness = testGoodness(chanList,i,curN); + if (curN > bestN || (curN == bestN && curGoodness > bestGoodness)) { + // Best so far, so remember it. + bestN = curN; + bestI = i; + bestGoodness = curGoodness; + // optional early termination test + //if (bestN >= groupSize && bestIsAdjacent) { goto finished; } + } + } else { + curN = 0; + } + } + prevFreeCh = chan; + } + //finished: + for (int j = 0; j < bestN; j++) { + results[j] = chanList[bestI+j]; + } + return bestN; +} + +// Allocate a group of channels for gprs. +// See comments at getChanGroup. +int GSMConfig::getTCHGroup(int groupSize,TCHFACCHLogicalChannel **results) +{ + ScopedLock lock(mLock); + int nfound = getChanGroup(mTCHPool,results); + for (int i = 0; i < nfound; i++) { + results[i]->debugGetL1()->setGPRS(true,NULL); + } + return nfound; +} + SDCCHLogicalChannel *GSMConfig::getSDCCH() { + LOG(DEBUG); ScopedLock lock(mLock); - SDCCHLogicalChannel *chan = getChan(mSDCCHPool); + LOG(DEBUG); + SDCCHLogicalChannel *chan = getChan(mSDCCHPool,0); + LOG(DEBUG); if (chan) chan->open(); + LOG(DEBUG); return chan; } -TCHFACCHLogicalChannel *GSMConfig::getTCH() +// (pat) By a very tortuous path, chan->open() calls L1Encoder::open() and L1Decoder::open(), +// which sets mActive in both and resets the timers. +TCHFACCHLogicalChannel *GSMConfig::getTCH( + bool forGPRS, // If true, allocate the channel to gprs, else to RR use. + bool onlyCN0) // If true, allocate only channels on the lowest ARFCN. { + LOG(DEBUG); ScopedLock lock(mLock); - TCHFACCHLogicalChannel *chan = getChan(mTCHPool); + //if (GPRS::GPRSDebug) { + // const unsigned sz = mTCHPool.size(); + // char buf[300]; int n = 0; + // for (unsigned i=0; iCN(),chan->TN(), + // chan->inUseByGPRS(),chan->recyclable()); + // } + // LOG(WARNING)<<"getTCH list:"<(mTCHPool,forGPRS); + // (pat) We have to open it or set gprs mode before returning to avoid a race. if (chan) { - chan->open(); - gReports.incr("OpenBTS.GSM.RR.ChannelAssignment"); + // The channels are searched in order from low to high, so if the first channel + // found is not on CN0, we have failed. + //LOG(DEBUG)<<"getTCH returns"<CN",chan->CN()); + if (onlyCN0 && chan->CN()) { return NULL; } + if (forGPRS) { + // (pat) Reserves channel for GPRS, but does not start delivering bursts yet. + chan->debugGetL1()->setGPRS(true,NULL); + return chan; + } + chan->open(); // (pat) LogicalChannel::open(); Opens mSACCH also. + gReports.incr("OpenBTS.GSM.RR.ChannelAssignment"); + } else { + //LOG(DEBUG)<<"getTCH returns NULL"; } + LOG(DEBUG); return chan; } @@ -204,6 +395,7 @@ template size_t chanAvailable(const vector& chanList { size_t count = 0; for (unsigned i=0; iinUseByGPRS()) { continue; } if (chanList[i]->recyclable()) count++; } return count; @@ -227,7 +419,7 @@ size_t GSMConfig::TCHAvailable() const size_t GSMConfig::totalLoad(const CCCHList& chanList) const { size_t total = 0; - for (int i=0; iload(); } return total; @@ -235,17 +427,41 @@ size_t GSMConfig::totalLoad(const CCCHList& chanList) const -template unsigned countActive(const vector& chanList) +unsigned countActive(const SDCCHList& chanList) +{ + unsigned active = 0; + const unsigned sz = chanList.size(); + for (unsigned i=0; irecyclable()) active++; + } + return active; +} + + +unsigned countActive(const TCHList& chanList) { unsigned active = 0; const unsigned sz = chanList.size(); // Start the search from a random point in the list. for (unsigned i=0; iinUseByGPRS()) continue; if (!chanList[i]->recyclable()) active++; } return active; } +unsigned countAvailable(const TCHList& chanList) +{ + unsigned available = 0; + const unsigned sz = chanList.size(); + // Start the search from a random point in the list. + for (unsigned i=0; iinUseByGPRS()) continue; + available++; + } + return available; +} + unsigned GSMConfig::SDCCHActive() const { @@ -257,6 +473,13 @@ unsigned GSMConfig::TCHActive() const return countActive(mTCHPool); } +unsigned GSMConfig::TCHTotal() const +{ + return countAvailable(mTCHPool); +} + + + unsigned GSMConfig::T3122() const { @@ -270,7 +493,7 @@ unsigned GSMConfig::growT3122() ScopedLock lock(mLock); unsigned retVal = mT3122; mT3122 += (random() % mT3122) / 2; - if (mT3122>max) mT3122=max; + if (mT3122>(int)max) mT3122=max; return retVal; } @@ -281,7 +504,7 @@ unsigned GSMConfig::shrinkT3122() ScopedLock lock(mLock); unsigned retVal = mT3122; mT3122 -= (random() % mT3122) / 2; - if (mT3122setSlot(TN,0); + radio->setSlot(TN,0); // (pat) 0 => Transciever.h enum ChannelCombination = FILL } @@ -303,14 +526,13 @@ void GSMConfig::createCombinationI(TransceiverManager& TRX, unsigned CN, unsigne LOG_ASSERT((CN!=0)||(TN!=0)); LOG(NOTICE) << "Configuring combination I on C" << CN << "T" << TN; ARFCNManager *radio = TRX.ARFCN(CN); - radio->setSlot(TN,1); + radio->setSlot(TN,1); // (pat) 1 => Transciever.h enum ChannelCombination = I TCHFACCHLogicalChannel* chan = new TCHFACCHLogicalChannel(CN,TN,gTCHF_T[TN]); chan->downstream(radio); Thread* thread = new Thread; thread->start((void*(*)(void*))Control::DCCHDispatcher,chan); chan->open(); gBTS.addTCH(chan); - } @@ -319,7 +541,7 @@ void GSMConfig::createCombinationVII(TransceiverManager& TRX, unsigned CN, unsig LOG_ASSERT((CN!=0)||(TN!=0)); LOG(NOTICE) << "Configuring combination VII on C" << CN << "T" << TN; ARFCNManager *radio = TRX.ARFCN(CN); - radio->setSlot(TN,7); + radio->setSlot(TN,7); // (pat) 7 => Transciever.h enum ChannelCombination = VII for (int i=0; i<8; i++) { SDCCHLogicalChannel* chan = new SDCCHLogicalChannel(CN,TN,gSDCCH8[i]); chan->downstream(radio); @@ -345,4 +567,95 @@ bool GSMConfig::hold() const +#if ENABLE_PAGING_CHANNELS + +// 5-27-2012 pat added: +// Routines for CCCH messages to add real paging channels. +// Added in the simplest possible way to avoid destabilizing anything. +// GPRS still needs a pretty major rewrite of the underlying CCCHLogicalChannel class +// to reduce the latency, but paging queues at least relieve the congestion on CCCH. +// In DRX [Discontinuous Reception] mode the MS listens only to a subset of CCCH based on its IMSI. +// This is a GPRS thing but dependent on the configuration of CCCH in our system. +// See: GSM 05.02 6.5.2: Determination of CCCH_GROUP and PAGING_GROUP for MS in idle mode. +void GSMConfig::crackPagingFromImsi( + unsigned imsiMod1000 // The phones imsi mod 1000, so just atoi the last 3 digits. + unsigned &paging_block_index, // Returns which of the paging ccchs to use. + unsigned &multiframe_index // Returns which 51-multiframe to use. + ) +{ + L3ControlChannelDescription mCC; + + // BS_CCCH_SDCCH_COMB is defined in GSM 05.02 3.3.2.3; + int bs_cc_chans; // The number of ccch timeslots per 51-multiframe. + bool bs_ccch_sdcch_comb; // temp var indicates if sdcch is on same TS as ccch. + switch (mCC.mCCCH_CONF) { + case 0: bs_cc_chans=1; bs_ccch_sdcch_comb=false; break; + case 1: bs_cc_chans=1; bs_ccch_sdcch_comb=true; break; + case 2: bs_cc_chans=2; bs_ccch_sdcch_comb=false; break; + case 4: bs_cc_chans=3; bs_ccch_sdcch_comb=false; break; + case 6: bs_cc_chans=4; bs_ccch_sdcch_comb=false; break; + default: + LOG(ERR) << "Invalid GSM.CCCH.CCCH-CONF value:"< mPCC.mBS_AG_BLKS_RES); + + // GSM 05.02 6.5.2: N is number of paging blocks "available" on one CCCH. + // The "available" is in quotes and not specifically defined, but I believe + // they mean after subtracting out BS_AG_BLKS_RES, as per 6.5.1 paragraph v). + unsigned pch_avail = agch_avail - mPCC.mBS_AG_BLKS_RES; + unsigned Ntotal = pch_avail * bs_pa_mfrms; + unsigned tmp = (imsiMod1000 % (bs_cc_chans * Ntotal)) % Ntotal; + unsigned paging_group = tmp % Ntotal; + paging_block_index = paging_group / (Ntotal / bs_pa_mfrms); + // And I quote: The required 51-multiframe occurs when: + // PAGING_GROUP div (N div BS_PA_MFRMS) = (FN div 51) mod (BS_PA_MFRMS) + multiframe_index = paging_group / (Ntotal % bs_pa_mfrms); +} + +void GSMConfig::sendPCH(const L3RRMessage& msg,unsigned imsiMod1000) +{ + unsigned paging_block_index; // which of the paging ccchs to use. + unsigned multiframe_index; // which 51-multiframe to use. + crackPagingFromImsi(imsiMod1000,paging_block_index, multiframe_index); + assert(multiframe_index < sMax_BS_PA_MFRMS); + CCCHLogicalChannel* ch = getPCH(paging_block_index); + ch->mPagingQ[multiframe_index].write(new L3Frame((const L3Message&)msg,UNIT_DATA)); +} + +Time GSMConfig::getPchSendTime(imsiMod1000) +{ + unsigned paging_block_index; // which of the paging ccchs to use. + unsigned multiframe_index; // which 51-multiframe to use. + crackPagingFromImsi(imsiMod1000,paging_block_index, multiframe_index); + assert(multiframe_index < sMax_BS_PA_MFRMS); + CCCHLogicalChannel* ch = getPCH(paging_block_index); + return ch->getNextPchSendTime(multiframe_index); +} +#endif + + // vim: ts=4 sw=4 diff --git a/GSM/GSMConfig.h b/GSM/GSMConfig.h index 0ea31a7c..3504b6fb 100644 --- a/GSM/GSMConfig.h +++ b/GSM/GSMConfig.h @@ -1,24 +1,18 @@ /* * Copyright 2008-2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -27,6 +21,7 @@ #ifndef GSMCONFIG_H #define GSMCONFIG_H +#include "Defines.h" #include #include @@ -43,9 +38,13 @@ namespace GSM { +// From GSM 05.02 6.5. +const unsigned sMax_BS_PA_MFRMS = 9; + class CCCHLogicalChannel; class SDCCHLogicalChannel; +class CBCHLogicalChannel; class TCHFACCHLogicalChannel; class CCCHList : public std::vector {}; @@ -73,6 +72,7 @@ class GSMConfig { CCCHList mPCHPool; ///< paging CCCH subchannels //@} + CBCHLogicalChannel* mCBCH; /**@name Allocatable channel pools. */ //@{ @@ -86,7 +86,7 @@ class GSMConfig { unsigned mBCC; ///< basestation color code //@} - GSMBand mBand; ///< BTS operating band + GSMBand mBand; ///< BTS operating band, or 0 for custom band Clock mClock; ///< local copy of BTS master clock @@ -96,6 +96,7 @@ class GSMConfig { L2Frame mSI2Frame; L2Frame mSI3Frame; L2Frame mSI4Frame; + L2Frame mSI13Frame; // pat added for GPRS //@} /**@name Encoded L3 frames to be sent on the SACCH. */ @@ -104,6 +105,16 @@ class GSMConfig { L3Frame mSI6Frame; //@} + /**@name Copies of system information messages as they were most recently generated. */ + //@{ + L3SystemInformationType1* mSI1; + L3SystemInformationType2* mSI2; + L3SystemInformationType3* mSI3; + L3SystemInformationType4* mSI4; + L3SystemInformationType5* mSI5; + L3SystemInformationType6* mSI6; + //@} + int mT3122; time_t mStartTime; @@ -115,12 +126,18 @@ class GSMConfig { InterthreadQueue mChannelRequestQueue; Thread mAccessGrantThread; - public: + unsigned mChangemark; - GSMConfig(); + void crackPagingFromImsi(unsigned imsiMod1000,unsigned &ccch_group,unsigned &paging_Index);; + + public: + + + GSMConfig(); + /** Initialize with parameters from gConfig. */ void init(); @@ -133,12 +150,22 @@ class GSMConfig { const L2Frame& SI2Frame() const { return mSI2Frame; } const L2Frame& SI3Frame() const { return mSI3Frame; } const L2Frame& SI4Frame() const { return mSI4Frame; } + const L2Frame& SI13Frame() const { return mSI13Frame; } // pat added for GPRS //@} /**@name Get references to L3 frames for SACCH SI messages. */ //@{ const L3Frame& SI5Frame() const { return mSI5Frame; } const L3Frame& SI6Frame() const { return mSI6Frame; } //@} + /**@name Get the messages themselves. */ + //@{ + const L3SystemInformationType1* SI1() const { return mSI1; } + const L3SystemInformationType2* SI2() const { return mSI2; } + const L3SystemInformationType3* SI3() const { return mSI3; } + const L3SystemInformationType4* SI4() const { return mSI4; } + const L3SystemInformationType5* SI5() const { return mSI5; } + const L3SystemInformationType6* SI6() const { return mSI6; } + //@} /** Get the current master clock value. */ Time time() const { return mClock.get(); } @@ -151,6 +178,7 @@ class GSMConfig { unsigned NCC() const { return mNCC; } GSM::Clock& clock() { return mClock; } const L3LocationAreaIdentity& LAI() const { return mLAI; } + unsigned changemark() const { return mChangemark; } //@} /** Return the BSIC, NCC:BCC. */ @@ -162,6 +190,12 @@ class GSMConfig { */ void regenerateBeacon(); + /** + SI5 is generated separately because it may get random + neighbors added each time it's sent. + */ + void regenerateSI5(); + /** Hold off on channel allocations; don't answer RACH. @param val true to hold, false to clear hold @@ -196,8 +230,28 @@ class GSMConfig { void addPCH(CCCHLogicalChannel* wCCCH) { mPCHPool.push_back(wCCCH); } /** Return a minimum-load AGCH. */ + // (pat) TODO: This strategy needs to change. + // There needs to be a common message queue for all CCCH timeslots from which the + // FEC can pull the next AGCH message if there is no paging message at that paging slot. + // And if someone besides pat works on this, note that gprs also wants + // to be able cancel messages after sending them in case conditions have changed, + // and also needs to know, a-priori, the exact frame number when the message + // is going to be sent, none of which works properly at the moment. CCCHLogicalChannel* getAGCH() { return minimumLoad(mAGCHPool); } +#if ENABLE_PAGING_CHANNELS + ///< (pat) Send a paging message for the specified imsi. + // This function should be used instead of getPCH(), etc. which should then be made private. + void sendPCH(const L3RRMessage& msg,unsigned imsiMod1000); + + ///< (pat) Return the approximate time of the next PCH message for this imsi. + // This routine should be elided after DRX mode in GPRS is fixed. + Time getPchSendTime(imsiMod1000); + + ///< (pat) Send a message on the avail AGCH. + void sendAGCH(const L3RRMessage& msg); +#endif + /** Return a minimum-load PCH. */ CCCHLogicalChannel* getPCH() { return minimumLoad(mPCHPool); } @@ -224,6 +278,17 @@ class GSMConfig { //@} + /**@ Manage the CBCH. */ + //@{ + + /** The add method is not mutex protected and should only be used during initialization. */ + void addCBCH(CBCHLogicalChannel *wCBCH) + { assert(mCBCH==NULL); mCBCH=wCBCH; } + + CBCHLogicalChannel* getCBCH() { return mCBCH; } + //@} + + /**@name Manage SDCCH Pool. */ //@{ /** The add method is not mutex protected and should only be used during initialization. */ @@ -245,11 +310,12 @@ class GSMConfig { /** The add method is not mutex protected and should only be used during initialization. */ void addTCH(TCHFACCHLogicalChannel *wTCH) { mTCHPool.push_back(wTCH); } /** Return a pointer to a usable channel. */ - TCHFACCHLogicalChannel *getTCH(); + TCHFACCHLogicalChannel *getTCH(bool forGPRS=false, bool onlyCN0=false); + int getTCHGroup(int groupSize,TCHFACCHLogicalChannel **results); /** Return true if an TCH is available, but do not allocate it. */ size_t TCHAvailable() const; /** Return number of total TCH. */ - unsigned TCHTotal() const { return mTCHPool.size(); } + unsigned TCHTotal() const; /** Return number of active TCH. */ unsigned TCHActive() const; /** Just a reference to the TCH pool. */ @@ -271,6 +337,9 @@ class GSMConfig { void createCombinationI(TransceiverManager &TRX, unsigned CN, unsigned TN); /** Combination VII is 8 SDCCHs. */ void createCombinationVII(TransceiverManager &TRX, unsigned CN, unsigned TN); + /** Combination XIII is a GPRS PDTCH: PDTCH/F+PACCH/F+PTCCH/F */ + // pat todo: This does not exist yet. + void createCombinationXIII(TransceiverManager &TRX, unsigned CN, unsigned TN); //@} /** Return number of seconds since starting. */ diff --git a/GSM/GSML1FEC.cpp b/GSM/GSML1FEC.cpp index ae86825f..650c6de5 100644 --- a/GSM/GSML1FEC.cpp +++ b/GSM/GSML1FEC.cpp @@ -1,25 +1,18 @@ /* * Copyright 2008-2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -27,16 +20,21 @@ #include "GSML1FEC.h" #include "GSMCommon.h" +#include "GSMTransfer.cpp" #include "GSMSAPMux.h" #include "GSMConfig.h" #include "GSMTDMA.h" #include "GSMTAPDump.h" #include +#include #include #include #include #include #include +#include + +#include "../GPRS/GPRSExport.h" #undef WARNING @@ -126,7 +124,9 @@ static const int powerCommand1900[32] = const int* pickTable() { - switch (gBTS.band()) { + unsigned band = gBTS.band(); + + switch (band) { case GSM850: case EGSM900: return powerCommandLowBand; @@ -183,16 +183,18 @@ unsigned encodePower(int power) L1Encoder::L1Encoder(unsigned wCN, unsigned wTN, const TDMAMapping& wMapping, L1FEC *wParent) :mDownstream(NULL), - mCN(wCN),mTN(wTN), mMapping(wMapping), - mTSC(gBTS.BCC()), // Note that TSC is hardcoded to the BCC. + mCN(wCN),mTN(wTN), + mTSC(gBTS.BCC()), // Note that TSC (Training Sequence Code) is hardcoded to the BCC. mParent(wParent), mTotalBursts(0), mPrevWriteTime(gBTS.time().FN(),wTN), mNextWriteTime(gBTS.time().FN(),wTN), - mRunning(false),mActive(false) + mRunning(false),mActive(false), + mEncrypted(ENCRYPT_NO), + mEncryptionAlgorithm(0) { - assert(mCNwriteHighSide(mFillerBurst); + mDownstream->writeHighSideTx(mFillerBurst,"idle"); rollForward(); } } @@ -312,6 +322,42 @@ unsigned L1Encoder::ARFCN() const return mDownstream->ARFCN(); } +int unhex(const char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + assert(0); +} + + +// Given IMSI, copy Kc. Return true iff there *is* a Kc. +bool imsi2kc(string wIMSI, unsigned char *wKc) +{ + string kc = gTMSITable.getKc(wIMSI.c_str()); + if (kc.length() == 0) return false; + while (kc.length() < 16) { + kc = '0' + kc; + } + assert(kc.length() == 16); + unsigned char *dst = wKc; + for (size_t p = 0; p < kc.length(); p += 2) { + *dst++ = (unhex(kc[p]) << 4) | (unhex(kc[p+1])); + } + return true; +} + + +// Turn on encryption phase-in, which is watching for bad frames and +// retrying them with encryption. +// Return false and leave encryption off if there's no Kc. +bool L1Decoder::decrypt_maybe(string wIMSI, int wA5Alg) +{ + if (!imsi2kc(wIMSI, mKc)) return false; + mEncrypted = ENCRYPT_MAYBE; + mEncryptionAlgorithm = wA5Alg; + return true; +} unsigned L1Decoder::ARFCN() const @@ -329,6 +375,7 @@ TypeAndOffset L1Decoder::typeAndOffset() const void L1Decoder::open() { + handoverPending(false); ScopedLock lock(mLock); if (!mRunning) start(); mFER=0.0F; @@ -336,6 +383,12 @@ void L1Decoder::open() mT3109.reset(); mT3101.set(); mActive = true; + // Turning off encryption when the channel closes would be a nightmare + // (catching all the ways, and performing the handshake under less than + // ideal conditions), so we leave encryption on to the bitter end, + // then clear the encryption flag here, when the channel gets reused. + mEncrypted = ENCRYPT_NO; + mEncryptionAlgorithm = 0; } @@ -386,7 +439,7 @@ void L1Decoder::countGoodFrame() static const float a = 1.0F / ((float)mFERMemory); static const float b = 1.0F - a; mFER *= b; - OBJLOG(DEBUG) <<"L1Decoder FER=" << mFER; + OBJLOG(INFO) <<"L1Decoder FER=" << mFER; } @@ -395,12 +448,36 @@ void L1Decoder::countBadFrame() static const float a = 1.0F / ((float)mFERMemory); static const float b = 1.0F - a; mFER = b*mFER + a; - OBJLOG(DEBUG) <<"L1Decoder FER=" << mFER; + OBJLOG(INFO) <<"L1Decoder FER=" << mFER; +} + + + + +void L1Encoder::handoverPending(bool flag) +{ + if (flag) { + bool ok = mDownstream->setHandover(mTN); + if (!ok) LOG(ALERT) << "handover setup failed"; + } else { + bool ok = mDownstream->clearHandover(mTN); + if (!ok) LOG(ALERT) << "handover clear failed"; + } } +void L1FEC::handoverPending(bool flag) +{ + assert(mEncoder); + assert(mDecoder); + mEncoder->handoverPending(flag); + mDecoder->handoverPending(flag); +} +// (pat) This can only be called during initialization, because installDecoder aborts +// if it is called twice on the same channel. +// This routine is used for traffic channels. void L1FEC::downstream(ARFCNManager* radio) { if (mEncoder) mEncoder->downstream(radio); @@ -443,7 +520,7 @@ void RACHL1Decoder::serviceLoop() RxBurst *rx = mQ.read(); // Yes, if we wait long enough that read will timeout. if (rx==NULL) continue; - writeLowSide(*rx); + writeLowSideRx(*rx); delete rx; } } @@ -465,7 +542,12 @@ void RACHL1Decoder::start() -void RACHL1Decoder::writeLowSide(const RxBurst& burst) +// (pat) Note that GPRS has an option to use a PRACH burst identical to RACH bursts. +// (pat) This routine is fed immediately from the radio in TRXManager; +// wDemuxTable points to this routine. +// The RACH is enqueued, and a separate thread runs AccessGrantResponder on the RACH, +// although I'm not sure why. +void RACHL1Decoder::writeLowSideRx(const RxBurst& burst) { // The L1 FEC for the RACH is defined in GSM 05.03 4.6. @@ -522,44 +604,132 @@ void RACHL1Decoder::writeLowSide(const RxBurst& burst) - XCCHL1Decoder::XCCHL1Decoder( unsigned wCN, unsigned wTN, const TDMAMapping& wMapping, L1FEC *wParent) - :L1Decoder(wCN,wTN,wMapping,wParent), - mBlockCoder(0x10004820009ULL, 40, 224), - mC(456), mU(228), - mP(mU.segment(184,40)),mDP(mU.head(224)),mD(mU.head(184)) + :L1Decoder(wCN,wTN,wMapping,wParent) +{ +} + +SharedL1Decoder::SharedL1Decoder() + : mBlockCoder(0x10004820009ULL, 40, 224), + mC(456), + mU(228), + mP(mU.segment(184,40)),mDP(mU.head(224)),mD(mU.head(184)), + mHParity(0x06f,6,8),mHU(18),mHD(mHU.head(8)) { for (int i=0; i<4; i++) { + mE[i] = SoftVector(114); mI[i] = SoftVector(114); // Fill with zeros just to make Valgrind happy. + mE[i].fill(.0); mI[i].fill(.0); } } -void XCCHL1Decoder::writeLowSide(const RxBurst& inBurst) +void XCCHL1Decoder::writeLowSideRx(const RxBurst& inBurst) { OBJLOG(DEBUG) <<"XCCHL1Decoder " << inBurst; // If the channel is closed, ignore the burst. if (!active()) { OBJLOG(DEBUG) <<"XCCHL1Decoder not active, ignoring input"; return; - } + } + // save frame number for possible decrypting + int B = mMapping.reverseMapping(inBurst.time().FN()) % 4; + mFN[B] = inBurst.time().FN(); + // Accept the burst into the deinterleaving buffer. // Return true if we are ready to interleave. if (!processBurst(inBurst)) return; + if (mEncrypted == ENCRYPT_YES) { + decrypt(); + } + if (mEncrypted == ENCRYPT_MAYBE) { + saveMi(); + } deinterleave(); if (decode()) { countGoodFrame(); mD.LSB8MSB(); handleGoodFrame(); } else { - countBadFrame(); + if (mEncrypted == ENCRYPT_MAYBE) { + // We don't want to start decryption until we get the (encrypted) layer 2 acknowledgement + // of the Ciphering Mode Command, so we start maybe decrypting when we send the command, + // and when the frame comes along, we'll see that it doesn't pass normal decoding, but + // when we try again with decryption, it will pass. Unless it's just noise. + OBJLOG(DEBUG) << "XCCHL1Decoder: try decoding again with decryption"; + restoreMi(); + decrypt(); + deinterleave(); + if (decode()) { + OBJLOG(DEBUG) << "XCCHL1Decoder: success on 2nd try"; + // We've successfully decoded an encrypted frame. Start decrypting all uplink frames. + mEncrypted = ENCRYPT_YES; + // Also start encrypting downlink frames. + parent()->encoder()->mEncrypted = ENCRYPT_YES; + parent()->encoder()->mEncryptionAlgorithm = mEncryptionAlgorithm; + countGoodFrame(); + mD.LSB8MSB(); + handleGoodFrame(); + } else { + countBadFrame(); + } + } else { + countBadFrame(); + } + } +} + + +void XCCHL1Decoder::saveMi() +{ + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 114; j++) { + mE[i].settfb(j, mI[i].softbit(j)); + } + } +} + + +void XCCHL1Decoder::restoreMi() +{ + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 114; j++) { + mI[i].settfb(j, mE[i].softbit(j)); + } + } +} + + +void XCCHL1Decoder::decrypt() +{ + // decrypt y + for (int i = 0; i < 4; i++) { + unsigned char block1[15]; + unsigned char block2[15]; + // 03.20 C.1.2 + // 05.02 3.3.2.2.1 + int fn = mFN[i]; + int t1 = fn / (26*51); + int t2 = fn % 26; + int t3 = fn % 51; + int count = (t1<<11) | (t3<<5) | t2; + if (mEncryptionAlgorithm == 1) { + A51_GSM(mKc, 64, count, block1, block2); + } else { + A53_GSM(mKc, 64, count, block1, block2); + } + for (int j = 0; j < 114; j++) { + if ((block2[j/8] & (0x80 >> (j%8)))) { + mI[i].settfb(j, 1.0 - mI[i].softbit(j)); + } + } } } @@ -586,6 +756,7 @@ bool XCCHL1Decoder::processBurst(const RxBurst& inBurst) inBurst.data2().copyToSegment(mI[B],57); // If the burst index is 0, save the time + // FIXME -- This should be moved to the deinterleave methods. if (B==0) mReadTime = inBurst.time(); @@ -603,7 +774,7 @@ bool XCCHL1Decoder::processBurst(const RxBurst& inBurst) -void XCCHL1Decoder::deinterleave() +void SharedL1Decoder::deinterleave() { // Deinterleave i[][] to c[]. // This comes directly from GSM 05.03, 4.1.4. @@ -619,7 +790,7 @@ void XCCHL1Decoder::deinterleave() } -bool XCCHL1Decoder::decode() +bool SharedL1Decoder::decode() { // Apply the convolutional decoder and parity check. // Return true if we recovered a good L2 frame. @@ -639,6 +810,11 @@ bool XCCHL1Decoder::decode() OBJLOG(DEBUG) <<"XCCHL1Decoder d[]:p[]=" << mDP; unsigned syndrome = mBlockCoder.syndrome(mDP); OBJLOG(DEBUG) <<"XCCHL1Decoder syndrome=" << hex << syndrome << dec; + // Simulate high FER for testing? + if (random()%100 < gConfig.getNum("Test.GSM.SimulatedFER.Uplink")) { + LOG(NOTICE) << "simulating dropped uplink frame at " << mReadTime; + return false; + } return (syndrome==0); } @@ -665,9 +841,17 @@ void XCCHL1Decoder::handleGoodFrame() OBJLOG(DEBUG) <<"XCCHL1Decoder d[]=" << mD; if (mUpstream) { + // Are we fuzzing ourselves? + if (random()%100 < gConfig.getNum("Test.GSM.UplinkFuzzingRate")) { + size_t i = random() % mD.size(); + mD[i] = 1 - mD[i]; + LOG(NOTICE) << "fuzzing input frame, flipped bit " << i; + } // Send all bits to GSMTAP - gWriteGSMTAP(ARFCN(),TN(),mReadTime.FN(), - typeAndOffset(),mMapping.repeatLength()>51,true,mD); + if (gConfig.getBool("Control.GSMTAP.GSM")) { + // FIXME -- This repeatLengh>51 is a bit of a hack. + gWriteGSMTAP(ARFCN(),TN(),mReadTime.FN(),typeAndOffset(),mMapping.repeatLength()>51,true,mD); + } // Build an L2 frame and pass it up. const BitVector L2Part(mD.tail(headerOffset())); OBJLOG(DEBUG) <<"XCCHL1Decoder L2=" << L2Part; @@ -678,17 +862,6 @@ void XCCHL1Decoder::handleGoodFrame() } -float SACCHL1Decoder::RSSI() const -{ - float sum=mRSSI[0]+mRSSI[1]+mRSSI[2]+mRSSI[3]; - return 0.25F*sum; -} - -float SACCHL1Decoder::timingError() const -{ - float sum=mTimingError[0]+mTimingError[1]+mTimingError[2]+mTimingError[3]; - return 0.25F*sum; -} @@ -702,16 +875,16 @@ bool SACCHL1Decoder::processBurst(const RxBurst& inBurst) // The actual phone settings change every 4 bursts, // so average over all 4. // RSSI is dB wrt full scale. - mRSSI[mRSSICounter] = inBurst.RSSI(); + mRSSI = inBurst.RSSI(); // Timing error is a float in symbol intervals. - mTimingError[mRSSICounter] = inBurst.timingError(); + mTimingError = inBurst.timingError(); + // Timestamp + mTimestamp = gBTS.clock().systime(inBurst.time()); OBJLOG(INFO) << "SACCHL1Decoder " << " RSSI=" << inBurst.RSSI() + << " timestamp=" << mTimestamp << " timingError=" << inBurst.timingError(); - mRSSICounter++; - if (mRSSICounter>3) mRSSICounter=0; - return XCCHL1Decoder::processBurst(inBurst); } @@ -728,8 +901,17 @@ void SACCHL1Decoder::handleGoodFrame() } - - +// Process the 184 bit frame, starting at offset, add parity, encode. +// Result is left in mI, representing 4 radio bursts. +void SharedL1Encoder::encodeFrame41(const BitVector &src, int offset, bool copy) +{ + if (copy) src.copyToSegment(mU,offset); + OBJLOG(DEBUG) << "XCCHL1Encoder before d[]=" << mD; + mD.LSB8MSB(); + OBJLOG(DEBUG) << "XCCHL1Encoder after d[]=" << mD; + encode41(); + interleave41(); +} XCCHL1Encoder::XCCHL1Encoder( @@ -737,31 +919,52 @@ XCCHL1Encoder::XCCHL1Encoder( unsigned wTN, const TDMAMapping& wMapping, L1FEC* wParent) - :L1Encoder(wCN,wTN,wMapping,wParent), - mBlockCoder(0x10004820009ULL, 40, 224), - mC(456), mU(228), - mD(mU.head(184)),mP(mU.segment(184,40)) + : SharedL1Encoder(), + L1Encoder(wCN,wTN,wMapping,wParent) { - // Set up the interleaving buffers. - for(int k = 0; k<4; k++) { - mI[k] = BitVector(114); - // Fill with zeros just to make Valgrind happy. - mI[k].fill(0); - } - mFillerBurst = TxBurst(gDummyBurst); // Set up the training sequence and stealing bits // since they'll be the same for all bursts. // stealing bits for a control channel, GSM 05.03 4.2.5, 05.02 5.2.3. - mBurst.Hl(1); - mBurst.Hu(1); + // (pat) For a GPRS channel these bits are used for the encoding, 1,1 implies CS-1. + // Since we will be sharing the channels between GSM and GPRS, we cannot depend + // on this preinitialization surviving. This is so minor, I am just going + // to set these anew inside transmit(). + //mBurst.Hl(1); + //mBurst.Hu(1); + // training sequence, GSM 05.02 5.2.3 gTrainingSequence[mTSC].copyToSegment(mBurst,61); +} + - // zero out u[] to take care of tail fields - mU.zero(); +// Default initialization is as for XCCH channels (SACCH) or CS-1 encoding. +// Pat says: From GSM04.03sec5.1 the 40 bit parity is generated by the polynomial: +// g(D) = (D23 + 1)*(D17 + D3 + 1) = 1 + D3 + D17 + D23 + D26 + D40, +// which are the bits set in Parity initialization below. +void SharedL1Encoder::initInterleave(int mIsize) +{ + // Set up the interleaving buffers. + for(int k = 0; k51,false,mU); + } - // Send to GSMTAP (must send mU = real bits !) - gWriteGSMTAP(ARFCN(),TN(),mNextWriteTime.FN(), - typeAndOffset(),mMapping.repeatLength()>51,false,mU); - // Encode data into bursts - OBJLOG(DEBUG) << "XCCHL1Encoder d[]=" << mD; - mD.LSB8MSB(); - OBJLOG(DEBUG) << "XCCHL1Encoder d[]=" << mD; - encode(); // Encode u[] to c[], GSM 05.03 4.1.2 and 4.1.3. - interleave(); // Interleave c[] to i[][], GSM 05.03 4.1.4. - transmit(); // Send the bursts to the radio, GSM 05.03 4.1.5. + // Copy the L2 frame into u[] for processing. + // GSM 05.03 4.1.1. + //LOG(DEBUG) << "mU=" << mU.inspect(); + //LOG(DEBUG) << "mD=" << mD.inspect(); + // Process the 184 bit frame, leave result in mI. + //mFECEnc.encodeFrame41(frame,headerOffset(),mFECEnc.mVCoder); + encodeFrame41(frame,headerOffset(), false); + const int qCS1[8] = { 1,1,1,1,1,1,1,1 }; // magically identifies CS-1. + transmit(mI,mE,qCS1); } - -void XCCHL1Encoder::encode() +void SharedL1Encoder::encode41() { // Perform the FEC encoding of GSM 05.03 4.1.2 and 4.1.3 @@ -853,7 +1055,7 @@ void XCCHL1Encoder::encode() -void XCCHL1Encoder::interleave() +void SharedL1Encoder::interleave41() { // GSM 05.03, 4.1.4. Verbatim. for (int k=0; k<456; k++) { @@ -865,11 +1067,16 @@ void XCCHL1Encoder::interleave() -void XCCHL1Encoder::transmit() +// (pat) This code is not used for gprs, but part of the L1Encoder is now shared +// with gprs, and the stealing bits may be modified if the channel is used for +// gprs, so this function is modified to set the stealing bits properly +// before each transmission, rather than having them be static. +// The qbits, also called stealing bits, are defined in GSM05.03. +// For GPRS they specify the encoding type: CS-1 through CS-4. +void L1Encoder::transmit(BitVector *mI, BitVector *mE, const int *qbits) { // Format the bits into the bursts. // GSM 05.03 4.1.5, 05.02 5.2.3 - waitToSend(); // Don't get too far ahead of the clock. if (!mDownstream) { @@ -879,15 +1086,60 @@ void XCCHL1Encoder::transmit() return; } - for (int B=0; B<4; B++) { + // add noise + // the noise insertion happens below, merged in with the ciphering + int p = gConfig.getFloat("GSM.Cipher.CCHBER") * (float)0xFFFFFF; + + for (int qi=0,B=0; B<4; B++) { mBurst.time(mNextWriteTime); + // encrypt y + if (mEncrypted == ENCRYPT_YES) { + unsigned char block1[15]; + unsigned char block2[15]; + unsigned char *kc = parent()->decoder()->kc(); + // 03.20 C.1.2 + // 05.02 3.3.2.2.1 + int fn = mNextWriteTime.FN(); + int t1 = fn / (26*51); + int t2 = fn % 26; + int t3 = fn % 51; + int count = (t1<<11) | (t3<<5) | t2; + if (mEncryptionAlgorithm == 1) { + A51_GSM(kc, 64, count, block1, block2); + } else { + A53_GSM(kc, 64, count, block1, block2); + } + for (int i = 0; i < 114; i++) { + int b = p ? (random() & 0xFFFFFF) < p : 0; + b = b ^ (block1[i/8] >> (7-(i%8))); + mE[B].settfb(i, mI[B].bit(i) ^ (b&1)); + } + } else { + if (p) { + for (int i = 0; i < 114; i++) { + int b = (random() & 0xFFFFFF) < p; + mE[B].settfb(i, mI[B].bit(i) ^ b); + } + } else { + // no noise or encryption. use mI below. + } + } + // Copy in the "encrypted" bits, GSM 05.03 4.1.5, 05.02 5.2.3. - OBJLOG(DEBUG) << "XCCHL1Encoder mI["<writeHighSide(mBurst); + mDownstream->writeHighSideTx(mBurst,"SCH"); rollForward(); } @@ -983,7 +1235,7 @@ void FCCHL1Encoder::generate() resync(); for (int i=0; i<5; i++) { mBurst.time(mNextWriteTime); - mDownstream->writeHighSide(mBurst); + mDownstream->writeHighSideTx(mBurst,"FCCH"); rollForward(); } sleep(1); @@ -1023,12 +1275,15 @@ void BCCHL1Encoder::generate() OBJLOG(DEBUG) << "BCCHL1Encoder " << mNextWriteTime; // BCCH mapping, GSM 05.02 6.3.1.3 // Since we're not doing GPRS or VGCS, it's just SI1-4 over and over. + // pat 8-2011: If we are doing GPRS, the SI13 must be in slot 4. switch (mNextWriteTime.TC()) { + // (pat) Maps to: XCCHL1Encoder::writeHighSide. case 0: writeHighSide(gBTS.SI1Frame()); return; case 1: writeHighSide(gBTS.SI2Frame()); return; case 2: writeHighSide(gBTS.SI3Frame()); return; case 3: writeHighSide(gBTS.SI4Frame()); return; - case 4: writeHighSide(gBTS.SI3Frame()); return; + case 4: writeHighSide(GPRS::GPRSConfig::IsEnabled() ? gBTS.SI13Frame() : gBTS.SI3Frame()); + return; case 5: writeHighSide(gBTS.SI2Frame()); return; case 6: writeHighSide(gBTS.SI3Frame()); return; case 7: writeHighSide(gBTS.SI4Frame()); return; @@ -1050,30 +1305,74 @@ TCHFACCHL1Decoder::TCHFACCHL1Decoder( mTCHParity(0x0b,3,50) { for (int i=0; i<8; i++) { + mE[i] = SoftVector(114); mI[i] = SoftVector(114); // Fill with zeros just to make Valgrind happy. mI[i].fill(.0); + mE[i].fill(.0); } } -void TCHFACCHL1Decoder::writeLowSide(const RxBurst& inBurst) +void TCHFACCHL1Decoder::writeLowSideRx(const RxBurst& inBurst) { + L1FEC *fparent = parent(); + if (fparent->mGprsReserved) { // Channel is reserved for gprs. + if (parent()->mGPRSFEC) { // If set, bursts are delivered to this FEC in GPRS. + GPRS::GPRSWriteLowSideRx(inBurst, parent()->mGPRSFEC); + } + return; // done + } OBJLOG(DEBUG) << "TCHFACCHL1Decoder " << inBurst; // If the channel is closed, ignore the burst. if (!active()) { OBJLOG(DEBUG) << "TCHFACCHL1Decoder not active, ignoring input"; return; } + if (mHandoverPending) { + // If this channel is waiting for an inbound handover, + // try to decode a handover access burst. + // GSM 05.03 4.9, 4.6 + // Based on the RACHL1Decoder. + + LOG(DEBUG) << "handover access " << inBurst; + + // Decode the burst. + const SoftVector e(inBurst.segment(49,36)); + e.decode(mVCoder,mHU); + LOG(DEBUG) << "handover access U=" << mHU; + // Check the tail bits -- should all the zero. + if (mHU.peekField(14,4)) return; + // Check the parity. + unsigned sentParity = ~mHU.peekField(8,6); + unsigned checkParity = mHD.parity(mHParity); + unsigned encodedBSIC = (sentParity ^ checkParity) & 0x03f; + LOG(DEBUG) << "handover access sentParity " << sentParity + << " checkParity " << checkParity + << " endcodedBSIC " << encodedBSIC; + if (encodedBSIC != gBTS.BSIC()) return; + // OK. So we got a burst. + mT3103.reset(); + mHD.LSB8MSB(); + unsigned ref = mHD.peekField(0,8); + LOG(INFO) << "handover access ref=" << ref; + + if (!Control::SaveHandoverAccess(ref,inBurst.RSSI(),inBurst.timingError(),inBurst.time())) return; + mUpstream->writeLowSide(HANDOVER_ACCESS); + return; + } processBurst(inBurst); } - - +// (pat) How the burst gets here: +// TRXManager.cpp has a wDemuxTable for each frame+timeslot with a pointer to +// a virtual L1Decoder::writeLowSideRx() function. For traffic channels, this maps to +// XCCHL1Decoder::writeLowSideRx(), which checks active(), and if true, +// then calls this, and if this returns true, goes ahead with decoding. bool TCHFACCHL1Decoder::processBurst( const RxBurst& inBurst) { // Accept the burst into the deinterleaving buffer. @@ -1095,10 +1394,21 @@ bool TCHFACCHL1Decoder::processBurst( const RxBurst& inBurst) inBurst.data1().copyToSegment(mI[B],0); inBurst.data2().copyToSegment(mI[B],57); + // save the frame numbers for each burst for possible decryption later + mFN[B] = inBurst.time().FN(); + // Every 4th frame is the start of a new block. // So if this isn't a "4th" frame, return now. if (B%4!=3) return false; + if (mEncrypted == ENCRYPT_MAYBE) { + saveMi(); + } + + if (mEncrypted == ENCRYPT_YES) { + decrypt(B); + } + // Deinterleave according to the diagonal "phase" of B. // See GSM 05.03 3.1.3. // Deinterleaves i[] to c[] @@ -1109,10 +1419,34 @@ bool TCHFACCHL1Decoder::processBurst( const RxBurst& inBurst) bool stolen = inBurst.Hl(); OBJLOG(DEBUG) <<"TCHFACCHL1Decoder Hl=" << inBurst.Hl() << " Hu=" << inBurst.Hu(); if (stolen) { - if (decode()) { + bool ok = decode(); + if (!ok && mEncrypted == ENCRYPT_MAYBE) { + // We don't want to start decryption until we get the (encrypted) layer 2 acknowledgement + // of the Ciphering Mode Command, so we start maybe decrypting when we send the command, + // and when the frame comes along, we'll see that it doesn't pass normal decoding, but + // when we try again with decryption, it will pass. Unless it's just noise. + OBJLOG(DEBUG) << "TCHFACCHL1Decoder: try decoding again with decryption"; + restoreMi(); + decrypt(-1); + // re-deinterleave + if (B==3) deinterleave(4); + else deinterleave(0); + // re-decode + ok = decode(); + if (ok) { + OBJLOG(DEBUG) << "TCHFACCHL1Decoder: success on 2nd try"; + // We've successfully decoded an encrypted frame. Start decrypting all uplink frames. + mEncrypted = ENCRYPT_YES; + // Also start encrypting downlink frames. + parent()->encoder()->mEncrypted = ENCRYPT_YES; + parent()->encoder()->mEncryptionAlgorithm = mEncryptionAlgorithm; + } + } + if (ok) { OBJLOG(DEBUG) <<"TCHFACCHL1Decoder good FACCH frame"; countGoodFrame(); mD.LSB8MSB(); + // This also resets T3109. handleGoodFrame(); } else { OBJLOG(DEBUG) <<"TCHFACCHL1Decoder bad FACCH frame"; @@ -1136,6 +1470,52 @@ bool TCHFACCHL1Decoder::processBurst( const RxBurst& inBurst) } +void TCHFACCHL1Decoder::saveMi() +{ + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 114; j++) { + mE[i].settfb(j, mI[i].softbit(j)); + } + } +} + +void TCHFACCHL1Decoder::restoreMi() +{ + for (int i = 0; i < 8; i++) { + for (int j = 0; j < 114; j++) { + mI[i].settfb(j, mE[i].softbit(j)); + } + } +} + + +void TCHFACCHL1Decoder::decrypt(int B) +{ + // decrypt x + unsigned char block1[15]; + unsigned char block2[15]; + int bb = B==7 ? 4 : 0; + int be = B<0 ? 8 : bb+4; + for (int i = bb; i < be; i++) { + // 03.20 C.1.2 + // 05.02 3.3.2.2.1 + int fn = mFN[i]; + int t1 = fn / (26*51); + int t2 = fn % 26; + int t3 = fn % 51; + int count = (t1<<11) | (t3<<5) | t2; + if (mEncryptionAlgorithm == 1) { + A51_GSM(mKc, 64, count, block1, block2); + } else { + A53_GSM(mKc, 64, count, block1, block2); + } + for (int j = 0; j < 114; j++) { + if ((block2[j/8] & (0x80 >> (j%8)))) { + mI[i].settfb(j, 1.0 - mI[i].softbit(j)); + } + } + } +} void TCHFACCHL1Decoder::deinterleave(int blockOffset ) @@ -1157,6 +1537,12 @@ bool TCHFACCHL1Decoder::decodeTCH(bool stolen) { // GSM 05.02 3.1.2, but backwards + // Simulate high FER for testing? + if (random()%100 < gConfig.getNum("Test.GSM.SimulatedFER.Uplink")) { + LOG(DEBUG) << "simulating dropped uplink vocoder frame at " << mReadTime; + stolen = true; + } + // If the frame wasn't stolen, we'll update this with parity later. bool good = !stolen; @@ -1203,23 +1589,33 @@ bool TCHFACCHL1Decoder::decodeTCH(bool stolen) mTCHD.unmap(g610BitOrder,260,payload); mVFrame.pack(newFrame); // Save a copy for bad frame processing. - memcpy(mPrevGoodFrame,newFrame,33); + mPrevGoodFrame.clone(mVFrame); } } + // We end up here for bad frames. + // We also jump here directly for stolen frames. if (!good) { // Bad frame processing, GSM 06.11. - // Attenuate block amplitudes and andomize grid positions. - char rawByte = mPrevGoodFrame[27]; - unsigned xmaxc = rawByte & 0x01f; - if (xmaxc>2) xmaxc -= 2; - else xmaxc = 0; + // Attenuate block amplitudes and randomize grid positions. + // The spec give the bit-packing format in GSM 06.10 1.7. + // Note they start counting bits from 1, not 0. + BitVector vbits = mPrevGoodFrame.payload(); + // Get avg xmax value from previous good frame. + int xmax = 0; for (unsigned i=0; i<4; i++) { - unsigned pos = random() % 4; - mPrevGoodFrame[6+7*i] = (rawByte & 0x80) | pos | xmaxc; - mPrevGoodFrame[7+7*i] &= 0x7F; + xmax += vbits.peekField(48+i*56-1,6); } - memcpy(newFrame,mPrevGoodFrame,33); + xmax /= 4; + // "Age" the frame. + for (unsigned i=0; i<4; i++) { + // decrement xmax + if (xmax>0) xmax--; + vbits.fillField(48+i*56-1,xmax,6); + // randomize grid positions + vbits.fillField(46+i*56-1,random(),2); + } + mPrevGoodFrame.pack(newFrame); } // Good or bad, we must feed the speech channel. @@ -1251,15 +1647,17 @@ TCHFACCHL1Encoder::TCHFACCHL1Encoder( const TDMAMapping& wMapping, L1FEC *wParent) :XCCHL1Encoder(wCN, wTN, wMapping, wParent), - mPreviousFACCH(false),mOffset(0), + mPreviousFACCH(true),mOffset(0), mTCHU(189),mTCHD(260), mClass1_c(mC.head(378)),mClass1A_d(mTCHD.head(50)),mClass2_d(mTCHD.segment(182,78)), mTCHParity(0x0b,3,50) { for(int k = 0; k<8; k++) { mI[k] = BitVector(114); + mE[k] = BitVector(114); // Fill with zeros just to make Valgrind happy. mI[k].fill(0); + mE[k].fill(0); } } @@ -1281,6 +1679,7 @@ void TCHFACCHL1Encoder::open() // There was over stuff here at one time to justify overriding the default. // But it's gone now. XCCHL1Encoder::open(); + mPreviousFACCH = true; } @@ -1324,6 +1723,11 @@ void TCHFACCHL1Encoder::encodeTCH(const VocoderFrame& vFrame) void TCHFACCHL1Encoder::sendFrame( const L2Frame& frame ) { OBJLOG(DEBUG) << "TCHFACCHL1Encoder " << frame; + // Simulate high FER for testing. + if (random()%100 < gConfig.getNum("Test.GSM.SimulatedFER.Downlink")) { + LOG(NOTICE) << "simulating dropped downlink frame at " << mNextWriteTime; + return; + } mL2Q.write(new L2Frame(frame)); } @@ -1358,20 +1762,24 @@ void TCHFACCHL1Encoder::dispatch() // Since Asterisk is local, latency should be small. OBJLOG(DEBUG) <<"TCHFACCHL1Encoder speechQ.size=" << mSpeechQ.size(); int maxQ = gConfig.getNum("GSM.MaxSpeechLatency"); - while (mSpeechQ.size() > maxQ) delete mSpeechQ.read(); + while ((int)mSpeechQ.size() > maxQ) delete mSpeechQ.read(); // Send, by priority: (1) FACCH, (2) TCH, (3) filler. if (L2Frame *fFrame = mL2Q.readNoBlock()) { OBJLOG(DEBUG) <<"TCHFACCHL1Encoder FACCH " << *fFrame; currentFACCH = true; + // Send to GSMTAP + if (gConfig.getBool("Control.GSMTAP.GSM")) { + gWriteGSMTAP(ARFCN(),TN(),mNextWriteTime.FN(),typeAndOffset(),mMapping.repeatLength()>51,false,*fFrame); + } // Copy the L2 frame into u[] for processing. // GSM 05.03 4.1.1. fFrame->LSB8MSB(); fFrame->copyTo(mU); // Encode u[] to c[], GSM 05.03 4.1.2 and 4.1.3. - encode(); - delete fFrame; + encode41(); OBJLOG(DEBUG) <<"TCHFACCHL1Encoder FACCH c[]=" << mC; + delete fFrame; // Flush the vocoder FIFO to limit latency. while (mSpeechQ.size()>0) delete mSpeechQ.read(); } else if (VocoderFrame *tFrame = mSpeechQ.readNoBlock()) { @@ -1382,14 +1790,27 @@ void TCHFACCHL1Encoder::dispatch() OBJLOG(DEBUG) <<"TCHFACCHL1Encoder TCH c[]=" << mC; } else { // We have no ready data but must send SOMETHING. - // This filler pattern was captured from a Nokia 3310, BTW. - static const BitVector fillerC("110100001000111100000000111001111101011100111101001111000000000000110111101111111110100110101010101010101010101010101010101010101010010000110000000000000000000000000000000000000000001101001111000000000000000000000000000000000000000000000000111010011010101010101010101010101010101010101010101001000011000000000000000000110100111100000000111001111101101000001100001101001111000000000000000000011001100000000000000000000000000000000000000000000000000000000001"); - fillerC.copyTo(mC); + if (!mPreviousFACCH) { + // This filler pattern was captured from a Nokia 3310, BTW. + static const BitVector fillerC("110100001000111100000000111001111101011100111101001111000000000000110111101111111110100110101010101010101010101010101010101010101010010000110000000000000000000000000000000000000000001101001111000000000000000000000000000000000000000000000000111010011010101010101010101010101010101010101010101001000011000000000000000000110100111100000000111001111101101000001100001101001111000000000000000000011001100000000000000000000000000000000000000000000000000000000001"); + fillerC.copyTo(mC); + } else { + // FIXME -- This could be a lot more efficient. + currentFACCH = true; + L2Frame frame(L2IdleFrame()); + frame.LSB8MSB(); + frame.copyTo(mU); + encode41(); + } OBJLOG(DEBUG) <<"TCHFACCHL1Encoder filler FACCH=" << currentFACCH << " c[]=" << mC; } // Interleave c[] to i[]. - interleave(mOffset); + interleave31(mOffset); + + // randomly toggle bits in control channel bursts + // the toggle happens below, merged in with the ciphering + int p = currentFACCH ? gConfig.getFloat("GSM.Cipher.CCHBER") * (float)0xFFFFFF : 0; // "mapping on a burst" // Map c[] into outgoing normal bursts, marking stealing flags as needed. @@ -1397,15 +1818,53 @@ void TCHFACCHL1Encoder::dispatch() for (int B=0; B<4; B++) { // set TDMA position mBurst.time(mNextWriteTime); + // encrypt x + if (mEncrypted == ENCRYPT_YES) { + unsigned char block1[15]; + unsigned char block2[15]; + unsigned char *kc = parent()->decoder()->kc(); + // 03.20 C.1.2 + // 05.02 3.3.2.2.1 + int fn = mNextWriteTime.FN(); + int t1 = fn / (26*51); + int t2 = fn % 26; + int t3 = fn % 51; + int count = (t1<<11) | (t3<<5) | t2; + if (mEncryptionAlgorithm == 1) { + A51_GSM(kc, 64, count, block1, block2); + } else { + A53_GSM(kc, 64, count, block1, block2); + } + for (int i = 0; i < 114; i++) { + int b = p ? (random() & 0xFFFFFF) < p : 0; + b = b ^ (block1[i/8] >> (7-(i%8))); + mE[B+mOffset].settfb(i, mI[B+mOffset].bit(i) ^ (b&1)); + } + } else { + if (p) { + for (int i = 0; i < 114; i++) { + int b = (random() & 0xFFFFFF) < p; + mE[B+mOffset].settfb(i, mI[B+mOffset].bit(i) ^ b); + } + } else { + // no noise and no encryption - use mI below + } + } // copy in the bits - mI[B+mOffset].segment(0,57).copyToSegment(mBurst,3); - mI[B+mOffset].segment(57,57).copyToSegment(mBurst,88); + if (p || mEncrypted == ENCRYPT_YES) { + mE[B+mOffset].segment(0,57).copyToSegment(mBurst,3); + mE[B+mOffset].segment(57,57).copyToSegment(mBurst,88); + } else { + // no noise and no encryption - use mI + mI[B+mOffset].segment(0,57).copyToSegment(mBurst,3); + mI[B+mOffset].segment(57,57).copyToSegment(mBurst,88); + } // stealing bits mBurst.Hu(currentFACCH); mBurst.Hl(mPreviousFACCH); // send OBJLOG(DEBUG) <<"TCHFACCHEncoder sending burst=" << mBurst; - mDownstream->writeHighSide(mBurst); + mDownstream->writeHighSideTx(mBurst,"FACCH"); rollForward(); } @@ -1419,7 +1878,7 @@ void TCHFACCHL1Encoder::dispatch() -void TCHFACCHL1Encoder::interleave(int blockOffset) +void TCHFACCHL1Encoder::interleave31(int blockOffset) { // GSM 05.03, 3.1.3 for (int k=0; k<456; k++) { @@ -1444,9 +1903,9 @@ void SACCHL1FEC::setPhy(const SACCHL1FEC& other) mSACCHEncoder->setPhy(*other.mSACCHEncoder); } -void SACCHL1FEC::setPhy(float RSSI, float timingError) +void SACCHL1FEC::setPhy(float RSSI, float timingError, double wTimestamp) { - mSACCHDecoder->setPhy(RSSI,timingError); + mSACCHDecoder->setPhy(RSSI,timingError,wTimestamp); mSACCHEncoder->setPhy(RSSI,timingError); } @@ -1466,12 +1925,13 @@ void SACCHL1Decoder::open() -void SACCHL1Decoder::setPhy(float wRSSI, float wTimingError) +void SACCHL1Decoder::setPhy(float wRSSI, float wTimingError, double wTimestamp) { // Used to initialize L1 phy parameters. - for (int i=0; i<4; i++) mRSSI[i]=wRSSI; - for (int i=0; i<4; i++) mTimingError[i]=wTimingError; - OBJLOG(INFO) << "SACCHL1Decoder RSSI=" << wRSSI << "timingError=" << wTimingError; + mRSSI=wRSSI; + mTimingError=wTimingError; + mTimestamp=wTimestamp; + OBJLOG(INFO) << "SACCHL1Decoder RSSI=" << wRSSI << " timingError=" << wTimingError << " timestamp=" << wTimestamp; } void SACCHL1Decoder::setPhy(const SACCHL1Decoder& other) @@ -1480,9 +1940,11 @@ void SACCHL1Decoder::setPhy(const SACCHL1Decoder& other) // from those of a preexisting established channel. mActualMSPower = other.mActualMSPower; mActualMSTiming = other.mActualMSTiming; - for (int i=0; i<4; i++) mRSSI[i]=other.mRSSI[i]; - for (int i=0; i<4; i++) mTimingError[i]=other.mTimingError[i]; - OBJLOG(INFO) << "SACCHL1Decoder actuals RSSI=" << mRSSI[0] << "timingError=" << mTimingError[0] + mRSSI=other.mRSSI; + mTimingError=other.mTimingError; + mTimestamp=other.mTimestamp; + OBJLOG(INFO) << "SACCHL1Decoder actuals RSSI=" << mRSSI << " timingError=" << mTimingError + << " timestamp=" << mTimestamp << " MSPower=" << mActualMSPower << " MSTiming=" << mActualMSTiming; } @@ -1602,10 +2064,9 @@ void SACCHL1Encoder::sendFrame(const L2Frame& frame) // Write physical header into mU and then call base class. // SACCH physical header, GSM 04.04 6.1, 7.1. - OBJLOG(INFO) <<"SACCHL1Encoder orders pow=" << mOrderedMSPower << " TA=" << mOrderedMSTiming; mU.fillField(0,encodePower(mOrderedMSPower),8); mU.fillField(8,(int)(mOrderedMSTiming+0.5F),8); // timing (GSM 04.04 6.1) - OBJLOG(DEBUG) << "SACCHL1Encoder phy header " << mU.head(16); + OBJLOG(INFO) <<"SACCHL1Encoder orders pow=" << mOrderedMSPower << " TA=" << mOrderedMSTiming << " with header " << mU.head(16); // Encode the rest of the frame. XCCHL1Encoder::sendFrame(frame); @@ -1615,5 +2076,17 @@ void SACCHL1Encoder::sendFrame(const L2Frame& frame) +void CBCHL1Encoder::sendFrame(const L2Frame& frame) +{ + // Sync to (FN/51)%8==0 at the start of a new block. + if (frame.peekField(4,4)==0) { + mNextWriteTime.rollForward(mMapping.frameMapping(0),51*8); + } + // Transmit. + XCCHL1Encoder::sendFrame(frame); +} + + + // vim: ts=4 sw=4 diff --git a/GSM/GSML1FEC.h b/GSM/GSML1FEC.h index 0892a72b..f1a3560d 100644 --- a/GSM/GSML1FEC.h +++ b/GSM/GSML1FEC.h @@ -2,31 +2,25 @@ * Copyright 2008-2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. */ #ifndef GSML1FEC_H #define GSML1FEC_H +#include "Defines.h" #include "Threads.h" #include @@ -36,13 +30,17 @@ #include "GSMTransfer.h" #include "GSMTDMA.h" +#include "a53.h" +#include "A51.h" + #include "GSM610Tables.h" #include +#include "../GPRS/GPRSExport.h" -class ARFCNManager; +class ARFCNManager; namespace GSM { @@ -61,9 +59,6 @@ class SACCHL1Decoder; class SACCHL1FEC; class TrafficTranscoder; - - - /* Naming convention for bit vectors follows GSM 05.03 Section 2.2. d[k] data @@ -74,12 +69,19 @@ class TrafficTranscoder; */ +enum EncryptionType { + ENCRYPT_NO, + ENCRYPT_MAYBE, + ENCRYPT_YES +}; + /** Abstract class for L1 encoders. In most subclasses, writeHighSide() drives the processing. + (pat) base class for: XCCHL1Encoder, GeneratorL1Encoder */ class L1Encoder { @@ -105,19 +107,31 @@ class L1Encoder { /**@ Internal state. */ //@{ + // (pat) The way this works is rollForward() sets mNextWriteTime to the next + // frame time specified in mMapping. Each logical channel combination has a + // custom serviceloop function running in a separate thread to multiplex the downstream data, + // and send an appropriate frame to ARFCNManager::writeHighSideTx. + // This is totally unlike decoders, for which AFCNManager:receiveBurst uses + // the encoder mapping (which it has cached) to send incoming bursts directly + // to the mapped L1Decoder::writeLowSideRx() for each frame. unsigned mTotalBursts; ///< total bursts sent since last open() GSM::Time mPrevWriteTime; ///< timestamp of pervious generated burst GSM::Time mNextWriteTime; ///< timestamp of next generated burst + volatile bool mRunning; ///< true while the service loop is running bool mActive; ///< true between open() and close() //@} - ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code + // (pat) Moved to classes that need the convolutional coder. + //ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code char mDescriptiveString[100]; public: + EncryptionType mEncrypted; + int mEncryptionAlgorithm; + /** The basic encoder constructor. @param wCN carrier index. @@ -136,6 +150,10 @@ class L1Encoder { mDownstream=wDownstream; } + ARFCNManager *getRadio() { return mDownstream; } + // Used by XCCHEncoder + void transmit(BitVector *mI, BitVector *mE, const int *qbits); + /**@name Accessors. */ //@{ const TDMAMapping& mapping() const { return mMapping; } @@ -155,6 +173,9 @@ class L1Encoder { /** Open the channel for a new transaction. */ virtual void open(); + /** Set mDownstream handover correlator mode. */ + void handoverPending(bool flag); + /** Returns true if the channel is in use by a transaction. For broadcast and unicast channels this is always true. @@ -174,6 +195,10 @@ class L1Encoder { const char* descriptiveString() const { return mDescriptiveString; } + L1FEC* parent() { return mParent; } + + GSM::Time getNextWriteTime() { resync(); return mNextWriteTime; } + protected: /** Roll write times forward to the next positions. */ @@ -200,16 +225,18 @@ class L1Encoder { }; - - /** An abstract class for L1 decoders. - writeLowSide() drives the processing. + writeLowSideRx() drives the processing. + // (pat) base class for: RACHL1Decoder, XCCHL1Decoder + // It would be more elegant to split this into two classes: a base class + // for both GPRS and RR, and the rest of this class that is RR specific. */ class L1Decoder { protected: + // (pat) Not used for GPRS SAPMux * mUpstream; /**@name Mutex-controlled state information. */ @@ -220,6 +247,7 @@ class L1Decoder { Z100Timer mT3101; ///< timer for new channels Z100Timer mT3109; ///< timer for existing channels Z100Timer mT3111; ///< timer for reuse of a closed channel + Z100Timer mT3103; ///< timer for handover //@} bool mActive; ///< true between open() and close() //@} @@ -230,6 +258,7 @@ class L1Decoder { volatile bool mRunning; ///< true if all required service threads are started volatile float mFER; ///< current FER estimate static const int mFERMemory=20; ///< FER decay time, in frames + volatile bool mHandoverPending; ///< if true, we are decoding handover bursts //@} /**@name Parameters fixed by the constructor, not requiring mutex protection. */ @@ -240,7 +269,13 @@ class L1Decoder { L1FEC* mParent; ///< a containing L1 processor, if any //@} - ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code + // (pat) Moved to classes that use the convolutional coder. + //ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code + + EncryptionType mEncrypted; + int mEncryptionAlgorithm; + unsigned char mKc[8]; + int mFN[8]; public: @@ -254,11 +289,14 @@ class L1Decoder { L1Decoder(unsigned wCN, unsigned wTN, const TDMAMapping& wMapping, L1FEC* wParent) :mUpstream(NULL), mT3101(T3101ms),mT3109(T3109ms),mT3111(T3111ms), + mT3103(gConfig.getNum("GSM.Timer.T3103")), mActive(false), mRunning(false), mFER(0.0F), mCN(wCN),mTN(wTN), - mMapping(wMapping),mParent(wParent) + mMapping(wMapping),mParent(wParent), + mEncrypted(ENCRYPT_NO), + mEncryptionAlgorithm(0) { // Start T3101 so that the channel will // become recyclable soon. @@ -291,7 +329,7 @@ class L1Decoder { bool recyclable() const; /** Connect the upstream SAPMux and L2. */ - void upstream(SAPMux * wUpstream) + virtual void upstream(SAPMux * wUpstream) { assert(mUpstream==NULL); // Only call this once. mUpstream=wUpstream; @@ -304,19 +342,29 @@ class L1Decoder { const TDMAMapping& mapping() const { return mMapping; } /** Accept an RxBurst and process it into the deinterleaver. */ - virtual void writeLowSide(const RxBurst&) = 0; + virtual void writeLowSideRx(const RxBurst&) = 0; /**@name Components of the channel description. */ //@{ unsigned TN() const { return mTN; } - unsigned ARFCN() const; ///< this comes from mUpstream - TypeAndOffset typeAndOffset() const; ///< this comes from mMapping + unsigned ARFCN() const; ///< this comes from mUpstream + TypeAndOffset typeAndOffset() const; ///< this comes from mMapping //@} + /** Control the processing of handover access busts. */ + void handoverPending(bool flag) + { + if (flag) mT3103.set(); + mHandoverPending=flag; + } - protected: + public: + L1FEC* parent() { return mParent; } // pat thinks it is not used virtual. - virtual L1FEC* parent() { return mParent; } + /** How much time left in T3101? */ + long debug3101remaining() { return mT3101.remaining(); } + + protected: /** Return pointer to paired L1 encoder, if any. */ virtual L1Encoder* sibling(); @@ -327,9 +375,12 @@ class L1Decoder { /** Mark the decoder as started. */ virtual void start() { mRunning=true; } + public: void countGoodFrame(); - void countBadFrame(); + + bool decrypt_maybe(string wIMSI, int wA5Alg); + unsigned char *kc() { return mKc; } }; @@ -338,6 +389,96 @@ class L1Decoder { /** The L1FEC encapsulates an encoder and decoder. + Notes by pat 8/2011: + A complete L2 <-> L1 handler includes a set of instances of classes L1FEC, L1Encoder, L1Decoder. + These are always wrapped by an instance of LogicalChannel, which defines the + complete L3 <-> L1 handler. The L1<->L2 handling is quite different for different + logical channels, so all these classes are always over-ridden by more specific ones + for each logical channel. The descendents of L1Encoder/L2Decoder classes + are not just encoders/decoders; together with the associated LogicalChannel class + they incorporate the complete upstream and downstream channel handler. + + Initialization: + All these instances are immortal (unlike GPRS PDCHL1FEC, which is allocated/deallocated + on demand.) The mEncoder and mDecoder below are set once + and never changed, to define the related set of L1FEC+L1Encoder+L2Decoder. + At startup, GSMConfig uses info from the tables in GPRSTDMA + to create a complete set of instances of all these classes for each logical channel, + in each physical channel to which they apply. (The C0T0 beach gets a different + set of classes than TCH Traffic channels, but every LogicalChannel descendent has + its own distinct set of L1FEC+L1Encoder+L1Decoder descendents.) + Note that there is an L1FEC+L1Encoder+L1Decoder per logical channel, not per + physical channel; they all share the physical channel resource, as described below. + The downstream end is connected to ARFCNManager in TRXManager.cpp. + The upstream end goes various places, connected at runtime through SAPMux, + or for some classes (example: RACH), directly to low-level managers. + + See also documentation in LogicalChannel::send(). + L2 -> L1 data flow is as follows: + L2 calls SAPMux::writeHighSide(L2Frame), + which calls L1FEC::writeHighSide(L2Frame), + L2 calls L1FEC::writeHighSide(L2Frame) directly, + which then calls (L1Encoder)mEncoder->writeHighSide(L2Frame) + This is overridden to provide the logical channel specific handling, + which is performed by descendents of L1Encoder. The frames may be processed + at that point (for example, cause RR setup/teardown based on the frame primitive) + or be passed downstream, in which case they usually go through sendFrame() below, + which is over-ridden to provide the logical-channel specific encoding. + Eventually, downstream frames go to L1Encoder::writeHighSideTx, which + delivers them to the ARFCNManager. + They may be delivered directly or spend time in an InterThreadQueue, + which is processed by a serviceLoop, (which may reside either in the L1Encoder + or LogicalChannel descendent) to synchronize them to the BTS frame clock + (by using rollForward() to set mPrevTime, mNextTime, and then waitToSend() to block.) + + L1 -> L2 data flow is as follows: + In TRXManager, the mDemuxTable, which was initialized from the GSMTDMA frame data, + is consulted to pass the radio burst to the appropriate logical channel, using + L1FEC::writeLowSideRx(RxBurst) in the appropriate L1FEC descendent. + From there, anything can happen. Four bursts need to be assembled and decoded. + For TCH, FACH and SACH, this happens in (L1Decoder descendent)::processBurst(), + which then calls countGoodFrame()+handleGoodFrame() or countBadFrame() if the + parity was wrong. handleGoodFrame() does the L1 housekeeping (start/stop timers, + remember power/timing parameters) then passes the frame up using SAPMux->writeLowSide(), + which calls some descendent L2DL. + For RACH, writeLowSideRx decodes the burst and sends a message directly + to gBTS.channelRequest(), which enqueues them for eventual processing + by AccessGrantResponder(). + + Routines: + The start() routine is usually called once to create a thread to start a serviceloop thread. + Radio bursts are then delivered to the class endpoints forever. + The channels are turned on/off by calling open()/close(), which sets the active flag + to determine whether they will process those bursts or drop them. + + GPRS Support: + The "L2Frame" used ubiquitously in this code is a GSM-specific L2 frame. + Now we want to add GPRS support with a new frame structure. + + I split the XCCHL1encoder/XCCHL1decoder classes into separate parts for handling + the logical channel flow, which remained in the original classes, and the + actual data encoding/decoding, which moved to SharedL1Encoder/SharedL2Encoder. + The new SharedL1Encoder/SharedL2Encoder are shared with GPRS. + + Almost all the other functions in the L1Encoder/L2Decoder are different for GPRS + because the channel is shared by multiple MS. So GPRS has its own + set of classes: PDCHL1FEC, PDCHL1Uplink, PDCHL1Downlink. + + Notice that the frame numbers used by GPRS for Radio Blocks are identical to the + data frame numbers for GSM RR TCH channels. Similarly, the GPRS timing advance channels + use the same frame numbers as GPRS RR SACCH (although we dont use those yet.) + We will allocate the GPRS channels dynamically from the TCH pool using + getTCH to allocate an existing TCH LogicalChannel class, which wont otherwise + be used for GPRS, except to return to the pool when GPRS signs off the channel. + The L1Decoder/L1Encoder classes will now be three state: inactive, active for RR, + active for GPRS. Uplink data will be diverted to GPRS code at the earliest point + possible, which is in XCCHL1Decoder::writeLowSideRx(). + + Another option was to completely bypass this code, modifing TRXManager, + either by changing the mDemuxTable to send radio bursts directly to the GPRS code, or + adding a new hook to simply send the entire timeslot to GPRS. + We might still want to go back and do that at some point, possibly when + we implement continuous timing advance. */ class L1FEC { @@ -347,31 +488,51 @@ class L1FEC { L1Decoder* mDecoder; public: + // The mGprsReserved variable prevents the GSM subsystem from using the channel. + // When the GPRS PDCHL1FEC is ready to receive bursts, it sets mGPRSFEC. + bool mGprsReserved; // If set, channel reserved for GPRS. + GPRS::PDCHL1FEC *mGPRSFEC; // If set, bursts are delivered to GPRS. + // Currently, this could go in TCHFACCHL1Decoder instead. + /** The L1FEC constructor is over-ridden for different channel types. But the default has no encoder or decoder. */ - L1FEC():mEncoder(NULL),mDecoder(NULL) {} + L1FEC():mEncoder(NULL),mDecoder(NULL) + , mGprsReserved(0) + , mGPRSFEC(0) + {} - /** This is no-op because these channels should not be destroyed. */ + /** This is no-op because these channels should not be destroyed. + (pat) We may allocate/deallocate GPRS channels on demand, + stealing GSM channels, so above statement may become untrue. + */ virtual ~L1FEC() {}; /** Send in an RxBurst for decoding. */ - void writeLowSide(const RxBurst& burst) - { assert(mDecoder); mDecoder->writeLowSide(burst); } + // (pat) I dont think this is ever called. Gotta love C++ + void writeLowSideRx(const RxBurst& burst) + { assert(mDecoder); mDecoder->writeLowSideRx(burst); } /** Send in an L2Frame for encoding and transmission. */ - void writeHighSide(const L2Frame& frame) - { assert(mEncoder); mEncoder->writeHighSide(frame); } + // (pat) not used for GPRS. + virtual void writeHighSide(const L2Frame& frame) + { + assert(mEncoder); mEncoder->writeHighSide(frame); + } /** Attach L1 to a downstream radio. */ void downstream(ARFCNManager*); /** Attach L1 to an upstream SAPI mux and L2. */ - void upstream(SAPMux* mux) + // (pat) not used for GPRS. + virtual void upstream(SAPMux* mux) { if (mDecoder) mDecoder->upstream(mux); } + /** set encoder and decoder handover pending mode. */ + void handoverPending(bool flag); + /**@name Ganged actions. */ //@{ void open(); @@ -379,34 +540,37 @@ class L1FEC { //@} - /**@name Pass-through actions. */ + /**@name Pass-through actions that concern the physical channel. */ //@{ TypeAndOffset typeAndOffset() const { assert(mEncoder); return mEncoder->typeAndOffset(); } - unsigned TN() const + unsigned TN() const // Timeslot number to use. { assert(mEncoder); return mEncoder->TN(); } - unsigned CN() const + unsigned CN() const // Carrier index. { assert(mEncoder); return mEncoder->CN(); } - unsigned TSC() const + unsigned TSC() const // Trainging sequence for this channel. { assert(mEncoder); return mEncoder->TSC(); } - unsigned ARFCN() const + unsigned ARFCN() const // Absolute Radio Frequence Channel Number. { assert(mEncoder); return mEncoder->ARFCN(); } - float FER() const + float FER() const // Frame Error Rate { assert(mDecoder); return mDecoder->FER(); } - bool recyclable() const + bool recyclable() const // Can we reuse this channel yet? { assert(mDecoder); return mDecoder->recyclable(); } - bool active() const; + bool active() const; // Channel in use? See L1Encoder + // (pat) This lovely function is unsed. + // TRXManager.cpp:installDecoder uses L1Decoder::mapping() directly. const TDMAMapping& txMapping() const { assert(mEncoder); return mEncoder->mapping(); } + // (pat) This function is unsed. const TDMAMapping& rcvMapping() const { assert(mDecoder); return mDecoder->mapping(); } @@ -415,6 +579,11 @@ class L1FEC { //@} + //void setDecoder(L1Decoder*me) { mDecoder = me; } + //void setEncoder(L1Encoder*me) { mEncoder = me; } + ARFCNManager *getRadio() { return mEncoder->getRadio(); } + bool inUseByGPRS() { return mGprsReserved; } + void setGPRS(bool reserved, GPRS::PDCHL1FEC *pch) { mGprsReserved = reserved; mGPRSFEC = pch; } L1Decoder* decoder() { return mDecoder; } L1Encoder* encoder() { return mEncoder; } @@ -433,7 +602,7 @@ class TestL1FEC : public L1FEC { public: - void writeLowSide(const RxBurst&); + void writeLowSideRx(const RxBurst&); void writeHighSide(const L2Frame&); void downstream(ARFCNManager *wDownstream) { mDownstream=wDownstream; } @@ -442,12 +611,14 @@ class TestL1FEC : public L1FEC { /** L1 decoder for Random Access (RACH). */ -class RACHL1Decoder : public L1Decoder { +class RACHL1Decoder : public L1Decoder +{ private: /**@name FEC state. */ //@{ + ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code Parity mParity; ///< block coder BitVector mU; ///< u[], as per GSM 05.03 2.2 BitVector mD; ///< d[], as per GSM 05.03 2.2 @@ -456,6 +627,9 @@ class RACHL1Decoder : public L1Decoder { // The RACH channel uses an internal FIFO, // because the channel allocation process might block // and we don't want to block the radio receive thread. + // (pat) I dont think this is used. I think TRXManager calls writeLowSideRx directly. + // The serviceLoop is still started, and watches mQ forever, hopefully + // waiting for a burst that never comes. RxBurstFIFO mQ; ///< a FIFO to decouple the rx thread Thread mServiceThread; ///< a thread to process the FIFO @@ -473,7 +647,7 @@ class RACHL1Decoder : public L1Decoder { void start(); /** Decode the burst and call the channel allocator. */ - void writeLowSide(const RxBurst&); + void writeLowSideRx(const RxBurst&); /** A loop to watch the FIFO. */ void serviceLoop(); @@ -486,14 +660,105 @@ void *RACHL1DecoderServiceLoopAdapter(RACHL1Decoder*); + +// This is just an encoder, nothing else, shared by RR and GPRS. +// This is the encoder specified in GSM05.03 sec 4.1, used for SACCH and GPRS CS-1. +// Why isnt this derived directly from L1Encoder, you ask? +// First it was because GPRS has multiple encoders for different encoding schemes +// and they all use a single L1Encoder attached to the radio. +// Second, because the GSM L1Encoder is not just an encoder, it is the complete stack +// down to the radio, whereas this class is just an encoder only. +// First case above is now inapplicable because the additional GPRS encoders are now +// derived from this one. +class SharedL1Encoder +{ + protected: + ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code + Parity mBlockCoder; + BitVector mC; ///< c[], as per GSM 05.03 2.2 + BitVector mU; ///< u[], as per GSM 05.03 2.2 + //BitVector mDP; ///< d[]:p[] (data & parity) + BitVector mP; ///< p[], as per GSM 05.03 2.2 + public: + BitVector mD; ///< d[], as per GSM 05.03 2.2 Incoming Data. + BitVector mI[4]; ///< i[][], as per GSM 05.03 2.2 Outgoing Data. + BitVector mE[4]; + + /** + Encode u[] to c[]. + Includes LSB-MSB reversal within each octet. + */ + void encode41(); + + /** + Interleave c[] to i[]. + GSM 05.03 4.1.4. + It is not virtual. + */ + void interleave41(); + + public: + + SharedL1Encoder(); + + //void encodeFrame41(const L2Frame &frame, int offset); + void encodeFrame41(const BitVector &frame, int offset, bool copy=true); + void initInterleave(int); +}; + +// Shared by RR and GPRS +class SharedL1Decoder +{ + protected: + + /**@name FEC state. */ + //@{ + ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code + Parity mBlockCoder; + public: + SoftVector mC; ///< c[], as per GSM 05.03 2.2 + BitVector mU; ///< u[], as per GSM 05.03 2.2 + BitVector mP; ///< p[], as per GSM 05.03 2.2 + BitVector mDP; ///< d[]:p[] (data & parity) + public: + BitVector mD; ///< d[], as per GSM 05.03 2.2 + SoftVector mE[4]; + SoftVector mI[4]; ///< i[][], as per GSM 05.03 2.2 + /**@name Handover Access Burst FEC state. */ + //@{ + Parity mHParity; ///< block coder for handover access bursts + BitVector mHU; ///< u[] for handover access, as per GSM 05.03 4.6 + BitVector mHD; ///< d[] for handover access, as per GSM 05.03 4.6 + //@} + //@} + + GSM::Time mReadTime; ///< timestamp of the first burst + + public: + + SharedL1Decoder(); + + void deinterleave(); + bool decode(); + SoftVector *result() { return mI; } +}; + + /** Abstract L1 decoder for most control channels -- GSM 05.03 4.1 */ -class XCCHL1Decoder : public L1Decoder { +class XCCHL1Decoder : + public SharedL1Decoder, + public L1Decoder +{ protected: + // Moved to SharedL1Decoder +#if 0 /**@name FEC state. */ //@{ - Parity mBlockCoder; + /**@name Normal Burst FEC state. */ + //@{ + Parity mBlockCoder; ///< block coder for normal bursts SoftVector mI[4]; ///< i[][], as per GSM 05.03 2.2 SoftVector mC; ///< c[], as per GSM 05.03 2.2 BitVector mU; ///< u[], as per GSM 05.03 2.2 @@ -501,15 +766,24 @@ class XCCHL1Decoder : public L1Decoder { BitVector mDP; ///< d[]:p[] (data & parity) BitVector mD; ///< d[], as per GSM 05.03 2.2 //@} - - GSM::Time mReadTime; ///< timestamp of the first burst - unsigned mRSSIHistory[4]; + /**@name Handover Access Burst FEC state. */ + //@{ + Parity mHParity; ///< block coder for handover access bursts + BitVector mHU; ///< u[] for handover access, as per GSM 05.03 4.6 + BitVector mHD; ///< d[] for handover access, as per GSM 05.03 4.6 + //@} + //@} +#endif public: XCCHL1Decoder(unsigned wCN, unsigned wTN, const TDMAMapping& wMapping, L1FEC *wParent); + void saveMi(); + void restoreMi(); + void decrypt(); + protected: /** Offset to the start of the L2 header. */ @@ -519,7 +793,7 @@ class XCCHL1Decoder : public L1Decoder { virtual ChannelType channelType() const = 0; /** Accept a timeslot for processing and drive data up the chain. */ - virtual void writeLowSide(const RxBurst&); + virtual void writeLowSideRx(const RxBurst&); /** Accept a new timeslot for processing and save it in i[]. @@ -529,6 +803,8 @@ class XCCHL1Decoder : public L1Decoder { */ virtual bool processBurst(const RxBurst&); + // Moved to SharedL1Encoder. +#if 0 /** Deinterleave the i[] to c[]. This virtual method works for all block-interleaved channels (xCCHs). @@ -542,6 +818,7 @@ class XCCHL1Decoder : public L1Decoder { @return True if frame passed parity check. */ bool decode(); +#endif /** Finish off a properly-received L2Frame in mU and send it up to L2. */ virtual void handleGoodFrame(); @@ -567,8 +844,6 @@ class SDCCHL1Decoder : public XCCHL1Decoder { }; - - /** L1 decoder for the SACCH. Like any other control channel, but with hooks for power/timing control. @@ -578,9 +853,9 @@ class SACCHL1Decoder : public XCCHL1Decoder { private: SACCHL1FEC *mSACCHParent; - unsigned mRSSICounter; - volatile float mRSSI[4]; ///< RSSI history , dB wrt full scale - volatile float mTimingError[4]; ///< Timing error histoty in symbol + volatile float mRSSI; ///< most recent RSSI, dB wrt full scale + volatile float mTimingError; ///< Timing error history in symbols + volatile double mTimestamp; ///< system time of most recent received burst volatile int mActualMSPower; ///< actual MS tx power in dBm volatile int mActualMSTiming; ///< actual MS tx timing advance in symbols @@ -593,10 +868,12 @@ class SACCHL1Decoder : public XCCHL1Decoder { SACCHL1FEC *wParent) :XCCHL1Decoder(wCN,wTN,wMapping,(L1FEC*)wParent), mSACCHParent(wParent), - mRSSICounter(0) - { - for (int i=0; i<4; i++) mRSSI[i]=0.0F; - } + mRSSI(0.0F), + mTimingError(0.0F), + mTimestamp(0.0), + mActualMSPower(0), + mActualMSTiming(0) + { } ChannelType channelType() const { return SACCHType; } @@ -612,18 +889,24 @@ class SACCHL1Decoder : public XCCHL1Decoder { bool processBurst(const RxBurst&); /** Set pyshical parameters for initialization. */ - void setPhy(float wRSSI, float wTimingError); + void setPhy(float wRSSI, float wTimingError, double wTimestamp); void setPhy(const SACCHL1Decoder& other); /** RSSI of most recent received burst, in dB wrt full scale. */ - float RSSI() const; + float RSSI() const { return mRSSI; } + + /** Artificially push down RSSI to induce the handset to push more power. */ + void RSSIBumpDown(float dB) { mRSSI -= dB; } /** Timing error of most recent received burst, symbol units. Positive is late; negative is early. */ - float timingError() const; + float timingError() const { return mTimingError; } + + /** Timestamp of most recent received burst. */ + double timestamp() const { return mTimestamp; } protected: @@ -645,10 +928,15 @@ class SACCHL1Decoder : public XCCHL1Decoder { /** L1 encoder used for many control channels -- mostly from GSM 05.03 4.1 */ -class XCCHL1Encoder : public L1Encoder { +class XCCHL1Encoder : + public SharedL1Encoder, + public L1Encoder +{ protected: + // Moved to SharedL1Encoder +#if 0 /**@name FEC signal processing state. */ //@{ Parity mBlockCoder; ///< block coder for this channel @@ -658,6 +946,7 @@ class XCCHL1Encoder : public L1Encoder { BitVector mD; ///< d[], as per GSM 05.03 2.2 BitVector mP; ///< p[], as per GSM 05.03 2.2 //@} +#endif public: @@ -670,6 +959,7 @@ class XCCHL1Encoder : public L1Encoder { protected: /** Process pending incoming messages. */ + // (pat) Messages may be control primitives. If it is data, it is passed to sendFrame() virtual void writeHighSide(const L2Frame&); /** Offset from the start of mU to the start of the L2 frame. */ @@ -677,7 +967,9 @@ class XCCHL1Encoder : public L1Encoder { /** Send a single L2 frame. */ virtual void sendFrame(const L2Frame&); - + // Moved to SharedL1Encoder + //virtual void transmit(BitVector *mI); +#if 0 /** Encode u[] to c[]. Includes LSB-MSB reversal within each octet. @@ -697,6 +989,7 @@ class XCCHL1Encoder : public L1Encoder { GSM 05.03 4.1.5, 05.02 5.2.3. */ virtual void transmit(); +#endif }; @@ -710,6 +1003,9 @@ class TCHFACCHL1Encoder : public XCCHL1Encoder { bool mPreviousFACCH; ///< A copy of the previous stealing flag state. size_t mOffset; ///< Current deinterleaving offset. + BitVector mE[8]; + // (pat) Yes, the mI here duplicates but overrides the same + // vector down in XCCHL1Encoder. BitVector mI[8]; ///< deinterleaving history, 8 blocks instead of 4 BitVector mTCHU; ///< u[], but for traffic BitVector mTCHD; ///< d[], but for traffic @@ -743,8 +1039,12 @@ class TCHFACCHL1Encoder : public XCCHL1Encoder { protected: + // GSM 05.03, 3.1.3 + void interleave31(int blockOffset); +#if 0 /** Interleave c[] to i[]. GSM 05.03 4.1.4. */ - virtual void interleave(int blockOffset); + virtual void interleave31(int blockOffset); +#endif /** Encode a FACCH and enqueue it for transmission. */ void sendFrame(const L2Frame&); @@ -773,6 +1073,7 @@ class TCHFACCHL1Decoder : public XCCHL1Decoder { protected: + SoftVector mE[8]; ///< deinterleaving history, 8 blocks instead of 4 SoftVector mI[8]; ///< deinterleaving history, 8 blocks instead of 4 BitVector mTCHU; ///< u[] (uncoded) in the spec BitVector mTCHD; ///< d[] (data) in the spec @@ -780,8 +1081,8 @@ class TCHFACCHL1Decoder : public XCCHL1Decoder { BitVector mClass1A_d; ///< the class 1A part of d[] SoftVector mClass2_c; ///< the class 2 part of c[] - VocoderFrame mVFrame; ///< unpacking buffer for vocoder frame - unsigned char mPrevGoodFrame[33]; ///< previous good frame. + VocoderFrame mVFrame; ///< unpacking buffer for current vocoder frame + VocoderFrame mPrevGoodFrame; ///< previous good frame Parity mTCHParity; @@ -798,17 +1099,22 @@ class TCHFACCHL1Decoder : public XCCHL1Decoder { /** TCH/FACCH has a special-case writeLowSide. */ - void writeLowSide(const RxBurst& inBurst); + void writeLowSideRx(const RxBurst& inBurst); /** Unlike other DCCHs, TCH/FACCH process burst calls deinterleave, decode, handleGoodFrame. */ bool processBurst( const RxBurst& ); + + void saveMi(); + void restoreMi(); + void decrypt(int B); /** Deinterleave i[] to c[]. */ void deinterleave(int blockOffset ); + // (pat) Routine does not exist. void replaceFACCH( int blockOffset ); /** @@ -837,7 +1143,9 @@ class TCHFACCHL1Decoder : public XCCHL1Decoder { This is base class for output-only encoders. These all have very thin L2/L3 and are driven by a clock instead of a FIFO. */ -class GeneratorL1Encoder : public L1Encoder { +class GeneratorL1Encoder : + public L1Encoder +{ private: @@ -880,7 +1188,7 @@ void *GeneratorL1EncoderServiceLoopAdapter(GeneratorL1Encoder*); class SCHL1Encoder : public GeneratorL1Encoder { private: - + ViterbiR2O4 mVCoder; ///< nearly all GSM channels use the same convolutional code Parity mBlockCoder; ///< block parity coder BitVector mU; ///< u[], as per GSM 05.03 2.2 BitVector mE; ///< e[], as per GSM 05.03 2.2 @@ -975,7 +1283,6 @@ class BCCHL1Encoder : public NDCCHL1Encoder { }; - /** L1 decoder for the SACCH. Like any other control channel, but with hooks for power/timing control. @@ -1171,10 +1478,12 @@ class SACCHL1FEC : public L1FEC { //@{ float RSSI() const { return mSACCHDecoder->RSSI(); } float timingError() const { return mSACCHDecoder->timingError(); } + double timestamp() const { return mSACCHDecoder->timestamp(); } int actualMSPower() const { return mSACCHDecoder->actualMSPower(); } int actualMSTiming() const { return mSACCHDecoder->actualMSTiming(); } void setPhy(const SACCHL1FEC&); - virtual void setPhy(float RSSI, float timingError); + virtual void setPhy(float RSSI, float timingError, double wTimestamp); + void RSSIBumpDown(int dB) { mSACCHDecoder->RSSIBumpDown(dB); } //@} }; diff --git a/GSM/GSML2LAPDm.cpp b/GSM/GSML2LAPDm.cpp index b21b7e4a..59ad1111 100644 --- a/GSM/GSML2LAPDm.cpp +++ b/GSM/GSML2LAPDm.cpp @@ -1,24 +1,16 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for +* licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -43,6 +35,7 @@ implementation, although no code is copied directly. #include "GSML2LAPDm.h" #include "GSMSAPMux.h" #include +#include using namespace std; using namespace GSM; @@ -72,7 +65,7 @@ void CCCHL2::writeHighSide(const GSM::L3Frame& l3) assert(mDownstream); assert(l3.primitive()==UNIT_DATA); L2Header header(L2Length(l3.L2Length())); - mDownstream->writeHighSide(L2Frame(header,l3)); + mDownstream->writeHighSide(L2Frame(header,l3,true)); } @@ -94,6 +87,7 @@ L2LAPDm::L2LAPDm(unsigned wC, unsigned wSAPI) mIdleFrame.fillField(8*0,(mC<<1)|1,8); // address mIdleFrame.fillField(8*1,3,8); // control mIdleFrame.fillField(8*2,1,8); // length + if (gConfig.getBool("GSM.Cipher.ScrambleFiller")) mIdleFrame.randomizeFiller(8*4); } @@ -102,7 +96,9 @@ void L2LAPDm::writeL1(const L2Frame& frame) OBJLOG(DEBUG) <<"L2LAPDm::writeL1 " << frame; //assert(mDownstream); if (!mDownstream) return; - ScopedLock lock(mLock); + // It is tempting not to lock this, but if we don't, + // the ::open operation can result in contention in L1. + ScopedLock lock(mL1Lock); mDownstream->writeHighSide(frame); } @@ -288,13 +284,16 @@ void L2LAPDm::open() OBJLOG(DEBUG); { ScopedLock lock(mLock); + OBJLOG(DEBUG); if (!mRunning) { + OBJLOG(DEBUG); // We can't call this from the constructor, // since N201 may not be defined yet. mMaxIPayloadBits = 8*N201(L2Control::IFormat); mRunning = true; mUpstreamThread.start((void *(*)(void*))LAPDmServiceLoopAdapter,this); } + OBJLOG(DEBUG); mL3Out.clear(); mL1In.clear(); clearCounters(); @@ -302,7 +301,9 @@ void L2LAPDm::open() mAckSignal.signal(); } + OBJLOG(DEBUG); if (mSAPI==0) sendIdle(); + OBJLOG(DEBUG); } @@ -484,6 +485,9 @@ void L2LAPDm::receiveFrame(const GSM::L2Frame& frame) case L2Control::UFormat: receiveUFrame(frame); break; } break; + case HANDOVER_ACCESS: + mL3Out.write(new L3Frame(HANDOVER_ACCESS)); + break; default: OBJLOG(ERR) << "unhandled primitive in L1->L2 " << frame; assert(0); @@ -577,8 +581,7 @@ void L2LAPDm::receiveUFrameSABM(const L2Frame& frame) } // Re-establishment procedure, GSM 04.06 5.6.3. // This basically resets the ack engine. - // We should not actually see this, as of rev 2.4. - OBJLOG(WARNING) << "reestablishment not really supported"; + // The most common reason for this is failed handover. sendUFrameUA(frame.PF()); clearCounters(); break; @@ -908,12 +911,21 @@ void L2LAPDm::sendUFrameUI(const L3Frame& l3) L2Control control(L2Control::UFormat,1,0x00); L2Length length(l3.L2Length()); L2Header header(address,control,length); - writeL1NoAck(L2Frame(header,l3)); + L2Frame l2f = L2Frame(header, l3); + // FIXME - + // The correct solution is to build an L2 frame in RadioResource.cpp and control the bits explicitly up there. + // But I don't know if the LogcialChannel class has a method for sending frames directly into L2. + if (l3.PD() == L3RadioResourcePD && l3.MTI() == L3RRMessage::PhysicalInformation) { + l2f.CR(true); + l2f.PF(false); + } + writeL1NoAck(l2f); } + void L2LAPDm::sendMultiframeData(const L3Frame& l3) { // See GSM 04.06 5.8.5 @@ -995,6 +1007,25 @@ bool L2LAPDm::stuckChannel(const L2Frame& frame) +void CBCHL2::writeHighSide(const GSM::L3Frame& l3) +{ + OBJLOG(DEBUG) <<"CBCHL2 incoming L3 frame: " << l3; + assert(mDownstream); + assert(l3.primitive()==UNIT_DATA); + assert(l3.size()==88*8); + L2Frame outFrame(DATA); + // Chop the L3 frame into 4 L2 frames. + for (unsigned i=0; i<4; i++) { + outFrame.fillField(0,0x02,4); + outFrame.fillField(4,i,4); + const BitVector thisSeg = l3.segment(i*22*8,22*8); + thisSeg.copyToSegment(outFrame,8); + OBJLOG(DEBUG) << "CBCHL2 outgoing L2 frame: " << outFrame; + mDownstream->writeHighSide(outFrame); + } +} + + // vim: ts=4 sw=4 diff --git a/GSM/GSML2LAPDm.h b/GSM/GSML2LAPDm.h index 815af92d..937b54c1 100644 --- a/GSM/GSML2LAPDm.h +++ b/GSM/GSML2LAPDm.h @@ -2,24 +2,16 @@ * Copyright 2008 Free Software Foundation, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -135,13 +127,38 @@ class CCCHL2 : public L2DL { void writeLowSide(const GSM::L2Frame&) { assert(0); } - L3Frame* readHighSide(unsigned /*timeout = 3600000*/) { assert(0); return NULL; } + L3Frame* readHighSide(unsigned timeout=3600000) { assert(0); return NULL; } void writeHighSide(const GSM::L3Frame&); }; +/** + A "thin" L2 for CBCH. + This is a downlink-only channel and does not use LAPDm. + See GSM 04.12 3.3.1. +*/ +class CBCHL2 : public L2DL { + + public: + + unsigned N201(GSM::L2Control::ControlFormat format) const { assert(0); } + + unsigned N200() const { return 0; } + + void open() {} + + void writeLowSide(const GSM::L2Frame&) { assert(0); } + + L3Frame* readHighSide(unsigned timeout=3600000) { assert(0); return NULL; } + + void writeHighSide(const GSM::L3Frame&); + +}; + + + @@ -392,6 +409,7 @@ class L2LAPDm : public L2DL { - This need not be called when the channel is closed, as L1 will generate its own filler pattern that is more appropriate in this condition. + - This does not need to be called for the SACCH or FACCH. */ virtual void sendIdle() { writeL1(mIdleFrame); } diff --git a/GSM/GSML3CCElements.cpp b/GSM/GSML3CCElements.cpp index 9e7cfb07..4dc408ed 100644 --- a/GSM/GSML3CCElements.cpp +++ b/GSM/GSML3CCElements.cpp @@ -1,25 +1,17 @@ -/**@file @brief Call Control messages, GSM 04.08 9.3 */ +/**@file + @brief Call Control messages, GSM 04.08 9.3 +*/ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -71,30 +63,40 @@ void L3BearerCapability::text(ostream& os) const } -void L3BCDDigits::parse(const L3Frame& src, size_t &rp, size_t numOctets) +void L3BCDDigits::parse(const L3Frame& src, size_t &rp, size_t numOctets, bool international) { unsigned i=0; size_t readOctets = 0; + if (international) mDigits[i++] = '+'; while (readOctets < numOctets) { unsigned d2 = src.readField(rp,4); unsigned d1 = src.readField(rp,4); readOctets++; - mDigits[i++]=d1+'0'; - if (d2!=0x0f) mDigits[i++]=d2+'0'; + mDigits[i++] = d1 == 10 ? '*' : d1 == 11 ? '#' : d1+'0'; + if (d2!=0x0f) mDigits[i++] = d2 == 10 ? '*' : d2 == 11 ? '#' : d2+'0'; if (i>maxDigits) L3_READ_ERROR; } mDigits[i++]='\0'; } +int encode(char c) +{ + return c == '*' ? 10 : c == '#' ? 11 : c-'0'; +} + + void L3BCDDigits::write(L3Frame& dest, size_t &wp) const { unsigned index = 0; unsigned numDigits = strlen(mDigits); + if (index < numDigits && mDigits[index] == '+') { + index++; + } while (index < numDigits) { - if ((index+1) < numDigits) dest.writeField(wp,mDigits[index+1]-'0',4); + if ((index+1) < numDigits) dest.writeField(wp,encode(mDigits[index+1]),4); else dest.writeField(wp,0x0f,4); - dest.writeField(wp,mDigits[index]-'0',4); + dest.writeField(wp,encode(mDigits[index]),4); index += 2; } } @@ -103,6 +105,7 @@ void L3BCDDigits::write(L3Frame& dest, size_t &wp) const size_t L3BCDDigits::lengthV() const { unsigned sz = strlen(mDigits); + if (*mDigits == '+') sz--; return (sz/2) + (sz%2); } @@ -131,7 +134,7 @@ void L3CalledPartyBCDNumber::parseV( const L3Frame &src, size_t &rp, size_t expe if (src.readField(rp, 1) != 1) L3_READ_ERROR; mType = (TypeOfNumber)src.readField(rp, 3); mPlan = (NumberingPlan)src.readField(rp, 4); - mDigits.parse(src,rp,expectedLength-1); + mDigits.parse(src,rp,expectedLength-1, mType == InternationalNumber); } @@ -154,7 +157,7 @@ void L3CallingPartyBCDNumber::writeV( L3Frame &dest, size_t &wp ) const { // If Octet3a is extended, then write 0 else 1. dest.writeField(wp, (!mHaveOctet3a & 0x01), 1); - dest.writeField(wp, mType, 3); + dest.writeField(wp, *digits() == '+' ? InternationalNumber : mType, 3); dest.writeField(wp, mPlan, 4); if(mHaveOctet3a){ diff --git a/GSM/GSML3CCElements.h b/GSM/GSML3CCElements.h index e0a75d36..f4a7c34e 100644 --- a/GSM/GSML3CCElements.h +++ b/GSM/GSML3CCElements.h @@ -2,24 +2,14 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -71,7 +61,7 @@ class L3BCDDigits { L3BCDDigits(const char* wDigits) { strncpy(mDigits,wDigits,sizeof(mDigits)-1); mDigits[sizeof(mDigits)-1]='\0'; } - void parse(const L3Frame& src, size_t &rp, size_t numOctets); + void parse(const L3Frame& src, size_t &rp, size_t numOctets, bool international = false); void write(L3Frame& dest, size_t &wp) const; /** Return number of octets needed to encode the digits. */ @@ -200,6 +190,9 @@ class L3Cause : public L3ProtocolElement { private: + // FIXME -- This should include any supplied diagnostics. + // See ticket GSM 04.08 10.5.4.11 and ticket #1139. + Location mLocation; unsigned mCause; diff --git a/GSM/GSML3CCMessages.cpp b/GSM/GSML3CCMessages.cpp index 877c7152..73fcf7a6 100644 --- a/GSM/GSML3CCMessages.cpp +++ b/GSM/GSML3CCMessages.cpp @@ -1,27 +1,19 @@ /** @file Call Control messags, GSM 04.08 9.3. */ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -57,10 +49,8 @@ ostream& GSM::operator<<(ostream& os, L3CCMessage::MessageType val) os << "Release Complete"; break; case L3CCMessage::Setup: os << "Setup"; break; - case L3CCMessage::EmergencySetup: - os << "Emergency Setup"; break; case L3CCMessage::CCStatus: - os <<"Status"; break; + os << "Status"; break; case L3CCMessage::CallConfirmed: os <<"Call Confirmed"; break; case L3CCMessage::CallProceeding: @@ -92,7 +82,6 @@ L3CCMessage * GSM::L3CCFactory(L3CCMessage::MessageType MTI) case L3CCMessage::Connect: return new L3Connect(); case L3CCMessage::Alerting: return new L3Alerting(); case L3CCMessage::Setup: return new L3Setup(); - case L3CCMessage::EmergencySetup: return new L3EmergencySetup(); case L3CCMessage::Disconnect: return new L3Disconnect(); case L3CCMessage::CallProceeding: return new L3CallProceeding(); case L3CCMessage::Release: return new L3Release(); diff --git a/GSM/GSML3CCMessages.h b/GSM/GSML3CCMessages.h index ef8d8e59..d48bce0a 100644 --- a/GSM/GSML3CCMessages.h +++ b/GSM/GSML3CCMessages.h @@ -4,24 +4,16 @@ * Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -65,7 +57,6 @@ class L3CCMessage : public L3Message { CallProceeding=0x02, Connect=0x07, Setup=0x05, - EmergencySetup=0x0e, ConnectAcknowledge=0x0f, Progress=0x03, //@} @@ -179,6 +170,7 @@ class L3CCStatus : public L3CCMessage mCause(wCause), mCallState(wCallState) {} + const L3Cause& cause() const { return mCause; } const L3CallState callState() const { return mCallState; } @@ -295,29 +287,6 @@ class L3Setup : public L3CCMessage }; -/** - GSM 04.08 9.3.8 -*/ -class L3EmergencySetup : public L3CCMessage -{ - - // We fill in IEs one at a time as we need them. - -public: - - L3EmergencySetup(unsigned wTI=7) - :L3CCMessage(wTI) - { } - - - int MTI() const { return EmergencySetup; } - void parseBody( const L3Frame &src, size_t &rp ) {} - size_t l2BodyLength() const { return 0; } -}; - - - - /** GSM 04.08 9.3.3 */ class L3CallProceeding : public L3CCMessage { diff --git a/GSM/GSML3CommonElements.cpp b/GSM/GSML3CommonElements.cpp index c3ffc4f5..df37e6d0 100644 --- a/GSM/GSML3CommonElements.cpp +++ b/GSM/GSML3CommonElements.cpp @@ -6,24 +6,16 @@ * Copyright 2008, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -342,9 +334,9 @@ void L3MobileStationClassmark3::text(ostream& os) const { os << "multiband=" << mMultiband; os << " A5/4=" << mA5_4; - os << " A5/5=" << mA5_4; - os << " A5/6=" << mA5_4; - os << " A5/7=" << mA5_4; + os << " A5/5=" << mA5_5; + os << " A5/6=" << mA5_6; + os << " A5/7=" << mA5_7; } diff --git a/GSM/GSML3CommonElements.h b/GSM/GSML3CommonElements.h index 7c045459..3cb99cc0 100644 --- a/GSM/GSML3CommonElements.h +++ b/GSM/GSML3CommonElements.h @@ -5,24 +5,16 @@ * Copyright 2008-2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/GSML3GPRSElements.cpp b/GSM/GSML3GPRSElements.cpp new file mode 100644 index 00000000..14e2b8e7 --- /dev/null +++ b/GSM/GSML3GPRSElements.cpp @@ -0,0 +1,375 @@ +/**@file @brief L3 Radio Resource messages related to GPRS */ +/* +* Copyright 2008, 2010 Free Software Foundation, Inc. +* Copyright 2011 Kestrel Signal Processing, Inc. +* +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include + +#include "GSML3RRMessages.h" +#include "../GPRS/GPRSExport.h" +#include + + +namespace GSM { + + +// GSM 04.60 sec 12.24 +void L3GPRSCellOptions::writeBits(L3Frame& dest, size_t &wp) const +{ + GPRS::GPRSCellOptions_t& gco = GPRS::GPRSGetCellOptions(); + dest.writeField(wp,gco.mNMO,2); + dest.writeField(wp,gco.mT3168Code,3); + dest.writeField(wp,gco.mT3192Code,3); + dest.writeField(wp,gco.mDRX_TIMER_MAX,3); + dest.writeField(wp,gco.mACCESS_BURST_TYPE,1); + dest.writeField(wp,gco.mCONTROL_ACK_TYPE,1); + dest.writeField(wp,gco.mBS_CV_MAX,4); + dest.writeField(wp,0,1); // optional PAN_ fields omitted. + LOG(INFO)<< "beacon"< mTFIAssignment; + Field_z<1> mPolling; // Set if MS is being polled for Packet Control Acknowledgement. + + // This part for Uplink Dynamic Allocation Mode [for packet uplink transfer]: + Field_z<3> mUSF; + Field_z<1> mUSFGranularity; + + // This part for Uplink Fixed Allocation Mode [for packet uplink transfer]: + Field_z<5> mAllocationBitmapLength; + Field_z<32> mAllocationBitmap; // variable sized, up to 32 bits + + // alpha, gamma for MS power control. See GSM05.08 + Field_z<4> mAlpha; Bool_z mAlphaPresent; // optional param + Field_z<5> mGamma; + + Field_z<4> mTimingAdvanceIndex; Bool_z mTimingAdvanceIndexPresent; // optional param + // From GSM 04.08 10.5.2.16, and I quote: + // The TBF starting time is coded using the same coding as the V format + // of the type 3 information element Starting Time (10.5.2.38). + + Field_z<16> mTBFStartingTime; Bool_z mTBFStartingTimePresent; // optional param + Field_z<2> mChannelCodingCommand; // CS-1, CS-2, CS-3 or CS-4. + Field_z<1> mTLLIBlockChannelCoding; + // (pat) We wont use the downlink power control parameters (P0, etc), so dont even bother. + // L3AssignmentPowerOption mPowerOption; + + // The following variables used only for Packet Downlink Assignment + Field_z<32> mTLLI; + Field_z<1> mRLCMode; + Field_z<1> mTAValid; // Is the timingadvance in the main Immediate Assignment Message valid? + + void setPacketUplinkAssignSingleBlock(unsigned TBFStartingTime); + void setPacketUplinkAssignDynamic(unsigned TFI, unsigned CSNum, unsigned USF); + void setPacketDownlinkAssign( + unsigned wTLLI, unsigned wTFI,unsigned wCSNum, unsigned wRLCMode,unsigned wTAValid); + void setPacketUplinkAssignFixed(); + void setPacketPowerOptions(unsigned wAlpha, unsigned wGamma); + void setPacketPollTime(unsigned TBFStartingTime); + void writePacketUplinkAssignment(MsgCommon &dest) const; + void writePacketDownlinkAssignment(MsgCommon &dest) const; + void writeIAPacketAssignment(MsgCommon &dest) const; + + void writeBits(L3Frame &dest, size_t &wp) const; + size_t lengthBits() const; + void text(std::ostream& os) const; + + L3IAPacketAssignment() { mPacketAssignmentType = PacketUplinkAssignUninitialized; /*redundant*/ } +}; + + + +#if 0 // This is currently unused, so lets indicate so. +/** GSM 04.60 12.13 */ +// NOTE: These are the power control parameters for assignment in GSM 4.60, +// not the power control parameters for the SI13 rest octets +// This is NOT a L3ProtocolElement; it is not in TLV format or byte aligned. +class L3GPRSPowerControlParameters : public GenericMessageElement +{ + + private: + + unsigned mAlpha; ///< GSM 04.60 Table 12.9.2 + // GSM04.60 12.13 + // (pat) There are 8 gamma values, one for each channel. + // sec 12.13 says the presence/absense of gamma may be used to denote + // timeslot for "an uplink TBF", presumably in the absense of a TIMESLOT ALLOCATION IE, + // but I dont see which uplink TBF assignment would use that and the spec does not say. + // I dont see why you need gamma in the SI13 message at all; Gamma is assigned + // in the uplink/downlink assignment messages as a non-optional element. + // I am going to leave them out entirely for now. + // unsigned mGamma[8]; + // bool mGammaPresent[8]; + + public: + + // Init alpha to the defalt value. + L3GPRSPowerControlParameters() : mAlpha(GPRS::GetPowerAlpha()) {} + + size_t lengthBits() const { return 4+8; } + void writeBits(L3Frame& dest, size_t &wp) const; + void text(std::ostream& os) const; + //void parseV( const L3Frame&, size_t&, size_t) { abort(); } + //void parseV(const L3Frame&, size_t&) { abort(); } +}; +#endif + +/** GSM 04.08 10.5.2.37b Power Control Parameters for SI13 Rest Octets */ +// Info has moved to 44.060 12.13. +// NOTE: This is not the same as the Global Power Control Parameters +// in GSM 44.060 12.9a, which include a Pb element. +class L3GPRSSI13PowerControlParameters : public GenericMessageElement +{ + // See GSM 5.08 10.2.1 + // (pat) The MS can regulate its own output power based on measurements it makes. + // See comments at GetPowerAlpha(). The alpha below is used for initial + // communication and may be over-ridden later when the MS starts talking to us. + // The other parameters are "forgetting factors" determining the window period for + // the MS measurements. I dont think the values (other than alpha itself) + // are critical because they are clamped to sane values in the formulas in GSM05.08. + unsigned mAlpha; ///< Range 0..10 See GSM 04.60 Table 12.9.2 + unsigned mTAvgW; // The MS measurement 'forgetting factor' in Packet Idle Mode. + unsigned mTAvgT; // The MS measurement 'forgetting factor' in Packet Transfer Mode. + unsigned mPCMeasChan; // Which channel Ms monitors: 0 => use BCCH, 1 => PDCH1 + unsigned mNAvgI; // 'Forgetting factor' for MS reporting to BTS. + public: + L3GPRSSI13PowerControlParameters(); + size_t lengthBits() const { return 4+5+5+1+4; } + void writeBits(L3Frame& dest, size_t &wp) const; + void text(std::ostream& os) const; +}; + +}; // namespace +#endif diff --git a/GSM/GSML3MMElements.cpp b/GSM/GSML3MMElements.cpp index 4a20b92d..4506035f 100644 --- a/GSM/GSML3MMElements.cpp +++ b/GSM/GSML3MMElements.cpp @@ -2,27 +2,19 @@ @brief Elements for Mobility Management messages, GSM 04.08 9.2. */ /* -* Copyright 2008, 2010 Free Software Foundation, Inc. +* Copyright 2008 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -48,7 +40,6 @@ ostream& GSM::operator<<(ostream& os, L3CMServiceType::TypeCode code) { switch (code) { case L3CMServiceType::MobileOriginatedCall: os << "MOC"; break; - case L3CMServiceType::EmergencyCall: os << "Emergency"; break; case L3CMServiceType::ShortMessage: os << "SMS"; break; case L3CMServiceType::SupplementaryService: os << "SS"; break; case L3CMServiceType::VoiceCallGroup: os << "VGCS"; break; @@ -57,6 +48,7 @@ ostream& GSM::operator<<(ostream& os, L3CMServiceType::TypeCode code) case L3CMServiceType::MobileTerminatedCall: os << "MTC"; break; case L3CMServiceType::MobileTerminatedShortMessage: os << "MTSMS"; break; case L3CMServiceType::TestCall: os << "Test"; break; + case L3CMServiceType::FuzzCall: os << "Fuzz"; break; default: os << "?" << (int)code << "?"; } return os; diff --git a/GSM/GSML3MMElements.h b/GSM/GSML3MMElements.h index bbf0428b..26c43429 100644 --- a/GSM/GSML3MMElements.h +++ b/GSM/GSML3MMElements.h @@ -4,24 +4,16 @@ * Copyright 2008-2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -43,7 +35,6 @@ class L3CMServiceType : public L3ProtocolElement { enum TypeCode { UndefinedType=0, MobileOriginatedCall=1, - EmergencyCall=2, ShortMessage=4, ///< specifically, MO-SMS SupplementaryService=8, VoiceCallGroup=9, @@ -52,6 +43,8 @@ class L3CMServiceType : public L3ProtocolElement { MobileTerminatedCall=100, ///< non-standard code MobileTerminatedShortMessage=101, ///< non-standard code TestCall=102, ///< non-standard code + HandoverCall=103, ///< non-standard code + FuzzCall=104, ///< non-standard code }; private: @@ -126,7 +119,7 @@ class L3NetworkName : public L3ProtocolElement { /** Set the network name, taking the default from gConfig. */ L3NetworkName(const char* wName, GSMAlphabet alphabet=ALPHABET_7BIT, - int wCI=gConfig.defines("GSM.ShowCountry")) + int wCI=gConfig.getBool("GSM.ShowCountry")) :L3ProtocolElement(), mAlphabet(alphabet), mCI(wCI) { strncpy(mName,wName,maxLen); mName[maxLen] = '\0'; } diff --git a/GSM/GSML3MMMessages.cpp b/GSM/GSML3MMMessages.cpp index 2f66e5d7..b4599f92 100644 --- a/GSM/GSML3MMMessages.cpp +++ b/GSM/GSML3MMMessages.cpp @@ -6,24 +6,16 @@ * Copyright 2008-2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -124,7 +116,8 @@ void L3MMMessage::text(ostream& os) const void L3LocationUpdatingRequest::parseBody( const L3Frame &src, size_t &rp ) { // skip updating type - rp += 4; + // (pat) Save this for debugging purposes. + mUpdateType = src.readField(rp,4); // skip ciphering ket sequence number rp += 4; mLAI.parseV(src,rp); @@ -136,7 +129,8 @@ void L3LocationUpdatingRequest::parseBody( const L3Frame &src, size_t &rp ) void L3LocationUpdatingRequest::text(ostream& os) const { L3MMMessage::text(os); - os << "LAI=("<. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -110,6 +102,7 @@ L3MMMessage* parseL3MM(const L3Frame& source); /** GSM 04.08 9.2.15 */ class L3LocationUpdatingRequest : public L3MMMessage { + unsigned mUpdateType; // (pat) Added for debugging. L3MobileStationClassmark1 mClassmark; L3MobileIdentity mMobileIdentity; // (LV) 1+len L3LocationAreaIdentity mLAI; diff --git a/GSM/GSML3Message.cpp b/GSM/GSML3Message.cpp index 89827d56..3d026d1d 100644 --- a/GSM/GSML3Message.cpp +++ b/GSM/GSML3Message.cpp @@ -1,25 +1,14 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -52,6 +41,7 @@ void L3Message::parse(const L3Frame& source) void L3Message::write(L3Frame& dest) const { size_t l3len = bitsNeeded(); + //printf("bitsneeded=%d\n",l3len); if (dest.size()!=l3len) dest.resize(l3len); size_t wp = 0; // write the standard L3 header @@ -133,10 +123,6 @@ ostream& GSM::operator<<(ostream& os, const L3Message& msg) - - - - GSM::L3Message* GSM::parseL3(const GSM::L3Frame& source) { if (source.size()==0) return NULL; @@ -258,6 +244,12 @@ ostream& GSM::operator<<(ostream& os, const L3ProtocolElement& elem) return os; } +ostream& GSM::operator<<(ostream& os, const GenericMessageElement& msg) +{ + msg.text(os); + return os; +} + diff --git a/GSM/GSML3Message.h b/GSM/GSML3Message.h index 11c93bda..adcd8b53 100644 --- a/GSM/GSML3Message.h +++ b/GSM/GSML3Message.h @@ -2,24 +2,14 @@ * Copyright 2008, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -69,13 +59,14 @@ class L3Message { /** Body length not including header but including rest octets. In subclasses with no rest octets, this returns l2BodyLength. + (pat) in BYTES!!! */ virtual size_t fullBodyLength() const =0; /** Return the expected message length in bytes, including L3 header, but not including rest octets. */ size_t L2Length() const { return l2BodyLength()+2; } - /** Length including header and rest octets. */ + /** Length ((pat) in BYTES!!) including header and rest octets. */ size_t FullLength() const { return fullBodyLength()+2; } /** Return number of BITS needed to hold message and header. */ @@ -303,6 +294,20 @@ class L3ProtocolElement { std::ostream& operator<<(std::ostream& os, const L3ProtocolElement& elem); +// Pat added: A Non-Aligned Message Element that is not an L3ProtocolElement because +// it is not in TLV format, and is not byte or half-byte aligned, +// but is rather just a stream of bits, often used in the Message RestOctets. +class GenericMessageElement { + public: + // We dont use these virtual functions except for text(). + // They are basically here as documentation. + virtual size_t lengthBits() const = 0; + virtual void writeBits(L3Frame& dest, size_t &wp) const = 0; + virtual void text(std::ostream& os) const = 0; +}; + +std::ostream& operator<<(std::ostream& os, const GenericMessageElement& elem); + }; // GSM diff --git a/GSM/GSML3RRElements.cpp b/GSM/GSML3RRElements.cpp index 33d45493..614ebd26 100644 --- a/GSM/GSML3RRElements.cpp +++ b/GSM/GSML3RRElements.cpp @@ -1,35 +1,25 @@ -/**@file - @brief Radio Resource messages, GSM 04.08 9.1. -*/ +/**@file @brief Radio Resource messages, GSM 04.08 9.1. */ /* * Copyright 2008, 2009 Free Software Foundation, Inc. -* Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2010, 2013 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ #include // for L3APDUData::text #include "GSML3RRElements.h" +#include "Defines.h" +#include "GSMConfig.h" #include @@ -105,6 +95,20 @@ void L3CellSelectionParameters::text(ostream& os) const +unsigned L3ControlChannelDescription::getBS_PA_MFRMS() +{ + unsigned bs_pa_mfrms = mBS_PA_MFRMS + 2; + if (bs_pa_mfrms != RN_BOUND(bs_pa_mfrms,2,sMax_BS_PA_MFRMS)) { + static bool printed_msg = false; + if (!printed_msg) { + LOG(ERR) << "Invalid BS_PA_MFRMS value, must be 2.."< BCCH Norm + } else { + dest.writeL(wp); } + while (wp & 7) { dest.writeL(wp); } // spare padding to byte boundary. + assert(wp-wpstart == lengthV() * 8); } void L3SI3RestOctets::text(ostream& os) const { + if (!mHaveSI3RestOctets) { return; } if (mHaveSelectionParameters) { os << "CBQ=" << mCBQ; os << " CELL_RESELECT_OFFSET=" << mCELL_RESELECT_OFFSET; os << " TEMPORARY_OFFSET=" << mTEMPORARY_OFFSET; - os << " PANALTY_TIME=" << mPENALTY_TIME; + os << " PENALTY_TIME=" << mPENALTY_TIME; } + if (mHaveGPRS) { + os << " RA_COLOUR=" << mRA_COLOUR; + } +} + + + +void L3HandoverReference::writeV(L3Frame& frame, size_t& wp) const +{ + frame.writeField(wp,mValue,8); +} + + +void L3HandoverReference::text(ostream& os) const +{ + os << "value=" << mValue; +} + + +size_t L3CipheringModeSetting::lengthV() const +{ + return 0; +} + +void L3CipheringModeSetting::writeV(L3Frame& frame, size_t& wp) const +{ + frame.writeField(wp, mCiphering? mAlgorithm-1: 0, 3); + frame.writeField(wp, mCiphering, 1); +} + +void L3CipheringModeSetting::text(ostream& os) const +{ + os << "ciphering=" << mCiphering; + os << " algorithm=A5/" << mAlgorithm; +} + +size_t L3CipheringModeResponse::lengthV() const +{ + return 0; +} + +void L3CipheringModeResponse::writeV(L3Frame& frame, size_t& wp) const +{ + frame.writeField(wp, 0, 3); + frame.writeField(wp, mIncludeIMEISV, 1); +} + +void L3CipheringModeResponse::text(ostream& os) const +{ + os << "includeIMEISV=" << mIncludeIMEISV; +} + +void L3SynchronizationIndication::writeV(L3Frame& frame, size_t& wp) const +{ + frame.writeField(wp,0xD,4); + frame.writeField(wp,mNCI,1); + frame.writeField(wp,mROT,1); + frame.writeField(wp,mSI,2); +} + +void L3SynchronizationIndication::text(ostream& os) const +{ + os << "NCI=" << (int)mNCI << " ROT=" << (int)mROT << " SI=" << mSI; +} + + +void L3CellDescription::writeV(L3Frame& frame, size_t &wp) const +{ + frame.writeField(wp, mARFCN>>8, 2); + frame.writeField(wp, mNCC, 3); + frame.writeField(wp, mBCC, 3); + frame.writeField(wp, mARFCN & 0x0ff, 8); } +void L3CellDescription::text(std::ostream& os) const +{ + os << " ARFCN=" << mARFCN; + os << " NCC=" << mNCC; + os << " BCC=" << mBCC; +} + + + + + + +void L3SI13RestOctets::writeV(L3Frame& dest, size_t &wp) const +{ + size_t wpstart = wp; + dest.writeH(wp); // Indicates Rest Octets are present. + + // FIXME -- Need to implement BCCH_CHANGE_MARK + // If implemented, BCCH_CHANGE_MARK would be a counter + // that is incremented when the BCCH content changes. + dest.writeField(wp,gBTS.changemark()%8,3); // BCCH_CHANGE_MARK + + dest.writeField(wp,0,4); // SI13_CHANGE_FIELD + + dest.writeField(wp,0,1); // no changemark or mobile allocation + // (pat) The GPRS "mobile allocation" is optional and does + // not indicate that GPRS service is present or absent. + + dest.writeField(wp,0,1); // no PBCCH (pat says: This is correct.) + dest.writeField(wp,mRAC,8); + dest.writeField(wp,mSPGC_CCCH_SUP,1); + dest.writeField(wp,mPRIORITY_ACCESS_THR,3); + dest.writeField(wp,mNETWORK_CONTROL_ORDER,2); + mCellOptions.writeBits(dest,wp); + mPowerControlParameters.writeBits(dest,wp); + while (wp & 7) { dest.writeL(wp); } // spare padding to byte bondary. + assert(wp-wpstart == lengthV() * 8); +} + + +void L3SI13RestOctets::text(ostream& os) const +{ + os << "RAC=" << mRAC; + os << " SPGC_CCCH_SUP=" << mSPGC_CCCH_SUP; + os << " PRIORITY_ACCESS_THR=" << mPRIORITY_ACCESS_THR; + os << " NETWORK_CONTROL_ORDER=" << mNETWORK_CONTROL_ORDER; + os << " cellOptions=(" << mCellOptions << ")"; + os << " powerControlParameters=(" << mPowerControlParameters << ")"; +} + + // vim: ts=4 sw=4 diff --git a/GSM/GSML3RRElements.h b/GSM/GSML3RRElements.h index 1932dead..fda4094b 100644 --- a/GSM/GSML3RRElements.h +++ b/GSM/GSML3RRElements.h @@ -2,25 +2,18 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -30,6 +23,7 @@ #include #include "GSML3Message.h" +#include "GSML3GPRSElements.h" #include @@ -55,7 +49,7 @@ class L3CellOptionsBCCH : public L3ProtocolElement { mPWRC=0; mDTX=2; // Configuarable values. - mRADIO_LINK_TIMEOUT= gConfig.getNum("GSM.RADIO-LINK-TIMEOUT"); + mRADIO_LINK_TIMEOUT= gConfig.getNum("GSM.CellOptions.RADIO-LINK-TIMEOUT"); } size_t lengthV() const { return 1; } @@ -87,7 +81,7 @@ class L3CellOptionsSACCH : public L3ProtocolElement { mPWRC=0; mDTX=2; // Configuarable values. - mRADIO_LINK_TIMEOUT=gConfig.getNum("GSM.RADIO-LINK-TIMEOUT"); + mRADIO_LINK_TIMEOUT=gConfig.getNum("GSM.CellOptions.RADIO-LINK-TIMEOUT"); } size_t lengthV() const { return 1; } @@ -144,10 +138,16 @@ class L3ControlChannelDescription : public L3ProtocolElement { private: + // (pat) 5-27-2012: I put in 'real' paging channels and used them for GPRS, + // but someone still needs to modify the GSM stack to use them and test them there. + // Then we could change the parameters below to provide more paging channels. + // See class CCCHCombinedChannel. + unsigned mATT; ///< 1 -> IMSI attach/detach unsigned mBS_AG_BLKS_RES; ///< access grant channel reservation unsigned mCCCH_CONF; ///< channel combination for CCCH unsigned mBS_PA_MFRMS; ///< paging channel configuration + // Note: This var is 0..7 representing BS_PA_MFRMS values 2..9. unsigned mT3212; ///< periodic updating timeout public: @@ -159,11 +159,14 @@ class L3ControlChannelDescription : public L3ProtocolElement { mBS_AG_BLKS_RES=2; // reserve 2 CCCHs for access grant mBS_PA_MFRMS=0; // minimum PCH spacing // Configurable values. - mATT=(unsigned)gConfig.defines("Control.LUR.AttachDetach"); + mATT=(unsigned)gConfig.getBool("Control.LUR.AttachDetach"); mCCCH_CONF=gConfig.getNum("GSM.CCCH.CCCH-CONF"); mT3212=gConfig.getNum("GSM.Timer.T3212")/6; } + // BS_PA_MFRMS is the number of 51-multiframes used for paging in the range 2..9. + unsigned getBS_PA_MFRMS(); + size_t lengthV() const { return 3; } void writeV(L3Frame& dest, size_t &wp) const; void parseV(const L3Frame&, size_t&) { assert(0); } @@ -257,8 +260,14 @@ class L3NeighborCellsDescription : public L3FrequencyList { public: - L3NeighborCellsDescription() - :L3FrequencyList(gConfig.getVector("GSM.CellSelection.Neighbors")) + L3NeighborCellsDescription() {} + + //L3NeighborCellsDescription() + // :L3FrequencyList(gConfig.getVector("GSM.CellSelection.Neighbors")) + //{} + + L3NeighborCellsDescription(const std::vector& neighbors) + :L3FrequencyList(neighbors) {} void writeV(L3Frame& dest, size_t &wp) const; @@ -322,7 +331,7 @@ class L3RACHControlParameters : public L3ProtocolElement { // Configurable values. mMaxRetrans = gConfig.getNum("GSM.RACH.MaxRetrans"); mTxInteger = gConfig.getNum("GSM.RACH.TxInteger"); - mAC = gConfig.getNum("GSM.RACH.AC"); + mAC = 0x0400; //NO EMERGENCY SERVICE - kurtis } size_t lengthV() const { return 3; } @@ -365,6 +374,7 @@ class L3PageMode : public L3ProtocolElement /** DedicatedModeOrTBF, GSM 04.08 10.5.2.25b */ class L3DedicatedModeOrTBF : public L3ProtocolElement { + // (pat) This is poorly named: mDownlink must be TRUE for a TBF, even if it is an uplink tbf. unsigned mDownlink; ///< Indicates the IA reset octets contain additional information. unsigned mTMA; ///< This is part of a 2-message assignment. unsigned mDMOrTBF; ///< Dedicated link (circuit-switched) or temporary block flow (GPRS/pakcet). @@ -372,9 +382,9 @@ class L3DedicatedModeOrTBF : public L3ProtocolElement { public: - L3DedicatedModeOrTBF() + L3DedicatedModeOrTBF(bool forTBF, bool wDownlink) :L3ProtocolElement(), - mDownlink(0), mTMA(0), mDMOrTBF(0) + mDownlink(wDownlink), mTMA(0), mDMOrTBF(forTBF) {} size_t lengthV() const { return 1; } @@ -387,7 +397,12 @@ class L3DedicatedModeOrTBF : public L3ProtocolElement { -/** ChannelDescription, GSM 04.08 10.5.2.5 */ +/** ChannelDescription, GSM 04.18 10.5.2.5 + (pat) The Packet Channel Description, GSM 04.18 10.5.2.25a, is the + same as the Channel Description except with mTypeAndOffset always 1, + and for the addition of indirect frequency hopping encoding, + which is irrelevant for us at the moment. +*/ class L3ChannelDescription : public L3ProtocolElement { @@ -416,7 +431,8 @@ class L3ChannelDescription : public L3ProtocolElement { /** Non-hopping initializer. */ L3ChannelDescription(TypeAndOffset wTypeAndOffset, unsigned wTN, unsigned wTSC, unsigned wARFCN) - :mTypeAndOffset(wTypeAndOffset),mTN(wTN), + :L3ProtocolElement(), + mTypeAndOffset(wTypeAndOffset),mTN(wTN), mTSC(wTSC), mHFlag(0), mARFCN(wARFCN), @@ -436,8 +452,24 @@ class L3ChannelDescription : public L3ProtocolElement { void parseV(const L3Frame&, size_t& , size_t) { assert(0); } void text(std::ostream&) const; + TypeAndOffset typeAndOffset() const { return mTypeAndOffset; } + unsigned TN() const { return mTN; } + unsigned TSC() const{ return mTSC; } + unsigned ARFCN() const { return mARFCN; } }; +/** GSM 040.08 10.5.2.5a */ +class L3ChannelDescription2 : public L3ChannelDescription { + + public: + + L3ChannelDescription2(TypeAndOffset wTypeAndOffset, unsigned wTN, + unsigned wTSC, unsigned wARFCN) + :L3ChannelDescription(wTypeAndOffset,wTN,wTSC,wARFCN) + { } + + L3ChannelDescription2() { } +}; @@ -734,6 +766,10 @@ class L3MeasurementResults : public L3ProtocolElement { L3MeasurementResults() :L3ProtocolElement(), mMEAS_VALID(false), + mRXLEV_FULL_SERVING_CELL(0), + mRXLEV_SUB_SERVING_CELL(0), + mRXQUAL_FULL_SERVING_CELL(0), + mRXQUAL_SUB_SERVING_CELL(0), mNO_NCELL(0) { } @@ -756,11 +792,11 @@ class L3MeasurementResults : public L3ProtocolElement { unsigned NO_NCELL() const { return mNO_NCELL; } unsigned RXLEV_NCELL(unsigned i) const { assert(i= 1 && wAlgorithm <= 7); + } + + size_t lengthV() const; + void writeV(L3Frame&, size_t& wp) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream&) const; +}; + +/** GSM 04.08 10.5.2.10 */ +class L3CipheringModeResponse : public L3ProtocolElement +{ + protected: + + bool mIncludeIMEISV; + + public: + + L3CipheringModeResponse(bool wIncludeIMEISV) + :mIncludeIMEISV(wIncludeIMEISV) + { } + + size_t lengthV() const; + void writeV(L3Frame&, size_t& wp) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream&) const; +}; + +/** GSM 04.08 10.5.2.39 */ +class L3SynchronizationIndication : public L3ProtocolElement +{ + protected: + + bool mNCI; + bool mROT; + int mSI; + + public: + + L3SynchronizationIndication(bool wNCI, bool wROT, int wSI = 0) + :L3ProtocolElement(), + mNCI(wNCI), + mROT(wROT), + mSI(wSI & 3) + {} + + L3SynchronizationIndication() { } + + size_t lengthV() const { return 1; } + void writeV(L3Frame &, size_t &wp ) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream&) const; + + unsigned NCI() const { return mNCI; } + unsigned ROT() const { return mROT; } +}; + + +/** GSM 04.08 10.5.2.28a */ +class L3PowerCommandAndAccessType : public L3PowerCommand { }; + + + + /** A special subclass for rest octets, just in case we need it later. */ @@ -800,12 +969,15 @@ class L3SI3RestOctets : public L3RestOctets { private: // We do not yet support the full parameter set. + bool mHaveSI3RestOctets; bool mHaveSelectionParameters; bool mCBQ; - unsigned mCELL_RESELECT_OFFSET; - unsigned mTEMPORARY_OFFSET; - unsigned mPENALTY_TIME; + unsigned mCELL_RESELECT_OFFSET; // 6 bits + unsigned mTEMPORARY_OFFSET; // 3 bits + unsigned mPENALTY_TIME; // 5 bits + unsigned mRA_COLOUR; // (pat) In GPRS_Indicator, 3 bits + bool mHaveGPRS; // (pat) public: @@ -820,6 +992,127 @@ class L3SI3RestOctets : public L3RestOctets { }; +#if 0 +/** GSM 04.60 12.24 */ +// (pat) Someone kindly added this before I got here. +// This info is included in the SI13 rest octets. +class L3GPRSCellOptions : public L3ProtocolElement { + + private: + + unsigned mNMO; // Network Mode of Operation See GSM 03.60 6.3.3.1 + unsigned mT3168; // range 0..7 + unsigned mT3192; // range 0..7 + unsigned mDRX_TIMER_MAX; + unsigned mACCESS_BURST_TYPE; + unsigned mCONTROL_ACK_TYPE; + unsigned mBS_VC_MAX; + + public: + + L3GPRSCellOptions() + :L3ProtocolElement(), + mNMO(2), // (pat) 2 == Network Mode of Operation III, which means + // GPRS attached MS uses Packet Paging channel + // if allocated (which it wont be), otherwise CCCH. + mT3168(gConfig.getNum("GPRS.CellOptions.T3168Code")), + mT3192(gConfig.getNum("GPRS.CellOptions.T3192Code")), + mDRX_TIMER_MAX(gConfig.getNum("GPRS.CellOptions.DRX_TIMER_MAX")), + mACCESS_BURST_TYPE(0), // (pat) 0 == use 8 bit format of Packet Channel Request Message. + mCONTROL_ACK_TYPE(1), // (pat) 1 == default format for Packet Control Acknowledgement + // is RLC/MAC block, ie, not special. + mBS_VC_MAX(gConfig.getNum("GPRS.CellOptions.BS_VC_MAX")) + { } + + size_t lengthV() const { return 2+3+3+3+1+1+4+1+1; } + void writeV(L3Frame& dest, size_t &wp) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream& os) const; +}; +#endif + + + +#if 0 +/** GSM 04.60 12.13 */ +class L3GPRSPowerControlParameters : public L3ProtocolElement { + + private: + + unsigned mALPHA; ///< GSM 04.60 Table 12.9.2 + + public: + + L3GPRSPowerControlParameters() + :L3ProtocolElement(), + mALPHA(gConfig.getNum("GPRS.PowerControl.ALPHA")) + { } + + size_t lengthV() const { return 4+8; } + void writeV(L3Frame& dest, size_t &wp) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream& os) const; +}; +#endif + + + +/** GSM 04.08 10.5.2.37b */ +class L3SI13RestOctets : public L3RestOctets { + + private: + + unsigned mRAC; ///< routing area code GSM03.03 + bool mSPGC_CCCH_SUP; ///< indicates support of SPLIT_PG_CYCLE on CCCH + unsigned mPRIORITY_ACCESS_THR; + unsigned mNETWORK_CONTROL_ORDER; ///< network reselection behavior + const L3GPRSCellOptions mCellOptions; + const L3GPRSSI13PowerControlParameters mPowerControlParameters; + + public: + + L3SI13RestOctets() + :L3RestOctets(), + mRAC(gConfig.getNum("GPRS.RAC")), // (pat) aka Routing Area Code. + mSPGC_CCCH_SUP(false), + // See GSM04.08 table 10.5.76: Value 6 means any priority packet access allowed. + mPRIORITY_ACCESS_THR(gConfig.getNum("GPRS.PRIORITY-ACCESS-THR")), + // (pat) GSM05.08 sec 10.1.4: This controls whether the MS + // does cell reselection or the network, and appears to apply + // only to GPRS mode. Value NC2 = 2 means the network + // performs cell reselection, but it has a side-effect that + // the MS continually sends Measurement reports, and these + // clog up the RACH channel so badly that the MS cannot send + // enough uplink messages to make the SGSN happy. + // The reporting interval values are as follows, + // defined in GSM04.60 11.2.23 table 11.2.23.2 + // NC_REPORTING_PERIOD_I - used in packet idle mode. + // NC_REPORTING_PERIOD_T - used in packet transfer mode. + // They can be in PSI5, SI2quarter (but I dont see them there), + // or by a PACKET MEASUREMENT ORDER msg. + // I am going to set this to 0 for now instead of 2. + // Update: Lets set it back to see if the MS keeps sending measurement reports when it is non-responsive. + // Update: If the MS is requested to make measurement reports it + // reduces the multislot capability. Measurements described 45.008 + mNETWORK_CONTROL_ORDER(gConfig.getNum("GPRS.NC.NetworkControlOrder")), + mPowerControlParameters() // redundant explicit call + { } + + size_t lengthBits() const + { return 1+3+4+1+1+8+1+3+2+mCellOptions.lengthBits()+mPowerControlParameters.lengthBits(); } + size_t lengthV() const + { return (lengthBits() + 7) / 8; } + + void writeV(L3Frame& dest, size_t &wp) const; + void parseV( const L3Frame&, size_t&, size_t) { abort(); } + void parseV(const L3Frame&, size_t&) { abort(); } + void text(std::ostream& os) const; + +}; + + } // GSM diff --git a/GSM/GSML3RRMessages.cpp b/GSM/GSML3RRMessages.cpp index 0bcb260f..09339510 100644 --- a/GSM/GSML3RRMessages.cpp +++ b/GSM/GSML3RRMessages.cpp @@ -2,27 +2,19 @@ @brief GSM Radio Resorce messages, from GSM 04.08 9.1. */ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. -* Copyright 2011 Kestrel Signal Processing, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -56,9 +48,6 @@ void L3Message::parseBody(const L3Frame&, size_t&) } - - - ostream& GSM::operator<<(ostream& os, L3RRMessage::MessageType val) { switch (val) { @@ -126,6 +115,18 @@ ostream& GSM::operator<<(ostream& os, L3RRMessage::MessageType val) os << "RR Status"; break; case L3RRMessage::ApplicationInformation: os << "Application Information"; break; + case L3RRMessage::HandoverCommand: + os << "Handover Command"; break; + case L3RRMessage::HandoverComplete: + os << "Handover Complete"; break; + case L3RRMessage::HandoverFailure: + os << "Handover Failure"; break; + case L3RRMessage::CipheringModeCommand: + os << "Ciphering Mode Command"; break; + case L3RRMessage::CipheringModeComplete: + os << "Ciphering Mode Complete"; break; + case L3RRMessage::PhysicalInformation: + os << "Physical Information"; break; default: os << hex << "0x" << (int)val << dec; } return os; @@ -152,6 +153,9 @@ L3RRMessage* GSM::L3RRFactory(L3RRMessage::MessageType MTI) case L3RRMessage::ClassmarkEnquiry: return new L3ClassmarkEnquiry(); case L3RRMessage::MeasurementReport: return new L3MeasurementReport(); case L3RRMessage::ApplicationInformation: return new L3ApplicationInformation(); + case L3RRMessage::HandoverComplete: return new L3HandoverComplete(); + case L3RRMessage::HandoverFailure: return new L3HandoverFailure(); + case L3RRMessage::CipheringModeComplete: return new L3CipheringModeComplete(); // Partial support just to get along with some phones. case L3RRMessage::GPRSSuspensionRequest: return new L3GPRSSuspensionRequest(); default: @@ -215,7 +219,8 @@ void L3PagingRequestType1::writeBody(L3Frame& dest, size_t &wp) const assert(sz<=2); // Remember to reverse orders of 1/2-octet fields. // Because GSM transmits LSB-first within each byte. - // channel needed codes + // (pat) No, it is because the fields are written MSB first here, + // and then later byte-reversed in the encoder before being sent to radio LSB first. dest.writeField(wp,channelNeededCode(mChannelsNeeded[1]),2); dest.writeField(wp,channelNeededCode(mChannelsNeeded[0]),2); // "normal paging", GSM 04.08 Table 10.5.63 @@ -320,6 +325,7 @@ void L3SystemInformationType3::writeBody(L3Frame& dest, size_t &wp) const - RACH Control Parameters 10.5.2.29 M V 3 - Rest Octets 10.5.2.34 O CSN.1 */ + size_t wpstart = wp; LOG(DEBUG) << dest; mCI.writeV(dest,wp); LOG(DEBUG) << dest; @@ -333,8 +339,9 @@ void L3SystemInformationType3::writeBody(L3Frame& dest, size_t &wp) const LOG(DEBUG) << dest; mRACHControlParameters.writeV(dest,wp); LOG(DEBUG) << dest; - if (mHaveRestOctets) mRestOctets.writeV(dest,wp); + /*if (mHaveRestOctets)*/ mRestOctets.writeV(dest,wp); LOG(DEBUG) << dest; + assert(wp-wpstart == fullBodyLength() * 8); } @@ -347,25 +354,75 @@ void L3SystemInformationType3::text(ostream& os) const os << " cellOptions=(" << mCellOptions << ")"; os << " cellSelectionParameters=(" << mCellSelectionParameters << ")"; os << " RACHControlParameters=(" << mRACHControlParameters << ")"; - if (mHaveRestOctets) os << " SI3RO=(" << mRestOctets << ")"; + /*if (mHaveRestOctets)*/ os << " SI3RO=(" << mRestOctets << ")"; +} + +L3SIType4RestOctets::L3SIType4RestOctets() +{ +#if GPRS_PAT|GPRS_TESTSI4 + mRA_COLOUR = gConfig.getNum("GPRS.RA_COLOUR"); +#endif +} + +// GSM04.08 sec 10.5.2.35 +void L3SIType4RestOctets::writeV(L3Frame &dest, size_t &wp) const +{ +#if GPRS_PAT|GPRS_TESTSI4 + dest.writeL(wp); // SI4 Rest Octets_O -> Optional selection parameters + dest.writeL(wp); // SI4 Rest Octets_O -> Optional Power offset + if (GPRS::GPRSConfig::IsEnabled()) { + dest.writeH(wp); // SI4 Rest Octets_O -> GPRS Indicator (present) + dest.writeField(wp,mRA_COLOUR,3); + dest.write0(wp); // SI13 message is sent on BCCH Norm schedule. + } else { + dest.writeL(wp); // SI4 Rest Octets_O -> GPRS Indicator (absent) + } + dest.writeL(wp); // Indicates 'Break Indicator' branch of message. + dest.writeL(wp); // Break Indicator == L means no extra info sent in SI Type 7 and 8. +#endif +} + +size_t L3SIType4RestOctets::lengthBits() const +{ +#if GPRS_PAT|GPRS_TESTSI4 + return 1 + 1 + (GPRS::GPRSConfig::IsEnabled() ? 5 : 1) + 1 + 1; +#else + return 0; +#endif +} + +void L3SIType4RestOctets::writeText(std::ostream& os) const +{ +#if GPRS_PAT|GPRS_TESTSI4 + if (GPRS::GPRSConfig::IsEnabled()) { + os << "GPRS enabled; RA_COLOUR=(" << mRA_COLOUR << ")"; + } +#endif } L3SystemInformationType4::L3SystemInformationType4() - :L3RRMessageNRO() + :L3RRMessageRO(), + mHaveCBCH(gConfig.getStr("Control.SMSCB.Table").length() != 0), + mCBCHChannelDescription(SDCCH_4_2,0,gConfig.getNum("GSM.Identity.BSIC.BCC"),gConfig.getNum("GSM.Radio.C0")) { } - size_t L3SystemInformationType4::l2BodyLength() const { size_t len = mLAI.lengthV(); len += mCellSelectionParameters.lengthV(); len += mRACHControlParameters.lengthV(); + if (mHaveCBCH) len += mCBCHChannelDescription.lengthTV(); return len; } +size_t L3SystemInformationType4::restOctetsLength() const +{ + return mType4RestOctets.lengthV(); +} + void L3SystemInformationType4::writeBody(L3Frame& dest, size_t &wp) const { @@ -375,9 +432,16 @@ void L3SystemInformationType4::writeBody(L3Frame& dest, size_t &wp) const - Cell Selection Parameters 10.5.2.4 M V 2 - RACH Control Parameters 10.5.2.29 M V 3 */ + size_t wpstart = wp; mLAI.writeV(dest,wp); mCellSelectionParameters.writeV(dest,wp); mRACHControlParameters.writeV(dest,wp); + if (mHaveCBCH) { + mCBCHChannelDescription.writeTV(0x64,dest,wp); + } + mType4RestOctets.writeV(dest,wp); + while (wp & 7) { dest.writeL(wp); } // Zero to byte boundary. + assert(wp-wpstart == fullBodyLength() * 8); } @@ -387,11 +451,14 @@ void L3SystemInformationType4::text(ostream& os) const os << "LAI=(" << mLAI << ")"; os << " cellSelectionParameters=(" << mCellSelectionParameters << ")"; os << " RACHControlParameters=(" << mRACHControlParameters << ")"; + if (mHaveCBCH) { + os << "CBCHChannelDescription=(" << mCBCHChannelDescription << ")"; + } + mType4RestOctets.writeText(os); } - void L3SystemInformationType5::writeBody(L3Frame& dest, size_t &wp) const { /* @@ -399,6 +466,12 @@ void L3SystemInformationType5::writeBody(L3Frame& dest, size_t &wp) const - BCCH Frequency List 10.5.2.22 M V 16 */ mBCCHFrequencyList.writeV(dest,wp); + wp -= 111; + int p = gConfig.getFloat("GSM.Cipher.RandomNeighbor") * (float)0xFFFFFF; + for (unsigned i = 1; i <= 111; i++) { + int b = ((random() & 0xFFFFFF) < p) | dest.peekField(wp, 1); + dest.writeField(wp, b, 1); + } } @@ -436,26 +509,31 @@ void L3SystemInformationType6::text(ostream& os) const os << " NCCPermitted=(" << mNCCPermitted << ")"; } - void L3ImmediateAssignment::writeBody( L3Frame &dest, size_t &wp ) const { -/* -- Page Mode 10.5.2.26 M V 1/2 -- Dedicated mode or TBF 10.5.2.25b M V 1/2 -- Channel Description 10.5.2.5 C V 3 -- Request Reference 10.5.2.30 M V 3 -- Timing Advance 10.5.2.40 M V 1 -(ignoring optional elements) -*/ + size_t wpstart = wp; + /* + - Page Mode 10.5.2.26 M V 1/2 + - Dedicated mode or TBF 10.5.2.25b M V 1/2 + - Channel Description 10.5.2.5 C V 3 + - Request Reference 10.5.2.30 M V 3 + - Timing Advance 10.5.2.40 M V 1 + (ignoring optional elements) + */ // reverse order of 1/2-octet fields + // (pat) Because we are writing MSB first here. mDedicatedModeOrTBF.writeV(dest, wp); mPageMode.writeV(dest, wp); - mChannelDescription.writeV(dest, wp); + mChannelDescription.writeV(dest, wp); // From L3ChannelDescription mRequestReference.writeV(dest, wp); mTimingAdvance.writeV(dest, wp); // No mobile allocation in non-hopping systems. - // A zero-length LV. Just write L=0. + // A zero-length LV. Just write L=0. (pat) LV, etc. defined in GSM04.07 sec 11.2.1.1 dest.writeField(wp,0,8); + //assert(wp-wpstart == l2BodyLength() * 8); + // Note: optional starting Time not implemented. + mIARestOctets.writeBits(dest,wp); + assert(wp-wpstart == fullBodyLength() * 8); } @@ -466,6 +544,7 @@ void L3ImmediateAssignment::text(ostream& os) const os << " ChannelDescription=("<=src.size()+2*8 && 0x01 == src.peekField(rp,8)) { + rp+=8; // Skip over the IEI type + mServiceSupport = src.readField(rp,8); + } +} +void L3GPRSSuspensionRequest::text(ostream& os) const +{ + L3RRMessage::text(os); + os < namespace GSM { @@ -42,6 +33,20 @@ namespace GSM { /** This a virtual class for L3 messages in the Radio Resource protocol. These messages are defined in GSM 04.08 9.1. + (pat) The GSM 04.08 and 04.18 specs list all these messages together, but say + nothing about their transport mechanisms, which are wildly different and described elsewhere. + For example, GPRS Mobility Management messages are handled inside the SGSN, + which wraps them in an LLC protocol and sends them to the RLC/MAC layer + (via BSSG and NS protocols, in which these messages reside temporarily) + which wraps them in an RLC Header, then wraps them with a MAC header, + before delivering to the radio transmit, which munges them even further. + And by the way, if the MS is in DTM (Dual Transfer Mode) the MAC layer + may (or should?) unwrap the L3 Message from its LLC wrapper and send it on a + dedicated RR message channel instead of sending it via GPRS PDCH. + + Note that the message numbers are reused several times in the tables + for different purposes, for example, MessageTypes for Mobility Management reuse + numbers in Message Types for Radio Rsource Management. */ class L3RRMessage : public L3Message { @@ -64,7 +69,7 @@ class L3RRMessage : public L3Message { SystemInformationType7=0x1f, SystemInformationType8=0x18, SystemInformationType9=0x04, - SystemInformationType13=0x00, + SystemInformationType13=0x00, // (pat) yes, this is correct. SystemInformationType16=0x3d, SystemInformationType17=0x3e, //@} @@ -89,10 +94,14 @@ class L3RRMessage : public L3Message { ///@name Handover //@{ HandoverCommand=0x2b, + HandoverComplete=0x2c, + HandoverFailure=0x28, + PhysicalInformation=0x2d, //@} ///@name ciphering //@{ CipheringModeCommand=0x35, + CipheringModeComplete=0x32, //@} ///@name miscellaneous //@{ @@ -108,12 +117,14 @@ class L3RRMessage : public L3Message { //@{ SynchronizationChannelInformation=0x100, ChannelRequest=0x101, + HandoverAccess=0x102, //@} ///@name application information - used for RRLP //@{ ApplicationInformation=0x38, //@} }; + static const char *name(MessageType mt); L3RRMessage():L3Message() { } @@ -129,6 +140,9 @@ std::ostream& operator<<(std::ostream& os, L3RRMessage::MessageType); /** Subclass for L3 RR Messages with no rest octets. */ +// (pat) L3RRMessagesRO subclass must define: +// l2BodyLength - length of body only, and there are no rest octets. +// writeBody() - writes the body. class L3RRMessageNRO : public L3RRMessage { public: @@ -140,6 +154,10 @@ class L3RRMessageNRO : public L3RRMessage { }; /** Subclass for L3 RR messages with rest octets */ +// (pat) L3RRMessagesRO subclass must define: +// l2BodyLength - length of body only, in bytes +// restOctetsLength - length of rest octets only, in bytes. +// writeBody() - writes the body AND the rest octets. class L3RRMessageRO : public L3RRMessage { public: @@ -293,7 +311,13 @@ class L3SystemInformationType2 : public L3RRMessageNRO { public: - L3SystemInformationType2():L3RRMessageNRO() {} + //L3SystemInformationType2():L3RRMessageNRO() {} + + L3SystemInformationType2(const std::vector& wARFCNs) + :L3RRMessageNRO(), + mBCCHFrequencyList(wARFCNs) + { } + void BCCHFrequencyList(const L3NeighborCellsDescription& wBCCHFrequencyList) { mBCCHFrequencyList = wBCCHFrequencyList; } @@ -335,14 +359,16 @@ class L3SystemInformationType3 : public L3RRMessageRO { L3CellSelectionParameters mCellSelectionParameters; L3RACHControlParameters mRACHControlParameters; - bool mHaveRestOctets; + // (pat) GSM04.08 table 9.32 says RestOctets are mandatory, so why are they optional here? + // I moved this control into the rest octets and changed the logic. + //bool mHaveRestOctets; + // (pat) The rest of the Type3 setup information is in L3SI3RestOctets: L3SI3RestOctets mRestOctets; public: L3SystemInformationType3() - :L3RRMessageRO(), - mHaveRestOctets(gConfig.defines("GSM.SI3RO")) + :L3RRMessageRO() { } void CI(const L3CellIdentity& wCI) { mCI = wCI; } @@ -372,6 +398,15 @@ class L3SystemInformationType3 : public L3RRMessageRO { +struct L3SIType4RestOctets +{ + unsigned mRA_COLOUR; + L3SIType4RestOctets(); + void writeV(L3Frame &dest, size_t &wp) const; + void writeText(std::ostream& os) const; + size_t lengthBits() const; + size_t lengthV() const { return (lengthBits()+7)/8; } +}; /** @@ -379,14 +414,19 @@ class L3SystemInformationType3 : public L3RRMessageRO { - Location Area Identification 10.5.1.3 M V 5 - Cell Selection Parameters 10.5.2.4 M V 2 - RACH Control Parameters 10.5.2.29 M V 3 + pats note: We included the GPRS Indicator in the rest octets for SI3, so I am assuming + we do not also have to included it in SI4. */ -class L3SystemInformationType4 : public L3RRMessageNRO { +class L3SystemInformationType4 : public L3RRMessageRO { private: L3LocationAreaIdentity mLAI; L3CellSelectionParameters mCellSelectionParameters; L3RACHControlParameters mRACHControlParameters; + bool mHaveCBCH; + L3ChannelDescription mCBCHChannelDescription; + L3SIType4RestOctets mType4RestOctets; public: @@ -403,7 +443,7 @@ class L3SystemInformationType4 : public L3RRMessageNRO { int MTI() const { return (int)SystemInformationType4; } size_t l2BodyLength() const; - + size_t restOctetsLength() const; void writeBody(L3Frame &dest, size_t &wp) const; void text(std::ostream&) const; }; @@ -423,7 +463,12 @@ class L3SystemInformationType5 : public L3RRMessageNRO { public: - L3SystemInformationType5():L3RRMessageNRO() { } + //L3SystemInformationType5():L3RRMessageNRO() { } + + L3SystemInformationType5(const std::vector& wARFCNs) + :L3RRMessageNRO(), + mBCCHFrequencyList(wARFCNs) + { } void BCCHFrequencyList(const L3NeighborCellsDescription& wBCCHFrequencyList) { mBCCHFrequencyList = wBCCHFrequencyList; } @@ -477,38 +522,114 @@ class L3SystemInformationType6 : public L3RRMessageNRO { }; +// GSM 04.08 10.5.2.16 +struct L3IARestOctets : public GenericMessageElement +{ + // Someday this may include frequence parameters, but now all it can be is Packet Assignment. + struct L3IAPacketAssignment mPacketAssignment; + + size_t lengthBits() const { + return mPacketAssignment.lengthBits(); + } + void writeBits(L3Frame &dest, size_t &wp) const { + mPacketAssignment.writeBits(dest, wp); + while (wp & 7) { dest.writeL(wp); } // fill out to byte boundary. + } + void text(std::ostream& os) const { + mPacketAssignment.text(os); + } +}; + /** Immediate Assignment, GSM 04.08 9.1.18 */ -class L3ImmediateAssignment : public L3RRMessageNRO { +// (pat) The elements below correspond directly to the parts of the Immediate Assignment +// message content as defined in 9.1.18, table 9.18. +// (pat) some deobfuscation. +// // Example from Control::AccessGrantResponder() +// L3ImmediateAssignment assign( // Initialization of assign message +// L3RequestReference( +// unsigned RA, // => L3RequestReference::mRA +// GSM::Time &when // => L3RequestReference::mT1p,mT2,mT3 +// ), // {/*empty body*/} +// LogicalChannel *LCH->channelDescription(), +// // returns L3ChannelDescription( +// // LCH->mL1.typeAndOffset(), // => L3ChannelDescription::mTypeAndOffset; +// // LCH->mL1.TN(), // => L3ChannelDescription::mTN +// // LCH->mL1.TSC(), // => L3ChannelDescription::mTSC +// // LCH->mL1.ARFCN()) // => L3ChannelDescription::mARFCN; +// // { L3ChannelDescription::mHFlag = 0, ::mMAIO = 0, ::mHSN = 0 } +// L3TimingAdvance( +// int initialTA // => L3TimingAdvance::mTimingAdvance +// // L3TimingAdvance : L3ProtocolElement() [virtual class, no constructor] +// ) +// ); + + +class L3ImmediateAssignment : public L3RRMessageRO +{ private: L3PageMode mPageMode; L3DedicatedModeOrTBF mDedicatedModeOrTBF; L3RequestReference mRequestReference; + // (pat) Note that either "Channel Description" or "Packet Channel Description" + // appears in the message. They are identical except for some new options + // added to Packet Channel Description. L3ChannelDescription mChannelDescription; L3TimingAdvance mTimingAdvance; + // Used for Packet uplink/downlink assignment messages, which, when transmitted + // on CCCH, appear in the rest octets of the Immediate Assignment message. + struct L3IARestOctets mIARestOctets; + public: + size_t restOctetsLength() const { return (mIARestOctets.lengthBits()+7)/8; } L3ImmediateAssignment( const L3RequestReference& wRequestReference, const L3ChannelDescription& wChannelDescription, - const L3TimingAdvance& wTimingAdvance = L3TimingAdvance(0)) - :L3RRMessageNRO(), + const L3TimingAdvance& wTimingAdvance = L3TimingAdvance(0), + const bool forTBF = false, bool wDownlink = false) + :L3RRMessageRO(), + mPageMode(0), + mDedicatedModeOrTBF(forTBF,wDownlink), mRequestReference(wRequestReference), mChannelDescription(wChannelDescription), mTimingAdvance(wTimingAdvance) {} + int MTI() const { return (int)ImmediateAssignment; } - size_t l2BodyLength() const { return 9; } + size_t l2BodyLength() const { + // 1/2: page mode + // 1/2: Dedicated mode or TBF + // 3: channel description or packet channel description + // 3: request reference + // 1: timing advance + // 1-9: Mobile Allocation (not implemented, so just 1 byte) + // 0: starting time, not implemented + // = 9 total bytes. + return 9; + } + + // (pat) Return the PacketAssignment part of the message for the client to fill in. + // I did it this way to cause the least change to the preexisting L3ImmediateAssignment class. + struct L3IAPacketAssignment *packetAssign() { return &mIARestOctets.mPacketAssignment; } + + + // (pat) writeBody called via: + // CCCHLogicalChannel:send(L3RRMessage msg) + // calls L3FrameFIFO mq.L3FrameFIFO::write(new L3Frame(L3Message msg,UNIT_DATA)); + // L3Frame::L3Frame(L3Message&,Primitive) : BitVector(msg.bitsNeeded) + // mPrimitive(wPrimitive), + // mL2Length(wPrimitive) { L3Message msg.write(); } + // L3Message::write() calls writeBody() void writeBody(L3Frame &dest, size_t &wp) const; void text(std::ostream&) const; - }; @@ -549,12 +670,17 @@ class L3ChannelRelease : public L3RRMessageNRO { private: L3RRCause mRRCause; + // 3GPP 44.018 10.5.2.14c GPRS Resumption. + // It is one bit to specify to the MS whether GPRS services should resume. + // Kinda important. + bool mGprsResumptionPresent; + bool mGprsResumptionBit; public: /** The default cause is 0x0, "normal event". */ L3ChannelRelease(const L3RRCause& cause = L3RRCause(0x0)) - :L3RRMessageNRO(),mRRCause(cause) + :L3RRMessageNRO(),mRRCause(cause),mGprsResumptionPresent(0) {} int MTI() const { return (int) ChannelRelease; } @@ -819,16 +945,37 @@ class L3ApplicationInformation : public L3RRMessageNRO { }; -/** GSM 04.08 9.1.13b */ -class L3GPRSSuspensionRequest : public L3RRMessageNRO { - - public: +/** GSM 04.08 (or 04.18) 9.1.13b */ +// 44.018 3.4.25 GPRS Suspension procedure. +// 44.018 3.4.13 RR Connection releasee procedure - GPRS resumption if includes: +// 10.5.2.14c GPRS Resumption IEI +class L3GPRSSuspensionRequest : public L3RRMessageNRO +{ public: + // The TLLI is what we most need out of this message to identify the MS. + // Note that TLLI is not just a simple number; encoding defined in 23.003. + // We dont worry about it here; the SGSN handles that. + uint32_t mTLLI; + // 3GPP 24.008 10.5.5.15 Routing Area Identification. + // Similar to L3LocationAreaIdentity but includes RAC too. + // We dont really care about it now, and when we do, all we will care + // is if it matches our own or not, so just squirrel away the 6 bytes as a ByteVector. + // This is an immediate object whose memory will be deleted automatically. + ByteVector mRaId; + // Suspension cause 44.018 10.5.2.47. + // Can be 0: mobile originated call, 1: Location area update, 2: SMS, or others + uint8_t mSuspensionCause; + // Optional service support, IEI=0x01. + // It is for MBMS and we dont really care about it, but get it anyway. + uint8_t mServiceSupport; + + // Must init only the optional elements: + L3GPRSSuspensionRequest() : mServiceSupport(0) {} int MTI() const { return (int) GPRSSuspensionRequest; } - size_t l2BodyLength() const { return 11; } - + size_t l2BodyLength() const { return 11; } // This is uplink only so not relevant. void parseBody(const L3Frame&, size_t&); + void text(std::ostream&) const; }; @@ -865,9 +1012,209 @@ class L3ClassmarkChange : public L3RRMessageNRO { }; -} // GSM +/** GSM 04.08 9.1.43a */ +// (pat) Someone kindly added this before I got here... +// SI13 includes GPRS Cell Options in its rest octets, +// so we broadcast it on BCCH if GPRS is supported. +class L3SystemInformationType13 : public L3RRMessageRO { + + protected: + + L3SI13RestOctets mRestOctets; + + public: + + L3SystemInformationType13() + :L3RRMessageRO() + { } + + int MTI() const { return (int)SystemInformationType13; } + size_t l2BodyLength() const { return 0; } + + size_t restOctetsLength() const { return mRestOctets.lengthV(); } + + void writeBody(L3Frame &dest, size_t &wp) const + { + size_t wpstart = wp; + mRestOctets.writeV(dest,wp); + assert(wp-wpstart == fullBodyLength() * 8); + } + + void text(std::ostream& os) const + { L3RRMessage::text(os); os << mRestOctets; } +}; + + + + +class L3HandoverCommand : public L3RRMessageNRO { + + protected: + + L3CellDescription mCellDescription; + L3ChannelDescription2 mChannelDescriptionAfter; + L3HandoverReference mHandoverReference; + L3PowerCommandAndAccessType mPowerCommandAccessType; + L3SynchronizationIndication mSynchronizationIndication; + + public: + + L3HandoverCommand(const L3CellDescription& wCellDescription, + const L3ChannelDescription2 wChannelDescriptionAfter, + const L3HandoverReference& wHandoverReference, + const L3PowerCommandAndAccessType& wPowerCommandAccessType, + const L3SynchronizationIndication& wSynchronizationIndication) + :L3RRMessageNRO(), + mCellDescription(wCellDescription), + mChannelDescriptionAfter(wChannelDescriptionAfter), + mHandoverReference(wHandoverReference), + mPowerCommandAccessType(wPowerCommandAccessType), + mSynchronizationIndication(wSynchronizationIndication) + { } + + int MTI() const { return (int) HandoverCommand; } + + size_t l2BodyLength() const; + void writeBody(L3Frame&, size_t&) const; + void text(std::ostream&) const; +}; + + + +/** GSM 04.08 9.1.16 */ +class L3HandoverComplete : public L3RRMessageNRO { + + protected: + + L3RRCause mCause; + + public: + + int MTI() const { return (int) HandoverComplete; } + + size_t l2BodyLength() const { return mCause.lengthV(); } + void parseBody(const L3Frame&, size_t&); + void text(std::ostream&) const; + + const L3RRCause& cause() const { return mCause; } +}; + +/** GSM 04.08 9.1.17 */ +class L3HandoverFailure : public L3RRMessageNRO { + + protected: + + L3RRCause mCause; + + public: + + int MTI() const { return (int) HandoverFailure; } + + size_t l2BodyLength() const { return mCause.lengthV(); } + void parseBody(const L3Frame&, size_t&); + void text(std::ostream&) const; + + const L3RRCause& cause() const { return mCause; } +}; + + +/** GSM 04.08 9.1.9 */ +class L3CipheringModeCommand : public L3RRMessageNRO { + + protected: + + L3CipheringModeSetting mCipheringModeSetting; + L3CipheringModeResponse mCipheringResponse; + + public: + + L3CipheringModeCommand(L3CipheringModeSetting wCipheringModeSetting, L3CipheringModeResponse wCipheringResponse) + : mCipheringModeSetting(wCipheringModeSetting), + mCipheringResponse(wCipheringResponse) + { } + + int MTI() const; + + size_t l2BodyLength() const { return 1; } + void writeBody(L3Frame&, size_t&) const; + void text(std::ostream&) const; +}; + +/** GSM 04.08 9.1.10 */ +class L3CipheringModeComplete : public L3RRMessageNRO { + + protected: + + public: + + int MTI() const; + + size_t l2BodyLength() const { return 0; } + void parseBody(const L3Frame&, size_t&); + void text(std::ostream&) const; +}; + + +/** GSM 04.08 9.1.28 */ +class L3PhysicalInformation : public L3RRMessageNRO { + + protected: + + L3TimingAdvance mTA; + + public: + + L3PhysicalInformation(const L3TimingAdvance& wTA) + :L3RRMessageNRO(), + mTA(wTA) + { } + + + int MTI() const { return (int) PhysicalInformation; } + + size_t l2BodyLength() const { return mTA.lengthV(); } + void writeBody(L3Frame&, size_t&) const; + void text(std::ostream&) const; +}; + + + +#if 0 +/** + GSM 04.08 9.1.14 + This messages has no parse or write methods, but is used to + transfer information from L1 to the control layer. +*/ +class L3HandoverAccess : public L3RRMessageNRO { + + protected: + + unsigned mReference; + float mTimingError; + float mRSSI; + + public: + + L3HandoverAccess(unsigned wReference, unsigned wTimingError, float wRSSI) + :L3RRMessageNRO(), + mReference(wReference),mTimingError(wTimingError),mRSSI(wRSSI) + { } + + int MTI() const { return (int) HandoverAccess; } + + size_t l2BodyLength() const { return 1; } + + unsigned reference() const { return mReference; } + float timingError() const { return mTimingError; } + float RSSI() const { return mRSSI; } +}; +#endif + + +} // GSM + #endif // vim: ts=4 sw=4 diff --git a/GSM/GSMLogicalChannel.cpp b/GSM/GSMLogicalChannel.cpp index f3a04c0b..5bcc1be8 100644 --- a/GSM/GSMLogicalChannel.cpp +++ b/GSM/GSMLogicalChannel.cpp @@ -1,28 +1,20 @@ /**@file Logical Channel. */ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -31,15 +23,16 @@ #include "GSML3RRElements.h" #include "GSML3Message.h" #include "GSML3RRMessages.h" +#include "GSMSMSCBL3Messages.h" #include "GSMLogicalChannel.h" #include "GSMConfig.h" #include #include #include +#include "GPRSExport.h" #include -#undef WARNING using namespace std; using namespace GSM; @@ -49,10 +42,14 @@ using namespace GSM; void LogicalChannel::open() { LOG(INFO); + LOG(DEBUG); if (mSACCH) mSACCH->open(); - if (mL1) mL1->open(); + LOG(DEBUG); + if (mL1) mL1->open(); // (pat) L1FEC::open() + LOG(DEBUG); for (int s=0; s<4; s++) { if (mL2[s]) mL2[s]->open(); + LOG(DEBUG) << "SAPI=" << s << " open complete"; } // Empty any stray transactions in the FIFO from the SIP layer. while (true) { @@ -61,9 +58,11 @@ void LogicalChannel::open() LOG(WARNING) << "flushing stray transaction " << *trans; // FIXME -- Shouldn't we be deleting these? } + LOG(DEBUG); } +// (pat) This is connecting layer2, not layer1. void LogicalChannel::connect() { mMux.downstream(mL1); @@ -75,9 +74,11 @@ void LogicalChannel::connect() } +// (pat) This is only called during initialization, using the createCombination*() functions. +// The L1FEC->downstream hooks the radio to this logical channel, permanently. void LogicalChannel::downstream(ARFCNManager* radio) { - assert(mL1); + assert(mL1); // This is L1FEC mL1->downstream(radio); if (mSACCH) mSACCH->downstream(radio); } @@ -115,23 +116,68 @@ void CCCHLogicalChannel::open() } -void CCCHLogicalChannel::serviceLoop() +// (pat) BUG TODO: TO WHOM IT MAY CONCERN: +// I am not sure this routine works properly. If there is no CCCH message (an L3Frame) +// in the queue immediately after the previous frame is sent, an idle frame is inserted. +// If a subsequent valid CCCH message (paging response or MS initiated RR call or packet +// uplink request) arrives it will be blocked until the idle frame is sent. +// Probably doesnt matter for RR establishment, but for packets, the extra 1/4 sec +// delay (length of a 51-multiframe) is going to hurt. +// Note that a GPRS Immediate Assignment message must know when this CCCH gets sent. +// Right now, it has to guess. +// pats TODO: Send the transceiver an idle frame rather than doing it here. +// This should be architecturally changed to a pull-system instead of push. +// Among other things, that would let us prioritize the responses +// (eg, emergency calls go first) and let the packet Immediate Assignment message be +// created right before being sent, when we are certain when the +// Immediate Assignment is being sent. +void CCCHLogicalChannel::serviceLoop() { // build the idle frame static const L3PagingRequestType1 filler; static const L3Frame idleFrame(filler,UNIT_DATA); +#if ENABLE_PAGING_CHANNELS + L3ControlChannelDescription mCC; + unsigned bs_pa_mfrms = mCC.getBS_PA_MFRMS(); +#endif // prime the first idle frame LogicalChannel::send(idleFrame); // run the loop while (true) { - L3Frame* frame = mQ.read(); + L3Frame* frame = NULL; +#if ENABLE_PAGING_CHANNELS + // Check for paging message for this specific paging slot first, + // and if none, send any message in the mQ. + // The multiframe paging logic is from GSM 05.02 6.5.3. + // See documentation at crackPagingFromImsi() which is used to + // get the messages into the proper mPagingQ. + GSM::Time next = getNextWriteTime(); + unsigned multiframe_index = (next.FN() / 51) % bs_pa_mfrms; + frame = mPagingQ[multiframe_index].read(); +#endif + if (frame == NULL) { + frame = mQ.read(); // (pat) This is a blocking read; mQ is an InterThreadQueue + } if (frame) { + // (pat) This tortuously calls XCCCHL1Encoder::transmit (see my documentation + // at LogicalChannel::send), which blocks until L1Encoder::mPrevWriteTime. + // Note: The q size is 0 while we are blocked here, so if we are trying + // to determine the next write time by adding the qsize, we are way off. + // Thats why there is an mWaitingToSend flag. + mWaitingToSend = true; // Waiting to send this block at mNextWriteTime. LogicalChannel::send(*frame); + mWaitingToSend = false; OBJLOG(DEBUG) << "CCCHLogicalChannel::serviceLoop sending " << *frame; delete frame; } if (mQ.size()==0) { + // (pat) The radio continues to send the last frame forever, + // so we only send one idle frame here. + // Unfortunately, this slows the response. + // TODO: Send a static idle frame to the Transciever and rewrite this. + mWaitingToSend = true; // Waiting to send an idle frame at mNextWriteTime. LogicalChannel::send(idleFrame); + mWaitingToSend = false; OBJLOG(DEBUG) << "CCCHLogicalChannel::serviceLoop sending idle frame"; } } @@ -144,6 +190,77 @@ void *GSM::CCCHLogicalChannelServiceLoopAdapter(CCCHLogicalChannel* chan) return NULL; } +#if ENABLE_PAGING_CHANNELS +// (pat) This routine is going to be entirely replaced with one that works better for gprs. +// In the meantime, just return a number that is large enough to cover +// the worst case, which assumes that the messages in mQ also +// must go out on the paging timeslot. +Time GSM::CCCHLogicalChannel::getNextPchSendTime(unsigned multiframe_index) +{ + L3ControlChannelDescription mCC; + // Paging is distributed over this many multi-frames. + unsigned bs_pa_mfrms = mCC.getBS_PA_MFRMS(); + + GSM::Time next = getNextWriteTime(); + unsigned next_multiframe_index = (next.FN() / 51) % bs_pa_mfrms; + assert(bs_pa_mfrms > 1); + assert(multiframe_index < bs_pa_mfrms); + assert(next_multiframe_index < bs_pa_mfrms); + int achload = mQ.size(); + if (mWaitingToSend) { achload++; } + + // Total wait time is time needed to empty queue, plus the time until the first + // paging opportunity, plus 2 times the number of guys waiting in the paging queue, + // but it is all nonsense because if a new agch comes in, + // it will displace the paging message because the q is sent first. + // This just needs to be totally redone, and the best way is not to figure out + // when the message will be sent at all, but rather use a call-back to gprs + // just before the message is finally sent. + int multiframesToWait = 0; + if (achload) { + multiframesToWait = bs_pa_mfrms - 1; // Assume worst case. + } else { + // If there is nothing else waiting, we can estimate better: + while (next_multiframe_index != multiframe_index) { + multiframe_index = (multiframe_index+1) % bs_pa_mfrms; + multiframesToWait++; + } + } + int total = achload + multiframesToWait + bs_pa_mfrms * mPagingQ[multiframe_index].size(); + + int fnresult = (next.FN() + total * 51) % gHyperframe; + GSM::Time result(fnresult); + LOG(DEBUG) << "CCCHLogicalChannel::getNextSend="<< next.FN() + <<" load="<setPhy(wRSSI,wTimingError); } +void LogicalChannel::setPhy(float wRSSI, float wTimingError, double wTimestamp) + { assert(mSACCH); mSACCH->setPhy(wRSSI,wTimingError,wTimestamp); } void LogicalChannel::setPhy(const LogicalChannel& other) { assert(mSACCH); mSACCH->setPhy(*other.SACCH()); } float LogicalChannel::RSSI() const { assert(mSACCH); return mSACCH->RSSI(); } float LogicalChannel::timingError() const { assert(mSACCH); return mSACCH->timingError(); } +double LogicalChannel::timestamp() const + { assert(mSACCH); return mSACCH->timestamp(); } int LogicalChannel::actualMSPower() const { assert(mSACCH); return mSACCH->actualMSPower(); } int LogicalChannel::actualMSTiming() const { assert(mSACCH); return mSACCH->actualMSTiming(); } +const L3MeasurementResults& LogicalChannel::measurementResults() const + { assert(mSACCH); return mSACCH->measurementResults(); } @@ -362,11 +497,28 @@ TCHFACCHLogicalChannel::TCHFACCHLogicalChannel( // SAP1 and SAP2 are not used. mL2[0] = new FACCHL2(1,0); mL2[3] = new FACCHL2(1,3); - mSACCH = new SACCHLogicalChannel(wCN,wTN,wMapping.SACCH()); + mSACCH = new SACCHLogicalChannel(wCN,wTN,wMapping.SACCH(),this); + connect(); +} + + + + +CBCHLogicalChannel::CBCHLogicalChannel(const CompleteMapping& wMapping) +{ + mL1 = new CBCHL1FEC(wMapping.LCH()); + mL2[0] = new CBCHL2; + mSACCH = new SACCHLogicalChannel(0,0,wMapping.SACCH(),this); connect(); } +void CBCHLogicalChannel::send(const L3SMSCBMessage& msg) +{ + L3Frame frame(UNIT_DATA,88*8); + msg.write(frame); + LogicalChannel::send(frame); +} @@ -398,6 +550,20 @@ void LogicalChannel::waitForPrimitive(Primitive primitive) } } +L3Frame* LogicalChannel::waitForEstablishOrHandover() +{ + while (true) { + L3Frame *req = recv(); + if (req==NULL) continue; + if (req->primitive()==ESTABLISH) return req; + if (req->primitive()==HANDOVER_ACCESS) return req; + LOG(INFO) << "LogicalChannel: Ignored primitive:"<primitive(); + delete req; + } + return NULL; // to keep the compiler happy +} + + ostream& GSM::operator<<(ostream& os, const LogicalChannel& chan) { diff --git a/GSM/GSMLogicalChannel.h b/GSM/GSMLogicalChannel.h index 710b30e0..f1fb3c40 100644 --- a/GSM/GSMLogicalChannel.h +++ b/GSM/GSMLogicalChannel.h @@ -4,24 +4,16 @@ * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -61,6 +53,7 @@ typedef InterthreadQueue TransactionFIFO; class SACCHLogicalChannel; class L3Message; class L3RRMessage; +class L3SMSCBMessage; /** @@ -70,6 +63,8 @@ class L3RRMessage; The concept of the logical channel and the channel types are defined in GSM 04.03. This is virtual class; specific channel types are subclasses. */ +// (pat) It would be nice to break this into two classes: one that has the base functionality +// that GPRS will not use, and one with all the RR specific channel stuff. class LogicalChannel { protected: @@ -118,12 +113,17 @@ class LogicalChannel { /**@name Pass-throughs. */ //@{ + // Pat 5-27-2012: Let the LogicalChannel know the next scheduled write time. + GSM::Time getNextWriteTime() { return mL1->encoder()->getNextWriteTime(); } + /** Set L1 physical parameters from a RACH or pre-exsting channel. */ - virtual void setPhy(float wRSSI, float wTimingError); + virtual void setPhy(float wRSSI, float wTimingError, double wTimestamp); /* Set L1 physical parameters from an existing logical channel. */ virtual void setPhy(const LogicalChannel&); + virtual const L3MeasurementResults& measurementResults() const; + /**@name L3 interfaces */ //@{ @@ -146,6 +146,48 @@ class LogicalChannel { */ virtual void send(const L3Frame& frame, unsigned SAPI=0) { + // (pat) Note that writeHighSide is overloaded per class hierarchy, and is also used + // for entirely unrelated classes, which are distinguishable (by humans, + // not by the compiler, which considers them unrelated functions) + // by arguments of L3Frame or L2Frame. + // + // For traffic channels: + // This function calls virtual L2DL::writeHighSide(L3Frame) which I think maps + // to L2LAPDm::writeHighSide() which interprets the primitive, and then + // sends traffic data through sendUFrameUI(L3Frame) which creates an L2Frame + // and sends it through several irrelevant functions to L2LAPDm::writeL1 + // which calls (SAPMux)mDownstream->SAPMux::writeHighSide(L2Frame), + // which does nothing but call mL1->writeHighSide(L2Frame), which is a pass-through + // except that the SapMux uses mDownStream which is copied from mL1, so there is a + // chance to redirect it. But wouldn't that be an error? + // Anyway, L1Encoder::writeHighSide is usually overridden. + // For TCH, it goes to XCCHL1Encoder::writeHighSide() which processes + // the L2Frame primitive, then sends traffic data to TCHFACCHL1Encoder::sendFrame(), + // which just enqueues the frame - it does not block. + // A thread runs GSM::TCHFACCHL1EncoderRoutine() which + // calls TCHFACCHL1Encoder::dispatch() which is synchronized with the gBTS clock, + // unsynchronized with the queue, because it must send data no matter what. + // Eventually it encodes the data and + // calls (ARFCNManager*)mDownStream->writeHighSideTx(), which writes to the socket. + // + // For CCCH channels: + // CCCHLogicalChannel::send(L3RRMessage) wraps the message in an L3Frame + // and enqueues the message on CCCHLogicalChannel::mQ. + // CCCHLogicalChannel::serviceLoop() pulls it out and sends it to + // LogicalChannel::send(L3Frame) [this function], which is virtual, but I dont think it + // is over-ridden, so message goes to L2DL::writeHighSide(L3Frame) which + // is over-ridden to CCCHL2::writeHighSide(L3Frame) which creates an L2Frame + // and calls (SAPMux)mDownstream->writeHighSide(L2Frame), which just + // calls (L1FEC)mDownStream->writeHighSide(L2Frame), which + // (because CCCHL1FEC is nearly empty) just + // calls (L1Encoder)mEncoder->writeHighSide(L2Frame), which maps + // to CCCHL1Encoder which maps to XCCHL1Encoder::writeHighSide(L2Frame), + // which processes the L2Frame primitive, and sends traffic data to + // XCCHL1Encoder::sendFrame(L2Frame), which encodes the frame and then calls + // XCCHL1Encoder::transmit(implicit mI arg with encoded burst) that + // finally blocks until L1Encoder::mPrevWriteTime occurs, then sets the + // burst time to L1Encoder::mNextWriteTime and + // calls (ARFCNManager*)mDownStream->writeHighSideTx() which writes to the socket. assert(mL2[SAPI]); LOG(DEBUG) << "SAP"<< SAPI << " " << frame; mL2[SAPI]->writeHighSide(frame); @@ -180,6 +222,9 @@ class LogicalChannel { */ void waitForPrimitive(GSM::Primitive primitive); + /** Block until a HANDOVER_ACCESS or ESTABLISH arrives. */ + L3Frame* waitForEstablishOrHandover(); + /** Block on a channel until a given primitive arrives. Any payload is discarded. Block indefinitely, no timeout. @@ -197,18 +242,20 @@ class LogicalChannel { //@{ /** Write a received radio burst into the "low" side of the channel. */ - virtual void writeLowSide(const RxBurst& burst) { assert(mL1); mL1->writeLowSide(burst); } + virtual void writeLowSide(const RxBurst& burst) { assert(mL1); mL1->writeLowSideRx(burst); } /** Return true if the channel is safely abandoned (closed or orphaned). */ - bool recyclable() const { assert(mL1); return mL1->recyclable(); } + virtual bool recyclable() const { assert(mL1); return mL1->recyclable(); } /** Return true if the channel is active. */ - bool active() const { assert(mL1); return mL1->active(); } + virtual bool active() const { assert(mL1); return mL1->active(); } /** The TDMA parameters for the transmit side. */ + // (pat) This lovely function is unused. Use L1Encoder::mapping() const TDMAMapping& txMapping() const { assert(mL1); return mL1->txMapping(); } /** The TDMAParameters for the receive side. */ + // (pat) This lovely function is unused. Use L1Decoder::mapping() const TDMAMapping& rcvMapping() const { assert(mL1); return mL1->rcvMapping(); } /** GSM 04.08 10.5.2.5 type and offset code. */ @@ -229,10 +276,14 @@ class LogicalChannel { virtual float RSSI() const; /** Uplink timing error. */ virtual float timingError() const; + /** System timestamp of RSSI and TA */ + virtual double timestamp() const; /** Actual MS uplink power. */ virtual int actualMSPower() const; /** Actual MS uplink timing advance. */ virtual int actualMSTiming() const; + /** Control whether to accept a handover. */ + void handoverPending(bool flag) { assert(mL1); mL1->handoverPending(flag); } //@} //@} // L1 @@ -277,6 +328,10 @@ class LogicalChannel { */ virtual void connect(); + public: + bool inUseByGPRS() { return mL1->inUseByGPRS(); } + + bool decryptUplink_maybe(string wIMSI, int wA5Alg) { return mL1->decoder()->decrypt_maybe(wIMSI, wA5Alg); } }; @@ -357,13 +412,15 @@ class SACCHLogicalChannel : public LogicalChannel { /** MeasurementResults from the MS. They are caught in serviceLoop, accessed for recording along with GPS and other data in MobilityManagement.cpp */ L3MeasurementResults mMeasurementResults; + const LogicalChannel *mHost; public: SACCHLogicalChannel( unsigned wCN, unsigned wTN, - const MappingPair& wMapping); + const MappingPair& wMapping, + const LogicalChannel* wHost); ChannelType type() const { return SACCHType; } @@ -375,10 +432,14 @@ class SACCHLogicalChannel : public LogicalChannel { //@{ float RSSI() const { return mSACCHL1->RSSI(); } float timingError() const { return mSACCHL1->timingError(); } + double timestamp() const { return mSACCHL1->timestamp(); } int actualMSPower() const { return mSACCHL1->actualMSPower(); } int actualMSTiming() const { return mSACCHL1->actualMSTiming(); } - void setPhy(float RSSI, float timingError) { mSACCHL1->setPhy(RSSI,timingError); } + void setPhy(float RSSI, float timingError, double wTimestamp) + { mSACCHL1->setPhy(RSSI,timingError,wTimestamp); } void setPhy(const SACCHLogicalChannel& other) { mSACCHL1->setPhy(*other.mSACCHL1); } + void RSSIBumpDown(int dB) { assert(mL1); mSACCHL1->RSSIBumpDown(dB); } + //@} /**@name Channel and neighbour cells stats as reported from MS */ @@ -386,6 +447,12 @@ class SACCHLogicalChannel : public LogicalChannel { const L3MeasurementResults& measurementResults() const { return mMeasurementResults; } //@} + /** Get active state from the host DCCH. */ + bool active() const { assert(mHost); return mHost->active(); } + + /** Get recyclable state from the host DCCH. */ + bool recyclable() const { assert(mHost); return mHost->recyclable(); } + protected: /** Read and process a measurement report, called from the service loop. */ @@ -400,9 +467,6 @@ class SACCHLogicalChannel : public LogicalChannel { void *SACCHLogicalChannelServiceLoopAdapter(SACCHLogicalChannel*); - - - /** Common control channel. The "uplink" component of the CCCH is the RACH. @@ -410,10 +474,14 @@ void *SACCHLogicalChannelServiceLoopAdapter(SACCHLogicalChannel*); bi-directional control channel. Common control channels are physically sub-divided into the common control channel (CCCH), the packet common control channel (PCCCH), and the Compact packet common control channel (CPCCCH)." + (pat) To implement DRX and paging I added the CCCHCombinedChannel to which CCCH messages + should now be sent, and this class is now just a private attachment point whose primary + purpose is to house the serviceloop for a single CCCH. */ class CCCHLogicalChannel : public NDCCHLogicalChannel { protected: + friend class GSMConfig; /* Because the CCCH is written by multiple threads, @@ -423,7 +491,14 @@ class CCCHLogicalChannel : public NDCCHLogicalChannel { Thread mServiceThread; ///< a thread for the service loop L3FrameFIFO mQ; ///< because the CCCH is written by multiple threads +#if ENABLE_PAGING_CHANNELS + L3FrameFIFO mPagingQ[sMax_BS_PA_MFRMS]; ///< A queue for each paging channel on this timeslot. +#endif bool mRunning; ///< a flag to indication that the service loop is running + bool mWaitingToSend; // If this is set, there is another CCCH message + // waiting in the encoder serviceloop. + // This variable is not mutex locked and could + // be incorrect, but it is not critical. public: @@ -432,7 +507,11 @@ class CCCHLogicalChannel : public NDCCHLogicalChannel { void open(); void send(const L3RRMessage& msg) - { mQ.write(new L3Frame((const L3Message&)msg,UNIT_DATA)); } + { + // DEBUG: + //LOG(WARNING) << "CCCHLogicalChannel2::write q"; + mQ.write(new L3Frame((const L3Message&)msg,UNIT_DATA)); + } void send(const L3Message&) { assert(0); } @@ -442,6 +521,27 @@ class CCCHLogicalChannel : public NDCCHLogicalChannel { /** Return the number of messages waiting for transmission. */ unsigned load() const { return mQ.size(); } + // (pat) GPRS needs to know exactly when the CCCH message will be sent downstream, + // because it needs to allocate an upstream radio block after that time, + // and preferably as quickly as possible after that time. + // For now, I'm going to punt on this and return the worst case. + // TODO: This is the wrong way to do this. + // First, this calculation should not be here; it will be hard for anyone maintaining + // the code and making changes that would affect this calculation to find it here. + // Second, it depends on what kind of C0T0 beacon we have. + // We should wait until it is time to send the message, then create it. + // To do this, either the CCCHLogicalChannel::serviceLoop should be rewritten, + // or we should hook XCCHL1Encoder::sendFrame(L2Frame) to modify the message + // if it is a packet message. Or more drastically, make the CCCHLogicalChannel::mQ + // queue hold internal messages not L3Frames, for example, for RACH a struct + // with the arrival time, RACH message, signal strength and timing advance, + // and delay generating the RRMessage until it is ready to send. + // + // But for now, just punt and send a frame time far enough in the future that it + // is guaranteed to work: + // Note: Time wraps at gHyperFrame. + Time getNextMsgSendTime(); + ChannelType type() const { return CCCHType; } friend void *CCCHLogicalChannelServiceLoopAdapter(CCCHLogicalChannel*); @@ -492,6 +592,33 @@ class TCHFACCHLogicalChannel : public LogicalChannel { +/** + Cell broadcast control channel (CBCH). + See GSM 04.12 3.3.1. +*/ +class CBCHLogicalChannel : public NDCCHLogicalChannel { + + protected: + + /* + The CBCH should be written be a single thread. + The input interface is *not* multi-thread safe. + */ + + public: + + CBCHLogicalChannel(const CompleteMapping& wMapping); + + void send(const L3SMSCBMessage& msg); + + void send(const L3Message&) { assert(0); } + + ChannelType type() const { return CBCHType; } + + +}; + + /**@name Test channels, not actually used in GSM. */ diff --git a/GSM/GSMSAPMux.cpp b/GSM/GSMSAPMux.cpp index 2113981d..35bfc5cd 100644 --- a/GSM/GSMSAPMux.cpp +++ b/GSM/GSMSAPMux.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/GSMSAPMux.h b/GSM/GSMSAPMux.h index 5cc75d98..9aa046c5 100644 --- a/GSM/GSMSAPMux.h +++ b/GSM/GSMSAPMux.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -42,6 +32,7 @@ class L2DL; A "service access point" in GSM/ISDN is analogous to port number in IP. GSM allows up to 4 SAPs, although only two are presently used. See GSM 04.05 5.2 for an introduction. + (pat) SAPs exist at every level in the OSI model. This should probably be called L2SAPMux. */ class SAPMux { diff --git a/GSM/GSMSMSCBL3Messages.cpp b/GSM/GSMSMSCBL3Messages.cpp new file mode 100644 index 00000000..f7d85b3f --- /dev/null +++ b/GSM/GSMSMSCBL3Messages.cpp @@ -0,0 +1,115 @@ +/* +* Copyright 2010 Kestrel Signal Processing, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + + + +#include "GSMSMSCBL3Messages.h" +#include + +using namespace GSM; +using namespace std; + + +void L3SMSCBSerialNumber::writeV(L3Frame& l3, size_t& wp) const +{ + l3.writeField(wp,mGS,2); + l3.writeField(wp,mMessageCode,10); + l3.writeField(wp,mUpdateNumber,4); +} + +void L3SMSCBSerialNumber::text(ostream& os) const +{ + os << "GS=" << mGS; + os << " MessageCode=" << mMessageCode; + os << " UpdateNumber=" << mUpdateNumber; +} + + +void L3SMSCBMessageIdentifier::writeV(L3Frame& l3, size_t& wp) const +{ + l3.writeField(wp,mValue,16); +} + +void L3SMSCBMessageIdentifier::text(ostream& os) const +{ + os << hex << "0x" << mValue << dec; +} + + +void L3SMSCBDataCodingScheme::writeV(L3Frame& l3, size_t& wp) const +{ + l3.writeField(wp,mValue,8); +} + +void L3SMSCBDataCodingScheme::text(ostream& os) const +{ + os << hex << "0x" << mValue << dec; +} + + +void L3SMSCBPageParameter::writeV(L3Frame& l3, size_t& wp) const +{ + l3.writeField(wp,mNumber,4); + l3.writeField(wp,mTotal,4); +} + +void L3SMSCBPageParameter::text(ostream& os) const +{ + os << mNumber << "/" << mTotal; +} + +void L3SMSCBContent::writeV(L3Frame& l3, size_t& wp) const +{ + for (unsigned i=0; i<82; i++) l3.writeField(wp,mData[i],8); +} + +void L3SMSCBContent::text(ostream& os) const +{ + os << hex; + for (unsigned i=0; i<82; i++) os << setw(2) << (int)mData[i]; + os << dec; +} + + + +ostream& GSM::operator<<(ostream& os, const L3SMSCBMessage& msg) +{ + msg.text(os); + return os; +} + + +void L3SMSCBMessage::write(L3Frame& frame) const +{ + size_t wp=0; + mSerialNumber.writeV(frame,wp); + mMessageIdentifier.writeV(frame,wp); + mDataCodingScheme.writeV(frame,wp); + mPageParameter.writeV(frame,wp); + mContent.writeV(frame,wp); +} + +void L3SMSCBMessage::text(ostream& os) const +{ + os << "serialNumber=(" << mSerialNumber << ")"; + os << " messageID=" << mMessageIdentifier; + os << " DCS=" << mDataCodingScheme; + os << " page=" << mPageParameter; + os << " content=(" << mContent << ")"; +} + + +// vim: ts=4 sw=4 diff --git a/GSM/GSMSMSCBL3Messages.h b/GSM/GSMSMSCBL3Messages.h new file mode 100644 index 00000000..64b9c1ad --- /dev/null +++ b/GSM/GSMSMSCBL3Messages.h @@ -0,0 +1,182 @@ +/* +* Copyright 2010 Kestrel Signal Processing, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + + + +#ifndef GSML3SMSCBMESSAGES_H +#define GSML3SMSCBMESSAGES_H + +#include "GSML3Message.h" +#include + +namespace GSM { + +/* Elements of SMSCB messages, from GSM 03.41 9.3. */ + +/** GSM 03.41 9.3.2.1 */ +class L3SMSCBSerialNumber : public L3ProtocolElement { + + private: + unsigned mGS; ///< geographic scope + unsigned mMessageCode; ///< code classifying message content + unsigned mUpdateNumber; ///< so MS knows to reload this message + + public: + + L3SMSCBSerialNumber(unsigned wGS, unsigned wMessageCode, unsigned wUpdateNumber): + mGS(wGS), + mMessageCode(wMessageCode), mUpdateNumber(wUpdateNumber) + { } + + void parseV(const L3Frame&, size_t&, size_t) { assert(0); } + void parseV(const L3Frame&, size_t&) { assert(0); } + + void writeV(L3Frame&, size_t&) const; + size_t lengthV() const { return 2; } + void text(std::ostream& os) const; + +}; + +/** GSM 03.41 9.3.2.2 */ +class L3SMSCBMessageIdentifier : public L3ProtocolElement { + + private: + unsigned mValue; + + public: + + L3SMSCBMessageIdentifier(unsigned wValue): + mValue(wValue) + { } + + void parseV(const L3Frame&, size_t&, size_t) { assert(0); } + void parseV(const L3Frame&, size_t&) { assert(0); } + + void writeV(L3Frame&, size_t&) const; + size_t lengthV() const { return 2; } + void text(std::ostream& os) const; + +}; + +/** GSM 03.41 9.3.2.3 */ +class L3SMSCBDataCodingScheme : public L3ProtocolElement { + + private: + unsigned mValue; + + public: + + L3SMSCBDataCodingScheme(unsigned wValue): + mValue(wValue) + { } + + void parseV(const L3Frame&, size_t&, size_t) { assert(0); } + void parseV(const L3Frame&, size_t&) { assert(0); } + + void writeV(L3Frame&, size_t&) const; + size_t lengthV() const { return 1; } + void text(std::ostream& os) const; + +}; + + +/** GSM 03.41 9.3.2.4 */ +class L3SMSCBPageParameter : public L3ProtocolElement { + + private: + unsigned mNumber; + unsigned mTotal; + + public: + + L3SMSCBPageParameter(unsigned wNumber, unsigned wTotal): + mNumber(wNumber),mTotal(wTotal) + { } + + void parseV(const L3Frame&, size_t&, size_t) { assert(0); } + void parseV(const L3Frame&, size_t&) { assert(0); } + + void writeV(L3Frame&, size_t&) const; + size_t lengthV() const { return 1; } + void text(std::ostream& os) const; + +}; + + + +/** GSM 03.41 9.3.2.5 */ +class L3SMSCBContent : public L3ProtocolElement { + + private: + char mData[82]; ///< raw data + + public: + + L3SMSCBContent(const char *wData) + { bcopy(wData,mData,82); } + + void parseV(const L3Frame&, size_t&, size_t) { assert(0); } + void parseV(const L3Frame&, size_t&) { assert(0); } + + void writeV(L3Frame&, size_t&) const; + size_t lengthV() const { return 82; } + void text(std::ostream& os) const; +}; + + + + +/** + L3 definition of the SMSCB message. + This message group does not follow the normal structure of + most Um L3 messages and is not an L3Message subclass. + See GSM 03.41 9.3.1. +*/ +class L3SMSCBMessage { + + private: + L3SMSCBSerialNumber mSerialNumber; + L3SMSCBMessageIdentifier mMessageIdentifier; + L3SMSCBDataCodingScheme mDataCodingScheme; + L3SMSCBPageParameter mPageParameter; + L3SMSCBContent mContent; + + public: + L3SMSCBMessage( + L3SMSCBSerialNumber wSerialNumber, + L3SMSCBMessageIdentifier wMessageIdentifier, + L3SMSCBDataCodingScheme wDataCodingScheme, + L3SMSCBPageParameter wPageParameter, + L3SMSCBContent wContent + ): + mSerialNumber(wSerialNumber), + mMessageIdentifier(wMessageIdentifier), + mDataCodingScheme(wDataCodingScheme), + mPageParameter(wPageParameter), + mContent(wContent) + { } + + void write(L3Frame&) const; + void text(std::ostream&) const; +}; + +std::ostream& operator<<(std::ostream&, const L3SMSCBMessage&); + +} + +#endif + +// vim: ts=4 sw=4 diff --git a/GSM/GSMTAPDump.cpp b/GSM/GSMTAPDump.cpp index 627b82f6..66a319c3 100644 --- a/GSM/GSMTAPDump.cpp +++ b/GSM/GSMTAPDump.cpp @@ -1,25 +1,15 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. */ @@ -31,8 +21,10 @@ UDPSocket GSMTAPSocket; void gWriteGSMTAP(unsigned ARFCN, unsigned TS, unsigned FN, - GSM::TypeAndOffset to, bool is_saach, bool ul_dln, - const BitVector& frame) + GSM::TypeAndOffset to, bool is_saach, + bool ul_dln, // (pat) This flag means uplink + const BitVector& frame, + unsigned wType) // Defaults to GSMTAP_TYPE_UM { char buffer[MAX_UDP_LENGTH]; int ofs = 0; @@ -93,9 +85,24 @@ void gWriteGSMTAP(unsigned ARFCN, unsigned TS, unsigned FN, scn = to - GSM::TCHH_0; break; + case GSM::TDMA_PDCH: // packet data traffic logical channel, full speed. + stype = GSMTAP_CHANNEL_PACCH; + //stype = GSMTAP_CHANNEL_PDCH; + scn = 0; // Is this correct? + break; + case GSM::TDMA_PTCCH: // packet data timing advance logical channel + stype = GSMTAP_CHANNEL_PTCCH; + scn = 0; + break; + case GSM::TDMA_PACCH: + stype = GSMTAP_CHANNEL_PACCH; + scn = 0; + break; default: + LOG(NOTICE) << "GSMTAP unsupported type-and-offset " << to; stype = GSMTAP_CHANNEL_UNKNOWN; scn = 0; + break; } if (is_saach) @@ -112,7 +119,7 @@ void gWriteGSMTAP(unsigned ARFCN, unsigned TS, unsigned FN, struct gsmtap_hdr *header = (struct gsmtap_hdr *)buffer; header->version = GSMTAP_VERSION; header->hdr_len = sizeof(struct gsmtap_hdr) >> 2; - header->type = GSMTAP_TYPE_UM; + header->type = wType; //GSMTAP_TYPE_UM; header->timeslot = TS; header->arfcn = htons(ARFCN); header->signal_dbm = 0; /* FIXME */ diff --git a/GSM/GSMTAPDump.h b/GSM/GSMTAPDump.h index b2fcd74b..62f5037c 100644 --- a/GSM/GSMTAPDump.h +++ b/GSM/GSMTAPDump.h @@ -1,22 +1,15 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. +* + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - */ @@ -31,9 +24,8 @@ void gWriteGSMTAP(unsigned ARFCN, unsigned TS, unsigned FN, GSM::TypeAndOffset to, bool is_sacch, bool ul_dln, - const BitVector& frame); - - + const BitVector& frame, + unsigned wType = GSMTAP_TYPE_UM); #endif // vim: ts=4 sw=4 diff --git a/GSM/GSMTDMA.cpp b/GSM/GSMTDMA.cpp index 8c86dcb1..ee818fff 100644 --- a/GSM/GSMTDMA.cpp +++ b/GSM/GSMTDMA.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -242,6 +232,23 @@ MAKE_TDMA_MAPPING(SACCH_C8_7U,SDCCH_8_7,false,true,0xFF,true,102); +// (pat) The basic 26-multi-frame has SACCH in timeslot #12 and an idle frame in timeslot #35. +// The entries below all follow this format, but a single SACCH message, +// comprised of 4 frames, starts at a different spot for each TCH, +// which is another way of saying that the TCH itself starts in a different place, +// and the odd numbered ones start with a 13 frame (half a multiframe) offset. +// The important point for GPRS is that these are the exact same frames needed +// for the continuous timing advance. If I want to use these, however, it means +// that the absolute frame number at which the GPRS 52-multiframe begins for each +// channel must be syncrhonized with these tables for each channel. +// More specifically, there are really only two cases, since we wont be using +// the SACCH frame-to-message assembling machinery. These cases are whether +// the SACCH frames start at a multiple of frame 0, or of frame 13. +// In other words, assuming all 104-multiframe below begin at time 0, the even numbered +// table entries return SACCH from frame #12 of each 26-multi-frame, +// and the odd ones return SACCH from frame #25, which would normally be the idle frame, +// if the 26-multi-frame were aligned at 0. +// This odd fact is going to get hard-coded into GPRS. const unsigned SACCH_TF_T0Frames[] = {12,38,64,90}; MAKE_TDMA_MAPPING(SACCH_TF_T0,TCHF_0,true,true,0x01,true,104); diff --git a/GSM/GSMTDMA.h b/GSM/GSMTDMA.h index d821a43e..7f2470ed 100644 --- a/GSM/GSMTDMA.h +++ b/GSM/GSMTDMA.h @@ -2,24 +2,14 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/GSMTransfer.cpp b/GSM/GSMTransfer.cpp index 7759d6cf..6580b2a0 100644 --- a/GSM/GSMTransfer.cpp +++ b/GSM/GSMTransfer.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -29,6 +19,7 @@ #include "GSMTransfer.h" #include "GSML3Message.h" +#include "Globals.h" using namespace std; @@ -217,6 +208,43 @@ void L2Frame::idleFill() } } +void L2Frame::randomizeFiller(unsigned start) +{ + /* for debugging + // no filler or first filler is 0x2b + if (start-8 < size() && peekField(start-8,8) != 0x2b) { + LOG(ALERT) << *this << " " << start; + assert(0); + } + // reset of filler is 0x2b + for (unsigned i = start; i < size(); i+=8) { + if (peekField(i,8) != 0x2b) { + LOG(ALERT) << *this << " " << start << " " << i; + assert(0); + } + } + */ + for (unsigned i = start; i < size(); i++) { + settfb(i, random() & 1); + } +} + +void L2Frame::randomizeFiller(const L2Header& header) +{ + switch (header.format()) { + case L2Header::FmtA: + case L2Header::FmtB: + randomizeFiller((header.length().L() + 4) * 8); + return; + case L2Header::FmtBbis: + case L2Header::FmtB4: + randomizeFiller((header.length().L() + 2) * 8); + return; + default: + return; + } +} + L2Frame::L2Frame(const BitVector& bits, Primitive prim) :BitVector(23*8),mPrimitive(prim) @@ -227,13 +255,18 @@ L2Frame::L2Frame(const BitVector& bits, Primitive prim) } -L2Frame::L2Frame(const L2Header& header, const BitVector& l3) +#include +L2Frame::L2Frame(const L2Header& header, const BitVector& l3, bool noran) :BitVector(23*8),mPrimitive(DATA) { idleFill(); + //printf("header.bitsNeeded=%d l3.size=%d this.size=%d\n", + //header.bitsNeeded(),l3.size(),this->size()); assert((header.bitsNeeded()+l3.size())<=this->size()); size_t wp = header.write(*this); l3.copyToSegment(*this,wp); + // FIXME - figure out why randomizeFiller doesn't like the "noran" headers + if (gConfig.getBool("GSM.Cipher.ScrambleFiller") && !noran) randomizeFiller(header); } @@ -242,6 +275,7 @@ L2Frame::L2Frame(const L2Header& header) { idleFill(); header.write(*this); + if (gConfig.getBool("GSM.Cipher.ScrambleFiller")) randomizeFiller(header); } @@ -306,6 +340,23 @@ L2Control::FrameType L2Frame::SFrameType() const +const L2Frame& GSM::L2IdleFrame() +{ + static volatile bool init = false; + static L2Frame idleFrame; + if (!init) { + init = true; + // GSM 04.06 5.4.2.3. + // As sent by the network. + idleFrame.fillField(8*0,3,8); // address + idleFrame.fillField(8*1,3,8); // control + idleFrame.fillField(8*2,1,8); // length + if (gConfig.getBool("GSM.Cipher.ScrambleFiller")) idleFrame.randomizeFiller(8*4); + } + return idleFrame; +} + + @@ -442,6 +493,7 @@ ostream& GSM::operator<<(ostream& os, Primitive prim) case UNIT_DATA: os << "UNIT_DATA"; break; case ERROR: os << "ERROR"; break; case HARDRELEASE: os << "HARDRELEASE"; break; + case HANDOVER_ACCESS: os << "HANDOVER_ACCESS"; break; default: os << "?" << (int)prim << "?"; } return os; diff --git a/GSM/GSMTransfer.h b/GSM/GSMTransfer.h index 540c1307..d4b517af 100644 --- a/GSM/GSMTransfer.h +++ b/GSM/GSMTransfer.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -27,6 +17,7 @@ #ifndef GSMTRANSFER_H #define GSMTRANSFER_H +#include "Defines.h" #include "Interthread.h" #include "BitVector.h" #include "GSMCommon.h" @@ -68,7 +59,8 @@ enum Primitive { DATA, ///< multiframe data transfer UNIT_DATA, ///< datagram-type data transfer ERROR, ///< channel error - HARDRELEASE ///< forced release after an assignment + HARDRELEASE, ///< forced release after an assignment + HANDOVER_ACCESS ///< received inbound handover access burst }; @@ -106,6 +98,7 @@ class TxBurst : public BitVector { /**@name Basic accessors. */ //@{ + // Since mTime is volatile, we can't return a reference. Time time() const { return mTime; } void time(const Time& wTime) { mTime = wTime; } //@} @@ -169,6 +162,7 @@ class RxBurst : public SoftVector { { } + // Since mTime is volatile, we can't return a reference. Time time() const { return mTime; } void time(const Time& wTime) { mTime = wTime; } @@ -286,7 +280,7 @@ class L2Control { /** Initialize a U or S frame. */ L2Control(ControlFormat wFormat=UFormat, unsigned wPF=0, unsigned bits=0) - :mFormat(wFormat),mPF(wPF),mSBits(bits),mUBits(bits) + :mFormat(wFormat),mNR(0),mNS(0),mPF(wPF),mSBits(bits),mUBits(bits) { assert(mFormat!=IFormat); assert(mPF<2); @@ -472,6 +466,9 @@ class L2Frame : public BitVector { public: + void randomizeFiller(unsigned start); + void randomizeFiller(const L2Header& header); + /** Fill the frame with the GSM idle pattern, GSM 04.06 2.2. */ void idleFill(); @@ -498,7 +495,7 @@ class L2Frame : public BitVector { The L3Frame must fit in the L2Frame. The primitive is DATA. */ - L2Frame(const L2Header&, const BitVector&); + L2Frame(const L2Header&, const BitVector&, bool noran=false); /** Make an L2Frame from a header with no payload. @@ -534,7 +531,10 @@ class L2Frame : public BitVector { /** Set/clear the PF bit. */ void PF(bool wPF) { mStart[8+3]=wPF; } - /** Look into the header and get the length of the payload. */ + /** + Look into the header and get the length of the payload. + Assumes A or B header, or B4 header with L2 pseudo length in L3. + */ unsigned L() const { return peekField(8*2,6); } /** Get the "more data" bit (M). */ @@ -551,6 +551,9 @@ class L2Frame : public BitVector { /** Return the CR bit, GSM 04.06 3.3.2. Assumes A or B header. */ bool CR() const { return mStart[6] & 0x01; } + + /** Set/clear the CR bit. */ + void CR(bool wCR) { mStart[6]=wCR; } /** Return truw if this a DCCH idle frame. */ bool DCCHIdle() const @@ -567,13 +570,16 @@ class L2Frame : public BitVector { }; + +/** Return a reference to the standard LAPDm downlink idle frame. */ +const L2Frame& L2IdleFrame(); + std::ostream& operator<<(std::ostream& os, const L2Frame& msg); typedef InterthreadQueueWithWait L2FrameFIFO; - /** Representation of a GSM L3 message in a bit vector. Bit ordering is MSB-first in each octet. @@ -602,7 +608,7 @@ class L3Frame : public BitVector { L3Frame(const L3Frame& f1, const L3Frame& f2) :BitVector(f1,f2),mPrimitive(DATA), mL2Length(f1.mL2Length + f2.mL2Length) - {} + { } /** Build from an L2Frame. */ L3Frame(const L2Frame& source) @@ -625,7 +631,7 @@ class L3Frame : public BitVector { /** Message Type Indicator, GSM 04.08 10.4. */ unsigned MTI() const { return peekField(8,8); } - /** TI value, GSM 04.07 11.2.3.1.3. */ + /** TI (transaction Identifier) value, GSM 04.07 11.2.3.1.3. */ unsigned TI() const { return peekField(0,4); } /** Return the associated primitive. */ @@ -646,13 +652,18 @@ class L3Frame : public BitVector { + std::ostream& operator<<(std::ostream& os, const L3Frame&); typedef InterthreadQueue L3FrameFIFO; -/** A vocoder frame for use in GSM/SIP contexts. */ +/** + A vocoder frame for use in GSM/SIP contexts. + This is based on RFC-3551 Section 4.5.8.1. + Note the 4-bit pad at the start of the frame, filled with b1101 (0xd). +*/ class VocoderFrame : public BitVector { public: @@ -666,7 +677,10 @@ class VocoderFrame : public BitVector { :BitVector(264) { unpack(src); } + /** Non-const form returns an alias. */ BitVector payload() { return tail(4); } + + /** Const form returns a copy. */ const BitVector payload() const { return tail(4); } }; diff --git a/GSM/Makefile.am b/GSM/Makefile.am index 3ae73f2f..4b8d1f84 100644 --- a/GSM/Makefile.am +++ b/GSM/Makefile.am @@ -20,8 +20,8 @@ include $(top_srcdir)/Makefile.common -AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(USB_INCLUDES) $(WITH_INCLUDES) $(OPENBTS_CPPFLAGS) -AM_CXXFLAGS = -Wall -pthread -ldl $(OPENBTS_CXXFLAGS) +AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) +#AM_CXXFLAGS = -O2 -g noinst_LTLIBRARIES = libGSM.la @@ -34,6 +34,7 @@ libGSM_la_SOURCES = \ GSML3CCElements.cpp \ GSML3CCMessages.cpp \ GSML3CommonElements.cpp \ + GSML3GPRSElements.cpp \ GSML3Message.cpp \ GSML3MMElements.cpp \ GSML3MMMessages.cpp \ @@ -44,6 +45,7 @@ libGSM_la_SOURCES = \ GSMTDMA.cpp \ GSMTransfer.cpp \ GSMTAPDump.cpp \ + GSMSMSCBL3Messages.cpp \ PowerManager.cpp\ PhysicalStatus.cpp @@ -56,6 +58,7 @@ noinst_HEADERS = \ GSML3CCElements.h \ GSML3CCMessages.h \ GSML3CommonElements.h \ + GSML3GPRSElements.h \ GSML3Message.h \ GSML3MMElements.h \ GSML3MMMessages.h \ @@ -67,6 +70,7 @@ noinst_HEADERS = \ GSMTransfer.h \ PowerManager.h \ GSMTAPDump.h \ + GSMSMSCBL3Messages.h \ gsmtap.h \ PhysicalStatus.h diff --git a/GSM/PhysicalStatus.cpp b/GSM/PhysicalStatus.cpp index bddb30aa..222edf51 100644 --- a/GSM/PhysicalStatus.cpp +++ b/GSM/PhysicalStatus.cpp @@ -1,31 +1,28 @@ /**@file Declarations for PhysicalStatus and related classes. */ /* -* Copyright 2010, 2011 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +/* + * Copyright 2010 Kestrel Signal Processing, Inc. + * All rights reserved. + * + */ + #include "PhysicalStatus.h" #include @@ -33,6 +30,7 @@ #include #include +#include #include #include @@ -58,7 +56,9 @@ static const char* createPhysicalStatus = { "TIME_ERR FLOAT DEFAULT NULL, " // timing advance error in symbol periods "TRANS_PWR INTEGER DEFAULT NULL, " // handset tx power in dBm "TIME_ADVC INTEGER DEFAULT NULL, " // handset timing advance in symbol periods - "FER FLOAT DEFAULT NULL " // uplink FER + "FER FLOAT DEFAULT NULL, " // uplink FER + "NCELL_ARFCN INTEGER DEFAULT NULL, " // ARFCN of strongest neighbor + "NCELL_RSSI INTEGER DEFAULT NULL " // RSSI of strongest neighbor ")" }; @@ -75,6 +75,10 @@ int PhysicalStatus::open(const char* wPath) LOG(EMERG) << "Cannot create TMSI table"; return 1; } + // Set high-concurrency WAL mode. + if (!sqlite3_command(mDB,enableWAL)) { + LOG(EMERG) << "Cannot enable WAL mode on database at " << wPath << ", error message: " << sqlite3_errmsg(mDB); + } return 0; } @@ -115,35 +119,86 @@ bool PhysicalStatus::setPhysical(const LogicalChannel* chan, assert(mDB); assert(chan); + // If MEAS_VALID is true, we don't have valid measurements. + // Really. See GSM 04.08 10.5.2.20. + if (measResults.MEAS_VALID()) return true; + ScopedLock lock(mLock); createEntry(chan); + int CN = -1; + if (measResults.NO_NCELL()>0) CN = measResults.BCCH_FREQ_NCELL(0); + int ARFCN = -1; + if (CN>=0) { + std::vector ARFCNList = gNeighborTable.ARFCNList(); + size_t sz = ARFCNList.size(); + if (sz!=0) { + if (CNRSSI(), chan->timingError(), - chan->actualMSPower(), chan->actualMSTiming(), - chan->FER(), - (unsigned)time(NULL), - chan->ARFCN(), - chan->descriptiveString()); + + if (ARFCN<0) { + sprintf(query, + "UPDATE PHYSTATUS SET " + "RXLEV_FULL_SERVING_CELL=%d, " + "RXLEV_SUB_SERVING_CELL=%d, " + "RXQUAL_FULL_SERVING_CELL_BER=%f, " + "RXQUAL_SUB_SERVING_CELL_BER=%f, " + "RSSI=%f, " + "TIME_ERR=%f, " + "TRANS_PWR=%u, " + "TIME_ADVC=%u, " + "FER=%f, " + "ACCESSED=%u, " + "ARFCN=%u " + "WHERE CN_TN_TYPE_AND_OFFSET==\"%s\"", + measResults.RXLEV_FULL_SERVING_CELL_dBm(), + measResults.RXLEV_SUB_SERVING_CELL_dBm(), + measResults.RXQUAL_FULL_SERVING_CELL_BER(), + measResults.RXQUAL_SUB_SERVING_CELL_BER(), + chan->RSSI(), chan->timingError(), + chan->actualMSPower(), chan->actualMSTiming(), + chan->FER(), + (unsigned)time(NULL), + chan->ARFCN(), + chan->descriptiveString()); + } else { + sprintf(query, + "UPDATE PHYSTATUS SET " + "RXLEV_FULL_SERVING_CELL=%d, " + "RXLEV_SUB_SERVING_CELL=%d, " + "RXQUAL_FULL_SERVING_CELL_BER=%f, " + "RXQUAL_SUB_SERVING_CELL_BER=%f, " + "RSSI=%f, " + "TIME_ERR=%f, " + "TRANS_PWR=%u, " + "TIME_ADVC=%u, " + "FER=%f, " + "ACCESSED=%u, " + "ARFCN=%u ," + "NCELL_ARFCN=%u, " + "NCELL_RSSI=%d " + "WHERE CN_TN_TYPE_AND_OFFSET==\"%s\"", + measResults.RXLEV_FULL_SERVING_CELL_dBm(), + measResults.RXLEV_SUB_SERVING_CELL_dBm(), + measResults.RXQUAL_FULL_SERVING_CELL_BER(), + measResults.RXQUAL_SUB_SERVING_CELL_BER(), + chan->RSSI(), chan->timingError(), + chan->actualMSPower(), chan->actualMSTiming(), + chan->FER(), + (unsigned)time(NULL), + chan->ARFCN(), + (unsigned)ARFCN, + measResults.RXLEV_NCELL_dBm(0), + chan->descriptiveString() + ); + } LOG(DEBUG) << "Query: " << query; diff --git a/GSM/PhysicalStatus.h b/GSM/PhysicalStatus.h index 29454167..57312e67 100644 --- a/GSM/PhysicalStatus.h +++ b/GSM/PhysicalStatus.h @@ -3,24 +3,16 @@ * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/PowerManager.cpp b/GSM/PowerManager.cpp index 074eac09..06f4595f 100644 --- a/GSM/PowerManager.cpp +++ b/GSM/PowerManager.cpp @@ -1,24 +1,14 @@ /* * Copyright 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/PowerManager.h b/GSM/PowerManager.h index db777d62..2af10245 100644 --- a/GSM/PowerManager.h +++ b/GSM/PowerManager.h @@ -1,24 +1,14 @@ /* * Copyright 2009 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/GSM/gsmtap.h b/GSM/gsmtap.h index dbc3d314..c2e73b75 100644 --- a/GSM/gsmtap.h +++ b/GSM/gsmtap.h @@ -79,9 +79,9 @@ #define GSMTAP_CHANNEL_TCH_H 0x0a #define GSMTAP_CHANNEL_CBCH51 0x0b #define GSMTAP_CHANNEL_CBCH52 0x0c -#define GSMTAP_CHANNEL_PDCH 0x0d -#define GSMTAP_CHANNEL_PTCCH 0x0e -#define GSMTAP_CHANNEL_PACCH 0x0f +#define GSMTAP_CHANNEL_PDCH 0x0d // pats note: This one is not implemented in wireshark. +#define GSMTAP_CHANNEL_PTCCH 0x0e // pats note: and neither is this one. +#define GSMTAP_CHANNEL_PACCH 0x0f // pats note: This is the one that is implemented. #define GSMTAP_CHANNEL_ACCH 0x80 /* ====== DO NOT MAKE UNAPPROVED MODIFICATIONS HERE ===== */ diff --git a/Globals/Defines.h b/Globals/Defines.h new file mode 100644 index 00000000..ed2795d5 --- /dev/null +++ b/Globals/Defines.h @@ -0,0 +1,44 @@ +// Pat added this file. +// We need an include file that is included before any other include files. +// Might I suggest that Range Networks specific global #defines be prefixed with RN_ + +#ifndef DEFINES_H +#define DEFINES_H + +// GPRS_1 turns on the SharedEncoder. It is the thing that keeps the modem from registering. +#define GPRS_ENCODER 1 // Use SharedL1Encoder and SharedL1Decoder +#define GPRS_TESTSI4 1 +#define GPRS_TEST 1 // Compile in other GPRS stuff. +#define GPRS_PAT 1 // Compile in GPRS code. Turn this off to get previous non-GRPS code, + // although I am not maintaining it so you may have to fix compile + // problems to use it. + +// __GNUG__ is true for g++ and __GNUC__ for gcc. +#if __GNUC__&0==__GNUG__ + +#define RN_UNUSED __attribute__((unused)) + +#define RN_UNUSED_PARAM(var) RN_UNUSED var + +// Pack structs onto byte boundaries. +// Note that if structs are nested, this must appear on all of them. +#define RN_PACKED __attribute__((packed)) + +#else + +// Suppress warning message about a variable or function being unused. +// In C++ you can leave out the variable name to suppress the 'unused variable' warning. +#define RN_UNUSED_PARAM(var) /*nothing*/ +#define RN_UNUSED /*not defined*/ +#define RN_PACKED /*not defined*/ +#endif + +// Bound value between min and max values. +#define RN_BOUND(value,min,max) ( (value)<(min) ? (min) : (value)>(max) ? (max) : (value) ) + +#define RN_PRETTY_TEXT(name) (" " #name "=(") << name << ")" +#define RN_PRETTY_TEXT1(name) (" " #name "=") << name +#define RN_WRITE_TEXT(name) os << RN_PRETTY_TEXT(name) +#define RN_WRITE_OPT_TEXT(name,flag) if (flag) { os << RN_WRITE_TEXT(name); } + +#endif diff --git a/Globals/Globals.cpp b/Globals/Globals.cpp index c3cad187..42886ea3 100644 --- a/Globals/Globals.cpp +++ b/Globals/Globals.cpp @@ -1,27 +1,19 @@ /**@file Global system parameters. */ /* -* Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc. +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -33,15 +25,17 @@ #define PROD_CAT "P" -const char *gVersionString = "release " VERSION " " PROD_CAT " built " __DATE__ " rev" SVN_REV " "; +#define FEATURES "+GPRS " + +const char *gVersionString = "release " VERSION FEATURES PROD_CAT " built " __DATE__ " rev" SVN_REV " "; const char* gOpenBTSWelcome = //23456789123456789223456789323456789423456789523456789623456789723456789 "OpenBTS\n" - "Copyright 2008, 2009, 2010, 2011 Free Software Foundation, Inc.\n" + "Copyright 2008, 2009, 2010 Free Software Foundation, Inc.\n" "Copyright 2010 Kestrel Signal Processing, Inc.\n" - "Copyright 2011, 2012 Range Networks, Inc.\n" - "Release " VERSION " " PROD_CAT " formal build date " __DATE__ " rev" SVN_REV "\n" + "Copyright 2011, 2012, 2013 Range Networks, Inc.\n" + "Release " VERSION " " PROD_CAT " formal build date " __DATE__ " rev" SVN_REV "\n" "\"OpenBTS\" is a registered trademark of Range Networks, Inc.\n" "\nContributors:\n" " Range Networks, Inc.:\n" @@ -53,15 +47,13 @@ const char* gOpenBTSWelcome = " Johnathan Corgan\n" " Others:\n" " Anne Kwong, Jacob Appelbaum, Joshua Lackey, Alon Levy\n" - " Alexander Chemeris, Alberto Escudero-Pascual, Thomas Tsou\n" + " Alexander Chemeris, Alberto Escudero-Pascual\n" "Incorporated L/GPL libraries and components:\n" " libosip2, LGPL, 2.1 Copyright 2001-2007 Aymeric MOIZARD jack@atosc.org\n" " libortp, LGPL, 2.1 Copyright 2001 Simon MORLAT simon.morlat@linphone.org\n" " libusb, LGPL 2.1, various copyright holders, www.libusb.org\n" -#ifdef HAVE_LIBREADLINE - ", libreadline, GPLv3, www.gnu.org/software/readline/" -#endif - "Incorporated BSD/MIT libraries and components:\n" + "Incorporated BSD/MIT-style libraries and components:\n" + " A5/1 Pedagogical Implementation, Simplified BSD License, Copyright 1998-1999 Marc Briceno, Ian Goldberg, and David Wagner\n" "Incorporated public domain libraries and components:\n" " sqlite3, released to public domain 15 Sept 2001, www.sqlite.org\n" "\n" @@ -70,9 +62,33 @@ const char* gOpenBTSWelcome = "including patent licsensing and radio spectrum licensing.\n" "All users of this software are expected to comply with applicable\n" "regulations and laws. See the LEGAL file in the source code for\n" - "more information." + "more information.\n" + "\n" ; CommandLine::Parser gParser; +GSM::Z100Timer watchdog; +Mutex watchdogLock; + +void gResetWatchdog() +{ + watchdogLock.lock(); + watchdog.set(gConfig.getNum("Control.WatchdogMinutes")*60*1000); + watchdogLock.unlock(); + LOG(DEBUG) << "reset watchdog timer, expires in " << watchdog.remaining() / 1000 << " seconds"; +} + +size_t gWatchdogRemaining() +{ + // Returns remaning time in seconds. + ScopedLock lock(watchdogLock); + return watchdog.remaining() / 1000; +} + +bool gWatchdogExpired() +{ + ScopedLock lock(watchdogLock); + return watchdog.expired(); +} diff --git a/Globals/Globals.h b/Globals/Globals.h index e6c0f721..048c86ed 100644 --- a/Globals/Globals.h +++ b/Globals/Globals.h @@ -1,26 +1,19 @@ +//#define RN_DEVELOPER_MODE 1 /**@file Global system parameters. */ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -31,15 +24,16 @@ #ifndef GLOBALS_H #define GLOBALS_H +#include #include #include #include #include #include +#include #include - /** Date-and-time string, defined in OpenBTS.cpp. */ extern const char* gDateTime; @@ -64,8 +58,16 @@ extern Control::TMSITable gTMSITable; /** The physical status reporting table */ extern GSM::PhysicalStatus gPhysStatus; -/** The subscriber registry */ +/** The subscriber registry and authenticator */ extern SubscriberRegistry gSubscriberRegistry; +/** The global transceiver interface. */ +extern TransceiverManager gTRX; + +/** A global watchdog timer. */ +void gResetWatchdog(); +size_t gWatchdogRemaining(); +bool gWatchdogExpired(); + extern ReportingTable gReports; #endif diff --git a/Globals/Makefile.am b/Globals/Makefile.am index 46d1f508..37d355d8 100644 --- a/Globals/Makefile.am +++ b/Globals/Makefile.am @@ -29,4 +29,6 @@ libglobals_la_SOURCES = Globals.cpp noinst_PROGRAMS = -noinst_HEADERS = Globals.h +noinst_HEADERS = \ + Globals.h \ + Defines.h diff --git a/LEGAL b/LEGAL index 87917e18..92806266 100644 --- a/LEGAL +++ b/LEGAL @@ -2,7 +2,7 @@ OpenBTS Most parts copyright 2008-2011 Free Software Foundation. Some parts copyright 2010 Kestrel Signal Processing, Inc. -Some parts copyright 2011 Range Networks, Inc. +Some parts copyright 2011-2013 Range Networks, Inc. Patent Laws diff --git a/Makefile.am b/Makefile.am index 33bdfe74..0254c201 100644 --- a/Makefile.am +++ b/Makefile.am @@ -20,7 +20,7 @@ include $(top_srcdir)/Makefile.common -DESTDIR = +DESTDIR := AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(USB_INCLUDES) $(WITH_INCLUDES) AM_CXXFLAGS = -Wall -pthread -ldl @@ -30,7 +30,6 @@ AM_CXXFLAGS = -Wall -pthread -ldl # Order must be preserved _SUBDIRS = \ config \ - AsteriskConfig \ sqlite3 \ CommonLibs \ Globals \ @@ -41,9 +40,12 @@ _SUBDIRS = \ SMS \ TRXManager \ Control \ + Peering \ + GPRS \ + SGSNGGSN \ apps \ - doc \ - tools + doc + #if UHD or USRP1 defined, build Transceiver52M as well if UHD @@ -62,7 +64,6 @@ endif endif EXTRA_DIST = \ - autogen.sh \ INSTALLATION \ LEGAL \ COPYING \ diff --git a/Makefile.common b/Makefile.common index c45be6d8..4cb6e803 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2,7 +2,7 @@ # Copyright 2008 Free Software Foundation, Inc. # # This software is distributed under the terms of the GNU Public License. -# See the COPING file in the main directory for details. +# See the COPYING file in the main directory for details. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,13 +18,14 @@ # along with this program. If not, see . # -#hack to get around symlink svn:externals in git -kurtis -top_srcdir = $(abs_top_srcdir) -top_builddir = $(abs_top_builddir) +AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(USB_INCLUDES) $(WITH_INCLUDES) $(OPENBTS_CPPFLAGS) +AM_CXXFLAGS = -Wall -pthread -ldl $(OPENBTS_CXXFLAGS) COMMON_INCLUDEDIR = $(top_srcdir)/CommonLibs CONTROL_INCLUDEDIR = $(top_srcdir)/Control GSM_INCLUDEDIR = $(top_srcdir)/GSM +GPRS_INCLUDEDIR = $(top_srcdir)/GPRS +SGSNGGSN_INCLUDEDIR = $(top_srcdir)/SGSNGGSN SIP_INCLUDEDIR = $(top_srcdir)/SIP SMS_INCLUDEDIR = $(top_srcdir)/SMS TRX_INCLUDEDIR = $(top_srcdir)/TRXManager @@ -32,24 +33,32 @@ GLOBALS_INCLUDEDIR = $(top_srcdir)/Globals CLI_INCLUDEDIR = $(top_srcdir)/CLI SQLITE_INCLUDEDIR = $(top_srcdir)/sqlite3 SR_INCLUDEDIR = $(top_srcdir)/SR +PEERING_INCLUDEDIR = $(top_srcdir)/Peering SVNDEV = -D'SVN_REV="$(shell svnversion -n $(top_builddir))"' STD_DEFINES_AND_INCLUDES = \ $(SVNDEV) \ + $(ACTIVEFEATURES) \ -I$(COMMON_INCLUDEDIR) \ -I$(CONTROL_INCLUDEDIR) \ -I$(GSM_INCLUDEDIR) \ + -I$(GPRS_INCLUDEDIR) \ + -I$(SGSNGGSN_INCLUDEDIR) \ -I$(SIP_INCLUDEDIR) \ -I$(SMS_INCLUDEDIR) \ -I$(TRX_INCLUDEDIR) \ -I$(GLOBALS_INCLUDEDIR) \ -I$(CLI_INCLUDEDIR) \ -I$(SR_INCLUDEDIR) \ + -I$(PEERING_INCLUDEDIR) \ -I$(SQLITE_INCLUDEDIR) +# These macros are referenced in apps/Makefile.am, which must be changed in sync with these. COMMON_LA = $(top_builddir)/CommonLibs/libcommon.la GSM_LA = $(top_builddir)/GSM/libGSM.la +GPRS_LA = $(top_builddir)/GPRS/libGPRS.la +SGSNGGSN_LA = $(top_builddir)/SGSNGGSN/libSGSNGGSN.la SIP_LA = $(top_builddir)/SIP/libSIP.la SMS_LA = $(top_builddir)/SMS/libSMS.la TRX_LA = $(top_builddir)/TRXManager/libtrxmanager.la @@ -57,6 +66,7 @@ CONTROL_LA = $(top_builddir)/Control/libcontrol.la GLOBALS_LA = $(top_builddir)/Globals/libglobals.la CLI_LA = $(top_builddir)/CLI/libcli.la SR_LA = $(top_builddir)/SR/libSR.la +PEERING_LA = $(top_builddir)/Peering/libpeering.la SQLITE_LA = $(top_builddir)/sqlite3/libsqlite.la -ldl MOSTLYCLEANFILES = *~ diff --git a/AsteriskConfig/Makefile.am b/Peering/Makefile.am similarity index 74% rename from AsteriskConfig/Makefile.am rename to Peering/Makefile.am index 09ee477c..63982e51 100644 --- a/AsteriskConfig/Makefile.am +++ b/Peering/Makefile.am @@ -1,5 +1,7 @@ # # Copyright 2008 Free Software Foundation, Inc. +# Copyright 2010 Kestrel Signal Processing, Inc. +# Copyright 2011 Range Networks, Inc. # # This software is distributed under the terms of the GNU Public License. # See the COPYING file in the main directory for details. @@ -20,14 +22,15 @@ include $(top_srcdir)/Makefile.common -# Install files in this directory -ourdatadir = $(datadir)/OpenBTS/Asterisk +AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) +AM_CXXFLAGS = -Wall -O3 -dist_ourdata_DATA = \ - cdr.conf \ - extensions.conf \ - indications.conf \ - logger.conf \ - modules.conf \ - README.AsteriskConf \ - sip.conf +noinst_LTLIBRARIES = libpeering.la + +libpeering_la_SOURCES = \ + NeighborTable.cpp \ + Peering.cpp + +noinst_HEADERS = \ + NeighborTable.h \ + Peering.h diff --git a/Peering/NeighborTable.cpp b/Peering/NeighborTable.cpp new file mode 100644 index 00000000..a3b6b846 --- /dev/null +++ b/Peering/NeighborTable.cpp @@ -0,0 +1,358 @@ +/* + * Copright 2011 Range Networks, Inc. + * All rights reserved. +*/ + +#include "NeighborTable.h" +#include "Peering.h" + +#include +#include + +#include +#include + +#include + +#include + +using namespace Peering; +using namespace std; + + + + + + +static const char* createNeighborTable = { + "CREATE TABLE IF NOT EXISTS NEIGHBOR_TABLE (" + "IPADDRESS TEXT UNIQUE NOT NULL, " // IP address of peer BTS + "UPDATED INTEGER DEFAULT 0, " // timestamp of last update + "HOLDOFF INTEGER DEFAULT 0, " // hold off until after this time + "C0 INTEGER DEFAULT NULL, " // peer BTS C0 ARFCN + "BSIC INTEGER DEFAULT NULL" // peer BTS BSIC + ")" +}; + + + + + + +void NeighborTable::NeighborTableInit(const char* wPath) +{ + int rc = sqlite3_open(wPath,&mDB); + if (rc) { + LOG(ALERT) << "Cannot open NeighborTable database: " << sqlite3_errmsg(mDB); + sqlite3_close(mDB); + mDB = NULL; + return; + } + if (!sqlite3_command(mDB,createNeighborTable)) { + LOG(ALERT) << "Cannot create Neighbor table"; + } + // Set high-concurrency WAL mode. + if (!sqlite3_command(mDB,enableWAL)) { + LOG(ALERT) << "Cannot enable WAL mode on database at " << wPath << ", error message: " << sqlite3_errmsg(mDB); + } + + // Fill the database. + fill(); +} + + + +void NeighborTable::fill() +{ + mConfigured.clear(); + // Stuff the neighbor ip addresses into the table without any other info. + // Let existing information persist for current neighbors. + // NeighborTable::refresh() will get updated infomation when it's available. + vector neighbors = gConfig.getVectorOfStrings("GSM.Neighbors"); + LOG(DEBUG) << "neighbor list length " << neighbors.size(); + unsigned short port = gConfig.getNum("Peering.Port"); + for (unsigned int i = 0; i < neighbors.size(); i++) { + struct sockaddr_in address; + const char *host = neighbors[i].c_str(); + LOG(DEBUG) << "resolving host name for " << host; + if (!resolveAddress(&address, host, port)) { + LOG(CRIT) << "cannot resolve host name for " << host; + // these two seem to want to get set even if addNeighbor isn't called + mBCCSet = getBCCSet(); + mARFCNList = getARFCNs(); + continue; + } + addNeighbor(&address); + } + // get a list of ipaddresses in neighbor table that aren't configured + vector toBeDeleted; + sqlite3_stmt *stmt; + const char *query = "SELECT IPADDRESS FROM NEIGHBOR_TABLE"; + if (sqlite3_prepare_statement(mDB,&stmt,query)) { + LOG(ALERT) << "read of neighbor table failed: " << query; + return; + } + int src = sqlite3_step(stmt); + while (src==SQLITE_ROW) { + const char* ipaddress = (const char*)sqlite3_column_text(stmt,0); + if (!ipaddress) { + LOG(ALERT) << "null address in neighbor table"; + src = sqlite3_step(stmt); + continue; + } + if (mConfigured.find(ipaddress) == mConfigured.end()) { + toBeDeleted.push_back(ipaddress); + } + src = sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + // remove entries in neighbor table that aren't configured + char query2[400]; + for (unsigned int i = 0; i < toBeDeleted.size(); i++) { + sprintf(query2, "delete from NEIGHBOR_TABLE where IPADDRESS = '%s'", toBeDeleted[i].c_str()); + sqlite3_command(mDB, query2); + } +} + + +void NeighborTable::addNeighbor(const struct ::sockaddr_in* address) +{ + ScopedLock lock(mLock); + // Get a string for the sockaddr_in. + char addrString[256]; + const char *ret = inet_ntop(AF_INET,&(address->sin_addr),addrString,255); + if (!ret) { + LOG(ERR) << "cannot parse peer socket address"; + return; + } + LOG(DEBUG) << "adding " << addrString << ":" << ntohs(address->sin_port) << " to neighbor table"; + + char query[200]; + sprintf(query, + "INSERT OR IGNORE INTO NEIGHBOR_TABLE (IPADDRESS) " + "VALUES ('%s:%d')", + addrString,(int)ntohs(address->sin_port)); + sqlite3_command(mDB,query); + // flag the entry as configured + char *p = 1+index(query, '\''); + char *q = index(p, '\''); + *q = 0; + mConfigured.insert(p); + + // update mBCCSet + mBCCSet = getBCCSet(); + + // update mARFCNList and check for a change + mARFCNList = getARFCNs(); +} + + + + + +bool NeighborTable::addInfo(const struct ::sockaddr_in* address, unsigned updated, unsigned C0, unsigned BSIC) +{ + ScopedLock lock(mLock); + // Get a string for the sockaddr_in. + char addrString[256]; + const char *ret = inet_ntop(AF_INET,&(address->sin_addr),addrString,255); + if (!ret) { + LOG(ERR) << "cannot parse peer socket address"; + return false; + } + LOG(DEBUG) << "updating " << addrString << ":" << ntohs(address->sin_port) << " in neighbor table"; + + char query[200]; + unsigned int dummy; // C0 is arbitrary integer column. just want to know if ipaddress is in table. + sprintf(query, "%s:%d", addrString,(int)ntohs(address->sin_port)); + if (!sqlite3_single_lookup(mDB, "NEIGHBOR_TABLE", "IPADDRESS", query, "C0", dummy)) { + LOG(NOTICE) << "Ignoring unsolicited 'RSP NEIGHBOR_PARAMS' from " << query; + return false; + } + sprintf(query, + "REPLACE INTO NEIGHBOR_TABLE (IPADDRESS,UPDATED,C0,BSIC,HOLDOFF) " + "VALUES ('%s:%d',%u,%u,%u,0) ", + addrString,(int)ntohs(address->sin_port),updated,C0,BSIC); + if (!sqlite3_command(mDB,query)) { + LOG(ALERT) << "write to neighbor table failed: " << query; + return false; + } + + // update mBCCSet + mBCCSet = getBCCSet(); + + // update mARFCNList and check for a change + std::vector newARFCNs = getARFCNs(); + bool change = (newARFCNs!=mARFCNList); + if (change) mARFCNList = newARFCNs; + + return change; +} + + +void NeighborTable::refresh() +{ + fill(); + time_t now = time(NULL); + time_t then = now - gConfig.getNum("Peering.Neighbor.RefreshAge"); + char query[400]; + sprintf(query,"SELECT IPADDRESS FROM NEIGHBOR_TABLE WHERE UPDATED < %u",(unsigned)then); + sqlite3_stmt *stmt; + if (sqlite3_prepare_statement(mDB,&stmt,query)) { + LOG(ALERT) << "read of neighbor table failed: " << query; + return; + } + int src = sqlite3_step(stmt); + while (src==SQLITE_ROW) { + const char* addrString = (const char*)sqlite3_column_text(stmt,0); + if (!addrString) { + LOG(ALERT) << "null address in neighbor table"; + src = sqlite3_step(stmt); + continue; + } + struct sockaddr_in address; + if (!resolveAddress(&address, addrString)) { + LOG(ALERT) << "cannot resolve neighbor address " << addrString; + src = sqlite3_step(stmt); + continue; + } + LOG(INFO) << "sending neighbor param request to " << addrString; + gPeerInterface.sendNeighborParamsRequest(&address); + src = sqlite3_step(stmt); + } + sqlite3_finalize(stmt); +} + + +char* NeighborTable::getAddress(unsigned BCCH_FREQ_NCELL, unsigned BSIC) +{ + // There is a potential race condition here where mARFCNList could have changed + // between the sending of SI5 and the receipt of the corresponding measurement report. + // That will not be a serious problem as long as BSICs are unique, + // which they should be. The method will just return NULL. + + LOG(DEBUG) << "BCCH_FREQ_NCELL=" << BCCH_FREQ_NCELL << " BSIC=" << BSIC; + char *retVal = NULL; + char query[500]; + int C0 = getARFCN(BCCH_FREQ_NCELL); + if (C0 < 0) return NULL; + sprintf(query,"SELECT IPADDRESS FROM NEIGHBOR_TABLE WHERE BSIC=%d AND C0=%d",BSIC,C0); + LOG(DEBUG) << query; + sqlite3_stmt *stmt; + int prc = sqlite3_prepare_statement(mDB,&stmt,query); + if (prc) { + LOG(ALERT) << "cannot prepare statement for " << query; + return NULL; + } + int src = sqlite3_run_query(mDB,stmt); + if (src==SQLITE_ROW) { + const char* ptr = (const char*)sqlite3_column_text(stmt,0); + retVal = strdup(ptr); + } + sqlite3_finalize(stmt); + return retVal; +} + +/* Return the ARFCN given its position in the BCCH channel list. + * The way I read GSM 04.08 10.5.2.20, they take the ARFCNs, sort them + * in ascending order, move the first to the last if it's 0, + * then BCCH-FREQ-NCELL is a position in that list. + */ +int NeighborTable::getARFCN(unsigned BCCH_FREQ_NCELL) +{ + ScopedLock lock(mLock); + LOG(DEBUG) << "BCCH_FREQ_NCELL=" << BCCH_FREQ_NCELL; + if (BCCH_FREQ_NCELL >= mARFCNList.size()) { + LOG(ALERT) << "BCCH-FREQ-NCELL not in BCCH channel list"; + return -1; + } + return mARFCNList[BCCH_FREQ_NCELL]; +} + + + +void NeighborTable::holdOff(const char* address, unsigned seconds) +{ + assert(address); + LOG(DEBUG) << "address " << address << " seconds " << seconds; + + if (!seconds) return; + + time_t holdoffTime = time(NULL) + seconds; + char query[200]; + sprintf(query,"UPDATE NEIGHBOR_TABLE SET HOLDOFF=%u WHERE IPADDRESS='%s'", + holdoffTime, address); + if (!sqlite3_command(mDB,query)) { + LOG(ALERT) << "cannot access neighbor table"; + } +} + +void NeighborTable::holdOff(const struct sockaddr_in* peer, unsigned seconds) +{ + if (!seconds) return; + + char addrString[256]; + const char *ret = inet_ntop(AF_INET,&(peer->sin_addr),addrString,255); + if (!ret) { + LOG(ERR) << "cannot parse peer socket address"; + return; + } + return holdOff(addrString,seconds); +} + + +bool NeighborTable::holdingOff(const char* address) +{ + unsigned holdoffTime; + if (!sqlite3_single_lookup(mDB,"NEIGHBOR_TABLE","IPADDRESS",address,"HOLDOFF",holdoffTime)) { + LOG(ALERT) << "cannot read neighbor table"; + return false; + } + time_t now = time(NULL); + LOG(DEBUG) << "hold-off time for " << address << ": " << holdoffTime << ", now: " << now; + return now < (time_t)holdoffTime; +} + +std::vector NeighborTable::getARFCNs() const +{ + char query[500]; + vector bcchChannelList; + sprintf(query,"SELECT C0 FROM NEIGHBOR_TABLE WHERE BSIC > -1 ORDER BY UPDATED DESC LIMIT %u", gConfig.getNum("GSM.Neighbors.NumToSend")); + sqlite3_stmt *stmt; + int prc = sqlite3_prepare_statement(mDB,&stmt,query); + if (prc) { + LOG(ALERT) << "cannot prepare statement for " << query; + return bcchChannelList; + } + int src = sqlite3_run_query(mDB,stmt); + while (src==SQLITE_ROW) { + unsigned ARFCN = sqlite3_column_int(stmt,0); + bcchChannelList.push_back(ARFCN); + src = sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + return bcchChannelList; +} + + + +int NeighborTable::getBCCSet() const +{ + int set = 0; + static const char query[] = "SELECT BSIC FROM NEIGHBOR_TABLE"; + sqlite3_stmt *stmt; + int prc = sqlite3_prepare_statement(mDB,&stmt,query); + if (prc) { + LOG(ALERT) << "cannot prepare statement for " << query; + return -1; + } + int src = sqlite3_run_query(mDB,stmt); + while (src==SQLITE_ROW) { + unsigned BCC = sqlite3_column_int(stmt,0); + set |= (1 << BCC); + src = sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + return set; +} + diff --git a/Peering/NeighborTable.h b/Peering/NeighborTable.h new file mode 100644 index 00000000..3520a95e --- /dev/null +++ b/Peering/NeighborTable.h @@ -0,0 +1,106 @@ +/**@ The global table of neighboring BTS units. */ +/* + * Copright 2011 Range Networks, Inc. + * All rights reserved. +*/ + + +#ifndef NEIGHBORTABLE_H +#define NEIGHBORTABLE_H + +#include +#include +#include +#include + +using namespace std; + + +struct sqlite3; + +namespace Peering { + +class NeighborTable { + + private: + + struct sqlite3* mDB; ///< database connection + std::vector mARFCNList; ///< current ARFCN list + int mBCCSet; ///< set of current BCCs + mutable Mutex mLock; + + set mConfigured; ///< which ipaddresses in the table are configured + + public: + + /** Age parameter value for undefined records. */ + static const unsigned UNKNOWN = 0xFFFFFFFF; + + // (pat) In order to prevent crashes caused by static initialization races, I added + // an empty constructor and moved initialization to the function NeighborTableInit. + NeighborTable() : mDB(0), mBCCSet(0) {} + /** Constructor opens the database and creates/populates the new table as needed. */ + void NeighborTableInit(const char* dbPath); + + /** Fill the neighbor table from GSM.Neighbors config. */ + void fill(); + + /** Create a new neighbor record if it does not already exist. */ + void addNeighbor(const struct sockaddr_in* address); + + /** + Add new information to a neighbor record to the table. + @return true if the neighbor ARFCN list changed + */ + bool addInfo(const struct sockaddr_in* address, unsigned updated, unsigned C0, unsigned BSIC); + + /** Gets age of parameters in seconds or UNDEFINED if unknown. */ + unsigned paramAge(const char* address); + + /** Returns a C-string that must be free'd by the caller. */ + char* getAddress(unsigned BCCH_FREQ_NCELL, unsigned BSIC); + + /** Return the ARFCN given its position in the BCCH channel list (GSM 04.08 10.5.2.20). */ + int getARFCN(unsigned BCCH_FREQ_NCELL); + + /** Start the holdoff timer on this neighbor. */ + void holdOff(const char* address, unsigned seconds); + + /** Start the holdoff timer on this neighbor. */ + void holdOff(const struct sockaddr_in* peer, unsigned seconds); + + /** Return true if we are holding off requests to this neighbor. */ + bool holdingOff(const char* address); + + /** Send out new requests for old entries. */ + void refresh(); + + /** + Get the neighbor cell ARFCNs as a vector. + mARFCNList is updated by every call to add(). + This returns a copy to be thread-safe. + */ + std::vector ARFCNList() const { ScopedLock lock(mLock); return mARFCNList; } + + /** Get the neighbor cell BCC set as a bitmask. */ + int BCCSet() const { return mBCCSet; } + + private: + + /** Get the neighbor cell BCC set as a bitmask. */ + int getBCCSet() const; + + /** Get the neighbor cell ARFCNs as a vector. */ + std::vector getARFCNs() const; +}; + + + +} //namespace + + + +extern Peering::NeighborTable gNeighborTable; + +#endif + diff --git a/Peering/Peering.cpp b/Peering/Peering.cpp new file mode 100644 index 00000000..338d65dc --- /dev/null +++ b/Peering/Peering.cpp @@ -0,0 +1,451 @@ +/**@file Messages for peer-to-peer protocol */ +/* + * Copright 2011 Range Networks, Inc. + * All rights reserved. +*/ + + +#include "Peering.h" +#include "NeighborTable.h" + +#include +#include +#include +#include +#include +#include + +#undef WARNING + +using namespace Peering; + + + + +void PeerMessageFIFOMap::addFIFO(unsigned transactionID) +{ + PeerMessageFIFO* newFIFO = new PeerMessageFIFO; + write(transactionID,newFIFO); +} + +void PeerMessageFIFOMap::removeFIFO(unsigned transactionID) +{ + if (!remove(transactionID)) { LOG(DEBUG) << "attempt to remove non-existent FIFO " << transactionID; } +} + + +char* PeerMessageFIFOMap::readFIFO(unsigned transactionID, unsigned timeout) +{ + PeerMessageFIFO* FIFO = readNoBlock(transactionID); + if (!FIFO) { + LOG(NOTICE) << "attempt to read non-existent FIFO " << transactionID; + return NULL; + } + return FIFO->read(timeout); +} + + +void PeerMessageFIFOMap::writeFIFO(unsigned transactionID, const char* msg) +{ + PeerMessageFIFO* FIFO = readNoBlock(transactionID); + if (!FIFO) { + LOG(NOTICE) << "attempt to write non-existent FIFO " << transactionID; + return; + } + FIFO->write(strdup(msg)); +} + + + +PeerInterface::PeerInterface() + :mSocket(gConfig.getNum("Peering.Port")), + mReferenceCounter(0) +{ + mSocket.nonblocking(); +} + + +void* foo(void*) +{ + gPeerInterface.serviceLoop1(NULL); + return NULL; +} + + +void* bar(void*) +{ + gPeerInterface.serviceLoop2(NULL); + return NULL; +} + + +void PeerInterface::start() +{ + // FIXME - mServer.start(serviceLoop, NULL); wouldn't compile and I'm not smart enough to figure it out + mServer1.start(foo, NULL); + mServer2.start(bar, NULL); +} + + +// mucking about every second with the neighbor tables is plenty +void* PeerInterface::serviceLoop1(void*) +{ + // gTRX.C0() needs some time to get ready + sleep(8); + while (1) { + gNeighborTable.refresh(); + sleep(1); + } + return NULL; +} + +// this loop services, among other things, the IND and ACK HANDOVER_COMPLETE, which affects the +// handover gap in the call. so it needs to be short. Like 10msec = 10000usec +void* PeerInterface::serviceLoop2(void*) +{ + // gTRX.C0() needs some time to get ready + sleep(8); + while (1) { + drive(); + usleep(10000); + } + return NULL; +} + + +void PeerInterface::drive() +{ + int numRead = mSocket.read(mReadBuffer); + if (numRead<0) { + return; + } + + mReadBuffer[numRead] = '\0'; + LOG(INFO) << "received " << mReadBuffer; + + process(mSocket.source(),mReadBuffer); +} + + +void PeerInterface::process(const struct sockaddr_in* peer, const char* message) +{ + // neighbor message? + if (strncmp(message+3," NEIGHBOR_PARAMS",16)==0) + return processNeighborParams(peer,message); + + // must be handover related + + // Initial inbound handover request? + if (strncmp(message,"REQ HANDOVER ",13)==0) + return processHandoverRequest(peer,message); + + // Handover response? ("Handover Accept" in the ladder.) + if (strncmp(message,"RSP HANDOVER ",13)==0) + return processHandoverResponse(peer,message); + + // IND HANDOVER_COMPLETE + if (strncmp(message,"IND HANDOVER_COMPLETE ", 22)==0) + return processHandoverComplete(peer,message); + + // IND HANDOVER_FAILURE + if (strncmp(message,"IND HANDOVER_FAILURE ", 21)==0) + return processHandoverFailure(peer,message); + + // Other handover messages go into the FIFO map. + // FIXME -- We need something here to spot malformed messages. + unsigned transactionID; + sscanf(message, "%*s %*s %u", &transactionID); + mFIFOMap.writeFIFO(transactionID,message); +} + + +void PeerInterface::processNeighborParams(const struct sockaddr_in* peer, const char* message) +{ + static const char rspFormat[] = "RSP NEIGHBOR_PARAMS %d %d"; + + LOG(DEBUG) << "got message " << message; + if (0 == strncmp(message,"REQ ",4)) { + // REQ? Send a RSP. + char rsp[100]; + sprintf(rsp, rspFormat, gTRX.C0(), gBTS.BSIC()); + sendMessage(peer,rsp); + return; + } + + if (0 == strncmp(message,"RSP ",4)) { + // RSP? Digest it. + unsigned C0, BSIC; + int r = sscanf(message, rspFormat, &C0, &BSIC); + if (r!=2) { + LOG(ALERT) << "badly formatted peering message: " << message; + return; + } + // Did the neighbor list change? + bool change = gNeighborTable.addInfo(peer,(unsigned)time(NULL),C0,BSIC); + // no change includes unsolicited RSP NEIGHBOR_PARAMS. drop it. + if (!change) return; + // It there a BCC conflict? + int ourBSIC = gBTS.BSIC(); + int BCC = BSIC & 0x07; + int ourBCC = ourBSIC & 0x07; + if (BCC == ourBCC) { LOG(ALERT) << "neighbor with matching BCC " << ourBCC; } + // Is there an NCC conflict? + int NCC = BSIC >> 3; + int NCCMaskBit = 1 << NCC; + int ourNCCMask = gConfig.getNum("GSM.CellSelection.NCCsPermitted"); + if ((NCCMaskBit & ourNCCMask) == 0) { + LOG(ALERT) << "neighbor with NCC " << NCC << " not in NCCsPermitted"; + } + // There was a change, so regenerate the beacon + gBTS.regenerateBeacon(); + return; + } + + LOG(ALERT) << "unrecognized message: " << message; +} + + +void PeerInterface::sendNeighborParamsRequest(const struct sockaddr_in* peer) +{ + sendMessage(peer,"REQ NEIGHBOR_PARAMS"); + // Get a string for the sockaddr_in. + char addrString[256]; + const char *ret = inet_ntop(AF_INET,&(peer->sin_addr),addrString,255); + if (!ret) { + LOG(ALERT) << "cannot parse peer socket address"; + return; + } + LOG(DEBUG) << "requested parameters from " << addrString << ":" << ntohs(peer->sin_port); +} + + +void PeerInterface::processHandoverRequest(const struct sockaddr_in* peer, const char* message) +{ + // This is "Handover Request" in the ladder diagram; we are "BS2" accepting it. + assert(message); + unsigned oldTransID; + if (!sscanf(message,"REQ HANDOVER %u ", &oldTransID)) { + LOG(ALERT) << "cannot parse peering message " << message; + return; + } + LOG(DEBUG) << message; + + // Break message into space-delimited tokens, stuff into a SimpleKeyValue and then unpack it. + SimpleKeyValue params; + params.addItems(message); + const char* IMSI = params.get("IMSI"); + GSM::L3MobileIdentity mobileID = GSM::L3MobileIdentity(IMSI); + //const char *callID = params.get("CallID"); + const char *proxy = params.get("Proxy"); + + // find existing transaction record if this is a duplicate REQ HANDOVER + Control::TransactionEntry* transaction = gTransactionTable.find(mobileID, oldTransID); + + // and the channel that goes with it + GSM::LogicalChannel* chan = NULL; + if (transaction) { + chan = transaction->channel(); + LOG(DEBUG) << *transaction; + } + + // if this is the first REQ HANDOVER + if (!transaction) { + + LOG(INFO) << "initial REQ HANDOVER for " << mobileID << " " << oldTransID; + + // Get a channel allocation. + // For now, we are assuming a full-rate channel. + // And check gBTS.hold() + time_t start = time(NULL); + if (!gBTS.hold()) chan = gBTS.getTCH(); + LOG(DEBUG) << "getTCH took " << (time(NULL) - start) << " seconds"; + + // FIXME -- Somehow, getting from getTCH above to the test below can take several seconds. + // FIXME -- #797. + + // If getTCH took so long that there's too little time left in T3101, ignore this REQ and get the next one. + if (chan && chan->SACCH()->debugGetL1()->decoder()->debug3101remaining() < 1000) { + LOG(NOTICE) << "handover TCH allocation took too long; risk of T3101 timeout; trying again"; + return; + } + + // If there's no channel available, send failure. + if (!chan) { + LOG(CRIT) << "congestion"; + char rsp[50]; + // RR Cause 101 "cell allocation not available" + // GSM 04.08 10.5.2.31 + sprintf(rsp,"RSP HANDOVER %u 101", oldTransID); + sendMessage(peer,rsp); + return; + } + + // build a new transaction record + transaction = new Control::TransactionEntry(peer,mReferenceCounter++,params,proxy,chan,oldTransID); + mFIFOMap.addFIFO(transaction->ID()); + gTransactionTable.add(transaction); + LOG(INFO) "creating new transaction " << *transaction; + + // Set the channel state. + chan->handoverPending(true); + } + + // Send accept. + const GSM::L3ChannelDescription desc = chan->channelDescription(); + char rsp[50]; + sprintf(rsp,"RSP HANDOVER %u 0 %u %u %u %u %u %u %u %u", + oldTransID, + transaction->inboundReference(), + gTRX.C0(), gBTS.NCC(), gBTS.BCC(), + desc.typeAndOffset(), desc.TN(), desc.TSC(), desc.ARFCN() + ); + sendMessage(peer,rsp); + return; +} + +void PeerInterface::processHandoverComplete(const struct sockaddr_in* peer, const char* message) +{ + + // This is "IND HANDOVER" in the ladder diagram; we are "BS1" receiving it. + unsigned transactionID; + if (!sscanf(message,"IND HANDOVER_COMPLETE %u", &transactionID)) { + LOG(ALERT) << "cannot parse peering message " << message; + return; + } + + // Don't need to HARDRELEASE channel because that happens (if all is successful) in + // outboundHandoverTransfer (Control/CallControl.cpp). + // Don't need to remove the transaction because that happens (if all is successful) in + // Control::callManagementLoop (Control/CallControl.cpp). + // So why is this here if we don't do anything? + // 1. IND/ACK HANDOVER_COMPLETE is useful indication of completion in OpenBTS log or GSMTAP trace. + // 2. This is a placeholder in case we DO need to do something later. + + // FIXME -- We need to speed up channel recycling. See #816. + + char rsp[50]; + sprintf(rsp,"ACK HANDOVER_COMPLETE %u", transactionID); + sendMessage(peer,rsp); + + return; +} + + +void PeerInterface::processHandoverFailure(const struct sockaddr_in* peer, const char* message) +{ + + // This indication means that inbound handover processing failed + // on the other BTS. This BTS cannot handover this call right now. + unsigned transactionID; + unsigned cause; + unsigned holdoff = gConfig.getNum("GSM.Handover.FailureHoldoff"); + unsigned res = sscanf(message,"IND HANDOVER_FAILURE %u %u %u", &transactionID, &cause, &holdoff); + if (res==0) { + LOG(ALERT) << "cannot parse peering message " << message; + return; + } + + if (res<3) { + LOG(ALERT) << "peering message missing parameters " << message; + } + + // Set holdoff on this BTS. + + // FIXME -- We need to decide what else to do here. See #817. + gNeighborTable.holdOff(peer,holdoff); + + char rsp[50]; + sprintf(rsp,"ACK HANDOVER_FAILURE %u", transactionID); + sendMessage(peer,rsp); + + return; +} + + + + +void PeerInterface::processHandoverResponse(const struct sockaddr_in* peer, const char* message) +{ + unsigned cause; + unsigned transactionID; + unsigned reference; + unsigned C0, NCC, BCC; + unsigned typeAndOffset, TN, TSC, ARFCN; + + // This is "Handover Accept" in the ladder diagram; we are "BS1" receiving it. + // FIXME -- Error-check for correct message format. + sscanf(message,"RSP HANDOVER %u %u %u %u %u %u %u %u %u %u", + &transactionID, &cause, + &reference, + &C0, &NCC, &BCC, + &typeAndOffset, &TN, &TSC, &ARFCN + ); + + if (cause) { + LOG(NOTICE) << "handover of transaction " << transactionID << " refused with cause " << cause; + return; + } + + Control::TransactionEntry *transaction = gTransactionTable.find(transactionID); + if (!transaction) { + LOG(NOTICE) << "received handover response with no matching transaction " << transactionID; + return; + } + + // Set the handover parameters and state to HandoverOutbound. + // The state change will trigger the call management loop + // to send the Handover Command to the handset. + transaction->setOutboundHandover( + GSM::L3HandoverReference(reference), + GSM::L3CellDescription(C0,NCC,BCC), + GSM::L3ChannelDescription2((GSM::TypeAndOffset)typeAndOffset,TN,TSC,ARFCN), + GSM::L3PowerCommandAndAccessType(), + GSM::L3SynchronizationIndication(true, true) + ); +} + + + + +void PeerInterface::sendMessage(const struct sockaddr_in* peer, const char *message) +{ + LOG(DEBUG) << "sending message: " << message; + ScopedLock lock(mLock); + mSocket.send((const struct sockaddr*)peer,message); +} + +bool PeerInterface::sendUntilAck(const Control::TransactionEntry* transaction, const char* message) +{ + LOG(DEBUG) << "sending message until ACK: " << message; + const struct sockaddr_in* peer = transaction->inboundPeer(); + char *ack = NULL; + unsigned timeout = gConfig.getNum("Peering.ResendTimeout"); + unsigned count = gConfig.getNum("Peering.ResendCount"); + while (!ack && count>0) { + sendMessage(peer,message); + ack = mFIFOMap.readFIFO(transaction->ID(),timeout); + count--; + } + + // Timed out? + if (!ack) { + LOG(ALERT) << "lost contact with peer: " << *transaction; + return false; + } + LOG(DEBUG) << "ack message: " << ack; + + // FIXME -- Check to be sure it's the right host acking the right message. + // See #832. + if (strncmp(ack,"ACK ",4)==0) { + free(ack); + return true; + } + + // Not the expected ACK? + LOG(CRIT) << "expecting ACK, got " << *ack; + free(ack); + return false; +} + + + diff --git a/Peering/Peering.h b/Peering/Peering.h new file mode 100644 index 00000000..400cbe73 --- /dev/null +++ b/Peering/Peering.h @@ -0,0 +1,159 @@ +/**@file Messages for peer-to-peer protocol */ +/* + * Copright 2011 Range Networks, Inc. + * All rights reserved. +*/ + + +#ifndef PERRINGMESSAGES_H +#define PERRINGMESSAGES_H + +#include +#include +#include +#include + + +namespace Control { +class TransactionEntry; +} + + + +namespace Peering { + +typedef InterthreadQueue _PeerMessageFIFO; + +class PeerMessageFIFO : public _PeerMessageFIFO { + + private: + + Timeval mExpiration; + + public: + + PeerMessageFIFO() + :_PeerMessageFIFO(), + mExpiration(gConfig.getNum("GSM.Timer.T3103")*2) + { } + + ~PeerMessageFIFO() { clear(); } + + bool expired() const { return mExpiration.passed(); } + +}; + + +class PeerMessageFIFOMap : public InterthreadMap { + + public: + + void addFIFO(unsigned transactionID); + + void removeFIFO(unsigned transactionID); + + /** Returned C-string must be free'd by the caller. */ + char *readFIFO(unsigned transactionID, unsigned timeout); + + /** Makes a copy of the string into the FIFO with strdup. */ + void writeFIFO(unsigned transactionID, const char* msg); + +}; + + + + +class PeerInterface { + + private: + + UDPSocket mSocket; + char mReadBuffer[2048]; + PeerMessageFIFOMap mFIFOMap; ///< one FIFO per active handover transaction + + Mutex mLock; + + volatile unsigned mReferenceCounter; + + Thread mServer1; + Thread mServer2; + + public: + + /** Initialize the interface. */ + PeerInterface(); + + /** Start the service loops. */ + void start(); + + /** service loops. */ + void* serviceLoop1(void*); + void* serviceLoop2(void*); + + void sendNeighborParamsRequest(const struct ::sockaddr_in* peer); + + /** + Send a message on the peering interface. + @param IP The IP address of the remote peer. + @param The message to send. + */ + void sendMessage(const struct ::sockaddr_in* peer, const char* message); + + /** + Send a message repeatedly until the ACK arrives. + @param transaction Carries the peer address and transaction ID. + @param message The IND message to send. + @erturn true on ack, false on timeout + */ + bool sendUntilAck(const Control::TransactionEntry*, const char* message); + + /** Remove a FIFO with the given transaction ID. */ + void removeFIFO(unsigned transactionID) { mFIFOMap.removeFIFO(transactionID); } + + + private: + + void drive(); + + /** + Parse and dispatch an inbound message. + */ + void parse(const struct ::sockaddr_in* peer, const char* message); + + + /** + Parse and process an inbound message. + @param peer The source address. + @param message The message text. + */ + void process(const struct ::sockaddr_in* peer, const char* message); + + //@{ + + /** Process the NEIGHBOR_PARMS message/response. */ + void processNeighborParams(const struct ::sockaddr_in* peer, const char* message); + + /** Process REQ HANDOVER. */ + void processHandoverRequest(const struct ::sockaddr_in* peer, const char* message); + + /** Process RSP HANDOVER. */ + void processHandoverResponse(const struct ::sockaddr_in* peer, const char* message); + + /** Process IND HANDOVER_COMPLETE */ + void processHandoverComplete(const struct sockaddr_in* peer, const char* message); + + /** Process IND HANDOVER_FAILURE */ + void processHandoverFailure(const struct sockaddr_in* peer, const char* message); + + //@} +}; + + + +} //namespace + +extern Peering::PeerInterface gPeerInterface; + +#endif + + diff --git a/README b/README index 3678c62b..2cd53b98 100644 --- a/README +++ b/README @@ -14,6 +14,7 @@ AsteriskConfig Asterisk configuration files for use with OpenBTS. CommonLib Common-use libraries, mostly C++ wrappers for basic facilities. Control Control-layer functions for the protocols of GSM 04.08 and SIP. GSM The GSM stack. +RRLP Radio Resource Location Protocol SIP Components of the SIP state machines ued by the control layer. SMS The SMS stack. SR The subscriber registry. @@ -38,7 +39,7 @@ By default, OpenBTS assumes the following UDP port assignments: These can be controlled in the CONFIG table in /etc/OpenBTS.db. Standrd paths: -/OpenBTS -- Binary installation. +/OpenBTS -- Binary installation and authorization keys. /etc/OpenBTS -- Configuration databases. /var/run/OpenBTS -- Real-time reporting databases. @@ -46,6 +47,10 @@ The script apps/setUpFiles.sh will create these directories and install the correct files in them. +Releases 2.5 and later include the smqueue SMS server. It is NOT part of the +normal GNU build process with the rest of OpenBTS. To build smqueue, go +into the smqueue directory and just type "make -f Makefile.standalone". + Release history: @@ -152,8 +157,7 @@ Release Name SVN Reposiory SVN Rev Comments added CLI "rxpower" command (r844) added CLI "unconfig" command -2.7 Natchitoches Range rxxx (never released publicly) - converted TMSITable to sqlite3 (r902) +2.7 Natchitoches Range rxxx converted TMSITable to sqlite3 (r902) sqlite3-based configuration (r???) converted Logger to syslogd (r903) added support for rest octets (r1022) @@ -162,7 +166,9 @@ Release Name SVN Reposiory SVN Rev Comments in-call delivery and submission of text messages (r1231) RFC-2833 DMTF (r1249) -2.8 Opelousas Range rxxx move databases to /etc and /var - RRLP aiding support - +2.8 Opelousas Range rxxx added SHA1/RSA image verification + move databases to /etc and /var + SIP-based authentication +2.9 Plaquemine Range socket-based remote CLI + merge-in of "S" Release diff --git a/SGSNGGSN/GPRSL3Messages.cpp b/SGSNGGSN/GPRSL3Messages.cpp new file mode 100644 index 00000000..96ea8700 --- /dev/null +++ b/SGSNGGSN/GPRSL3Messages.cpp @@ -0,0 +1,1441 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include "GPRSL3Messages.h" +//#include "TBF.h" +#include "Sgsn.h" +#include "Ggsn.h" +#include "Utils.h" + +namespace SGSN { +class SgsnError; + +static bool sEnableMsRaCap = true; // MsRaCap had alot of bugs, so provide a way to disable it. + +#define CASENAME(x) case x: return #x; + +const char *L3GmmMsg::name(unsigned mt, bool ornull) +{ + switch ((MessageType)mt) { + CASENAME(AttachRequest) + CASENAME(AttachAccept) + CASENAME(AttachComplete) + CASENAME(AttachReject) + CASENAME(DetachRequest) + CASENAME(DetachAccept) + CASENAME(RoutingAreaUpdateRequest) + CASENAME(RoutingAreaUpdateAccept) + CASENAME(RoutingAreaUpdateComplete) + CASENAME(RoutingAreaUpdateReject) + CASENAME(ServiceRequest) + CASENAME(ServiceAccept) + CASENAME(ServiceReject) + CASENAME(PTMSIReallocationCommand) + CASENAME(PTMSIReallocationComplete) + CASENAME(AuthenticationAndCipheringReq) + CASENAME(AuthenticationAndCipheringResp) + CASENAME(AuthenticationAndCipheringRej) + CASENAME(AuthenticationAndCipheringFailure) + CASENAME(IdentityRequest) + CASENAME(IdentityResponse) + CASENAME(GMMStatus) + CASENAME(GMMInformation) + default: + return ornull ? 0 : "unrecognized L3GmmMsg type"; + } +} + +void L3GmmMsg::text(std::ostream&os) const +{ + os < src.size()) { // last one will have nextrp == src.size() + SGSNERROR("invalid message size in "<>4,4); // First nibble is in the high nibble of first byte. + for (unsigned i = 1; i < mLen; i++) { + // The dang nibbles are backwards. What nimrods. + result.appendField(mIdData[i]&0xf,4); + // Last byte may only have one nibble: + // Note that the even/odd indication includes the single nibble + // in the first byte, so we counter-intuitively check isodd here + // instead of iseven. + if (i < mLen-1 || isodd) { + result.appendField((mIdData[i]>>4)&0xf,4); + } + } +} + +// 24.008 10.5.1.4 Mobile Identity +//void GmmMobileIdentityIE::setIMSI(ByteVector &imsi) +//{ +// int len = imsi.size(); +// if (len == 0) { +// mPresent = false; +// return; +// } +// mPresent = true; +// mTypeOfId = 1; // IMSI +//} + +ByteVector GmmMobileIdentityIE::getImsi() const +{ + assert(isImsi()); // Caller was supposed to check this. + ByteVector imsi(8); + decodeIM(imsi); + return imsi; +} + +const string GmmMobileIdentityIE::getAsBcd() const +{ + ByteVector tmp(12); // 12 is overkill. + decodeIM(tmp); + return tmp.hexstr(); + + //unsigned i; + //for (i=0; i 8) { + LLCWARN( "unexpected Mobile Identity length:"< 8) { + LLCWARN("invalid Mobile Identity TMSI length (must be <=8):"< 8) datalen = 8; + unsigned char *datap = pp.begin() + rp + 1; + + mTypeOfId = typeIdByte & 7; + int isOdd = typeIdByte & 8; + switch (mTypeOfId) { + case 0: // No identity + break; + case 4: // TMSI or P-TMSI + if (len != 4) { + LLCWARN("unexpected Mobile Identity TMSI length:"<>4; + memcpy(&mIdData[1],datap,datalen); + mNumDigits = (len-1)*2 - (isOdd?1:0); + break; + case 5: // TMGI and MBMS session id. + memcpy(&mIdData[0],datap,datalen); + break; + default: + LLCWARN("unexpecited Mobile Identity type:" << mTypeOfId); + break; + } + // Assume it is p-tmsi and get it. +#endif + rp += mLen; +} + +void GmmMobileIdentityIE::text(std::ostream&os) const +{ + if (!mPresent) { os << "not present"; return; } + switch (mTypeOfId) { + case 1: os << LOGVAR2("IMSI",getAsBcd()); break; + case 2: os << LOGVAR2("IMEI",getAsBcd()); break; + case 3: os << LOGVAR2("IMEISV",getAsBcd()); break; + case 4: os << LOGHEX2("(P)TMSI",mTmsi); break; + case 5: os << LOGVAR2("TMGI",ByteVectorTemp((char*)mIdData,mLen).hexstr()); break; + case 6: os << LOGVAR2("type6",ByteVectorTemp((char*)mIdData,mLen).hexstr()); break; + case 7: os << LOGVAR2("type7",ByteVectorTemp((char*)mIdData,mLen).hexstr()); break; + } +} + +// write the length and value, but not the IEI type. +void GmmMobileIdentityIE::appendLV(ByteVector &msg) +{ + if (mTypeOfId == 4) { + msg.appendByte(5); // Length of IE for TMSI/PTMSI + msg.appendByte(0xf4); + msg.appendUInt32(mTmsi); + } else { + msg.appendByte(mLen); + msg.append(mIdData,mLen); + } +} + +// 3GPP 24.008 10.5.6.5 in octets/sec +static const unsigned sPeakThroughputTableSize = 10; +static unsigned sPeakThroughputTable[sPeakThroughputTableSize] = { + /* 0 */ 0, // 0 means 'subscribed peak throughput' + /* 1 */ 1000, + /* 2 */ 2000, + /* 3 */ 4000, + /* 4 */ 8000, + /* 5 */ 16000, + /* 6 */ 32000, + /* 7 */ 64000, + /* 8 */ 128000, + /* 9 */ 256000, +}; + +void SmQoS::setPeakThroughput(unsigned bytepSec) // In Bytes/sec +{ + for (unsigned code = 1; code < sPeakThroughputTableSize; code++) { + if (bytepSec <= sPeakThroughputTable[code]) { setPeakThroughputCode(code); } + } + setPeakThroughputCode(sPeakThroughputTableSize-1); +} + + +// Result is Bytes/sec +unsigned SmQoS::getPeakThroughput() +{ + unsigned code = getPeakThroughputCode(); + return code <= 9 ? sPeakThroughputTable[code] : 0; +} + +// 3GPP 24.008 10.5.6.5 in octets/hour +static const unsigned sMeanThroughputTableSize = 19; +static unsigned sMeanThroughputTable[sMeanThroughputTableSize] = { + /* 0 */ 0, + /* 1 */ 100, + /* 2 */ 200, + /* 3 */ 500, + /* 4 */ 1*1000, + /* 5 */ 2*1000, + /* 6 */ 5*1000, + /* 7 */ 10*1000, + /* 8 */ 20*1000, + /* 9 */ 50*1000, + /* 10 */ 100*1000, + /* 11 */ 200*1000, + /* 12 */ 500*1000, + /* 13 */ 1*1000000, + /* 14 */ 2*1000000, + /* 15 */ 5*1000000, + /* 16 */ 10*1000000, + /* 17 */ 20*1000000, + /* 18 */ 40*1000000 +}; + +// Result is in Bytes/hour +// 0 means best effort. +unsigned SmQoS::getMeanThroughput() +{ + unsigned code = getMeanThroughputCode(); + return code <= 18 ? sMeanThroughputTable[code] : 0; +} + +// We probably will not use this, just set 'best effort' using setMeanThroughputCode() +void SmQoS::setMeanThroughput(unsigned bytepHour) // KBytes/sec +{ + for (unsigned code = 1; code < sMeanThroughputTableSize; code++) { + if (bytepHour <= sMeanThroughputTable[code]) { setMeanThroughputCode(code); } + } + setMeanThroughputCode(sMeanThroughputTableSize-1); +} + +// In kilobits/s. 24.008 table 10.5.165 +void SmQoS::setMaxBitRate(unsigned kbitps, bool uplink) +{ + unsigned code; + if (kbitps == 0) { code = 0xff; } + else if (kbitps <= 64) { code = kbitps; } + else if (kbitps <= 568) { code = 0x40 + ((kbitps-64)/8); } + else if (kbitps <= 8640) { code = 0x80 + ((kbitps-576)/64); } + else { + // There is a way to encode this using extended bytes, but we wont need it. + // Just use the max value: + code = 0xfe; + } + if (uplink) { + setMaxBitRateUplinkCode(code); + } else { + setMaxBitRateDownlinkCode(code); + } +} + +// Return -1 if not defined. +int SmQoS::getMaxBitRate(bool uplink) +{ + // The IE is variable sized and may not include these fields. + if (size() <= 6) {return -1;} + unsigned code = uplink ? getMaxBitRateUplinkCode() : getMaxBitRateDownlinkCode(); + if (code == 0xff) { return 0; } + switch (code & 0xc0) { + case 0: return code; + case 0x40: return 64 + 8*(code & 0x3f); + default: return 576 + 64*(code & 0x3f); + } +} + + +// 3GPP 24.008 10.5.6.5 +// Set defaults for PS [Packet Switched] services. +// For streaming video, set udp to true. +// Specify rates in KBytes/sec, using K=1000 not 1024. +void SmQoS::defaultPS(unsigned rateDownlink, unsigned rateUplink) +{ + setDelayClass(4); // best effort + // Reliability Class: + // 3 => unacknowledged GTP and LLC, acknowledged RLC, protected data. + // 4 => unacknowledged GTP and LLC, RLC, protected data. + // 5 => unacknowledged GTP and LLC, RLC, unprotected data. + setReliabilityClass(3); + //mPeakThroughput = 6); // 32k/s 1 is lowest peak throughput + unsigned peakThroughput = 1000* max(rateUplink,rateDownlink); + setPeakThroughput(peakThroughput); + setPrecedenceClass(2); // normal + setMeanThroughput(0x1f); // best effort + // Traffic class: 3 => Interactive, 4=background, 2=streaming, 1=conversational + setTrafficClass(3); + setDeliveryOrder(2); // unordered + setDeliveryOfErrSdu(2); // yes, or could use 1 = nodetect + setMaxSduSize(0x99); // 1520 bytes + setMaxBitRate(8*rateDownlink,0); + setMaxBitRate(8*rateUplink,1); + //setMaxBitRateForUplinkCode(0x3f); // 63kbps; anything from 1 to 0x3f is value * 1k + //setMaxBitRateForDownlinkCode(0x3f); // 63kbps + setResidualBER(1); // 5e-2 + setSduErrorRatio(1); // 1e-2 + setTransferDelay(0x10); // 200ms + setTrafficHandlingPriority(3); // value 1-3. + setGuaranteedBitRateUplinkCode(0xff); // 0k, ignored for traffic class interactive. + setGuaranteedBitRateDownlinkCode(0xff); // 0k, ignored for traffic class interactive. + setSignalingIndication(0); // not optimized + // Source statistics: 0=unknown, 1 =speech, but: + // "The Source Statistics Descriptor value is ignored if the Traffic Class + // is Interactive class or Background Class" + setSourceStatisticsDescriptor(0); +} + +const char *AccessTechnologyType2Name(AccessTechnologyType type) +{ + switch (type) { + CASENAME(GSM_P) + CASENAME(GSM_E) + CASENAME(GSM_R) + CASENAME(GSM_1800) + CASENAME(GSM_1900) + CASENAME(GSM_450) + CASENAME(GSM_480) + CASENAME(GSM_850) + CASENAME(GSM_750) + CASENAME(GSM_T380) + CASENAME(GSM_T410) + CASENAME(GSM_UNUSED) + CASENAME(GSM_710) + CASENAME(GSM_T810) + default: return "unknown AccessTechnologyType"; + } +} + +const char *AccessCapabilities::CapName(CapType type) const +{ + switch (type) { + //CASENAME(eAccessTechnologyType) + CASENAME(RFPowerCapability) + CASENAME(A5Bits) + CASENAME(ESInd) + CASENAME(PS) + CASENAME(VGCS) + CASENAME(VBS) + // multislot capabilities: + CASENAME(HSCSDMultislotClass) + CASENAME(GPRSMultislotClass) + CASENAME(GPRSExtendedDynamicAllocationCapability) + CASENAME(SMS_VALUE) + CASENAME(SM_VALUE) + CASENAME(ECSDMultislotClass) + CASENAME(EGPRSMultislotClass) + CASENAME(EGPRSExtendedDynamicAllocationCapability) + CASENAME(DTMGPRSMultiSlotClass) + CASENAME(SingleSlotDTM) + CASENAME(DTMEGPRSMultiSlotClass) + // Additions in release 99: + CASENAME(EightPSKPowerCapability) + CASENAME(COMPACTInterferenceMeasurementCapability) + CASENAME(RevisionLevelIndicator) + CASENAME(UMTSFDDRadioAccessTechnologyCapability) + CASENAME(UMTS384McpsTDDRadioAccessTechnologyCapability) + CASENAME(CDMA2000RadioAccessTechnologyCapability) + // Additions in release 4: + CASENAME(UMTS128McpsTDDRadioAccessTechnologyCapability) + CASENAME(GERANFeaturePackage1) + CASENAME(ExtendedDTMGPRSMultiSlotClass) + CASENAME(ExtendedDTMEGPRSMultiSlotClass) + CASENAME(ModulationBasedMultislotClassSupport) + // Additions in release 5: + CASENAME(HighMultislotCapability) + //eGMSKPowerClass + //EightPSKPowerClass + default: return "unknown CapName"; + } +} + +void AccessCapabilities::parseAccessCapabilities( + ByteVector &bv, // bytevector we are parsing + size_t &rp, // location in bytevector + AccessCapabilities *prev, // Previous capabilities or null. + size_t end) // end of capabilities list +{ + mCaps[RFPowerCapability] = bv.readField(rp,3); + if (bv.readField(rp,1)) { mCaps[A5Bits] = bv.readField(rp,7); } + else if (prev) { mCaps[A5Bits] = prev->mCaps[A5Bits]; } + mCaps[ESInd] = bv.readField(rp,1); + mCaps[PS] = bv.readField(rp,1); + mCaps[VGCS] = bv.readField(rp,1); + mCaps[VBS] = bv.readField(rp,1); + if (bv.readField(rp,1)) { // multislot capability struct present + if (bv.readField(rp,1)) { + mCaps[HSCSDMultislotClass] = bv.readField(rp,5); + } + if (bv.readField(rp,1)) { + mCaps[GPRSMultislotClass] = bv.readField(rp,5); + mCaps[GPRSExtendedDynamicAllocationCapability] = bv.readField(rp,1); + } + if (bv.readField(rp,1)) { + mCaps[SMS_VALUE] = bv.readField(rp,4); + mCaps[SM_VALUE] = bv.readField(rp,4); + } + // Additions in release 99: Just scan past some of these, dont bother saving. + if (rp >= end) return; + if (bv.readField(rp,1)) { // ECSD multslot class + bv.readField(rp,5); // Toss it. + } + if (rp >= end) return; + if (bv.readField(rp,1)) { // EGPRS multslot class + mCaps[EGPRSMultislotClass] = bv.readField(rp,5); + mCaps[EGPRSExtendedDynamicAllocationCapability] = bv.readField(rp,1); + } + if (rp >= end) return; + if (bv.readField(rp,1)) { // DTM GPRS Multi Slot Class present + mCaps[DTMGPRSMultiSlotClass] = bv.readField(rp,2); + mCaps[SingleSlotDTM] = bv.readField(rp,1); + if (bv.readField(rp,1)) { // DTM EGPRS Multi Slot Class present + mCaps[DTMEGPRSMultiSlotClass] = bv.readField(rp,2); + } + } + } else if (prev) { // No multislot struct means same as previous. + for (int i = HSCSDMultislotClass; i <= DTMEGPRSMultiSlotClass; i++) { + mCaps[i] = prev->mCaps[i]; + } + } + + // Additions in release 99 + if (rp >= end) return; + if (bv.readField(rp,1)) { mCaps[EightPSKPowerCapability] = bv.readField(rp,2); } + if (rp >= end) return; + mCaps[COMPACTInterferenceMeasurementCapability] = bv.readField(rp,1); + mCaps[RevisionLevelIndicator] = bv.readField(rp,1); + mCaps[UMTSFDDRadioAccessTechnologyCapability] = bv.readField(rp,1); + mCaps[UMTS384McpsTDDRadioAccessTechnologyCapability] = bv.readField(rp,1); + mCaps[CDMA2000RadioAccessTechnologyCapability] = bv.readField(rp,1); + // Additions in release 4: + if (rp >= end) return; + mCaps[UMTS128McpsTDDRadioAccessTechnologyCapability] = bv.readField(rp,1); + if (rp >= end) return; + mCaps[GERANFeaturePackage1] = bv.readField(rp,1); + if (rp >= end) return; + if (bv.readField(rp,1)) { + mCaps[ExtendedDTMGPRSMultiSlotClass] = bv.readField(rp,2); + mCaps[ExtendedDTMEGPRSMultiSlotClass] = bv.readField(rp,2); + } + if (rp >= end) return; + mCaps[ModulationBasedMultislotClassSupport] = bv.readField(rp,1); + // Additions in release 5: + if (rp >= end) return; + if (bv.readField(rp,1)) { + mCaps[HighMultislotCapability] = bv.readField(rp,2); + } + // Rest ignored. +} + +AccessCapabilities::CapType AccessCapabilities::mPrintList[] = { + GPRSMultislotClass, + GPRSExtendedDynamicAllocationCapability, + GERANFeaturePackage1 + }; + +void AccessCapabilities::text2(std::ostream &os,bool verbose) const +{ + if (!sEnableMsRaCap ) {return;} + if (!verbose) { // TODO: how to switch to get the full list? + // Short list: + for (unsigned j = 0; j < sizeof(mPrintList)/sizeof(CapType); j++) { + unsigned cap = mPrintList[j]; + if (mCaps[cap] != -1) { os < 0) { // Otherwise it is an error on the part of the MS. + *mCList[numTechs].mCaps = *mCList[numTechs-1].mCaps; + } + // There are two more fields: GMSK Power Class and 8PSK Power Class + // which are included in the length and so skipped over without any more code here. + } else { + size_t trp = rp; + AccessCapabilities *prev = numTechs ? &mCList[numTechs-1] : 0; + mCList[numTechs].parseAccessCapabilities(*this,trp,prev,rp+len); + } + // Advance rp. + rp += len; + if (rp + 15 >= sizeBits()) { break; } // Not enough room left for anything useful. + if (readField(rp,1) == 0) { break; } // End of list marker. + } catch(ByteVectorError) { // oops! + break; // End of that. + } + } +} + +void MsRaCapability::text2(std::ostream &os, bool verbose) const +{ + if (!sEnableMsRaCap ) {return;} + for (int numTechs = 0; numTechs < sMsRaCapMaxTypes; numTechs++) { + if (! mCList[numTechs].mValid) {continue;} + // Dont bother to print the types that Range does not support. + AccessTechnologyType atype = mCList[numTechs].mTechType; + switch (atype) { + case GSM_E: case GSM_850: case GSM_1800: case GSM_1900: + os << (verbose ? "\t" : " "); + os <<" MsRaCapability[" << AccessTechnologyType2Name(atype) << "]=("; + if (mCList[numTechs].mSameAsPrevious) { + os <<"same"; + } else { + mCList[numTechs].text2(os,verbose); + } + os <<")\n"; + default: continue; + } + } +} + +void MsRaCapability::text(std::ostream &os) const { text2(os,0); } + +void L3GmmMsgServiceRequest::gmmParseBody(L3GmmFrame &src, size_t &rp) +{ + mServiceType = src.getNibble(rp,0); + mCypheringKeySequenceNumber = src.getNibble(rp,1); + rp++; + mMobileId.parseLV(src,rp); + if (rp < src.size()) { + unsigned iei = src.readIEI(rp); + if (iei == 0x32) { + rp++; // skip length + mPdpContextStatus.mStatus[0] = src.readByte(rp); + mPdpContextStatus.mStatus[1] = src.readByte(rp); + } + } +} + +void L3GmmMsgServiceAccept::gmmWriteBody(ByteVector &msg) +{ + msg.appendByte(0x32); + msg.appendByte(0x02); + msg.appendByte(mPdpContextStatus.mStatus[0]); + msg.appendByte(mPdpContextStatus.mStatus[1]); +} + +void L3GmmMsgServiceReject::gmmWriteBody(ByteVector &msg) +{ + msg.appendByte(mGmmCause); +} + +void L3GmmMsgRAUpdateRequest::gmmParseBody(L3GmmFrame &src, size_t &rp) +{ + unsigned update_type = src.getNibble(rp,0); + mUpdateType = update_type & 7; + mFollowOnRequestPending = !!(update_type&8); + mCypheringKeySequenceNumber = src.getNibble(rp,1); + rp++; + mOldRaId.parseElement(src,rp); + mMsRadioAccessCapability = src.readLVasBV(rp); + gmParseIEs(src,rp,"RAUpdateRequest"); +} + +void L3GmmMsgRAUpdateAccept::gmmWriteBody(ByteVector &msg) +{ + //msg.appendByte(RoutingAreaUpdateAccept); + msg.appendField(mUpdateResult,4); // nibbles reversed. + msg.appendField(mForceToStandby,4); + //msg.appendByte(mPeriodicRAUpdateTimer.getIEValue()); + mPeriodicRAUpdateTimer.appendElement(msg); + GMMRoutingAreaIdIE mRaId; + mRaId.raLoad(); + mRaId.appendElement(msg); + // End of mandatory IEs. + + // Add the allocated P-TMSI: + if (mAllocatedPTmsi) { + msg.appendByte(0x18); // Allocate P-TMSI IEI + GmmMobileIdentityIE midtmp; + midtmp.setTmsi(mAllocatedPTmsi); + midtmp.appendLV(msg); + } + + // Add the mobile identity that it sent to us: + //msg.appendByte(0x23); // MS identity IEI. + //mMobileId.appendLV(msg); + + if (mTmsi) { + msg.appendByte(0x23); // MS identity IEI. + GmmMobileIdentityIE idtmp; + idtmp.setTmsi(mTmsi); + idtmp.appendLV(msg); + } + + // 10.5.7.1 PDP Context Status + // And I quote: "This IE shall be included by the Network". Hmm. + // If you set this to zeros the MS relinquishes its PDP contexts. + { + msg.appendByte(0x32); + msg.appendByte(2); // length is 2 bytes + msg.appendByte(mPdpContextStatusCurrent.mStatus[0]); + msg.appendByte(mPdpContextStatusCurrent.mStatus[1]); + } +} + +void L3GmmMsgRAUpdateReject::gmmWriteBody(ByteVector &msg) +{ + //msg.appendByte(RoutingAreaUpdateReject); + msg.appendByte(mGmmCause); + msg.appendByte(0); // spare half octet and force-to-standby +} + +void L3GmmMsgAttachAccept::gmmWriteBody(ByteVector &msg) +{ + mPeriodicRAUpdateTimer.setSeconds(gConfig.getNum("SGSN.Timer.RAUpdate")); + mReadyTimer.setSeconds(gConfig.getNum("SGSN.Timer.Ready")); + + //msg.appendByte(AttachAccept); // message type + msg.appendField(mForceToStandby,4); // high nibble first. + msg.appendField(mAttachResult,4); + //msg.appendByte(mPeriodicRAUpdateTimer.getIEValue()); + mPeriodicRAUpdateTimer.appendElement(msg); + // Next byte is SMS and TOM8 message priority. + // I am hard coding them to the lowest value, which is 4. + msg.appendByte(0x44); + GMMRoutingAreaIdIE mRaId; + mRaId.raLoad(); + mRaId.appendElement(msg); + // End of mandatory elements. + + // Add the allocated P-TMSI: + if (mPTmsi) { + // TLV Allocated P-TMSI, but only if we send it. + GmmMobileIdentityIE idtmp; + idtmp.setTmsi(mPTmsi); + msg.appendByte(0x18); // Allocated P-TMSI IEI + idtmp.appendLV(msg); + } + + // 6-7-2012: Removed this. Per 24.008 9.4.2: Attach Accept Description, + // I now believe the second mobile identity is included only to set + // TMSI in case of a combined attach, so we should not include + // this IE unless using NMO 1 and we support the combined attach. + //if (mMobileId.mPresent) { + // msg.appendByte(0x23); // MS identity IEI. + // mMobileId.appendLV(msg); + //} + // Note: you can also set timer T3302, T3319, T3323 values. + // These are all MM timers in Table 11.3a page 545, and not too interesting. +} + +void L3GmmMsgAttachRequest::gmmParseBody(L3GmmFrame &src, size_t &rp) +{ + mMsNetworkCapability = src.readLVasBV(rp); + mAttachType = src.getNibble(rp,0); + mCypheringKeySequenceNumber = src.getNibble(rp,1); + rp++; // skip to end of nibbles we just read, above. + mDrxParameter = src.readUInt16(rp); + mMobileId.parseLV(src,rp); + mOldRaId.parseElement(src,rp); + mMsRadioAccessCapability = src.readLVasBV(rp); + gmParseIEs(src,rp,"GmmAttachRequest"); +} + +// 9.5.4.2 Mobile Originated Detach Request. +void L3GmmMsgDetachRequest::gmmParseBody(L3GmmFrame &src, size_t &rp) +{ + mDetachType = 0xf & src.readByte(rp); // Low nibble is detach type, high nibble is unused. + mMobileId.parseLV(src,rp); + while (rp < src.size()) { + unsigned iei = src.readIEI(rp); + switch (iei) { + case 0x18: // P-TMSI + mMobileId.parseLV(src,rp); + mMobileIdPresent = true; + break; + case 0x19: // P-TMSI signature. Skip it. + src.skipLV(rp,3,3,"P-TMSI signature"); + break; + default: // Unknown IEI. + src.skipLV(rp); + break; + } + } +} + +// 9.5.4.1 Network Originated Detach Request. +void L3GmmMsgDetachRequest:: gmmWriteBody(ByteVector &msg) +{ + msg.appendByte((mForceToStandby<<4) | mDetachType); + if (mGmmCausePresent) { + msg.appendByte(0x25); + msg.appendUInt16(mGmmCause); + } +} + +void L3GmmMsgDetachRequest::textBody(std::ostream &os) const +{ + // The contents are different in uplink and downlink directions. + // We just print anything that has a 'present' flag set. + os< src.size()) { // last one will have nextrp == src.size() + SGSNERROR("invalid message size in ActivatePdpContextRequest"); + return; + } + switch (iei) { + case 0x28: + mApName = src.readLVasBV(rp); // 10.5.6.1 + continue; + case 0x27: + mPco = src.readLVasBV(rp); // 10.5.6.3 + continue; + default: + rp = nextrp; + continue; + } + } +} + +// 3GPP 24.007 11.2.3.1.3 Transaction Identifier. +// First bit is the TIflag that must be set for a message reply. +// And I quote: "A message has a TI flag set to "0" when it belongs to transaction initiated by its sender, +// and to "1" otherwise." +// Transaction id values > 7 requre an extension byte. +void L3SmDlMsg::appendTiPd(ByteVector &msg) +{ + unsigned tiFlag = isSenseCmd() ? 0 : 0x8; + // Note: nibbles are reversed, as always for L3 messages. + if (mTransactionId < 7) { + msg.appendField(tiFlag|mTransactionId,4); // Transaction id. The 0x8 indicates this is a command. + msg.appendField(GSM::L3GPRSSessionManagementPD,4); // protocol discriminator. + } else { + msg.appendField(tiFlag|0x7,4); // Magic value indicates additional transaction id field present. + msg.appendField(GSM::L3GPRSSessionManagementPD,4); // protocol discriminator. + msg.appendByte(0x80|mTransactionId); // The 0x80 is a required but meaningless extension indicator. + } +} + + +void L3SmMsgActivatePdpContextAccept::smWriteBody(ByteVector &msg) +{ + //msg.appendByte(L3SmMsg::ActivatePDPContextAccept); + msg.appendByte(mLlcSapi); + // LV: QoS. + msg.appendByte(mQoS.size()); + msg.append(mQoS); + msg.appendField(0,4); // "spare half octet" + msg.appendField(mRadioPriority,4); + // TLV: Pdp Address + msg.appendByte(0x2b); // IEI + msg.appendByte(mPdpAddress.size()); + msg.append(mPdpAddress); + + // TLV: Protocol Configuration Options. + msg.appendByte(0x27); // IEI + msg.appendByte(mPco.size()); + msg.append(mPco); +} + +// 24.008 9.5.14 +void L3SmMsgDeactivatePdpContextRequest::smParseBody(L3SmFrame &src, size_t &rp) +{ + //size_t rp = parseSmHeader(src); + if (rp >= src.size()) { + SGSNERROR("DeactivatePdpContextRequest too short according to spec"); + mCause = 0; + return; // But we dont care. + } + mCause = src.readByte(rp); + // optional ieis: + while (rp < src.size()) { + unsigned iei = src.readIEI(rp); + if ((iei & 0xf0) == 0x90) { + mTearDownIndicator = iei & 0x1; // 10.5.6.10 + continue; + } + if (rp >= src.size()) {break;} // This is an error, but we dont care. + int len = src.getByte(rp); + if (iei == 0x27) { + mPco = src.readLVasBV(rp); // 10.5.6.3 + continue; + } + // Ignoring; optional MBMS protocol configuration options + // Ignoring: any IEs not in our spec. + rp = rp + len + 1; + } +} + +void L3SmMsgDeactivatePdpContextRequest::smWriteBody(ByteVector &msg) +{ + //msg.appendByte(L3SmMsg::DeactivatePDPContextRequest); + msg.appendByte(mCause); + if (mTearDownIndicator) { + msg.appendByte(0x91); + } + // Ignoring: + // optional protcol configuration options + // optional MBMS protocol configuration options +} +void L3SmMsgDeactivatePdpContextRequest::textBody(std::ostream &os) const +{ + os< +//#include "GPRSInternal.h" +#include "Configuration.h" +#include "GSMCommon.h" +#include "GSML3Message.h" +#include "ByteVector.h" +#include "Utils.h" +#include "ScalarTypes.h" +#include "SgsnBase.h" +//#include "GPRSL3New.h" +extern ConfigurationTable gConfig; +using namespace std; +#define DEHEXIFY(c) ((c)>=10 ? (c)-10+'a' : (c)+'0') + +namespace SGSN { +struct LlcEntity; + +// 10.5.5.18 Update Type uses all four values. +// 10.5.5.17 Update Result uses the first two values. +// RA is for GPRS, and LA is for GSM CS [circuit switched]. +enum RAUpdateType { + RAUpdated = 0, CombinedRALAUpdated = 1, CombinedRALAWithImsiAttach = 2, PeriodicUpdating = 3 +}; + + +#if 0 +// An L3Message as known by the GPRS code. +// The L3Message is not really applicable here, because we do not +// really know enough about this message to set the bodylength etc. yet. +//struct GPRSL3Message : ByteVector { +// unsigned mSkip; +// unsignd mPD; +// unsigned mMITI; +// +// // Parse out the common L3Message header +// GPRSL3Message(ByteVector &vec) : ByteVector(vec.begin(),vec,size()) +// { +// mSkip = getByte(0) >> 4; +// mPD = getByte(0) & 0xf; +// mMITI = getByte(1); +// } +//}; +#endif + +// The L3Message is built on L3Frame which assumes that messages are in BitVectors, +// but for GPRS messages come from RLC (and LLC and LLE) and they are ByteVectors. +class L3GprsMsg : public Text2Str +{ public: + virtual void text(std::ostream &os) const = 0; + virtual int MTI() const =0; + virtual const char *mtname() const =0; +}; + +// All Gmm and Sm messages need a sense that is either reply or command. +// For SM messages, the sense is encoded in the transaction-identifier field. +// For both types, the sense is also encoded in one of the LLC headers. +// Cant be be too too redundant redundant. +class L3GprsDlMsg : public virtual L3GprsMsg +{ public: + enum MsgSense { senseInvalid, senseReply, senseCmd } mSense; + L3GprsDlMsg(MsgSense wSense) : mSense(wSense) {} + // This dummy constructor is used for uplink messages, when message can be both up and downlink: + L3GprsDlMsg() : mSense(senseInvalid) {} + // void setSense(MsgSense wSense) { mSense = wSense; } //unused. + bool isSenseCmd() { + assert(mSense != senseInvalid); + return mSense == senseCmd; + } + virtual void gWrite(ByteVector &msg) = 0; +}; + + +// An L3Frame for Gprs. +class L3GprsFrame : public ByteVector +{ + public: + void dump(std::ostream &os); + L3GprsFrame(ByteVector &vec) : ByteVector(vec) {} + + GSM::L3PD getPD() { return (GSM::L3PD)getNibble(0,0); } // protocol descriminator + //unsigned getSkip() { return getNibble(0,1); } // skip indicator + virtual unsigned getMsgType() { assert(0); } + // TODO: Handle extended transaction id 24.007 + virtual unsigned getBodyOffset() { assert(0); } + + unsigned mIEI; // cache for message parsers. + + unsigned readIEI(size_t &rp) { + mIEI = readByte(rp); + return mIEI; + } + + ByteVector readLVasBV(size_t &rp) { + int len = readByte(rp); + ByteVector result; result.clone(segment(rp,len)); + rp += len; + return result; + } + // Skip over an IE we dont care about: + void skipLV(size_t &rp) { + int len = readByte(rp); + rp += len; + } + + // The minlen and maxlen are of the IE itself, excluding to Type (if any) and Length byte. + void skipLV(size_t &rp, int minlen, int maxlen, const char *text) { + int len = readByte(rp); + if (len < minlen || len > maxlen) { + LLCWARN("unexpected message length for iei:"< 0 (2-sec interval) or 1 (minutes) or 2 (deci-hours) or 7 deactivated + unsigned mValue; // 5 bit range. + GprsTimerIE() : mUnits(7), mValue(0) {} // Deactivate. + void setSeconds(unsigned numSeconds) { + // 6-2013: 0 value now disables. + if (numSeconds == 0) { mUnits = 7; mValue = 0; return; } + if (numSeconds > 62) { setMinutes((numSeconds+59)/60); return; } + mUnits = 0; // 2-second increments. + mValue = numSeconds/2; // + } + void setMinutes(unsigned numMinutes) { + if (numMinutes >= 200) { + // Deactivate + mUnits = 7; mValue = 0; + } else if (numMinutes > 186) { + // Use maximum value. + mUnits = 2; mValue = 31; + } else if (numMinutes > 31) { + mUnits = 2; // deci-hours + mValue = (numMinutes+5)/6; + } else { + mUnits = 1; // minutes + mValue = numMinutes; + } + } + unsigned getSeconds() const { + switch (mUnits) { + case 0: return mValue*2; + case 1: return mValue*60; + case 2: return mValue*600; + case 7: return 0; // special value means disabled. + default: return 0xffffffff; // error. + } + } + unsigned getIEValue() const { return (mUnits << 5) | (mValue & 0x1f); } + void appendElement(ByteVector &msg) { msg.appendByte(getIEValue()); } +}; + +// 24.008 10.5.5.15 +// See also GSM::L3LocationAreaIdentity +struct GMMRoutingAreaIdIE : Text2Str +{ + unsigned char mMCC[3], mMNC[3]; + uint16_t mLAC; + uint8_t mRAC; + void parseElement(ByteVector &pp, size_t &rp); + void raLoad(); + void appendElement(ByteVector &msg); + void text(std::ostream&os) const; + GMMRoutingAreaIdIE(); // constructor zeros mMCC and mMNC + bool valid() { return mMCC[0] || mMCC[1] || mMCC[2]; } // Has IE been set? +}; + +// 24.008 10.5.1.4 Mobile Identity +// We dont really care what the MS/UE tells us except for TMSI, +// so treat TMSI specially, and otherwise just save the whole thing. +struct GmmMobileIdentityIE : Text2Str +{ + Field_z<3> mTypeOfId; // From the first byte. + // Either mTmsi or mIdData+mLen is valid. + uint32_t mTmsi; // tmsi or ptmsi + unsigned char mIdData[8]; // Original data from the IEI. + unsigned mLen; // Length of mIdData = length of IE. + bool mPresent; + GmmMobileIdentityIE() : mPresent(false) {} + + bool isImsi() const { return mTypeOfId == 1; } + bool isTmsi() const { return mTypeOfId == 4; } // TMSI or P-TMSI + + // Parse out an IMSI from the data, and return it in the ByteVector, which must have room to hold it, + // and return true if it was found, false otherwise. + ByteVector getImsi() const; + uint32_t getTmsi() const { assert(isTmsi()); return mTmsi; } + void decodeIM(ByteVector &result) const; + void parseLV(ByteVector &pp, size_t &rp); + const string getAsBcd() const; // its not bcd but readable + void text(std::ostream&os) const; + // write the length and value, but not the IEI type. + void appendLV(ByteVector &msg); + + void setTmsi(uint32_t tmsi) { + mPresent = true; + mTypeOfId = 4; + mTmsi = tmsi; // TMSI or P-TMSI + } +}; + +// We keep only the ByteVector of the QoS IE itself, +// but the description in 24.008 of the IE numbers octets starting at 3, +// and bits 8..1, so we will do the same: +#define DEFINE_QOS_FIELD(name,octet,bitr,length) \ + unsigned get##name() { return getField2(octet-3,8-bitr,length); } \ + void set##name(unsigned val) { setField2(octet-3,8-bitr,val,length); } + +// 3GPP 24.008 10.5.6.5 Quality of Service +// We leave it as a ByteVector, read the values out of it. +struct SmQoS : public ByteVector { + // We dont fill in every bit, and the unused ones are defined as zero, + // so just zero the whole thing before starting. + SmQoS(ByteVector &bv) : ByteVector(bv) {} + SmQoS(unsigned size) : ByteVector(size) {fill(0);} + // Octet 3 (meaning first byte in the QoS ByteVector.) + DEFINE_QOS_FIELD(DelayClass,3,6,3) + DEFINE_QOS_FIELD(ReliabilityClass,3,3,3) + // Octet 4: + DEFINE_QOS_FIELD(PeakThroughputCode,4,8,4) + DEFINE_QOS_FIELD(PrecedenceClass,4,3,3) + // Octet 5: + DEFINE_QOS_FIELD(MeanThroughputCode,5,5,5) + // Octet 6: + DEFINE_QOS_FIELD(TrafficClass,6,8,3) + DEFINE_QOS_FIELD(DeliveryOrder,6,5,2) + DEFINE_QOS_FIELD(DeliveryOfErrSdu,6,3,3) + // Octet 7: + DEFINE_QOS_FIELD(MaxSduSize,7,8,8) + // Octet 8,9: (actual 0-based bytes number 5 and 6) + DEFINE_QOS_FIELD(MaxBitRateUplinkCode,8,8,8) + DEFINE_QOS_FIELD(MaxBitRateDownlinkCode,9,8,8) + // Octet 10: + DEFINE_QOS_FIELD(ResidualBER,10,8,4) + DEFINE_QOS_FIELD(SduErrorRatio,10,4,4) + // Octet 11: + DEFINE_QOS_FIELD(TransferDelay,11,8,6) + DEFINE_QOS_FIELD(TrafficHandlingPriority,11,2,2) + // Octet 12,13: + // "The Guaranteed Bit Rate is ignored if the Traffic Class is + // Interactive or Background class or the max bit rate for downlink is 0 kBps + // 0xff implies 0kBps, ie, unspecified. + DEFINE_QOS_FIELD(GuaranteedBitRateUplinkCode,12,8,8) + DEFINE_QOS_FIELD(GuaranteedBitRateDownlinkCode,13,8,8) + // Octet 14: + DEFINE_QOS_FIELD(SignalingIndication,14,5,1) + DEFINE_QOS_FIELD(SourceStatisticsDescriptor,14,4,4) + // Optional Octets 15-18 are for extented Max and Guaranteed bit-rates. + + // Encoding methods using raw methods above: + + // Maximum and Guaranteed bit rate for uplink and downlink have + // normal and extended versions, where the extended versions + // add yet another extra octet at the end of the QoS IE. + // Return -1 if not specified in the IE, because it was too short, + // in which case the caller must fall back on PeakThroughput. + void setMaxBitRate(unsigned val, bool uplink); + int getMaxBitRate(bool uplink); + + unsigned getPeakThroughput(); + void setPeakThroughput(unsigned bytePSec); + // We probably dont ever need mean throughput, because you can set it to 'best effort', + // implying that only peak throughput is significant. + unsigned getMeanThroughput(); + void setMeanThroughput(unsigned bytePHour); + void defaultPS(unsigned rateDownlink, unsigned rateUplink); +}; + +// 24.008 10.5.5.12a MS Radio Access Capability +// 5-15-2015, David says Range supports: +// 850: GSM 850, 900: GSM E, 1800: GSM 1800, 1900: GSM 1900 +enum AccessTechnologyType { + GSM_P = 0, + GSM_E = 1, + GSM_R = 2, + GSM_1800 = 3, + GSM_1900 = 4, + GSM_450 = 5, + GSM_480 = 6, + GSM_850 = 7, + GSM_750 = 8, + GSM_T380 = 9, + GSM_T410 = 10, + GSM_UNUSED = 11, + GSM_710 = 12, + GSM_T810 = 13 +}; +const char *AccessTechnologyType2Name(AccessTechnologyType type); + +// 24.008 10.5.5.12a MS Radio Access Capability +// 45.002 appendix B explains GPRS Multislot Class. +// There is so much junk in here I am taking a new tack. +// Keep the capabilities in a single array indexed by these names. +// A value of -1 indicates not present. +struct AccessCapabilities { + Bool_z mValid; + AccessTechnologyType mTechType; + Bool_z mSameAsPrevious; + enum CapType { + //AccessTechnologyType, + RFPowerCapability, + A5Bits, + ESInd, + PS, + VGCS, + VBS, + // multislot capabilities: + // Warning: parsing code assumes multislot caps are in the order below. + HSCSDMultislotClass, + GPRSMultislotClass, + GPRSExtendedDynamicAllocationCapability, + SMS_VALUE, + SM_VALUE, + ECSDMultislotClass, // multislot additions in release 99 + EGPRSMultislotClass, + EGPRSExtendedDynamicAllocationCapability, + DTMGPRSMultiSlotClass, + SingleSlotDTM, + DTMEGPRSMultiSlotClass, + // Additions in release 99: + EightPSKPowerCapability, + COMPACTInterferenceMeasurementCapability, + RevisionLevelIndicator, + UMTSFDDRadioAccessTechnologyCapability, + UMTS384McpsTDDRadioAccessTechnologyCapability, + CDMA2000RadioAccessTechnologyCapability, + // Additions in release 4: + UMTS128McpsTDDRadioAccessTechnologyCapability, + GERANFeaturePackage1, // Finally, something we care about. + ExtendedDTMGPRSMultiSlotClass, + ExtendedDTMEGPRSMultiSlotClass, + ModulationBasedMultislotClassSupport, + // Addigions in release 5: + HighMultislotCapability, + //GMSKPowerClass, + //8PSKPowerClass, + CapsMax // Here to indicate the length of the required list. + }; + // These are the capabilities that are actually of some interest to us, + // and will be printed in the message: + static CapType mPrintList[]; + const char *CapName(CapType type) const; + short mCaps[CapsMax]; + AccessCapabilities() { for (int i = 0; i < CapsMax; i++) { mCaps[i] = -1; } } + int getCap(CapType captype) { // Return cap value or -1. + if (!mValid) return -1; + assert(captype < CapsMax); + return mCaps[captype]; + } + void parseAccessCapabilities(ByteVector &bv,size_t &rp,AccessCapabilities *prev,size_t end); + void text2(std::ostream &os,bool verbose) const; + void text(std::ostream &os) const; +}; + +// 24.008 10.5.5.12a MS Radio Access Capability +// It is a list of structures, each of which is either an AccessCapabilitiesStruct +// or an AdditionalAccessTechnologiesStruct +struct MsRaCapability : public ByteVector { + static const int sMsRaCapMaxTypes = 4; // Keep the first four. + AccessCapabilities mCList[sMsRaCapMaxTypes]; // Keep first four. + void parseMsRaCapability(); + void text2(std::ostream &os,bool verbose) const; + void text(std::ostream &os) const; + MsRaCapability(const ByteVector &bv) : ByteVector(bv) { if (bv.size()) parseMsRaCapability(); } +}; + +struct PdpContextStatus : public Text2Str +{ + unsigned char mStatus[2]; + PdpContextStatus() { mStatus[0] = mStatus[1] = 0; } + void text(std::ostream &os) const { + os <<"PdpContextStatus="< mCypheringKeySequenceNumber; // Only bottom 3 bits used. + // value 7 from MS means no key available, which it should be + // if we have not sent an Authentication Request. + Field_z<16> mDrxParameter; + GmmMobileIdentityIE mMobileId; // AttachRequest: PtmsiOrImsi; RAUpdate: mPTmsi; + Bool_z mTmsiStatus; // 10.5.5.4: does ms have a valid TMSI? + ByteVector mMsRadioAccessCapability;// Required element in both AttachRequest and RAUpdate. + GMMRoutingAreaIdIE mOldRaId; // Required element in both AttachRequest and RAUpdate. + Field_z<24> mOldPtmsiSignature; + // For an incoming RAUpdate or AttachRequest, this is the mRequestedReadyTimerValue. + Field_z<8> mRequestedReadyTimerValue; // Unit is encoded per 10.5.7.3 + + ByteVector mMsNetworkCapability; + + // For RA update the PDP context status indicates which PDP contexts + // are still active in the GGSN, because you can switch SGSNs + // while still having active PDP contexts. + PdpContextStatus mPdpContextStatus; + GmmMobileIdentityIE mAdditionalMobileId; + + void gmParseIEs(L3GmmFrame &src, size_t &rp, const char *culprit); + void text(std::ostream &os) const { + os < mCypheringKeySequenceNumber; // Only bottom 3 bits used. + GmmMobileIdentityIE mMobileId; // PTmsi; + Field<4> mServiceType; // Only bottom 3 bits are used + + // For RA update the PDP context status indicates which PDP contexts + // are still active in the GGSN, because you can switch SGSNs + // while still having active PDP contexts. + PdpContextStatus mPdpContextStatus; + // MBMS context status -- not implemented yet + // Uplink data status -- not implemented yet; + + int MTI() const {return ServiceRequest;} + void gmmParseBody(L3GmmFrame &src, size_t &rp); + void textBody(std::ostream &os) const { + os < mUpdateType; // 10.5.5.18: + // 0 => RA updating, 1 => Combined RA/LA updating + // 2 => Combined RA/LA updating with IMSI attach + // 3 => Periodic updating. + bool mFollowOnRequestPending; + + int MTI() const {return RoutingAreaUpdateRequest;} + void gmmParseBody(L3GmmFrame &src, size_t &rp); + void textBody(std::ostream &os) const { + //os <<"RAUpdateRequest" < mAttachResult; // 1=> GPRS only attach, 3=>combined GPRS/IMSI attach. + Bool_z mForceToStandby; + GprsTimerIE mPeriodicRAUpdateTimer; + GprsTimerIE mReadyTimer; + uint32_t mPTmsi; + // Per 24.008 9.4.2: Attach Accept Description, + // it sounds like the second mobile identity is included only to set + // a TMSI in case of a combined attach, so we should not include + // this IE unless using NMO 1 and we support the combined attach. + // 6-7-2012: Previously I had been returning the IMSI in this spot, + // and that worked ok, but I am removing it as incorrect. + GmmMobileIdentityIE mMobileId; + + int MTI() const {return AttachAccept;} + + // Constructor prior to 6-7-2012: + L3GmmMsgAttachAccept(unsigned wAttachResult, uint32_t wPTmsi, + GmmMobileIdentityIE /*wMobileId*/) : + L3GmmDlMsg(senseReply), + mAttachResult(wAttachResult), + mPTmsi(wPTmsi) + //mMobileId(wMobileId) + { + } + + // New constructor, no mobile id. + L3GmmMsgAttachAccept(unsigned wAttachResult, uint32_t wPTmsi): + L3GmmDlMsg(senseReply), + mAttachResult(wAttachResult), + mPTmsi(wPTmsi) + { + } + + void gmmWriteBody(ByteVector &msg); + void textBody(std::ostream &os) const { + //os <<"AttachAccept "; + unsigned RAUpdateIE = mPeriodicRAUpdateTimer.getIEValue(); + os < mAttachType; // 10.5.5.2 + + int MTI() const {return AttachRequest;} + void gmmParseBody(L3GmmFrame &src, size_t &rp); + + void textBody(std::ostream &os) const { + os < mDetachType, mForceToStandby; + // The GmmCause and ForceToStandby are only present in the downlink direction. + uint16_t mGmmCause; + Bool_z mGmmCausePresent; + + // The tmsi is only present in the uplink direction. + GmmMobileIdentityIE mMobileId; // This is supposed to be a PTMSI only, so why encoded as a MobileIdentity IE? + Bool_z mMobileIdPresent; + + // In the uplink direction there is also a P-TMSI signature that we dont use, so we dont parse. + // In fact, the MS is not supposed to include it if we didnt sent it one in an earlier message. + + // This message is bidirectional. This constructor is for reading one in: + L3GmmMsgDetachRequest() {} + void gmmParseBody(L3GmmFrame &src, size_t &rp); + + // This message is bidirectional. This constructor is for making one to send downstream: + L3GmmMsgDetachRequest(unsigned type, unsigned cause) : + L3GmmDlMsg(senseCmd), + mDetachType(type), + mForceToStandby(0), + mGmmCause(cause), + mGmmCausePresent(cause != 0) + {} + void gmmWriteBody(ByteVector &msg); + + void textBody(std::ostream &os) const; +}; + +// 3GPP 24.008 9.4.6 Detach Accept +struct L3GmmMsgDetachAccept : L3GmmUlMsg, L3GmmDlMsg +{ + int MTI() const {return DetachAccept;} + Field_z<4> mForceToStandby; + + // This message is bidirectional. This constructor is for reading one in: + L3GmmMsgDetachAccept() {} + void gmmParseBody(L3GmmFrame &/*src*/, size_t &/*rp*/) { + // Nothing at all. The presence of this message is the indication. + } + + // This message is bidirectional. This constructor is for making one to send downstream. + // Good old C++ requires us to make the constructor arguments unique so we will pass in the useless ForceToStandby. + L3GmmMsgDetachAccept(unsigned wForceToStandby) : + L3GmmDlMsg(senseReply), + mForceToStandby(wForceToStandby) + {} + void gmmWriteBody(ByteVector &msg); + + void textBody(std::ostream &os) const; +}; + +struct L3GmmMsgIdentityRequest : L3GmmDlMsg +{ + Field<4> mIdentityType, mForceToStandby; + int MTI() const {return IdentityRequest;} + L3GmmMsgIdentityRequest() : + L3GmmDlMsg(senseCmd), + mIdentityType(1), // 1 means IMSI, the only kind we ever ask for. + mForceToStandby(0) + {} + void gmmWriteBody(ByteVector &msg); + void textBody(std::ostream &os) const; +}; + +// 24.008 9.4.9 Authenticaion and ciphering request. +struct L3GmmMsgAuthentication : L3GmmDlMsg +{ + int MTI() const {return AuthenticationAndCipheringReq;} + // We wont use any of the IEs: + // Ciphering algorithm - always 0 + // IMEISV request - request IMEI in response, nope. + // Force to standyby - nope + // A&C reference number - just used to match up Authentication Response to this message. + ByteVector mRand; // 128 bit random number. + // GPRS ciphering key sequence - nope + // AUTN - if specified, it is a UMTS type challenge. nope. + void gmmWriteBody(ByteVector &msg); + L3GmmMsgAuthentication(ByteVector &rand) : L3GmmDlMsg(senseCmd), mRand(rand) + { + assert(rand.size() == 16); + } + void textBody(std::ostream &os) const { + os < +#include +#include "LLC.h" +#define GGSN_IMPLEMENTATION 1 +#include "SgsnBase.h" +#include "Sgsn.h" +#include "Ggsn.h" +#include "miniggsn.h" +//#include "MSInfo.h" // To dump MSInfo +#include "GPRSL3Messages.h" +#define CASENAME(x) case x: return #x; + + // GSM04.08 (24.008 better) 9.5.1 describes Activate PDP Context Request Message. + // The incoming IP address is in pdp->eua.v + // GSM04.08 10.5.6.4 describes the Packed Data Protocol Address. + // If second byte (the length) is 2 and type is IP, DHCP assigns an IP address. + // We dont support anything else. + // We are just going to ignore the incoming address, and assign it one from our range. + // We are permitted to change IPv6 to use IPv4, but since we ignore it all, doesnt matter. + // GSM04.08 10.5.6.4 describes Packet Data Protocol Configuration Options. + // This is there solely to support optional PPP tunneling all the way from + // TE (Terminal Equipment) attached to the MS, through the SGSN and GGSN, and out + // to some internet endpoint elsewhere. Special support is needed both to establish + // the PPP connection and to service it, since it is its own protocol and packets + // for PPP connection types need to be passed through the GGSN. + // We will not support PPP now. + +namespace SGSN { +Ggsn gGgsn; + +static uint32_t mg_dns[2] = {0,0}; + +const char *SmCause::name(unsigned mt, bool ornull) +{ + switch (mt) { + CASENAME(Operator_Determined_Barring) + CASENAME(MBMS_bearer_capabilities_insufficient_for_the_service) + CASENAME(LLC_or_SNDCP_failure) + CASENAME(Insufficient_resources) + CASENAME(Missing_or_unknown_APN) + CASENAME(Unknown_PDP_address_or_PDP_type) + CASENAME(User_authentication_failed) + CASENAME(Activation_rejected_by_GGSN_Serving_GW_or_PDN_GW) + CASENAME(Activation_rejected_unspecified) + CASENAME(Service_option_not_supported) + CASENAME(Requested_service_option_not_subscribed) + CASENAME(Service_option_temporarily_out_of_order) + CASENAME(NSAPI_already_used) + CASENAME(Regular_deactivation) + CASENAME(QoS_not_accepted) + CASENAME(Network_failure) + CASENAME(Reactivation_required) + CASENAME(Feature_not_supported) + CASENAME(Semantic_error_in_the_TFT_operation) + CASENAME(Syntactical_error_in_the_TFT_operation) + CASENAME(Unknown_PDP_context) + CASENAME(Semantic_errors_in_packet_filter) + CASENAME(Syntactical_errors_in_packet_filter) + CASENAME(PDP_context_without_TFT_already_activated) + CASENAME(Multicast_group_membership_timeout) + CASENAME(Activation_rejected_BCM_violation) + CASENAME(PDP_type_IPv4_only_allowed) + CASENAME(PDP_type_IPv6_only_allowed) + CASENAME(Single_address_bearers_only_allowed) + CASENAME(Collision_with_network_initiated_request) + CASENAME(Invalid_transaction_identifier_value) + CASENAME(Semantically_incorrect_message) + CASENAME(Invalid_mandatory_information) + CASENAME(Message_type_nonexistent_or_not_implemented) + CASENAME(Message_type_not_compatible_with_the_protocol_state) + CASENAME(Information_element_nonexistent_or_not_implemented) + CASENAME(Conditional_IE_error) + CASENAME(Message_not_compatible_with_the_protocol_state) + CASENAME(Protocol_error_unspecified) + CASENAME(APN_restriction_value_incompatible_with_active_PDP_context) + case 0: + return ornull ? 0 : "SmCause type 0"; + default: + return ornull ? 0 : "SmCause type unrecognized"; + } +} + +static void sethighpri() +{ + pthread_t me = pthread_self(); + int policy; struct sched_param sp; + pthread_getschedparam(me,&policy,&sp); + SGSNLOG("service loop"<active()) { + struct pollfd fds[1]; + fds[0].fd = tun_fd; + fds[0].events = POLLIN; + fds[0].revents = 0; // being cautious + // We time out occassionally to check if the user wants to shut the sgsn down. + if (-1 == poll(fds,1,ggsn->mStopTimeout)) { + SGSNERROR("ggsn: poll failure"); + return 0; + } + if (fds[0].revents & POLLIN) { + miniggsn_handle_read(); + } + } + return 0; +} + +void *miniGgsnWriteServiceLoop(void *arg) +{ + sethighpri(); + Ggsn *ggsn = (Ggsn*)arg; + while (ggsn->active()) { + // 8-6-2012 This interthreadqueue is clumping things up. Try taking out the timeout. + //PdpPdu *npdu = ggsn->mTxQ.read(ggsn->mStopTimeout); + PdpPdu *npdu = ggsn->mTxQ.read(); + if (npdu) { + miniggsn_snd_npdu_by_mgc(npdu->mgp, npdu->mpdu.begin(), npdu->mpdu.size()); + delete npdu; + } + } + return 0; +} + +void addShellRequest(const char *wCmd,GmmInfo*gmm,PdpContext *pdp) +{ + ShellRequest *req = new ShellRequest(); + req->msrCommand = wCmd; + req->msrArg1 = gmm->mImsi.hexstr(); + if (pdp && pdp->mgp) { + char ipaddr[40], nsapi[10]; + ip_ntoa(pdp->mgp->mg_ip, ipaddr); + req->msrArg2 = ipaddr; + sprintf(nsapi,"%d",pdp->mNSapi); + req->msrArg3 = nsapi; + } + gGgsn.mShellQ.write(req); +} + +void addShellRequest(const char *wCmd,const char *arg1) +{ + ShellRequest *req = new ShellRequest(); + req->msrCommand = wCmd; + req->msrArg1 = arg1; + gGgsn.mShellQ.write(req); +} + +// Lurk on the mShellQ and execute a shell script to process requests found. +void *miniGgsnShellServiceLoop(void *arg) +{ + Ggsn *ggsn = (Ggsn*)arg; + std::string shname = gConfig.getStr("GGSN.ShellScript"); + while (ggsn->active()) { + ShellRequest *req = ggsn->mShellQ.read(ggsn->mStopTimeout); + if (! req) continue; + runcmd("/bin/sh","sh",shname.c_str(), req->msrCommand.c_str(), req->msrArg1.c_str(), + req->msrArg2.c_str(),req->msrArg3.c_str()); + delete req; + } + return 0; +} + +// Return true on success +bool Ggsn::start() +{ + if (gGgsn.mActive) { return false; } + if (!miniggsn_init()) { return false; } + gGgsn.mGgsnRecvThread.start(miniGgsnReadServiceLoop,&gGgsn); + gGgsn.mGgsnSendThread.start(miniGgsnWriteServiceLoop,&gGgsn); + if (gConfig.getStr("GGSN.ShellScript").size() > 1) { + gGgsn.mGgsnShellThread.start(miniGgsnShellServiceLoop,&gGgsn); + gGgsn.mShellThreadActive = true; + } + gGgsn.mActive = true; + //time_t now; time(&now); + //char timebuf[30]; + //ctime_r(&now,timebuf); + //addShellRequest("Start",timebuf); + addShellRequest("Start",""); + return true; +} + +void Ggsn::stop() +{ + if (!gGgsn.mActive) {return;} + gGgsn.mGgsnRecvThread.join(); + gGgsn.mGgsnSendThread.join(); + if (gGgsn.mShellThreadActive) { + gGgsn.mGgsnShellThread.join(); + gGgsn.mShellThreadActive = false; + } + gGgsn.mActive = false; +} + +// Call this to update the PCO for retransmission to the MS. +// We cannot just make up a PCO ahead of time and then send it out to +// all the MS for two reasons: +// o The incoming options have uniquifying transaction identifiers that are +// different each time, so we must modify the incoming pcoReq each time while +// carefully preserving those modifiers. +// o There are two major formats for the PCO options - see below. +static void setPco(ByteVector &resultpco, ByteVector &pcoReq) +{ + if (mg_dns[0] == 0) { ip_finddns(mg_dns); } + + // GSM 24.008 10.5.6.3: Procotol Configuration Options. + // These are the "negotiated" address params wrapped in PPP, + // except they are not very negotiated; we are going to cram them + // into the MS and it can accept them or die. + // The first byte is the header, followed by any number of: + // protocol id (2 octets) + // length of contents (1 octet) + // I have seen this supplied by the MS in two different ways: + // - as two separate 0x8021 requests, each with a single option request + // (Blackberry) + // - as a single 0x8021 request with two option requests for the two DNS servers. + // (Samsung phone) + resultpco.clone(pcoReq); + unsigned char *pc = resultpco.begin(); + unsigned char *end = pc + resultpco.size(); + __attribute__((unused)) const char *protname = ""; + if (*pc++ != 0x80) { + MGERROR("SGSN: Unrecognized PCO Config Protocol: %d\n",pc[-1]); + } else while (pc < end) { + unsigned proto = (pc[0] << 8) + pc[1]; + pc += 2; + unsigned ipcplen = *pc++; + if (proto == 0x8021) { // IP Control Protocol. + // IPCP looks like this: + // 1 byte: command: 1 => configure-request + // 1 byte: transaction uniquifying identifier. + // 2 bytes: total length of ipcp (even though length above was a byte) + // Followed by IPCP options, where each option is: + // 1 byte: option code + // 1 byte: total option length N (should be 6) + // N bytes: data + if (pc[0] == 1) { // command = configure-request + // pc[1] is the uniquifying identifier, leave it alone. + // pc[2]&pc[3] are another length. + // Followed by one or more 6 byte option consisting of: + // pc[4]: IPCP option code + // pc[5]: length (6) + // pc[6-9]: data + // Note: the 4 byte header length is included in the option_len + unsigned ipcp_len = pc[3]; // pc[2] better be 0. + unsigned char *op; + for (op = &pc[4]; op < pc + ipcp_len; op += op[1]) { + const char *what = ""; + switch (op[0]) { + case 0x81: // primary dns. + pc[0] = 2; // IPCP command = ACK + if (op[1] < 6) { + bad_ipcp_opt_len: + MGERROR("SGSN: Invalid PCO IPCP Config Option Length: opt=0x%x len=%d\n", + op[0], op[1]); + goto next_protocol; + } + memcpy(&op[2], &mg_dns[0], 4); // addr in network order. + break; + case 0x83: // secondary dns + pc[0] = 2; // IPCP command = ACK + if (op[1] < 6) { goto bad_ipcp_opt_len; } + memcpy(&op[2], &mg_dns[mg_dns[1] ? 1 : 0], 4); // addr in network order. + break; + case 2: + what = "IP Compression Protocol"; goto bad_ipcp; + case 0x82: + what = "primary NBNS [NetBios Name Service]"; goto bad_ipcp; + case 0x84: + what = "secondary NBNS [NetBios Name Service]"; goto bad_ipcp; + default: bad_ipcp: + // It would be nice to send an SMS message that the phone is set up improperly. + MGWARN("SGSN: warning: ignoring PDP Context activation IPCP option %d %s\n",pc[4],what); + break; + } + } + } + } else if (proto == 0xc021) { // LCP: + protname = "(LCP [Link Control Protocol] for PPP)"; + goto unsupported_protocol; + } else if (proto == 0xc223) { // CHAP: + protname = "(CHAP [Challenge-Handshake Authentication Protocol] for PPP)"; + goto unsupported_protocol; + } else if (proto == 0xc023) { // PAP: Password authentication protocol. + protname = "(PAP [Password Authentication Protocol] for PPP)"; + goto unsupported_protocol; + } else { + // If we see any of these options are non-empty, the MS may be configured to use PPP. + // It is hopeless; user must reconfigure the MS. + unsupported_protocol: + if (ipcplen) { + // 6-12: Remove this message for the 3.0 release because we get + // lots of bogus complaints about PAP. + //MGWARN("SGSN: warning: ignoring PDP Context activation sub-protocol 0x%x %s; MS may require reconfiguration.\n",proto,protname); + } + } + next_protocol: + pc += ipcplen; + } +} + +static void setIpAddr(ByteVector &result,mg_con_t *mgp) +{ + // This is the IP address, only IPv4 supported. + // If the MS asks for IPv6, it is supposed to accept IPv4 anyway. + result = ByteVector(6);// IPv4 address + 2 byte header. + // 3GPP 24.008 10.5.6.4 + result.setByte(0,0x01); // IETF allocated address, which is all we support. + result.setByte(1,0x21); // IPv4. + // This is a special case - we cannot use setUIint32 because it converts to network order, + // but the ip address is already in network order, which is what 3GPP requires, so just copy it. + memcpy(result.begin()+2, &mgp->mg_ip, 4); +} + +// TODO: We have to set the transaction identifier for the PdpContext? +//void sendPdpDeactivateAll(SgsnInfo *si, unsigned cause) //SmCause::Cause cause) +void sendPdpDeactivateAll(SgsnInfo *si, SmCause::Cause cause) +{ + // TODO: what should transactionId be? + // This is a downlink command, and we are supposed to allocate a new transaction id for each one, + // but since this is the only one we ever send, maybe 0 is ok. + int transactionId = 0; + L3SmMsgDeactivatePdpContextRequest deact(transactionId,(SmCause::Cause)cause,true); + //si->getLlcEntity(LlcSapi::GPRSMM)->lleWriteHighSide(deact); + si->sgsnWriteHighSideMsg(deact); +} + +//void sendSmStatus(SgsnInfo *si,SmCause::Cause cause) +//{ +// L3SmMsgSmStatus smstat(cause); +// LlcDlFrame bv(1000); +// smstat.smwrite(bv); +// SGSNLOG("Sending "<getLlcEntity(LlcSapi::GPRSMM) +// ->lleWriteHighSide(bv,false,"pdp context reject"); +//} + +static void sendPdpContextReject(SgsnInfo *si,SmCause::Cause cause,unsigned ti) +{ + L3SmMsgActivatePdpContextReject pdpRej(ti,cause); + si->sgsnWriteHighSideMsg(pdpRej); +} + +void sendPdpContextAccept(SgsnInfo *si, PdpContext *pdp) +{ + L3SmMsgActivatePdpContextAccept pdpa(pdp->mTransactionId); + pdpa.mLlcSapi = pdp->mLlcSapi; + + // On the blackberry, the qosReq is 3 bytes, but it will fail is you just return that. + // Must return the whole shebang. + SmQoS resultQoS(12); // The full 12 byte QoS works. + resultQoS.defaultPS(pdp->mRabStatus.mRateDownlink,pdp->mRabStatus.mRateUplink); + pdpa.mQoS = resultQoS; + + pdpa.mRadioPriority = 2; // 2 is a medium priority. Why do we pass this to the MS at all? + setPco(pdpa.mPco,pdp->mPcoReq); + setIpAddr(pdpa.mPdpAddress,pdp->mgp); + // No Packet Flow Identifier - not implemented. + // No SM cause, unless "the network accepts the requested PDN connectivity with restrictions." + SGSNLOG("Sending "<sgsnWriteHighSideMsg(pdpa); + addShellRequest("PdpActivate",si->getGmm(),pdp); +} + +// TODO: All the messages below should include the phone tlli. +static void handleActivatePdpContextRequest(SgsnInfo *si, L3SmMsgActivatePdpContextRequest &pdpr) +{ + unsigned ti = pdpr.mTransactionId; + const char *name = "Activate Pdp Context Request:"; + // For UMTS, the MS that does not support GPRS is supposed to set the LlcSapi + // to "LLC-non-assigned" which is value 0, and then the Network is supposed to send the same one back. + // So just dont bother to validate llc sapi at all in UMTS, since we dont use it for anything. +#if RN_UMTS == 0 + if (! LlcEngine::isValidDataSapi(pdpr.mLlcSapi)) { + SGSNWARN(name<<"invalid llc sapi:"< 15) { + SGSNWARN(name<<"invalid ns sapi:"<isRegistered()) { + sendPdpContextReject(si,SmCause::Message_not_compatible_with_the_protocol_state,ti); + // Send a GMM message too, just to be safe. + sendImplicitlyDetached(si); + return; + } + GmmInfo *gmm = si->getGmm(); + assert(gmm); // if isRegistered is true then mGmmp is set by definition. + + PdpContext *pdp = gmm->getPdp(pdpr.mNSapi); + bool duplicateRequest = false; + if (pdp) { + if (pdp->mTransactionId == pdpr.mTransactionId) { + // Another request for the same pdp. + duplicateRequest = true; + if (pdp->mNSapi != (int) pdpr.mNSapi) { + // TODO: We should punt at this point. + SGSNWARN(name<<"duplicate request with different ns sapi:"<mPTmsi,pdpr.mNSapi); + if (mgp == NULL) { + SGSNERROR(name<<"out of ip addresses"); + sendPdpContextReject(si,SmCause::Insufficient_resources,ti); + return; + } + + // Okey dokey. Allocate and hook up. + //pdp = allocPdp(pdpr.mNSapi,pdpr.mLlcSapi,mgp); + //LlcEntityUserData *userdatalle = si->getLlcEntityUserData(pdpr.mLlcSapi); + //sndcp = new Sndcp(pdpr.mNSapi,pdpr.mLlcSapi,userdatalle); + + pdp = new PdpContext(gmm,mgp,pdpr.mNSapi,pdpr.mLlcSapi); + gmm->connectPdp(pdp,mgp); + + //mgp->mg_pdp = pdp; + //sndcp->setPdp(pdp); + +#if RN_UMTS + // For UMTS the PDP context creation is two-part. + // At this point we must allocate the RB [Radio Bearer] for the UE + // and we must wait for the acknowledgement message from the UE + // before we can send the final ActivatePdpContextAccept message. + // Like this: + // L3 ActivatePdpContextRequest, NSAPI=n (n=5..15) + // MS ---------------------------------> Network + // RRC RadioBearerSetup, RbId=n + // MS <--------------------------------- Network + // RRC RadioBearerSetupComplete + // MS ---------------------------------> Network + // L3 ActivatePdpContextAccept NSAPI=n + // MS <--------------------------------- Network + pdp->mRabStatus = SgsnAdapter::allocateRabForPdp(si->mMsHandle,pdpr.mNSapi,pdpr.mQoS); + switch (pdp->mRabStatus.mStatus) { + case RabStatus::RabFailure: + SGSNERROR(name<<"Rab Allocation Failure:"<mRabStatus.mFailCode)); + sendPdpContextReject(si,pdp->mRabStatus.mFailCode,ti); + return; + case RabStatus::RabPending: + pdp->mUmtsStatePending = true; + pdp->mPendingPdpr = pdpr; + return; + case RabStatus::RabAllocated: + // The Rab was allocated previously by this UE. + // Fall through to resend the accept message. + break; + default: assert(0); + } +#else + // It is gprs. The link is already allocated. + // We ignore the QoS request; what a joke - it barely goes low enough to specify the GPRS bandwidth. + // For now, just use the base throughput of the GPRS link which is 2.5KBytes/sec less overhead. + // TODO: set the uplink/downlink rates from the multi-slot class of the MS. + pdp->mRabStatus.mStatus = RabStatus::RabAllocated; + pdp->mRabStatus.mRateUplink = pdp->mRabStatus.mRateDownlink = 2; +#endif + } + + // This must be set each time, because it has uniquifying ids in it: + pdp->update(pdpr); + + sendPdpContextAccept(si,pdp); + + /***** + // Create the accept message. 3GPP 24.008 9.5.2. + //L3SmMsgActivatePdpContextAccept pdpa(pdpr.mTransactionId); + L3SmMsgActivatePdpContextAccept pdpa(pdp->mTransactionId); + // Note: we send back the llc sapi was sent to us, even if it is invalid. + pdpa.mLlcSapi = pdpr.mLlcSapi; + // Using the qos params from the sender did not work. + // Send our own default qos params: + setQoS(pdpa.mQoS,pdpr.mQoS); + pdpa.mRadioPriority = 2; // 2 is a medium priority. Why do we pass this to the MS at all? + setPco(pdpa.mPco,pdpr.mPco); + setIpAddr(pdpa.mPdpAddress,pdp->mgp); + // No Packet Flow Identifier - not implemented. + // No SM cause, unless "the network accepts the requested PDN connectivity with restrictions." + SGSNLOG("Sending "<sgsnWriteHighSideMsg(pdpa); + ****/ +} + +void handleDeactivatePdpContextRequest(SgsnInfo *si, L3SmMsgDeactivatePdpContextRequest &deact) +{ + unsigned nsapi; + unsigned nsapiMask = 0; // Mask of nsapi that were freed. + if (deact.mTearDownIndicator) { + nsapiMask = si->freePdpAll(false); + } else { + // Look for the pdp with this transaction identifier. + bool found = false; + for (nsapi = 0; nsapi < 16; nsapi++) { // 0-4 are unused, but be safe. + PdpContext *pdp; + if ((pdp = si->getPdp(nsapi))) { + if (pdp->mTransactionId == deact.mTransactionId) { + addShellRequest("PdpDeactivate",si->getGmm(),pdp); + si->freePdp(nsapi); + nsapiMask = 1< +#include +#include +#include "SgsnBase.h" +#include "SgsnExport.h" +#include "GPRSL3Messages.h" +#include "GSMCommon.h" // For Z100Timer - I really dont want to include the other stuff here. +#include "miniggsn.h" + +namespace SGSN { +class LlcEntityGmm; +class L3GprsFrame; +void *miniGgsnReadServiceLoop(void *arg); +void *miniGgsnWriteServiceLoop(void *arg); +void sendPdpDeactivateAll(SgsnInfo *si, SmCause::Cause cause); +void sendSmStatus(SgsnInfo *si,SmCause::Cause cause); +void sendPdpContextAccept(SgsnInfo *si, PdpContext *pdp); + +struct PdpPdu : SingleLinkListNode { + //PdpPdu *mNext; + ByteVector mpdu; + //PdpContext *mpdp; + mg_con_t *mgp; + public: + //PdpPdu *next() { return mNext; } + //void setNext(PdpPdu*wNext) { mNext = wNext; } + PdpPdu(ByteVector wpdu,mg_con_t *wmgp) : mpdu(wpdu), mgp(wmgp) { RN_MEMCHKNEW(PdpPdu) } + ~PdpPdu() { RN_MEMCHKDEL(PdpPdu) } +}; + + +struct ShellRequest { + std::string msrCommand; + std::string msrArg1, msrArg2, msrArg3; +}; +void addShellRequest(const char *wCmd,GmmInfo*gmm,PdpContext *pdp=0); +void addShellRequest(const char *wCmd,const char *arg); + +class Ggsn { + // Normally a PdpContext is associated with a BVCI to identify the BTS + // and the tlli, which is included with each message to the GGSN and comes + // from the L2 layer - every message includes tlli from the MSInfo. + // For our case, we can associate it directly with the Sndcp instance. + // The transaction identifier comes from the L3 message and is needed to establish + // secondary pdp contexts, which we dont support yet. + // It is conceivable that the PdpContext can be deleted while there + bool mActive; + Thread mGgsnRecvThread; + Thread mGgsnSendThread; + Thread mGgsnShellThread; + Bool_z mShellThreadActive; + public: + static const unsigned mStopTimeout = 3000; // How often the service loops check for active. + InterthreadQueue2 > mTxQ; + InterthreadQueue2 mShellQ; + + public: + static void handleL3SmMsg(SgsnInfo *si, L3GprsFrame &frame); + + // If it returns false, the service loop exits. + bool active() { return true; } + + static void stop(); + // Return true on success + static bool start(); + +}; +extern Ggsn gGgsn; + +// The PdpContext is a holder for an IP address and controlling information like QoS. +// The downstream is attached to an Sndcp entity, which is attached to an LlcEntity, +// which points to an MSInfo, which gets the TBF to send the data downlink. +// The upstream is an mg_con_t, which is a raw IP address. +// There can be multiple PDPContexts for a single IP address. +// That is the case if secondary PDP contexts are activated - +// the different ones have different TFTs [Traffic Flow Templates]. +// Looks to me like the primary purpose is to route different IP protocols to different SAPs, +// or to have different priority packets for the same IP address, +// routed way up here at this level. +// Currently we dont deal with that. +// +// When the PDPContext is destroyed, we must keep the mg_con_t unused for a while because: +// 1. There could be messages in the Ggsn TxQ that still refer to the PdpContext. +// 2. We are required to by the spec, because there could be incoming packets on +// the IP address for some time after it is released. +// Note: the L3 Create PDP Context message is sent to the MM SAPI of the llc, +// not the LLC sap to which this is attached. +struct Sndcp; +class PdpContext +{ public: + // A real pdp context would live in a GGSN and include a TEID [tunnel endpoint] + // consisting of the sgsn id and the TLLI, but we dont need it because we have a direct + // pointer to the SgsnInfo entity. This is a vast simplication, particularly because when + // either a TLLI or PdpContext is modified in a traditional system, those changes have to go into + // a nebulous pending state until they ripple downward, and an acknowledgement from the MS ripples + // all the way back up. This is particularly confusing for the TLLI, since that is what we use + // to talk to the MS, so the L3 and L2 normally use different TLLIs during the TLLI assignment process. + // For UMTS, we just send the message directly to the ms associated with the SgsnInfo + // via the UEAdapter. + // GPRS is a bit more complicated because we have to take a little side-trip through LLC: + // Oh, the PdpContext is connected to the...SgsnInfo, + // and the SgsnInfo is connected to the...LLCEngine + // and the LLCEngine is connected to the...Sndcp + // and the Sndcp is connected to the...LlcEntityUserData, + // and the LlcEntityUserData is connected to the...LlcEntity + // and the LlcEntity is connected to the...SgsnInfo [again] + // and the SgsnInfo is connected to the...MSInfo + // and the MSInfo is connected to the...TBF + // and the TBF is connected to the RLCEngine... + // Dem bones gonna walk around... + GmmInfo *mpcGmm; + int mNSapi; + // LlcSapi is not used in UMTS, but we are supposed to put the same value from the incoming messages + // we received in the response messages anyway. + int mLlcSapi; + mg_con_t *mgp; // Contains the IP address. + int mTransactionId; // From the L3 message that created this PdpContext, needed during deactivation?? +#if SNDCP_IN_PDP + Sndcp *mSndcp1; // Not used in UMTS. +#endif + + void pdpWriteLowSide(ByteVector &payload); + void pdpWriteHighSide(unsigned char *packet, unsigned packetlen); + + // Once the connection is set up we dont care about this stuff any more, + // but we have to cache it for UMTS because the PdpContextAccept message is not sent out instantly. + // You cant save the pco - each one has uniquifying identifiers in it. + ByteVector mPcoReq; // Requested Protocol Config Options - from L3 uplink message. + ByteVector mQoSReq; // Requested QoS, which we are not currently using. + //ByteVector mPdpAddr; + //ByteVector mApName; + + RabStatus mRabStatus; // After the link is allocated, we set this information about it. + // This info goes out in the Pdp Context allocation message. + + + // PDP STATES: + // 3GPP 24.008 6.1.2.2 defines "Session Management States on the Network Side" + // Those states are are applicable only to network-initiated PDP context state changes, + // unused for MS-initiated PSP context activation/modification etc. + // In GPRS, there are only two states - when the MS requests a PDP context, it becomes active. + // There is no received indication of success, although we could get an indication + // of whether the message was successfully delivered from L2. + // For UMTS, after we receive a PDP activiate/modify/etc from the UE, we must first + // do UMTS RadioBearer setup/modification/teardown before replying. That puts the PDP Context + // into a pending a state that is not related to "Session Management States on the Network Side". + // This state is reset when we receive the radiobearer setup acknowledgment, + // at which time we send the PDP ActivateRequestAccept. These messages are sent + // in acknowledged mode, but we are not currently using that acknowledgement. + // If the UE asks for it again, we will send it again. + bool mUmtsStatePending; + L3SmMsgActivatePdpContextRequest mPendingPdpr; // the request upon which we are pending, since the pending has to be cleared by the Rab setup response handler + + // I dont think we have to remember that state here. + // It is ok to resend the PDP Context Accept message each time we receive a + // UMTS Radio Bearer Setup Complete message. + + // For the purposes of transmitting a state indication to the MS in any L3 message, + // there are only two possibilities: PDP-INACTIVE or any other state. + // For GPRS, if the PdpContext exists, it is active. + // Note that gprs suspension occurs in the GMM layer (below us), + // and is a substate of GMM-REGISTERED, so I dont think it affects us in the SM layer at all. + bool isPdpInactive() { +#if RN_UMTS + // For UMTS: we are supposed to wait until we receive the radioBearerSetupComplete + return mUmtsStatePending; +#else + // For GPRS: we exist, therefore we are active. + return false; +#endif + } + //PdpContext(Sndcp *wSndcp, mg_con_t *wmgp, L3SmMsgActivatePdpContextRequest &pdpr); + //PdpContext(int wNSapi, mg_con_t *wmgp, L3SmMsgActivatePdpContextRequest &pdpr); + PdpContext(GmmInfo *wgmm, mg_con_t *wmgp, int nsapi, int llcsapi); + ~PdpContext(); + void update(L3SmMsgActivatePdpContextRequest &pdpr); +}; +#if GGSN_IMPLEMENTATION + // For GPRS the Sndcp deletes us when it is deleted; there is no other way. + PdpContext::~PdpContext() { + // Make sure nobody points at us, although a dangling Sndcp with no PdpContext would be an error. + //if (mpdpDownstream && mpdDownstream->getPdp()) { + // mpdpDownstream->setPdp(0); + //} + //mpdpDownstream = 0; // Just being tidy. + if (mgp) { mg_con_close(mgp); mgp = 0; } + } + // void setPco(); The pco could conceivably vary by PdpContext type, but we arent worrying about it. + + // Update based on most recent request. + // Critical for UMTS because the PdpContextAccept message is delayed + // so we have to save this info from the request. + void PdpContext::update(L3SmMsgActivatePdpContextRequest &pdpr) + { + mPcoReq = pdpr.mPco; + mQoSReq = pdpr.mQoS; + mTransactionId = pdpr.mTransactionId; + } + + PdpContext::PdpContext(GmmInfo *wgmm, mg_con_t *wmgp, int nsapi, int llcsapi) : + mpcGmm(wgmm), + mNSapi(nsapi), + mLlcSapi(llcsapi), + //mNSapi(pdpr.mNSapi), + //mLlcSapi(pdpr.mLlcSapi), + mgp(wmgp), + //mTransactionId(pdpr.mTransactionId), + mSndcp1(NULL), + //mPcoReq(pdpr.mPco), + //mQoSReq(pdpr.mQoS), + //mPdpAddr(pdpr.mPdpAddress), + //mApName(pdpr.mApName), + //mTransactionId(pdpr.mTransactionId) + mUmtsStatePending(0) + { + //mT3385.configure(gConfig.getNum("UMTS.Timers.T3385",8)); + //mT3395.configure(gConfig.getNum("UMTS.Timers.T3395",8)); + } + + void PdpContext::pdpWriteLowSide(ByteVector &payload) { + SNDCPDEBUG("pdpWriteLowSide"<mgp); + gGgsn.mTxQ.write(newpdu); + } + void PdpContext::pdpWriteHighSide(unsigned char *packet, unsigned packetlen) { + SNDCPDEBUG("pdpWriteHighSide"<snWriteHighSide(sdu); + mpcGmm->getSI()->sgsnWriteHighSide(sdu,mNSapi); + } +#endif + +}; // namespace +#endif diff --git a/SGSNGGSN/LLC.cpp b/SGSNGGSN/LLC.cpp new file mode 100644 index 00000000..bd2a0351 --- /dev/null +++ b/SGSNGGSN/LLC.cpp @@ -0,0 +1,751 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#define LLC_IMPLEMENTATION 1 +#include "GPRSL3Messages.h" +#include "Sgsn.h" +#include "Ggsn.h" +#include "LLC.h" +#define CASENAME(x) case x: return #x; + +namespace SGSN { + +const char *LlcSapi::name(type sapi) +{ + switch (sapi) { + CASENAME(GPRSMM) + CASENAME(TOM2) + CASENAME(UserData3) + CASENAME(UserData5) + CASENAME(SMS) + CASENAME(TOM8) + CASENAME(UserData9) + CASENAME(UserData11) + default: return "unrecognized LlcSapi"; + } +} + +const char *LLCFormat::name(type format) +{ + switch (format) { + CASENAME(Invalid) + CASENAME(I) + CASENAME(S) + CASENAME(UI) + CASENAME(U) + CASENAME(ISack) + CASENAME(SSack) + default: return "unrecognized LLCFormat"; + } +} + +LLCFormat::type LlcFrame::getFormat() +{ + if (size() < 2) { return LLCFormat::Invalid; } + unsigned tag = getControl(0); + if ((tag & 0x80) == 0) { + return (getControl(2) & 3) == 3 ? LLCFormat::ISack : LLCFormat::I; + } else if ((tag & 0x40) == 0) { + return (getControl(1) & 3) == 3 ? LLCFormat::SSack : LLCFormat::S; + } else if ((tag & 0x20) == 0) { + return LLCFormat::UI; + } else { + return LLCFormat::U; + } +} + +// C++ note: You cannot just return an LlcMsg here, or the result is back-converted to LlcMsg. +// You must return a pointer, even though they are barely supported by C++. Gotta love it. +LlcMsg *LlcFrame::switchFrame() +{ + switch (getFormat()) { + case LLCFormat::U: + return new LlcFrameU(*this); + case LLCFormat::UI: { + return new LlcFrameUI(*this); + //LlcFrameUI result = LlcFrameUI(*this); + //LLCDEBUG << "switchFrame result="<getLlcSapi(),false); + uframe.appendUHeader(LlcDefs::UCMD_XID,true); // Leaves append pointer at data. + // And I quote: "As an optimisation, parameters confirming the requested + // value smay be omitted from the XID response." + // So you can just return the xid header. + // However, the multitech modem did not accept that, but does succeed + // if you send the Full XID response string. + //if (gConfig.getNum("GPRS.XID.Full",1)) // There is no reason for this to be an option + if (1) { + for (int n = 0; n < totlen;) { + bool xl = xids.getBit2(n,0); + int xidtype = xids.getField2(n,1,5); + int xidlen = xl ? xids.getField2(n,6,8) : xids.getField2(n,6,2); + n += (xl ? 2 : 1); + unsigned value = 0; + if (xidlen <= 4) { + value = xids.getField2(n,0,8*xidlen); + uframe.appendXidItem(xidtype, xidlen,value); + LLCWARN("LLC XID"< 4 is the L3 params, just hope we dont get those. + LLCWARN("LLC ignoring over-length XID parameter:" + <lleWriteRaw(uframe,"xid cmd"); + } catch (ByteVectorError) { + LLCWARN("over-run error parsing LLC XID command"); + } +} + +void LlcFrameU::llcProcess(LlcEntity *lle) +{ + // Note: The U frame exists to send a command via the S and M fields, + // which are defined in 04.64 6.4; See enum U_M_Commands + // Sec 8.5.4 says we should discard unrecognized if we are in TLL Assigned/ADM state. + // Sec 8.8.4 has a table that says the same. + // Sec 8.2 describes the P/F bit, says we should return a U command, + // so I tried returning DM, but it did not make the multitech modem work. + int cmd = getUM(); + if (cmd == UCMD_XID) { + ByteVector xids = ByteVector(*this); + xids.trimLeft(2); // Chop off the U frame header. + LLCWARN("LLC XID frame received"<getLlcSapi())); + handleXid(lle,xids); + } else { + const char *cmdname = "?"; + switch (cmd) { + case UCMD_SABM: cmdname = "SABM"; break; + case UCMD_XID: cmdname = "XID"; break; + case UCMD_DM: cmdname = "DM"; break; + case UCMD_DISC: cmdname = "DISC"; break; + case UCMD_UA: cmdname = "UA"; break; + case UCMD_FRMR: cmdname = "FRMR"; break; + case UCMD_NULL: cmdname = "null"; break; + } + LLCWARN("LLC U frame ignored"<getLlcSapi())); + } +} + + +// The checksum has already been chopped off. +void LlcFrameUI::llcProcess(LlcEntity *lle) +{ + // This is a data frame. + LLCDEBUG("UI::llcProcess"); + ByteVector payload(tail(UIHeaderLength)); + lle->lleUplinkData(payload); +} + +void LlcFrameUI::writeUIHeader(unsigned wNU /*, bool pf*/) +{ + bool wE = 0; // no encryption. + bool wPM = 1; // Checksum FCS is over everything. + setField2(controlOffset,0,0x18,5); // UI format tag and unused bits. + setField2(controlOffset,5,wNU,9); // frame number. + setField2(controlOffset+1,6,wE,1); + setField2(controlOffset+1,7,wPM,1); +} + +void LlcEngine::allocSndcp(SgsnInfo *si, unsigned nsapi, unsigned llcsapi) +{ + //LlcEntityUserData *userdatalle = si->mLlcEngine->getLlcEntityUserData(llcsapi); + LlcEntityUserData *userdatalle = getLlcEntityUserData(llcsapi); + new Sndcp(nsapi,llcsapi,userdatalle); +} + +#if 0==SNDCP_IN_PDP +void LlcEngine::freeSndcp(unsigned nsapi) +{ + Sndcp *sndcp = mSndcp[nsapi]; + mSndcp[nsapi] = 0; + if (sndcp) delete sndcp; + + // TODO: This is wrong - LlcEntity is by llc sapi, not nsapi. + // So I am just commenting it out. + // Must reset the LLC state machine also. + //LlcEntity *lle = getLlcEntity(nsapi); + //if (lle) {lle->reset();} // Better not be 'if' +} +#endif + +void LlcEngine::llcWriteHighSide(ByteVector &sdu,int nsapi) +{ +#if SNDCP_IN_PDP + PdpContext *pdp = mLleGmm.mSI->getPdp(nsapi); + if (!pdp) { + LLCWARN("llcWriteHighSide to unconfigured nsapi:"<mSndcp1; +#else + Sndcp *sndcp = mSndcp[nsapi]; +#endif + if (sndcp) { + sndcp->sndcpWriteHighSide(sdu); + } else { + assert(0); // not possible because Sndcp and PdpContext allocated/deallocated together. + } +} + +void LlcEngine::llcWriteLowSide(ByteVector &bv,SgsnInfo *si) +{ + if (bv.size() < 2) { return; } + LlcFrame lframe(bv); + int llcsapi = lframe.getSapi(); + LLCDEBUG("llcWriteLowSide sapi="<lleWriteLowSide(lframe); +} + +//LlcEntity * SgsnInfo::getLlcEntity(unsigned llcSapi) +//{ +// return mllcEngine->getLlcEntity(llcSapi); +//} + +LlcEntityUserData * LlcEngine::getLlcEntityUserData(unsigned llcSapi) +{ + return dynamic_cast(getLlcEntity(llcSapi)); +} + +LlcEntityGmm *LlcEngine::getLlcGmm() +{ + return dynamic_cast(getLlcEntity(LlcSapi::GPRSMM)); +} + +void LlcEntity::lleWriteLowSide(LlcFrame &frame) +{ + mVUR++; + frame.llcProcess1(this); +} + + +void LlcEntity::lleWriteRaw(ByteVector &frame, const char *descr) +{ + gLlcParity.appendFCS(frame); + mSI->sgsnSend2MsHighSide(frame,descr,0); + //GPRS::DownlinkQPdu *dlpdu = new GPRS::DownlinkQPdu(); + //dlpdu->mDlData = uiframe; + //LLCDEBUG("llewriteHighSide:"<<(ByteVector)frame); + //dlpdu->mDescr = std::string(descr); + //mSI->getMS()->msDownlinkQueue.write(dlpdu); +} + +// Write a UI frame for unacknowledged information. +void LlcEntity::lleWriteHighSide(LlcDlFrame &frame, bool isCmd, const char *descr) +{ + // Prepend the LLC header; the bv already has room allocated. + frame.growLeft(LlcFrame::UIHeaderLength); + frame.writeAddrHeader(getLlcSapi(),isCmd); + //LlcFrameUI uiframe(frame.begin()); + LlcFrameUI uiframe(frame); + uiframe.writeUIHeader(mVU++); + lleWriteRaw(frame,descr); + +} + +void LlcEntityGmm::lleUplinkData(ByteVector &payload) +{ + LLCDEBUG("LlcEntityGmm lleUplinkData"); + // This is an l3 message. + handleL3Msg(mSI,payload); +} + +// Warning: The MS can send uplink data before attaching or creating pdpcontext, +// for example, if the bts is rebooted. +void LlcEntityUserData::lleUplinkData(ByteVector &payload) +{ + // okey dokey, this goes to the sndcp. + SndcpFrame sframe(payload); + unsigned nsapi = sframe.getNSapi(); + // The NSAPI is pre-configured by an L3 PDP Context Activation message. + // If it does not exist, the MS and BTS are out of sync, possible after a crash, + // or invalid RA-Update. + Sndcp *sndcp = getSndcp(nsapi); + if (sndcp == 0) { + if (! mSI->isRegistered()) { + // The MS has not done an Attach. + // This happens if the BTS comes on and the MS was previously talking to us. + // 24.008 Annex G: We can send "ImplicitlyDeattached" and I quote: + // "This cause is sent ..., or if the GMM context data related + // to the subscription dose (sic) not exist in the SGSN e.g. + // because of a SGSN restart." + // Update: This does not appear to do the job on the Blackberry. + LLCINFO("received packet to detached MS on nsapi="<sndcpWriteLowSide(sframe); +} + +//Sndcp *LlcEntityUserData::getSndcp(unsigned nsapi) { return getSgsnInfo()->mSndcp[nsapi]; } +//void LlcEntityUserData::setSndcp(unsigned nsapi, Sndcp*ptr) { getSgsnInfo()->mSndcp[nsapi] = ptr; } +#if SNDCP_IN_PDP +Sndcp *LlcEntityUserData::getSndcp(unsigned nsapi) +{ + // The pdp will be NULL if the MS sends uplink data before allocating a PdpContext. + PdpContext *pdp = mSI->getPdp(nsapi); + return pdp ? pdp->mSndcp1 : NULL; +} +void LlcEntityUserData::setSndcp(unsigned nsapi, Sndcp*ptr) { mSI->getPdp(nsapi)->mSndcp1 = ptr; } +#else +Sndcp *LlcEntityUserData::getSndcp(unsigned nsapi) { return mSI->mLlcEngine->mSndcp[nsapi]; } +void LlcEntityUserData::setSndcp(unsigned nsapi, Sndcp*ptr) { mSI->mLlcEngine->mSndcp[nsapi] = ptr; } +#endif + +// We dont know the length of S SACK format, so return the minimum length. +int LlcFrameDump::headerLength() +{ + switch (mFormat) { + case LLCFormat::I: return 4; + case LLCFormat::S: return 3; + case LLCFormat::UI: return 3; + case LLCFormat::U: return 2; + case LLCFormat::ISack: return 5 + 1 + (mK+1+7)/8; + case LLCFormat::SSack: return 3; // Minimum length is probably 4, not 3. + case LLCFormat::Invalid: return -1; + default: assert(0); + } +} + +void LlcFrameDump::llcParseDump() +{ + + mK = 0; // headerLength uses mK, so set to 0 for first test here. + + switch (mFormat) { + case LLCFormat::U: + mPF = getBitR1(controlOffset,5); + mM = getByte(controlOffset) & 0xf; + break; + case LLCFormat::UI: + mNU = getFieldR1(controlOffset,3,9); + mE = getBitR1(controlOffset+1,2); + mPM = getBitR1(controlOffset+1,1); + break; + case LLCFormat::ISack: + mK = getByte(controlOffset+3) & 0x1f; + // Check again, now that we know the real mK + if ((int)size() < headerLength()) { mFormat = LLCFormat::Invalid; return; } + // Fall through + case LLCFormat::I: + mA = getBitR1(controlOffset,7); + mNS = getFieldR1(controlOffset,5,9); + mNR = getFieldR1(controlOffset+1,3,9); + mS = getByte(controlOffset+2) & 0x3; + break; + case LLCFormat::SSack: + case LLCFormat::S: + mA = getBitR1(controlOffset,6); + mNR = getFieldR1(controlOffset,3,9); + break; + default: assert(0); + } +} + +void LlcFrameDump::textHeader(std::ostream &os) +{ + os << "format=" <isPdpInactive(); } +unsigned Sndcp::getMaxPduSize() { return mlle->getMaxPduSize(); } +SgsnInfo *Sndcp::getSgsnInfo() { return mlle->mSI; } + +// If we have all the segments for pdu num, send it off. +// If force, delete it even if incomplete. +void Sndcp::flush(unsigned num, bool force) +{ + OneSdu *sp = &mSegs[num%sMemory]; + unsigned i; + if (sp->mSegCount) { // We have received the final segment. + unsigned totsize = 0; + // Do we have all the segments yet? + for (i = 0; i < sp->mSegCount; i++) { + unsigned size = sp->segs[i].size(); + if (size == 0) { break; } // failure. + totsize += size; + } + if (i == sp->mSegCount) { // success. + SNDCPDEBUG("flush"<mSegCount)); + ByteVector result(totsize); + result.setAppendP(0); + for (i = 0; i < sp->mSegCount; i++) { + result.append(sp->segs[i]); + sp->segs[i].clear(); + } + sp->mSegCount = 0; + //mPdp->pdpWriteLowSide(result); + getSgsnInfo()->sgsnSend2PdpLowSide(mNSapi,result); + //PdpContext *pdp = mlle->getSgsnInfo()->getPdp(mNSapi); + //assert(pdp); + //pdp->pdpWriteLowSide(result); + return; + } + SNDCPDEBUG("flush still pending"<mSegCount)); + } else { + // Anything there at all? This is just for a message. + for (i = 0; i < 16; i++) { + if (sp->segs[i].size()) { + SNDCPDEBUG("flush still pending"<segs[i].clear(); + } + sp->mSegCount = 0; + } +} + +int Sndcp::diffSNS(int v1, int v2) +{ + int diff = v1 - v2; + if (diff < (int)mSNS/2) diff += mSNS; + if (diff > (int)mSNS/2) diff -= mSNS; + return diff; +} + +// uplink data from MS comes in here. +void Sndcp::sndcpWriteLowSide(SndcpFrame &frame) +{ + // Todo: segment it. + unsigned segnum = frame.getSegmentNumber(); + unsigned pdunum = frame.getPduNumber(); + ByteVector payload(frame.getPayload()); + SNDCPDEBUG("uplink packet"<lleWriteHighSide(result,true,"user pdu"); +} + +// downlink data from internet comes in here. +// It needs to be segmented and sent to LLC Entity for yet another header. +void Sndcp::sndcpWriteHighSide(ByteVector &sdu) +{ + // Set the first byte flags. + unsigned flags = mNSapi; + flags |= T_BIT; // UNITDATA PDU + flags |= F_BIT; // First segment. + // Segment the pdu. + unsigned segnum = 0; + unsigned segsize = getMaxPduSize(); + segsize -= 12; // be safe. If you dont do this, the blackberry rejects the packets. + for (; sdu.size() > segsize; segnum++) { + flags |= M_BIT; // Not last segment. + ByteVector seg(sdu.segment(0,segsize)); + sndcpWriteSegment(seg,segnum,flags); + sdu.trimLeft(segsize); + flags &= ~F_BIT; // Not first segment. + } + flags &= ~M_BIT; // Now it is the last segment. + sndcpWriteSegment(sdu,segnum,flags); + mSendNPdu = (mSendNPdu+1) % mSNS; +} + +// invert the low width bits of x. +static uint32_t revbits(uint32_t x, unsigned width) +{ + x &= ((uint32_t)1<> 1; + } + return result; +} + +// Pre-compute the CRC divisors for each possible byte. +static void genParityTab(uint32_t invGen, uint32_t *tab) +{ + for (int i = 0; i < 256; i++) { + uint32_t crc = i; + + for (int b = 7; b >= 0; b--) { + unsigned bit = crc & 1; + crc >>= 1; + if (bit) { crc ^= invGen; } + } + tab[i] = crc; + } +} + +Parity32::Parity32(uint32_t generator, unsigned width, bool invertFirst) +{ + mMask = (((uint32_t)1<> 1) | (1<<31); + } else { + mInvertedGenerator = revbits(mMask & generator,24); + } + genParityTab(mInvertedGenerator,mTab); +} + +uint32_t Parity32::computeCrc(unsigned char *str, int len) +{ + uint32_t crc=mInitialRemainder; + unsigned char *bp = str, *ep = str + len; + while (bp < ep) { + crc = (crc >> 8) ^ mTab[(crc ^ *bp++) & 0xff]; + } + return (~crc) & mMask; + + // As a comment, this is the identical algorithm to the above, without the table lookup: + /*** + for (int l = 0; l < len; l++) { + crc = crc ^ str[l]; + for (int b = 7; b >= 0; b--) + { + unsigned bit = crc & 1; + crc >>= 1; + if (bit) { crc ^= lsbgen; } + } + } + ***/ +} + +uint32_t Parity32::computeCrc(ByteVector &bv) +{ + return computeCrc(bv.begin(),bv.size()); +} + +extern "C" { int gprs_llc_fcs(uint8_t *data, unsigned int len); }; + +void LlcParity::appendFCS(ByteVector &bv) +{ + uint32_t fcs = computeCrc(bv); + // append 24-bit fcs LSB first. + bv.appendByte(fcs&0xff); + bv.appendByte((fcs>>8)&0xff); + bv.appendByte((fcs>>16)&0xff); + + // Double check: +#if 0 + uint32_t oldcrc = gprs_llc_fcs(bv.begin(),bv.size()-3); + if (fcs != oldcrc) { + printf("CRC ERROR: old=%d new=%d\n",oldcrc,fcs); + } else { + printf("CRC matches\n"); + } +#endif +} + +// Check the FCS in the last 3 bytes of bytevector. +bool LlcParity::checkFCS(ByteVector &bv) +{ + unsigned len = bv.size(); + uint32_t fcs = (bv.getByte(len-1)<<16) | (bv.getByte(len-2)<<8) | bv.getByte(len-3); + uint32_t computedFCS = computeCrc(bv.begin(),len-3); + return fcs == computedFCS; +} + +LlcParity gLlcParity; // The one and only parity generator needed. + +}; // namespace diff --git a/SGSNGGSN/LLC.h b/SGSNGGSN/LLC.h new file mode 100644 index 00000000..d7c4b0be --- /dev/null +++ b/SGSNGGSN/LLC.h @@ -0,0 +1,622 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +/**@file LLC objects, from GSM 04.64. */ +//#include "GPRSInternal.h" // for LLCWARN + +#ifndef LLC_H +#define LLC_H + +#include +#include "SgsnBase.h" +#include "GPRSL3Messages.h" +#include +//#include "TBF.h" + +namespace GPRS { class MSInfo; } + +namespace SGSN { +struct LlcEntity; +class SgsnInfo; +struct Sgsn; +class PdpContext; +struct Sndcp; + +// GSM04.64 6.2.3 table 2. +struct LlcSapi { + enum type { + GPRSMM = 1, // receives all control messages, both GMM an SM protocols. + TOM2 = 2, + UserData3 = 3, + UserData5 = 5, + SMS = 7, + TOM8 = 8, + UserData9 = 9, + UserData11 = 11, + }; + static const char *name(type sapi); +}; + +struct LLCFormat { + enum type { + Invalid, I, S, U, UI, ISack, SSack + }; + static const char *name(type format); +}; + +// (pat) This is a generic parity generator for up to 32 bit parity. + +// Pats Notes: The generator-based algorithm in BitVector.h did not work +// for the LLC FCS when I tried to pre-invert the remainder. +// Here is a new one based on table lookups, which is better for ByteVectors. +// An N-bit CRC is always N+1 bits long where the first and last bits are 1. +// The CRC is the remainder of division of the input by the generator. +// The complicated description of the LLC FCS is describing a normal 24-bit CRC +// but they are setting the remainder to all ones beforehand (to catch the input +// error of leading 0s) and inverting it after (to catch the input error of trailing 0s.) +// Algorithm here is based on LSB-first algorithm from wikipedia "Computation of CRC" +// So we have to pre-reverse the bits of the CRC generator. +// The top bit of the CRC sets the shift-register output to 0 for the division, +// so the division result comes out 0, but those bits are not relevant to the CRC, +// which is the remainder, because they are all shifted away. +// So we chop the top bit off. +class Parity32 +{ + uint32_t mTab[256]; // Precomputed crc remainders. + uint32_t mInvertedGenerator; + uint32_t mInitialRemainder; + uint32_t mMask; + public: + Parity32(uint32_t generator, unsigned width, bool invertFirst); + uint32_t computeCrc(unsigned char *str, int len); + uint32_t computeCrc(ByteVector &bv); +}; + + +// 04.64 5.5 FCS [Frame Check Sequence] field, aka parity. +// The CRC shall be the ones complement of the sum (modulo 2) of: +// the remainder of xk (x23 + x22 + x21 +... + x2 + x + 1) divided (modulo 2) by +// the generator polynomial, where k is the number of bits of the dividend; +// plus the remainder of the division (modulo 2) by the generator polynomial +// of the product of x24 by the dividend. +// The CRC-24 generator polynomial is: +// G(x) = x24 + x23 + x21 + x20 + x19 + x17 + x16 + x15 + x13 + x8 + x7 + x5 + x4 + x2 + 1 +class LlcParity : public Parity32 +{ + static const uint32_t sFCSGenerator = + (1<<24) + (1<<23) + (1<<21) + (1<<20) + (1<<19) + (1<<17) + (1<<16) + + (1<<15) + (1<<13) + (1<<8) + (1<<7) + (1<<5) + (1<<4) + (1<<2) + 1; + public: + LlcParity() : Parity32(sFCSGenerator,24,true) {}; + void appendFCS(ByteVector &bv); + bool checkFCS(ByteVector &bv); // true if parity ok. +}; +extern LlcParity gLlcParity; + +struct LlcDefs { + // sec 6.4 LLC commands for U-format frames, passed in LlcFrame::mM above. + enum U_M_Commands { // Some are commands and some are responses. + UCMD_NULL = 0, // null command + UCMD_DM = 1, // DM response: Disconnected Mode Response. + // "An LLE shall transmit a DM response to any valid command + // received that it cannot action." + // However, I think the multitech modem sends this as a command. + UCMD_DISC = 4, // DISC command: Disconnect - terminate ABM mode + UCMD_UA = 6, // UA response: acknowledge SABM or DISC command + UCMD_SABM = 7, // SABM command: Set Async Balanced (aka acknowledged) Mode + UCMD_FRMR = 8, // FRMR response: Frame Reject response - includes a bunch of info; see spec. + UCMD_XID = 0xb // XID command or response: Exchange Identification - used to set parameters. + }; + // sec 6.4 LLC commands for S-format frames, passed in LlcFrame::mS above. + enum U_S_Commands { + SCMD_RR = 0, + SCMD_ACK = 1, + SCMD_RNR = 2, + SCMD_SACK = 3 + }; +}; + +struct LlcMsg { + virtual void llcProcess(LlcEntity *lle) = 0; + virtual const char *typeName() { return "generic"; } +}; + +// Note: The frame formats are in 04.64 6.3. +struct LlcFrame : public LlcDefs, public ByteVector +{ + static const unsigned addrOffset = 0; + static const unsigned controlOffset = 1; + static const unsigned UIHeaderLength = 3; // 1 byte for addr, 2 for header. + + // Address fields: + unsigned getSapi() { return getByte(addrOffset) & 0xf; } + bool getLlcPD() { return getBitR1(addrOffset,8); } // 1 means LLC frame. Good grief. + bool getCR() { return getBitR1(addrOffset,7); } + LLCFormat::type getFormat(); // get the format from the ByteVector. + + LlcFrame(const ByteVector &vec) : ByteVector(vec) { } + LlcFrame(unsigned size) : ByteVector(size) { } + + //LlcFrame(const LlcFrame &other) { *this = other; } + + // Return nth control byte. 0 is the first byte after the address byte, + // ie, LLC header byte 1. + unsigned getControl(unsigned nth) { + unsigned w = nth + controlOffset; + return size() <= w ? 0 : getByte(w); + } + + // Write the LLC header. + void writeAddrHeader(unsigned sapi, bool isCmd) { + setField(0,0,1); // PD bit always 0. + setField(1,isCmd,1); // Command/Response set to 1 for a downlink command. + setField(2,0,2); // 2 unused bits. + setField(4,sapi,4); + } + void appendAddrHeader(unsigned sapi, bool isCmd) { + writeAddrHeader(sapi, isCmd); + setAppendP(1); + } + + LlcMsg *switchFrame(); + void llcProcess1(LlcEntity *lle); +}; + +struct LlcDlFrame : public LlcFrame +{ + // Allocate room for all the downlink headers that will be needed: + // The size is the needed payload size. + // Add room for all the downstream headers and trailers: + // sndcp header up to 4 bytes, but it allocates its own, so all we need are: + // llc header 3 bytes + // fcs trailer 3 bytes + // We will overkill it a bit, and and let the downstream prepend their headers. + LlcDlFrame(unsigned size) : LlcFrame(size+12) { + trimLeft(8); // Room for headers. + setAppendP(0); + } +}; + +// 05.64 6.3 +struct LlcFrameI : public LlcFrame, public LlcMsg +{ + LlcFrameI(ByteVector &f) : LlcFrame(f) {} + + bool getA() { return getBit2(controlOffset,1); } + unsigned getNS() { return getField2(controlOffset,3,9); } + unsigned getNR() { return getField2(controlOffset+1,5,9); } + unsigned getS() { return getField2(controlOffset+2,6,2); } // S1 and S2 bits. + + void llcProcess(LlcEntity *lle) { // We dont handle them. + LLCWARN("LLC unexpected I frame ignored"<= 4. + appendField(xidtype,5); + appendField(len,8); + appendField(0,2); // 2 unused bits. + appendField(value,8*len); + } + } +}; + +// 3GPP 04.64 Logical Link Entity part of LLC. +// There is one of these for each data LLC SAPI for each MS. +// The LLC SAPIs are supposed to correspond to QoS [Quality of Service] classes; +// the final NSAPI [Network SAPIs] that are connected to PDPContexts +// are the on the high side of the SNDCP entity for user-data LLC SAPIs only. +// There can be a many-to-one mapping of SNDCP entity to LLC SAPI. +// The pdu number in the header is used to discard duplicates (04.64 8.4.2), +// with a memory of 32 frames, which is redundant for data SAPIs because +// SNDCP also discards PDUs, as well as assembling and reordering. +// LLC has three states which affect LLC Entity: +// o TLLI Unassigned state. Can only use UI and XID frames for SAPI = 1. +// o TLLI assigned (by the SGSN, from higher layers) - affects all entities. +// o ABM state - acknowledged data state - per entity. +// Unacknowledged frames may also be sent in ABM mode. +// The SAP [Service Access Points] are defined in LlcSapi above. +struct LlcEntity +{ + // 6.3.5.5 Unacknowledged mode state variables: + static const unsigned mSNS = 512; // modulo arithment + unsigned mVU; // Unconfirmed send state variable + unsigned mVUR; // Unconfirmed receive state variable. + unsigned mN201U; // Max number of bytes in UI data field. + // This is used by the SNDCP to split the data. + //GPRS::MSInfo *mMS; // The MS who ultimately owns us. + //LlcEntity(GPRS::MSInfo *ms) : mMS(ms) { reset(); } + //GPRS::MSInfo *getMS() { return mMS; } + SgsnInfo *mSI; // The SgsnInfo in which we reside. + LlcEntity(SgsnInfo *wSI) : mSI(wSI) {} + + //SgsnInfo *getSgsnInfo(); + + void reset() { + mVU = mVUR = 0; + // 8.9.8: LLC layer parameter default values. + // It varies by SAPI, but for user data default is 500, max 1520. + // For other sapis length wont be exceeded anyway so dont worry about them. + mN201U = 500; + } + + virtual void lleUplinkData(ByteVector &payload) = 0; + virtual unsigned getLlcSapi() = 0; + //Sndcp *getSndcp(unsigned nsapi); + //void setSndcp(unsigned nsapi,Sndcp*); + void lleWriteLowSide(LlcFrame &frame); + void lleWriteHighSide(LlcDlFrame &frame, bool isCmd, const char *descr); + void lleWriteHighSide(L3GprsDlMsg &msg); + void lleWriteRaw(ByteVector &frame, const char *descr); +}; +#if LLC_IMPLEMENTATION + //SgsnInfo *LlcEntity::getSgsnInfo() { return mSI; } +#endif + +// Attached to one of the user-data SAPIs for an MS +struct LlcEntityUserData : public LlcEntity +{ + unsigned mLlcSapi; // The LLC sapi of this entity. + unsigned mN201U; + LlcEntityUserData(unsigned wLlcSapi, SgsnInfo *si) : + LlcEntity(si), + mLlcSapi(wLlcSapi) + { + mN201U = 500; // Default max size for pdus. + } + + unsigned getLlcSapi() { return mLlcSapi; } + unsigned getMaxPduSize() { return mN201U; } + Sndcp *getSndcp(unsigned nsapi); + void setSndcp(unsigned nsapi, Sndcp*ptr); + void lleUplinkData(ByteVector &payload); +}; +#if LLC_IMPLEMENTATION +#endif + +// Attached to the LLC GPRSMM sapi for an MS. +struct LlcEntityGmm : public LlcEntity +{ + LlcEntityGmm(SgsnInfo *si) : LlcEntity(si) {} + // The payload is in L3 message. + void lleUplinkData(ByteVector &payload); // calls: Sgsn::handleL3Msg(this,&payload); + unsigned getLlcSapi() { return 1; } +}; + +// 3GPP 04.64: SNDCP, with yet another stupid header. +// It is a miracle any data gets through at all. +// The NSAPI are on the high (network) side, and the SAPI are the low side at LLC. +// There can be multiple NSSAPI, each with a PDP context, attached to each SAPI. +// The L3 Activate PDP context message specifies both NSAPI and LLC SAPI. +struct SndcpFrame : public ByteVector +{ + + SndcpFrame(ByteVector &bv) : ByteVector(bv) { } + + bool getF() { return getBit(1); } // First segment indicator. + bool getT() { return getBit(2); } // 0 - DATA(acked) 1 - UNITDATA + bool getM() { return getBit(3); } // More bit: 1 => more segments. + unsigned getNSapi() { return getField(4,4); } // NSAPI on sndcp network high side. + // Dcomp and Pcomp are only extent if F bit is set. + unsigned getDcomp() { return getField2(1,0,4); } // data compression + unsigned getPcomp() { return getField2(1,4,4); } // protocol compression + ByteVector getPayload() { return tail((getT() ? 3 : 2)+(getF()?1:0)); } + + // For UNITDATA (unacknowledged mode) - T bit == 1 + unsigned getSegmentNumber() { return getField2(getF()?2:1,0,4); } + unsigned getPduNumber() { + if (getT()) { // unacknowledged mode. + return getField2(getF()?2:1,4,12); + } else { // acknowledged mode. + return getField2(getF()?2:1,0,8); + } + } + + // For DATA (acknowled mode) - T bit == 0 + //unsigned getAMPduNumber() { return getField2(2,0,8); } +}; + +class Sndcp +{ + enum { // Address field bits + F_BIT = 0x40, + T_BIT = 0x20, + M_BIT = 0x10 + }; + // Our identifiers: + static const unsigned sUmSNS = 4096; // Module for Unacknowledged Mode, sequence number space. + static const unsigned sAmSNS = 256; // Module for Acknowledged Mode, sequence number space. + unsigned mSNS; // Current modulo + unsigned mNSapi; // 5..16 + unsigned mLlcSapi; // One of the LLC UserData sapis. + UInt_z mRecvNPdu; + UInt_z mSendNPdu; + LlcEntityUserData *mlle; + + public: + //PdpContext *mPdp; + //void setPdp(PdpContext *pdp) { mPdp = pdp; } + //PdpContext *getPdp() { return mPdp; } + SgsnInfo *getSgsnInfo(); + + // I dont think it is possible in our case for an sndcp to exist in pdp-inactive state. + //bool isPdpInactive(); + + // Data reassembly Queues: + // We are supposed to reorder pdus. + // We dont care about the order, since they are going to the internet, but the packets may + // arrive in multiple segments that have become unordered, possibly due to multi-slot transmission, + // so we need to handle it. We will only reorder the last sMemory pdus. + static const unsigned sMemory = 32; + struct OneSdu { + UInt_z mSegCount; // Number of segs, derived from 'm' bit. + ByteVector segs[16]; // The segments. + }; + OneSdu mSegs[sMemory]; // This stuff is all deleted automatically. + + Sndcp(unsigned wNSapi, unsigned wLlcSapi, LlcEntityUserData *mlle); + ~Sndcp(); + + private: + // If we have all the segments for pdu num, send it off. + // If force, delete it even if incomplete. + void flush(unsigned num, bool force); + int diffSNS(int v1, int v2); + // SDU segmented to this size. May be negotiated using XID command, which we dont implement. + unsigned getMaxPduSize(); + void sndcpWriteSegment(ByteVector &pduSeg, unsigned segnum, unsigned flags); + + public: + // downlink data from internet comes in here. + // It needs to be segmented and sent to LLC. + void sndcpWriteHighSide(ByteVector &sdu); + // uplink data from MS comes in here. + void sndcpWriteLowSide(SndcpFrame &frame); +}; +#if LLC_IMPLEMENTATION +Sndcp::Sndcp(unsigned wNSapi, unsigned wLlcSapi, LlcEntityUserData *wlle) : + mSNS(sUmSNS), + mNSapi(wNSapi), + mLlcSapi(wLlcSapi), + mlle(wlle) + //,mPdp(0) +{ + mlle->setSndcp(mNSapi,this); +} + +Sndcp::~Sndcp() +{ + // Test mlle and mPdp and setting to 0 after are cautious overkill. + // The setSndcp() is also redundant, since our caller does it too. + if (mlle) {mlle->setSndcp(mNSapi,0); mlle = 0;} + //if (mPdp) {delete mPdp; mPdp = 0;} +} +#endif + +// A connection is identified by DLCI [Data Link Connection Identifier] which is a TLLI+SAP. +// However, our LlcEngine corresponds directly with an SgsnInfo which corresponds +// directly with the MSInfo, so we dont use this. +//struct LlcDlci { +// MSInfo *ms; +// unsigned char mSapi; +//}; + +// An LLC engine for use with a single MS. +struct LlcEngine { + // A set of LLC LLE [logical link entities] for use in an MS. + // These use predefined SAPIs, and here they are. + // The different user-data ones are supposed to correspond to different priorities, + // but we ignore the distinction. + LlcEntityGmm mLleGmm; + LlcEntityUserData mLleUserData3; + LlcEntityUserData mLleUserData5; + LlcEntityUserData mLleUserData9; + LlcEntityUserData mLleUserData11; + + // An sndcp entity for each NSAPI that is in use, ie, for each allocated pdpcontext. + // Allocated/deallocated on demand and at the same time as the PdpContext they connect with. + // The LlcEntityUserData are connected to these in a one-to-many mapping, + // ie, each LlcEntity may be tied to one or more Sndcp. +#if 0==SNDCP_IN_PDP + Sndcp *mSndcp[16]; // 0-4 are reserved (same as UMTS), but we just allocate the whole array + // and index it directly with nsapi [Network SAPI] +#endif + + LlcEngine(SgsnInfo *si) : + mLleGmm(si), + mLleUserData3(3,si), + mLleUserData5(5,si), + mLleUserData9(9,si), + mLleUserData11(11,si) + { + RN_MEMCHKNEW(LlcEngine) +#if 0==SNDCP_IN_PDP + memset(mSndcp,0,sizeof(mSndcp)); +#endif + } + + ~LlcEngine() { RN_MEMCHKDEL(LlcEngine) } + + static bool isValidDataSapi(unsigned sapi) { + switch (sapi) { + case LlcSapi::UserData3: + case LlcSapi::UserData5: + case LlcSapi::UserData9: + case LlcSapi::UserData11: + return true; + default: + return false; + } + } + + LlcEntity *getLlcEntity(unsigned sapi) { + switch (sapi) { + case LlcSapi::GPRSMM: return &mLleGmm; + case LlcSapi::UserData3: return &mLleUserData3; + case LlcSapi::UserData5: return &mLleUserData5; + case LlcSapi::UserData9: return &mLleUserData9; + case LlcSapi::UserData11: return &mLleUserData11; + default: return 0; + } + } + + // The LLC Entities. The GMM ones process L3 messages and the UserData ones are for...guess what? + // The LlcEntityUserData communicates to one or more Sndcp on its high side. + LlcEntityGmm *getLlcGmm(); + LlcEntityUserData *getLlcEntityUserData(unsigned llcSapi); + + // The bv is an LLC message. + // It may cause the LLC to send some download messages, + // or it may be an information frame on a UserData sapi + // whose payload goes to GGSN via an Sndcp entity. + void llcWriteLowSide(ByteVector &bv,SgsnInfo *si); + + void llcWriteHighSide(ByteVector &sdu,int nsapi); + + void allocSndcp(SgsnInfo *si, unsigned nsapi, unsigned llcsapi); + void freeSndcp(unsigned nsapi); +}; + + +// GSM04.64 6.2 +// (pat) The RLC/MAC layer knows nothing about the contents of an LlcFrame; it just passes data +// back and forth between the MS and SGSN. +// This is used to dump out the complete header. +struct LlcFrameDump : public LlcFrame +{ + LLCFormat::type mFormat; + + // I, I+S format is for acknowledged mode. + // UI format is for unacknowledged mode. + // U format is for LLC control only, includes no data. + // Control fields. Which are valid depends on format: + bool mE; // UI format only + bool mPM; // UI format only + bool mA; // I or S format + bool mPF; // U format only + unsigned mM; // U format only + unsigned mS; // I or S formats. + unsigned mK; // I Sack format bitmap length. + unsigned mNS, mNR, mNU; + + // Return -1 if we dont know how long, which is S SACK format. + int headerLength(); + + //LlcFrameDump(const ByteVector &vec) : LlcFrame(vec) + LlcFrameDump(const ByteVector &vec) : LlcFrame(vec) + { + mFormat = getFormat(); + llcParseDump(); + } + void llcParseDump(); + + void textHeader(std::ostream &os); + void textContent(std::ostream &os, bool verbose); + void text(std::ostream &os); +}; + +}; // namespace GPRS + +#endif + diff --git a/SGSNGGSN/Makefile.am b/SGSNGGSN/Makefile.am new file mode 100644 index 00000000..a4cd1732 --- /dev/null +++ b/SGSNGGSN/Makefile.am @@ -0,0 +1,46 @@ + +# +# Copyright 2008 Free Software Foundation, Inc. +# +# This software is distributed under the terms of the GNU Public License. +# See the COPYING file in the main directory for details. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +include $(top_srcdir)/Makefile.common + +AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) + +#AM_CXXFLAGS = -O2 -g + +noinst_LTLIBRARIES = libSGSNGGSN.la + + +libSGSNGGSN_la_SOURCES = \ + Sgsn.cpp \ + Ggsn.cpp \ + GPRSL3Messages.cpp \ + iputils.cpp \ + miniggsn.cpp \ + LLC.cpp \ + SgsnCli.cpp + +noinst_HEADERS = \ + Ggsn.h \ + GPRSL3Messages.h \ + LLC.h \ + miniggsn.h \ + SgsnBase.h \ + Sgsn.h diff --git a/SGSNGGSN/Sgsn.cpp b/SGSNGGSN/Sgsn.cpp new file mode 100644 index 00000000..2c7f0119 --- /dev/null +++ b/SGSNGGSN/Sgsn.cpp @@ -0,0 +1,1677 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include +#include +#include +#include +#include +//#include "RList.h" +#include "LLC.h" +//#include "MSInfo.h" +#include "GPRSL3Messages.h" +#include "Ggsn.h" +#include "Sgsn.h" +#include "Utils.h" +#include "Globals.h" +//#include "MAC.h" +#include "miniggsn.h" +using namespace Utils; +#define CASENAME(x) case x: return #x; +#define SRB3 3 + +using namespace SIP; + +namespace SGSN { +typedef std::list SgsnInfoList_t; +static SgsnInfoList_t sSgsnInfoList; +typedef std::list GmmInfoList_t; +static GmmInfoList_t sGmmInfoList; +static Mutex sSgsnListMutex; // One lock sufficient for all lists maintained by SGSN. +static void dumpGmmInfo(); +#if RN_UMTS +static void sendAuthenticationRequest(SgsnInfo *si, string IMSI); +#endif + +//static void killOtherTlli(SgsnInfo *si,uint32_t newTlli); +static SgsnInfo *sgsnGetSgsnInfoByHandle(uint32_t mshandle, bool create); +static int getNMO(); + +bool sgsnDebug() +{ + return gConfig.getBool("SGSN.Debug") || gConfig.getBool("GPRS.Debug"); +} + +bool enableMultislot() +{ + return gConfig.getNum("GPRS.Multislot.Max.Downlink") > 1 || + gConfig.getNum("GPRS.Multislot.Max.Uplink") > 1; +} + +const char *GmmCause::name(unsigned mt, bool ornull) +{ + switch (mt) { + CASENAME(IMSI_unknown_in_HLR) + CASENAME(Illegal_MS) + CASENAME(IMEI_not_accepted) + CASENAME(Illegal_ME) + CASENAME(GPRS_services_not_allowed) + CASENAME(GPRS_services_and_non_GPRS_services_not_allowed) + CASENAME(MS_identity_cannot_be_derived_by_the_network) + CASENAME(Implicitly_detached) + CASENAME(PLMN_not_allowed) + CASENAME(Location_Area_not_allowed) + CASENAME(Roaming_not_allowed_in_this_location_area) + CASENAME(GPRS_services_not_allowed_in_this_PLMN) + CASENAME(No_Suitable_Cells_In_Location_Area) + CASENAME(MSC_temporarily_not_reachable) + CASENAME(Network_failure) + CASENAME(MAC_failure) + CASENAME(Synch_failure) + CASENAME(Congestion) + CASENAME(GSM_authentication_unacceptable) + CASENAME(Not_authorized_for_this_CSG) + CASENAME(No_PDP_context_activated) + // 0x30 to 0x3f - retry upon entry into a new cell? + CASENAME(Semantically_incorrect_message) + CASENAME(Invalid_mandatory_information) + CASENAME(Message_type_nonexistent_or_not_implemented) + CASENAME(Message_type_not_compatible_with_the_protocol_state) + CASENAME(Information_element_nonexistent_or_not_implemented) + CASENAME(Conditional_IE_error) + CASENAME(Message_not_compatible_with_the_protocol_state) + CASENAME(Protocol_error_unspecified) + default: + return ornull ? 0 : "unrecognized GmmCause type"; + } +} + +SgsnInfo::SgsnInfo(uint32_t wMsHandle) : + //mState(GmmState::GmmNotOurTlli), + mGmmp(0), + mLlcEngine(0), + mMsHandle(wMsHandle), + mT3310FinishAttach(15000), // 15 seconds + mT3370ImsiRequest(6000) // 6 seconds + // mSuspended(0), +{ + //memset(mOldMcc,0,sizeof(mOldMcc)); + //memset(mOldMnc,0,sizeof(mOldMnc)); + time(&mLastUseTime); +#if RN_UMTS == 0 + mLlcEngine = new LlcEngine(this); +#endif + sSgsnInfoList.push_back(this); +} + +SgsnInfo::~SgsnInfo() +{ + if (mLlcEngine) {delete mLlcEngine;} +} + +void SgsnInfo::sirm() +{ + std::ostringstream ss; + sgsnInfoDump(this,ss); + SGSNLOG("Removing SgsnInfo:"<getGmm(); + if (gmm && gmm->getSI() == si) { + // You cannot delete this si by itself. Must delete the GmmInfo instead. + return false; + } + si->sirm(); + return true; +} + +// This is the generalized printer to identify an SgsnInfo. +// The alternate sgsnInfoDump is used only for gmmDump and prints +// only that info that is not duplicated in the Gmm. +std::ostream& operator<<(std::ostream& os, const SgsnInfo*si) +{ + MSUEAdapter *ms = si->getMS(); + if (ms) { + os << ms->msid(); + } else { +#if RN_UMTS + os << LOGHEX2("URNTI", si->mMsHandle); +#else + os << LOGHEX2("TLLI", si->mMsHandle); +#endif + } + if (si->getGmm()) { os << LOGVAR2("imsi",si->getGmm()->mImsi.hexstr()); } + return os; +} + +// Reset this connection, for example, because it is doing a GmmDetach or a new GmmAttach. +void SgsnInfo::sgsnReset() +{ + freePdpAll(true); + if (mLlcEngine) { mLlcEngine->getLlcGmm()->reset(); } +} + +// The operator is allowed to choose the P-TMSI allocation strategy, subject to the constraints +// that they should not collide in the same routing area, must not be all 1s, and we dont allow all 0s either. +// Note that the MS sends RA [Routing Area] along with TMSI. +// The MS creates a local TLLI from the P-TMSI by setting the top two bits, +// so the P-TMSI is really limited to 30 bits. +// For UMTS, the URNTI consists of a 20-bit (really 16-bit, because it must fit in that) UE id +// plus a 12 bit SRNC id. +static uint32_t gPTmsiNext = 0; +static uint32_t allocatePTmsi() +{ + if (gPTmsiNext == 0) { + // Add in the time to the starting TMSI so if the BTS is restarted there is a better chance + // of not using the same tmsis over again. + time_t now; + time(&now); + gPTmsiNext = ((now&0xff)<<12) + 1; + } + if (gPTmsiNext == 0 || gPTmsiNext >= (1<<30)) { gPTmsiNext = 1; } + return gPTmsiNext++; + //return Tlli::makeLocalTlli(gPTmsiNext++); +} + +MSUEAdapter *SgsnInfo::getMS() const +{ + // The MSInfo struct disappears after a period of time, so look it up. + //return GPRS::gL2MAC.macFindMSByTLLI(mMsHandle,0); + return SgsnAdapter::findMs(mMsHandle); +} + +GmmInfo::GmmInfo(ByteVector &imsi): + mImsi(imsi), mState(GmmState::GmmDeregistered), msi(0) +{ + memset(mPdps,0,sizeof(mPdps)); + mPTmsi = allocatePTmsi(); + mGprsMultislotClass = -1; // -1 means invalid. + mAttachTime = 0; + // Must set activityTime to prevent immediate removal from list by another phone simultaneously connection. + setActivity(); + ScopedLock lock(sSgsnListMutex); + sGmmInfoList.push_back(this); +} + +GmmInfo::~GmmInfo() +{ + freePdpAll(true); +} + +// Assumes sSgsnListMutex is locked on entry. +static void GmmRemove(GmmInfo *gmm) +{ + std::ostringstream ss; + gmmInfoDump(gmm,ss,0); + SGSNLOG("Removing gmm:"<getGmm() == gmm || gmm->getSI() == si) { + si->sirm(); // yes this is suboptimal, but list is short + } + } +#if 0 + for (SgsnInfoList_t::iterator itr = sSgsnInfoList.begin(); itr != sSgsnInfoList.end(); ) { + SgsnInfo *si = *itr; + if (si->getGmm() == gmm) { + itr = sSgsnInfoList.erase(itr); + delete si; + } else { + itr++; + } + } +#endif + sGmmInfoList.remove(gmm); + delete gmm; +} + + +// This is for use by the Command Line Interface +void cliGmmDelete(GmmInfo *gmm) +{ + ScopedLock lock(sSgsnListMutex); + GmmRemove(gmm); +} + +PdpContext *GmmInfo::getPdp(unsigned nsapi) +{ + //return mSndcp[nsapi] ? mSndcp[nsapi]->mPdp : 0; + assert(nsapi < sNumPdps); + setActivity(); + return mPdps[nsapi]; +} + +// True if the pdpcontext is not in state PDP-INACTIVE +bool GmmInfo::isNSapiActive(unsigned nsapi) +{ + assert(nsapi < sNumPdps); + return !(mPdps[nsapi] == 0 || mPdps[nsapi]->isPdpInactive()); +} + +// This status is sent back to the MS in messages to indicate what the Network thinks +// what PDPContexts are currently in use. +PdpContextStatus GmmInfo::getPdpContextStatus() +{ + PdpContextStatus result; + for (int i = 0; i <= 7; i++) { + if (isNSapiActive(i)) { result.mStatus[0] |= (1<mNSapi >= 0 && pdp->mNSapi < (int)sNumPdps); + mPdps[pdp->mNSapi] = pdp; + // getSI() should never NULL. The mLlcEngine is null in umts. + SgsnInfo *si = getSI(); + assert(si); + if (si->mLlcEngine) { si->mLlcEngine->allocSndcp(si,pdp->mNSapi,pdp->mLlcSapi); } + mg_con_open(mgp,pdp); +} + +// Return TRUE if the pdp was allocated. +bool GmmInfo::freePdp(unsigned nsapi) +{ + assert(nsapi < sNumPdps); + PdpContext *pdp = mPdps[nsapi]; + mPdps[nsapi] = 0; + if (pdp) delete pdp; // This disconnects the mgp also. + // getSI() should never be NULL. The mLlcEngine is null in umts. +#if SNDCP_IN_PDP + // sndcp is in the PdpContext and deleted automatically. + // Do we want to reset the LLC Sapi? Doubt it because it is shared. +#else + LlcEngine *llc = getSI() ? getSI()->mLlcEngine : NULL; + if (llc) { llc->freeSndcp(nsapi); } +#endif + return !!pdp; +} + +void SgsnInfo::deactivateRabs(unsigned nsapiMask) +{ +#if RN_UMTS + MSUEAdapter *ms = getMS(); + if (ms) { + ms->msDeactivateRabs(nsapiMask); + } else { + SGSNERROR("ggsn: DeactivatePdpContextRequest: MS not found "<deactivateRabs(rabMask); } + } + if (rabMask) addShellRequest("PdpDeactivateAll",this); + return rabMask; +} + +void SgsnInfo::sgsnSend2PdpLowSide(int nsapi, ByteVector &packet) +{ + PdpContext *pdp = getPdp(nsapi); + assert(pdp); + pdp->pdpWriteLowSide(packet); +} + +// The rbid is not used by GPRS, and is just 0. +void SgsnInfo::sgsnSend2MsHighSide(ByteVector &pdu,const char *descr, int rbid) +{ + MSUEAdapter *ms = getMS(); +#if RN_UMTS + // TODO: It would be safer not to call getMS, but just send the dlpdu through + // an InterthreadQueue and let the UMTS or GPRS L2 handle that part in its own thread. + // In that case we have to add oldTlli to the message also. + if (!ms) { + SGSNWARN("no corresponding MS for URNTI " << mMsHandle); + return; + } + // For UMTS we pass the rbid which is an intrinsic part of this channel. + // TODO: Update UMTS to use DownlinkPdu too. + ms->msWriteHighSide(pdu,rbid,descr); +#else + GmmInfo *gmm = getGmm(); + uint32_t tlli, aliasTlli = 0; + if (gmm && gmm->isRegistered()) { + tlli = gmm->getTlli(); // The TLLI based on the assigned P-TMSI. + } else { + // We send the message using the TLLI of the SgsnInfo, + // which is the one the MS used to talk to us. + tlli = mMsHandle; + // If we know the P-TMSI that will be used for the local TLLI + // for this MS after the attach procedure, notify L2. + if (gmm) { aliasTlli = gmm->getTlli(); } + if (aliasTlli == tlli) { aliasTlli = 0; } // Be tidy; but dont think this can happen. + } + if (!ms) { + LOG(WARNING) << "no corresponding MS for TLLI " << mMsHandle; + return; + } + GprsSgsnDownlinkPdu *dlpdu = new GprsSgsnDownlinkPdu(pdu,tlli,aliasTlli,descr); + //ms->msWriteHighSide(dlpdu); + // This is thread safe: + // Go ahead and enqueue it even if there is no MS + SgsnAdapter::saWriteHighSide(dlpdu); +#endif +} + +void SgsnInfo::sgsnWriteHighSideMsg(L3GprsDlMsg &msg) +{ +#if RN_UMTS + // bypass llc + ByteVector bv(1000); + bv.setAppendP(0,0); + msg.gWrite(bv); + SGSNLOG("Sending "<llcWriteHighSide(sdu,nsapi); +#endif +} + +// TLLI 03.03 2.6, Specified in binary: +// starts with 11 - local tlli +// starts with 10 - foreign tlli +// starts with 01111 - random tlli +// starts with 01110 - auxiliary tlli. +// TLLI may not be all 1s, and if it starts with one of the above, cant be all 0s either. +//struct Tlli { +// enum Type { Unused, LocalTlli, ForeignTlli, RandomTlli, AuxTlli, UnknownTlli }; +// static Type tlli2Type(uint32_t tlli) { +// unsigned toptwo = tlli >> (32-2); // It is unsigned, dont have to mask. +// if (toptwo == 0x3) return LocalTlli; +// if (toptwo == 0x2) return ForeignTlli; +// unsigned topfive = tlli >> (32-5); // It is unsigned, dont have to mask. +// if (topfive == 0x0f) return RandomTlli; +// if (topfive == 0x0e) return AuxTlli; +// return UnknownTlli; +// } +// //static uint32_t tlli2ptmsi(uint32_t tlli) { return tlli & ~sLocalTlliMask; } +// // Make a local TLLI +// //static uint32_t makeLocalTlli(uint32_t tmsi) { return tmsi | sLocalTlliMask; } +//}; + +// Return Network Mode of Operation 1,2,3 +static int getNMO() +{ + return gConfig.getNum("GPRS.NMO"); +} + +void sendAttachAccept(SgsnInfo *si) +{ + si->mT3310FinishAttach.reset(); + GmmInfo *gmm = si->getGmm(); + assert(gmm); + //L3GmmMsgAttachAccept aa(si->attachResult(),gmm->getPTmsi(),si->mAttachMobileId); + uint32_t ptmsi = gmm->getPTmsi(); + L3GmmMsgAttachAccept aa(si->attachResult(),ptmsi); + // We are finished with the attach procedure now. + // Note that we are using the si (and TLLI) that the message was sent on. + // If the BTS and the MS disagreed on the attach state at the start of this procedure, + // we reset the MS registration to match what the MS thinks to make sure we will + // use the old TLLI in the si, not the new one based on the PTMSI. + si->sgsnWriteHighSideMsg(aa); +} + +static void handleAttachStep(SgsnInfo *si) +{ + GmmInfo *gmm = si->getGmm(); + if (!gmm) { // This cannot happen. + SGSNERROR("No imsi found for MS during Attach procedure"< Network + // RRC SecurityModeCommand + // MS <--------------------------------- Network + // RRC SecurityModeComplete + // MS ---------------------------------> Network + // L3 AttachAccept + // MS <--------------------------------- Network + // (pat) Update: Havind added the authentication for NMO I in here, + // so the above procedure is now moved to + + // If we are in NMO 2, authentication was allegedly already done by + // the Mobility Management protocol layer, in which case there is + // a Kc sitting in the TMSI table. + // We need to pass it a nul-terminated IMSI string. + string IMSI = gmm->mImsi.hexstr(); + //int len = gmm->mImsi.size(); + //char imsi[len+2]; + //memcpy(imsi,gmm->mImsi.hexstr().c_str(),len); + //imsi[len] = 0; + LOG(INFO) << "Looking up Kc for imsi " << IMSI; + string Kcs = gTMSITable.getKc(IMSI.c_str()); + if (Kcs.length() <= 1) { + SGSNERROR("No Kc found for MS in TMSI table during Attach procedure"<setGmmState(GmmState::GmmDeregistered); + sendAttachAccept(si); +#endif +} + +#if RN_UMTS +// Called from UMTS when it receives the SecurityModeComplete or SecurityModeFailure msg. +void MSUEAdapter::sgsnHandleSecurityModeComplete(bool success) +{ + SgsnInfo *si = sgsnGetSgsnInfo(); + // The si would only be null if the UE sent us a spurious SecurityModeComplete command. + if (si == NULL) { + SGSNERROR("Received spurious SecurityMode completion command for UE:"<mT3310FinishAttach.active()) { + SGSNERROR("Received security response after T3310 expiration for UE:"< '9') ? ((ch & 0x0f) + 9) : (ch & 0x0f); + rand.setField(i*4,ch,4); + } + L3GmmMsgAuthentication amsg(rand); + si->sgsnWriteHighSideMsg(amsg); + si->mRAND = rand; +} +#endif + +static void handleAuthenticationResponse(SgsnInfo *si, L3GmmMsgAuthenticationResponse &armsg) +{ + if (Sgsn::isUmts()) { + GmmInfo *gmm = si->getGmm(); + if (!gmm) { + SGSNERROR("No imsi found for MS during Attach procedure"<mImsi.hexstr(); + string RAND = si->mRAND.hexstr(); + // verify SRES + bool success = false; + try { + SIPEngine engine(gConfig.getStr("SIP.Proxy.Registration").c_str(),IMSI.c_str()); + SGSNLOG("waiting for registration on IMSI: " << IMSI); + string SRESstr = armsg.mSRES.hexstr(); + success = engine.Register(SIPEngine::SIPRegister, &RAND, IMSI.c_str(), SRESstr.c_str()); + } + catch(SIPTimeout) { + SGSNLOG("SIP authentication timed out. Is the proxy running at " << gConfig.getStr("SIP.Proxy.Registration")); + // TODO: Reject + return; + } + + if (!success) return; + + LOG(INFO) << "Looking up Kc for imsi " << IMSI; + string Kcs = gTMSITable.getKc(IMSI.c_str()); + if (Kcs.length() <= 1) { + SGSNERROR("No Kc found for MS in TMSI table during Attach procedure"<mMsHandle,Kcs); +#endif + } +} + +static void handleIdentityResponse(SgsnInfo *si, L3GmmMsgIdentityResponse &irmsg) +{ + if (! si->mT3310FinishAttach.active()) { + // Well that is interesting. We got a spurious identity response. + SGSNERROR("unexpected message:"<mGmmp. + + // Use the imsi as the mobileId in the AttachAccept. + //si->mAttachMobileId = irmsg.mMobileId; + handleAttachStep(si); + //si->mT3310FinishAttach.reset(); + //GmmInfo *gmm = findGmmByImsi(passbyreftmp,si); // Always succeeds - creates if necessary. + // TODO: Why do we send the mobileid? It seems to Work this way, just wondering, because + // the message is delivered to the MS based on the L2 connection as defined by si. + //L3GmmMsgAttachAccept aa(si->attachResult(),gmm->getPTmsi(),irmsg.mMobileId); + //si->sgsnWriteHighSideMsg(aa); + } +} + +void AttachInfo::stashMsgInfo(GMMAttach &msgIEs, + bool isAttach) // true: attach request; false: RAUpdate +{ + // Save the MCC and MNC from which the MS drifted in on for reporting. + // We only save them the first time we see them, because I am afraid + // after that they will revert to our own MCC and MNC. + if (! mPrevRaId.valid()) { mPrevRaId = msgIEs.mOldRaId; } + + //if (mOldMcc[0] == 0 && mOldMcc[1] == 0) { + // for (int i = 0; i < 3; i++) { mOldMcc[i] = DEHEXIFY(msgIEs.mOldRaId.mMCC[i]); } + //} + //if (mOldMnc[0] == 0 && mOldMnc[1] == 0) { + // for (int i = 0; i < 3; i++) { mOldMnc[i] = DEHEXIFY(msgIEs.mOldRaId.mMNC[i]); } + //} + + // If a PTMSI was specified in the AttachRequest we need to remember it. + if (isAttach && msgIEs.mMobileId.isTmsi()) { + mAttachReqPTmsi = msgIEs.mMobileId.getTmsi(); + } + + if (msgIEs.mMsRadioAccessCapability.size()) { + mMsRadioAccessCap = msgIEs.mMsRadioAccessCapability; + } + //mAttachMobileId = msgIEs.mMobileId; +} + +void AttachInfo::copyFrom(AttachInfo &other) +{ + if (! mPrevRaId.valid()) { mPrevRaId = other.mPrevRaId; } + if (! mAttachReqPTmsi) { mAttachReqPTmsi = other.mAttachReqPTmsi; } + if (! mAttachReqType) { mAttachReqType = other.mAttachReqType; } + if (other.mMsRadioAccessCap.size()) { + mMsRadioAccessCap = other.mMsRadioAccessCap; + } +} + +void sendImplicitlyDetached(SgsnInfo *si) +{ + L3GmmMsgGmmStatus statusMsg(GmmCause::Implicitly_detached); + si->sgsnWriteHighSideMsg(statusMsg); + // The above didn't do it, so try sending one of these too: + // Detach type 1 means re-attach required. + //L3GmmMsgDetachRequest dtr(1,GmmCause::Implicitly_detached); + // 7-2012: Tried taking out the cause to stop the Multitech modem + // sending 'invalid mandatory information'. + // The only reason obvious to send that is in 24.008 8.5 is an unexpected IE, + // so maybe it is the cause. But it did not help. + L3GmmMsgDetachRequest dtr(1,0); + si->sgsnWriteHighSideMsg(dtr); +} + +// The ms may send a P-TMSI or IMSI in the mobile id. +static void handleAttachRequest(SgsnInfo *si, L3GmmMsgAttachRequest &armsg) +{ + switch ((AttachType) (unsigned) armsg.mAttachType) { + case AttachTypeGprsWhileImsiAttached: + SGSNLOG("NOTICE attach type "<<(int)armsg.mAttachType <mtAttachInfo.mAttachReqType = AttachTypeGprs; + break; + case AttachTypeCombined: + if (getNMO() != 1) { + // The MS should not have done this. + LOG(ERR)<<"Combined Attach attempt incompatible with NMO 1 "<mtAttachInfo.mAttachReqType = AttachTypeCombined; + break; + } + //uint32_t newptmsi; + + // Save info from the message: + si->mtAttachInfo.stashMsgInfo(armsg,true); + + // Re-init the state machine. + // If the MS does a re-attach, we may have an existing SgsnInfo from earlier, so we must reset it now: + // si->sgsnReset(); // 6-3-2012: changed to just freePdpAll. + si->freePdpAll(true); + + GmmInfo *gmm = si->getGmm(); + // 7-1-2012: Working on multitech modem failure to reattach bug. + // I tried taking this out to send an extra identity request, + // but then the modem did not respond to that identity request, + // just like before it did not respond to the second attach request. + // Even after deleting all but that single SgsnInfo, and modifying the msid + // to print both tllis, and looking at pat.log the message is definitely + // sent on the correct TLLi. + // But if you tell the modem to detach and then try attach again, + // then the modem uses a new TLLI and sends an IMSI, so it thinks + // it was attached, but it is sending an attach request anyway, with a PTMSI. + // But the first attach used a PTMSI, and it succeeded. + // Things to try: send protocol incompabible blah blah. + // Try converting to local tlli (0xc...); I tried that before but maybe + // the tlli change procedure was wrong back then. + // Send a detach, although I think the modem ignores this. + if (gmm) { + // We already have an IMSI for this MS, where the MS was identified by some TLLI + // associated with this SgsnInfo, which means we already did + // the IdentityResponse challenge. Just use it. + } else { + // We need an imsi. If it is not in the message, we will need to ask for it. + ByteVector imsi; + // There is a slight problem that we only have 6 seconds to register the MS, + // which may not be enough time to do the IdentityResponse Challenge. + // Therefore we save the IMSI associated with the TLLI that we got from the Identity response + // challenge in the SgsnInfo, and when the MS tries again with the same TLLI, + // we can skip the IdentityRequest phase. + //if (si->mImsi.size()) { + // // Already did the identity challange; use the previously queried imsi from this ms. + // imsi = si->mImsi; + //} else { + // If the MS did not send us an IMSI already, ask for one. + if (armsg.mMobileId.isImsi()) { + // The MS included the IMSI in the attach request + imsi = armsg.mMobileId.getImsi(); + findGmmByImsi(imsi,si); // Create the gmm and associate with si. + } else { + // 3GPP 24.008 11.2.2 When T3370 expires we can send another Identity Request. + // However we are also going to use it inverted, and send Identity Requests + // no closer together than T3370. + // If this expires, the MS will try again. + if (! si->mT3370ImsiRequest.active() || si->mT3370ImsiRequest.expired()) { + // Send off a request for the imsi. + L3GmmMsgIdentityRequest irmsg; + si->mT3370ImsiRequest.set(); + // We only use the timer in this case, so we only set it in this case, instead + // of at the top of this function. + si->mT3310FinishAttach.set(); + si->sgsnWriteHighSideMsg(irmsg); + } + return; + } + //} + //SgsnInfo *si2 = Sgsn::findAssignedSgsnInfoByImsi(imsi); + //newptmsi = si2->mMsHandle; + } +#if 0 + // We dont care if the MS already had a P-TMSI. + // If it is doing an attach, go ahead and assign a new one. + if (!si->mAllocatedTmsiTlli) { + si->mAllocatedTmsiTlli = Sgsn::allocateTlli(); + } + // We cant set the tlli in the MS until it has received the new tlli, + // because we have to use the previous tlli to talk to it. +#endif + // This was for testing: + //L3GmmMsgIdentityRequest irmsg; + //si->sgsnWriteHighSideMsg(irmsg); + + // We are assigning this ptmsi to the MS. + handleAttachStep(si); + //si->mT3310FinishAttach.reset(); + //L3GmmMsgAttachAccept aa(si->attachResult(),gmm->getPTmsi(),armsg.mMobileId); + //si->sgsnWriteHighSideMsg(aa); +} + + +static void handleAttachComplete(SgsnInfo *si, L3GmmMsgAttachComplete &acmsg) +{ + // The ms is acknowledging receipt of the new tlli. + GmmInfo *gmm = si->getGmm(); + if (! gmm) { + // The attach complete does not match this ms state. + // Happens, for example, when you first turn on the bts and the ms + // is still trying to complete a previous attach. Ignore it. + // The MS will timeout and try to attach again. + SGSNLOG("Ignoring spurious Attach Complete" << si); + // Dont send a reject because we did not reject anything. + return; + } + //SGSNLOG("attach complete gmm="<<((uint32_t)gmm)); + gmm->setGmmState(GmmState::GmmRegisteredNormal); + gmm->setAttachTime(); +#if RN_UMTS +#else + // Start using the tlli associated with this imsi/ptmsi when we talk to the ms. + si->changeTlli(true); +#endif + addShellRequest("GprsAttach",gmm); + +#if 0 // nope, we are going to pass the TLLI down with each message and let GPRS deal with it. + //if (! Sgsn::isUmts()) { + // // Update the TLLI in all the known MS structures. + // // Only the SGSN knows that the MSInfo with these various TLLIs + // // are in fact the same MS. But GPRS needs to know because + // // the MS will continue to use the old TLLIs, and it will botch + // // up if, for example, it is in the middle of a procedure on one TLLI + // // and the MS is using another TLLI, which is easy to happen given the + // // extremely long lag times in message flight. + // // The BSSG spec assumes there only two TLLIs, but I have seen + // // the Blackberry use three simultaneously. + // SgsnInfo *sip; + // uint32_t newTlli = gmm->getTlli(); + // RN_FOR_ALL(SgsnInfoList_t,sSgsnInfoList,sip) { + // if (sip->getGmm == gmm) { + // UEAdapter *ms = sip->getMS(); + // // or should we set the ptmsi?? + // if (ms) ms->changeTlli(newTlli); + // } + // } + //} +#endif +} + +static void handleDetachRequest(SgsnInfo *si) +{ + L3GmmMsgDetachAccept detachAccept(0); + GmmInfo *gmm = si->getGmm(); + if (!gmm) { + // Hmm, but fall through, because it is certainly detached. + } else { + gmm->setGmmState(GmmState::GmmDeregistered); + } + si->sgsnWriteHighSideMsg(detachAccept); + si->sgsnReset(); + if (gmm) addShellRequest("GprsDetach",gmm); +} + +static void sendRAUpdateReject(SgsnInfo *si,unsigned cause) +{ + L3GmmMsgRAUpdateReject raur(cause); + si->sgsnWriteHighSideMsg(raur); +} + +// TODO: Need to follow 4.7.13 of 24.008 +static void handleServiceRequest(SgsnInfo *si, L3GmmMsgServiceRequest &srmsg) +{ + GmmInfo *gmm = si->getGmm(); + // TODO: Should we check the PTmsi and the PDP context status??? + if (!gmm) { + L3GmmMsgServiceReject sr(GmmCause::Implicitly_detached); + si->sgsnWriteHighSideMsg(sr); + return; + } else { + gmm->setActivity(); + L3GmmMsgServiceAccept sa(si->getPdpContextStatus()); + si->sgsnWriteHighSideMsg(sa); + } +} + +// 24.008 4.7.5, and I quote: +// "The routing area updating procedure is always initiated by the MS. +// It is only invoked in state GMM-REGISTERED." +// The MS may send an mMobileId containing a P-TMSI, and it sends TmsiStatus +// telling if it has a valid TMSI. +static void handleRAUpdateRequest(SgsnInfo *si, L3GmmMsgRAUpdateRequest &raumsg) +{ + bool sendTmsi = 0; + RAUpdateType updatetype = (RAUpdateType) (unsigned)raumsg.mUpdateType; + switch (updatetype) { + case RAUpdated: + case PeriodicUpdating: + updatetype = RAUpdated; + if (getNMO() == 1) { + updatetype = CombinedRALAUpdated; + } + break; + case CombinedRALAUpdated: + case CombinedRALAWithImsiAttach: // As of 4-29-2012, we dont even save the imsi. + if (getNMO() != 1) { + // TODO: Should we send a reject, or an accept with a different updatetype? + // I think the type should have matched the NMO broadcast in the beacon, + // so we should reject. + // Warning: This reject is saved in the MS semi-permanently, + // and it will not try again. + // DEBUG: Try just accepting the LAUpdate unconditionally... + //sendRAUpdateReject(si,GmmCause::Location_Area_not_allowed); + //return; + LOG(ERR)<<"Routing Area combined Location Area Update request incompatible with NMO 1 "<mtAttachInfo.stashMsgInfo(raumsg,false); + + GmmInfo *gmm = si->getGmm(); + if (! gmm) { + // The MS has not registered with us yet, so reject the RAUpdate. + // Doesnt seem like this should be always needed, because we want to accept anyone. + // But this seems to work, and it didnt work when I didnt do this. + // This makes the MS come back with an AttachRequest message. + // I have seen the Blackberry trying RAUpdate with the same foreign TLLI about 10 times, + // and getting rejects before coming back with the AttachRequest, but it eventually did. + // TODO: Maybe use a different cause for foreign TLLI. + // And I quote, from 24.008 Annex G.6: + // "Cause value = 9 MS identity cannot be derived by the network + // "This cause is sent to the MS when the network cannot derive the MS's identity from + // "the P-TMSI in case of inter-SGSN routing area update. + // "Cause value = 10 Implicitly detached + // "This cause is sent to the MS either if the network has implicitly detached the MS, + // "e.g. some while after the Mobile reachable timer has expired, + // "or if the GMM context data related to the subscription dose not exist in the + // "SGSN e.g. because of a SGSN restart. + // Also, see 4.7.1.5.4 which gives the specific behavior the MS to each of these causes. + // Specifically, cause 10 is the correct one to make the MS reregister and get new contexts. + + // I did not try just assigning a new TMSI in the RAUpdateAccept message. + // Also have not tried just echoing back the TLLI that the MS used in L2 as + // the allocated-TMSI in this response, which might work, + // but is not procedurally correct if the TMSI came from a different routing area. + + if (raumsg.mPdpContextStatus.anyDefined()) { + // If the MS thinks it has PDP contexts, we need to explicitly release them. + // TODO: We do that in the raaccept message... + // Let MS establish a session, then turn BTS off and on; the MS continues to + // use the old IP addresses with its new pdp contexts. + // Not exactly sure why, maybe my bug somewhere. + // 4-30: I tried putting this back in but it did not work - + // The blackberry continued to send RAUpdateRequest that indicated it + // did not tear them down. + // 24.008 4.7.5.1.3 Indicates all you have to do is send an RaUpdateAccept + // with the pdpstatus zeroed out. + //sendSmStatus(si,SmCause::Unknown_PDP_address_or_PDP_type); + //sendPdpDeactivateAll(si, SmCause::Unknown_PDP_address_or_PDP_type); + } + // 4.7.5.1.4 says that 'cause 9' shall make the MS delete its P-TMSI, + // enter state GMM-DEREGISTERED, and subsequently automatically initiate GPRS attach. + // However, it didnt work for me on the blackberry after a test that ended in TBF failure. + // Had to turn off the MS and turn it back on, then ok. + // Maybe it had entered state LIMITED.SERVICE, + // which prevents attaches, or maybe NO.CELL.AVAILABLE. + // It was trying to register with the mobile-id set to no value. + // Cause 10 looks like it might be better: MS releases PDP contexts, + // enters GMM-DEREGISTERED.NORMAL, and forces a new attach. + sendRAUpdateReject(si,GmmCause::Implicitly_detached); + return; + } else { + gmm->setActivity(); + /** wrong + if (si->getState() != GmmState::GmmDeregistered) { + //sendRAUpdateReject(si,GmmCause::MessageNotCompatibleWithProtocolState); + // This is the cause that the opensgsn sends: + sendRAUpdateReject(si,GmmCause::MS_identity_cannot_be_derived_by_the_network); + return; + } + ***/ + + // The RAUpdate result is yes only if the MS has attached to us. + // Note that this message gets back to the originating MS guaranteed at layer2, + // regardless of whatever tmsi/mobile-id we put in this message. + // TODO: Do we need to set the allocated P-TMSI or not? Not sure. + // The blackberry did not work without it, but it may have been sql open-registration was wrong. + // DONT DO THIS: + //if (gConfig.defines("SGSN.RAUpdateIncludeTmsi") && gConfig.getNum("SGSN.RAUpdateIncludeTmsi")) { + // tmsi = si->mTlli; + //} + + //if (updatetype == CombinedRALAUpdated) { + // DEBUG: try this + // Send an authentication request to make the MS happy about this. + //sendAuthenticationRequest(si); + //} + + // We are not integrated with the OpenBTS stack yet, + // so if we need a tmsi just make one up. + uint32_t ptmsi = gmm->getPTmsi(); + L3GmmMsgRAUpdateAccept raa(updatetype, si->getPdpContextStatus(),ptmsi, + sendTmsi ? ptmsi : 0); + si->sgsnWriteHighSideMsg(raa); + } +} + +static void handleRAUpdateComplete(SgsnInfo *si, L3GmmMsgRAUpdateComplete &racmsg) +{ + // Do not need to do anything. +} + +// This message may arrive on a DCCH channel via the GSM RR stack, rather than a GPRS message, +// and as such, could be running in a separate thread. +// We queue the message for processing. +// The suspension may be user initiated or by the MS doing some RR messages, +// most often, Location Area Update. The spec says we are supposed to freeze +// the LLC state and continue after resume. But in the permanent case any incoming packets +// will be hopelessly stale after resumption, so we just toss them. Note that web sites chatter +// incessantly with keepalives even when they look quiescent to the user, and we dont want +// all that crap to back up in the downlink queue. +// In the temporary case, which is only a second or two, we will attempt to preserve the packets +// to prevent a temporary loss of service. I have observed that the MS first stops responding +// to the BSS for about a second before sending the RACH to initiate the RR procedure, +// so there is no warning at all. However, we MUST cancel the TBFs. If we dont, and after +// finishing the RR procedure the MS gets back to GPRS before the previous TBFs timeout, +// it assumes they are new TBFs, which creates havoc, because the acknacks do not correspond +// to the previous TBF. This generates the "STUCK" condition, up to a 10 second loss of service, +// and I even saw the Blackberry detach and reattach to recover. +// In either case the MS signals resumption by sending us anything on the uplink. +// WARNING: This runs in a different thread. +bool Sgsn::handleGprsSuspensionRequest(uint32_t wTlli, + const ByteVector &wraid) // The Routing Area id. +{ + SGSNLOG("Received GPRS SuspensionRequest for"<getMS(); + if (ms == NULL) { + // This is a serious internal error. + SGSNERROR("L3 message "<mMsHandle)); + return; + } + switch (mt) { + case L3GmmMsg::AttachRequest: { + L3GmmMsgAttachRequest armsg; + armsg.gmmParse(frame); + SGSNLOG("Received "<sgsnSend2PdpLowSide(rbid, payload); +//} + +// The handle is the URNTI and the rbid specfies the rab. +// In gprs, the handle is the TLLI and all the rab info is encoded into the +// payload with LLC headers so rbid is not used, which was a pretty dopey design. +void MSUEAdapter::sgsnWriteLowSide(ByteVector &payload, uint32_t handle, unsigned rbid) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(handle,true); // Create if necessary. +#if RN_UMTS + // No Pdcp, so just send it off. + si->sgsnSend2PdpLowSide(rbid, payload); +#else + si->mLlcEngine->llcWriteLowSide(payload,si); +#endif +} + +#if RN_UMTS +void MSUEAdapter::sgsnHandleL3Msg(uint32_t handle, ByteVector &msgFrame) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(handle,true); // Create if necessary. + handleL3Msg(si,msgFrame); +} +#endif + +void handleL3Msg(SgsnInfo *si, ByteVector &bv) +{ + unsigned pd = 0; + try { + L3GprsFrame frame(bv); + if (frame.size() == 0) { // David saw this happen. + //SGSNWARN("completely empty L3 uplink message "<mMsHandle == handle) {result=si; continue;} +#if RN_UMTS +#else +#if NEW_TLLI_ASSIGN_PROCEDURE + if (si->mAltTlli == handle) {result=si;continue;} +#endif +#endif + // Kill off old ones, except ones that are the primary one for a gmm. + GmmInfo *gmm = si->getGmm(); + if (gmm==NULL || gmm->getSI() != si) { + if (now - si->mLastUseTime > idletime) { si->sirm(); } + } + } + if (result) { + time(&result->mLastUseTime); + return result; + } + if (!create) { return NULL; } + + // Make a new one. + SgsnInfo *sinew = new SgsnInfo(handle); + return sinew; +} + +// Now we create the SgsnInfo for the assigned ptmsi as soon as the ptmsi is created, +// even if the MS has not used it yet. +//GmmInfo *SgsnInfo::findGmm() +//{ +// if (mGmmp) { return mGmmp; } // Hooked up previously. +// return NULL; +// Old comment: +// For GPRS, the MS contacts with some random tlli, then we create a GmmInfo and a PTMSI, +// and send the PTMSI to the MS, but the GmmInfo is not yet hooked to any SgsnInfos. +// The MS will then call us again using a TLLI derived from the PTMSI, +// and we hook up that SgsnInfo to the GmmInfo right here. +// if (! Sgsn::isUmts()) { +// uint32_t tlli = mMsHandle; +// // Only a local TLLI can be converted to a P-TMSI to look up the Gmm context. +// if (Tlli::tlli2Type(tlli) == Tlli::LocalTlli) { +// uint32_t ptmsi = Tlli::tlli2ptmsi(tlli); +// GmmInfo *gmm; +// RN_FOR_ALL(GmmInfoList_t,sGmmInfoList,gmm) { +// if (gmm->mPTmsi == ptmsi) { +// SGSNLOG("Hooking up"<setGmm(gmm); +// gmm->msi = this; +// return gmm; +// } +// } +// } +// } else { +// // In UMTS the Gmm context is indexed by URNTI. +// // If this doesnt work right, we will need to look up the Gmm context +// // from the ptmsi in the L3 messages. +// } +// return NULL; +//} + +// Works, but not currently used: +void MSUEAdapter::sgsnFreePdpAll(uint32_t mshandle) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(mshandle,false); + if (si) si->freePdpAll(true); +} + +// Forces it to exist if it did not already. +static SgsnInfo *sgsnGetSgsnInfoByHandle(uint32_t mshandle, bool create) +{ + // We cant cache this thing for GPRS because it changes + // during the TLLI assignment procedure. + // We could cache it for UMTS, but that assumes the lifetime of the SgsnInfo + // is greater than the UE, both of which are controlled by user parameters, + // so to be safe, we are just going to look it up every time. + // TODO: go back to caching it in UMTS only. + //if (! mSgsnInfo) { + //uint32_t mshandle = msGetHandle(); + //mSgsnInfo = findSgsnInfoByHandle(mshandle,create); + //} + //return mSgsnInfo; + return findSgsnInfoByHandle(mshandle,create); +} + +#if RN_UMTS +SgsnInfo *MSUEAdapter::sgsnGetSgsnInfo() +{ + uint32_t mshandle = msGetHandle(); + return findSgsnInfoByHandle(mshandle,false); +} +#else +void MSUEAdapter::sgsnSendKeepAlive() +{ + // TODO +} +#endif + +#if RN_UMTS + // not applicable +#else +static void parseCaps(GmmInfo *gmm) +{ + if (/*gmm->mGprsMultislotClass == -1 &&*/ gmm->mgAttachInfo.mMsRadioAccessCap.size()) { + MsRaCapability caps(gmm->mgAttachInfo.mMsRadioAccessCap); + gmm->mGprsMultislotClass = caps.mCList[0].getCap(AccessCapabilities::GPRSMultislotClass); + gmm->mGprsGeranFeaturePackI = caps.mCList[0].getCap(AccessCapabilities::GERANFeaturePackage1); + } +} + + +int MSUEAdapter::sgsnGetMultislotClass(uint32_t mshandle) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(mshandle,false); + if (!si) { return -1; } + GmmInfo *gmm = si->getGmm(); // Must be non-null or we would not be here. + if (!gmm) { return -1; } // But dont crash if I'm mistaken. + parseCaps(gmm); + return gmm->mGprsMultislotClass; +} + +bool MSUEAdapter::sgsnGetGeranFeaturePackI(uint32_t mshandle) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(mshandle,false); + if (!si) { return -1; } + GmmInfo *gmm = si->getGmm(); // Must be non-null or we would not be here. + if (!gmm) { return -1; } // But dont crash if I'm mistaken. + parseCaps(gmm); + return gmm->mGprsGeranFeaturePackI; +} +#endif + +GmmState::state MSUEAdapter::sgsnGetRegistrationState(uint32_t mshandle) +{ + SgsnInfo *si = sgsnGetSgsnInfoByHandle(mshandle,false); + if (!si) { return GmmState::GmmDeregistered; } + GmmInfo *gmm = si->getGmm(); // Must be non-null or we would not be here. + if (!gmm) { return GmmState::GmmDeregistered; } + return gmm->getGmmState(); +} + + +#if RN_UMTS +void MSUEAdapter::sgsnHandleRabSetupResponse(unsigned rabId, bool success) +{ + SgsnInfo *si = sgsnGetSgsnInfo(); + if (si == NULL) { + // Dont think this can happen, but be safe. + SGSNERROR("Received spurious RabSetupResponse for UE:"<getPdp(rabId); + if (pdp==NULL) return; // FIXME: Not sure what to do here + if (pdp->mUmtsStatePending) { + pdp->update(pdp->mPendingPdpr); + pdp->mUmtsStatePending = false; + } + sendPdpContextAccept(si,pdp); + } else { + // We do NOT want to send a RAB teardown message - we got here because + // the RAB setup did not work in the first place. Just free it. + si->freePdp(rabId); + } +} +#endif + +const char *GmmState::GmmState2Name(GmmState::state state) +{ + switch (state) { + CASENAME(GmmDeregistered) + CASENAME(GmmRegistrationPending) + CASENAME(GmmRegisteredNormal) + CASENAME(GmmRegisteredSuspsended) + } + return ""; +} + +// The alternate sgsnInfoPrint is used only for gmmDump and prints +void sgsnInfoDump(SgsnInfo *si,std::ostream&os) +{ + //if (si == gmm->getSI()) {continue;} // Already printed the main one. + uint32_t handle = si->mMsHandle; + os << "SgsnInfo"<mtAttachInfo; + if (ati->mPrevRaId.valid()) { os << " prev:"; ati->mPrevRaId.text(os); } + if (!si->getGmm()) { os << " no gmm"; } + os << endl; +} + +void gmmInfoDump(GmmInfo *gmm,std::ostream&os,int options) +{ + os << " GMM Context:"; + os << LOGVAR2("imsi",gmm->mImsi.hexstr()); + os << LOGHEX2("ptmsi",gmm->mPTmsi); + os << LOGHEX2("tlli",gmm->getTlli()); + os << LOGVAR2("state",GmmState::GmmState2Name(gmm->getGmmState())); + time_t now; time(&now); + os << LOGVAR2("age",(gmm->mAttachTime ? now - gmm->mAttachTime : 0)); + os << LOGVAR2("idle",now - gmm->mActivityTime); + SgsnInfo *si = gmm->getSI(); + if (!(options & printNoMsId)) { + if (si) { // Can this be null? No, but dont crash. + // The mPrevRaId is generally invalid in the SgsnInfo for the GMM, + // because it is the one we assigned, and the routing info is in the SgsnInfo + // the MS initially called in on. + //os << LOGVAR2("oldMCC",si->mOldMcc); + //os << LOGVAR2("oldMNC",si->mOldMnc); + // The GPRS ms struct will disappear shortly after the MS stops communicating with us. + MSUEAdapter *ms = si->getMS(); + if (ms) { os << ms->msid(); } + else { os << " MS=not_active"; } + } + } + + os << " IPs="; + int pdpcnt = 0; + for (unsigned nsapi = 0; nsapi < GmmInfo::sNumPdps; nsapi++) { + if (gmm->isNSapiActive(nsapi)) { + // FIXME: Darn it, we need to lock the pdp contexts for this too. + // Go ahead and do it anyway, because collision is very low probability. + PdpContext *pdp = gmm->getPdp(nsapi); + mg_con_t *mgp; // Temp variable reduces probability of race; the mgp itself is immortal. + if (pdp && (mgp=pdp->mgp)) { + char buf[30]; + if (pdpcnt) {os <<",";} + os << ip_ntoa(mgp->mg_ip,buf); + } + pdpcnt++; + } + } + if (pdpcnt == 0) { os <<"none"; } + os << endl; + + if (options & printDebug) { + // Print out all the SgsnInfos associated with this GmmInfo. + RN_FOR_ALL(SgsnInfoList_t,sSgsnInfoList,si) { + if (si->getGmm() != gmm) {continue;} + os <<"\t"; // this sgsn is associated with the GmmInfo just above it. + sgsnInfoDump(si,os); + } + } + + // Now the caps: + if ((options & printCaps) && gmm->mgAttachInfo.mMsRadioAccessCap.size()) { + MsRaCapability caps(gmm->mgAttachInfo.mMsRadioAccessCap); + caps.text2(os,true); + } + //os << endl; // This is extra. There is one at the end of the caps. +} + +void gmmDump(std::ostream &os) +{ + // The sSgsnListMutex exists for this moment: to allow this cli list routine + // to run from a foreign thread. + ScopedLock lock(sSgsnListMutex); + int debug = sgsnDebug(); + GmmInfo *gmm; + RN_FOR_ALL(GmmInfoList_t,sGmmInfoList,gmm) { + gmmInfoDump(gmm,os,debug ? printDebug : 0); + os << endl; // This is extra. There is one at the end of the caps. + } + // Finally, print out SgsnInfo that are not yet associated with a GmmInfo. + if (debug) { + SgsnInfo *si; + RN_FOR_ALL(SgsnInfoList_t,sSgsnInfoList,si) { + if (! si->getGmm()) { sgsnInfoDump(si,os); } + } + } +} + +void dumpGmmInfo() +{ + if (sgsnDebug()) { + std::ostringstream ss; + gmmDump(ss); + SGSNLOG(ss.str()); + } +} + +void SgsnInfo::setGmm(GmmInfo *gmm) +{ + // Copy pertinent info from the Routing Update or Attach Request message into the GmmInfo. + gmm->mgAttachInfo.copyFrom(mtAttachInfo); + this->mGmmp = gmm; +} + +#if NEW_TLLI_ASSIGN_PROCEDURE +static void killOtherTlli(SgsnInfo *si,uint32_t newTlli) +{ + SgsnInfo *othersi = findSgsnInfoByHandle(newTlli,false); + if (othersi && othersi != si) { + if (othersi->getGmm()) { + // This 'impossible' situation can happen if an MS that + // is already attached tries to attach again. + // Easy to reproduce by pulling the power on an MS. (Turning off may not be enough.) + // For example: + // MS -> attachrequest TLLI=80000001; creates new SgsnInfo 80000001 + // MS <- attachaccept + // MS -> attachcomplete TLLI=c0000001; SgsnInfo 80000001 changed to c0000001 + // MS gets confused (turn off or pull battery) + // MS -> attachrequest TLLI=80000001; creates new SgsnInfo 80000001 + // MS <- attachaccept TLLI=80000001; <--- PROBLEM 1! See below. + // MS -> attachcomplete TLLI=c0000001; <--- PROBLEM 2: Both SgsnInfo exist. + // PROBLEM 1: We have already issued the TLLI change command so L2 + // is now using the new TLLI. We will use the old TLLI (80000001) + // in the attachaccept, which will cause a 'change tlli' procedure in L2 + // to switch back to TLLI 80000001 temporarily. + // PROBLEM 2: Solved by deleting the original registered SgsnInfo (c0000001 above) + // and then caller will change the TLLI of the unregistred one (80000001 above.) + SGSNWARN("Probable repeat attach request: TLLI change procedure"<sirm(); + } + } +} +#endif + +// Switch to the new tlli. If now, do it now, otherwise, just allocate the new SgsnInfo, +#if NEW_TLLI_ASSIGN_PROCEDURE +// just set altTlli to the future TLLI we will be using after the attach procedure, +#endif +// which we do ahead of time so we can inform GPRS L2 that the new and old TLLIs are the same MS. +// Return the new si. In the new tlli procedure, it is the same as the old. +// ------------------ +// Note the following sequence, easy to reproduce by pulling the power on an MS: +// MS -> attachrequest TLLI=80000001; creates new SgsnInfo 80000001 +// MS <- attachaccept +// MS -> attachcomplete TLLI=c0000001; SgsnInfo 80000001 changed to c0000001 +// MS gets confused (turn off or pull battery) +// MS -> attachrequest TLLI=80000001; creates new SgsnInfo 80000001 +// MS <- attachaccept TLLI=80000001; <--- PROBLEM 1! See below. +// MS -> attachcomplete TLLI=c0000001; <--- PROBLEM 2: Both SgsnInfo exist. +// PROBLEM 1: We have already issued the TLLI change command so L2 +// is now using the new TLLI. Easy fix is we use the old TLLI (80000001) +// in the attachaccept, which will cause a 'change tlli' procedure in L2 +// to switch back to TLLI 80000001 temporarily until we receive attachcomplete. +// PROBLEM 2: The SgsnInfo for the new tlli will already exist. +// ???? Solved by deleting the original registered SgsnInfo (c0000001 above) +// ???? and then caller will change the TLLI of the unregistred one (80000001 above.) +SgsnInfo * SgsnInfo::changeTlli(bool now) +{ + GmmInfo *gmm = getGmm(); + uint32_t newTlli = gmm->getTlli(); +#if NEW_TLLI_ASSIGN_PROCEDURE + SgsnInfo *si = this; + if (si->mMsHandle != newTlli) { + killOtherTlli(si,newTlli); + if (now) { + si->mAltTlli = si->mMsHandle; + si->mMsHandle = newTlli; + } else { + si->mAltTlli = newTlli; + } + } + return si; +#else + SgsnInfo *othersi = findSgsnInfoByHandle(newTlli,true); + // We will use the new tlli for downlink l3 messages, eg, pdp context messages, + // unless they use some other SI specifically, like AttachAccept + // must be sent on the SI tha the AttachRequest arrived on. + othersi->setGmm(gmm); + if (now) { gmm->msi = othersi; } + return othersi; +#endif +} + +// If si, forces the GmmInfo for this imsi to exist and associates it with that si. +// If si == NULL, return NULL if gmm not found - used for CLI. +GmmInfo *findGmmByImsi(ByteVector &imsi, SgsnInfo *si) +{ + ScopedLock lock(sSgsnListMutex); + GmmInfo *gmm, *result = NULL; + // 24.008 11.2.2: Implicit Detach timer default is 4 min greater + // than T3323, which can be provided in AttachAccept, otherwise + // defaults to T3312, which defaults to 54 minutes. + int attachlimit = gConfig.getNum("SGSN.Timer.ImplicitDetach"); // expiration time in seconds. + time_t now; time(&now); + RN_FOR_ALL(GmmInfoList_t,sGmmInfoList,gmm) { + if (now - gmm->mActivityTime > attachlimit) { + GmmRemove(gmm); + continue; + } + if (gmm->mImsi == imsi) { result = gmm; } + } + if (result) { + if (si) si->setGmm(result); + return result; + } + if (!si) { return NULL; } + + // Not found. Make a new one in state Registration Pending. + gmm = new GmmInfo(imsi); + gmm->setGmmState(GmmState::GmmRegistrationPending); + si->setGmm(gmm); + gmm->msi = si; +#if RN_UMTS + // For UMTS, the si is indexed by URNTI, which is invariant, so hook up and we are finished. +#else + // We hook up the GMM context to the SgsnInfo corresponding to the assigned P-TMSI, + // even if that SgsnInfo does not exist yet, + // rather than the SgsnInfo corresponding to the current TLLI, which could be anything. + // The MS will use the SgsnInfo for the P-TMSI to talk to us after a successful attach. + si->changeTlli(false); +//#if NEW_TLLI_ASSIGN_PROCEDURE +// // 3GPP 04.64 7.2.1.1 and 8.3: Perform the TLLI reassignment procedure. +// // Change the TLLI in the SgsnInfo the MS is currently using. +// // The important point is that the LLC state does not change. +// uint32_t newTlli = gmm->getTlli(); +// killOtherTlli(si,newTlli); +// // Do the TLLI reassignment. +// if (si->mMsHandle != newTlli) { // Any other case is extremely unlikely. +// // We must continue using the existing MsHandle until we +// // receive the AttachComplete message from the MS, but mark +// // that the new tlli is an alias for this TLLI. +// si->mAltTlli = newTlli; +// } +//#else +// SgsnInfo *newsi = findSgsnInfoByTlli(gmm->getTlli(),true); +// newsi->setGmm(gmm); +// // NO, not until attachComplete: gmm->msi = newsi; // Use this one instead. +//#endif +#endif + return gmm; +} + +void RabStatus::text(std::ostream &os) const +{ + os <<"RabStatus(mStatus="; + switch (mStatus) { + case RabIdle: os << "idle"; break; + case RabFailure: os << "failure"; break; + case RabPending: os << "pending"; break; + case RabAllocated: os << "allocated"; break; + case RabDeactPending: os << "deactPending"; break; + } + os<getGmm(); // Must be non-null or we would not be here. + if (!gmm) { os << " GMM state unknown\n"; return; } + gmmInfoDump(gmm,os,options); +} + +}; // namespace diff --git a/SGSNGGSN/Sgsn.h b/SGSNGGSN/Sgsn.h new file mode 100644 index 00000000..c05a47ad --- /dev/null +++ b/SGSNGGSN/Sgsn.h @@ -0,0 +1,332 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef _SGSN_H_ +#define _SGSN_H_ +#include "GPRSL3Messages.h" +#include "SgsnExport.h" +#include "GSMCommon.h" // For Z100Timer +#ifndef MIN +#define MIN(a,b) ((a)<=(b)?(a):(b)) +#endif +#ifndef RN_FOR_ALL +#define RN_FOR_ALL(type,list,var) \ + for (type::iterator itr = (list).begin(); \ + itr == (list).end() ? 0 : ((var=*itr++),1);) +#define RN_FOR_ALL_WITH_ITR(type,list,var,itr) \ + type::iterator itr; \ + for (type::iterator itr1 = (list).begin(),itr=itr1; \ + itr1 == (list).end() ? 0 : ((itr=itr1),(var=*itr1++),1);) +#endif + +#define NEW_TLLI_ASSIGN_PROCEDURE 0 // NO, dont use this. + +namespace SGSN { + +extern bool enableMultislot(); +extern void sendImplicitlyDetached(SgsnInfo *si); + +// 10.5.5.2 AttachType can take all 3 values. +// 10.5.5.1 Attach Result can only be GPRS or Combined, not value 2. +enum AttachType { + AttachTypeGprs = 1, AttachTypeGprsWhileImsiAttached = 2, AttachTypeCombined = 3 +}; +class SgsnInfo; + +const static uint32_t sLocalTlliMask = 0xc0000000; + +struct AttachInfo { + AttachType mAttachReqType; + uint32_t mAttachReqPTmsi; // Saved if specified in the attach request. + // MsRadioAcces caps saved from the AttachRequest to go in GmmInfo when it is created. + ByteVector mMsRadioAccessCap; + GMMRoutingAreaIdIE mPrevRaId; + void stashMsgInfo(GMMAttach &msgIEs, bool isAttach); + void copyFrom(AttachInfo &other); + + AttachInfo() : + // wont be used until initialized, but init to invalid value for copyFrom. + mAttachReqType((AttachType)0), + mAttachReqPTmsi(0) + {} +}; + +// This is the state for a single MS identified by IMSI. +// No IMSI, no GmmInfo; this struct is not allocated until the MS coughs up an IMSI. +// TODO: We may, however, eventually keep a list of known TMSI and/or IMSI in +// an sql data-base somewhere so the phone can subsequently attach without +// having to send us an IMSI, however, this will still be per-IMSI. +// +class GmmInfo +{ + public: + ByteVector mImsi; // The IMSI. + uint32_t mPTmsi; // The unique P-TMSI we create for this Gmm context. + uint32_t getPTmsi() const { return mPTmsi; } + uint32_t getTlli() const { return mPTmsi | sLocalTlliMask; } + static const unsigned sNumPdps = 16; + time_t mAttachTime; + time_t mActivityTime; + int mGprsMultislotClass; // -1 means invalid. + Bool_z mGprsGeranFeaturePackI; + AttachInfo mgAttachInfo; // Copied from SgsnInfo. + void setActivity() { time(&mActivityTime); } + void setAttachTime() { time(&mAttachTime); mActivityTime = mAttachTime; } + + private: + // mState starts in GmmDeregistered. + GmmState::state mState; + + // PDPContext are allocated on request, via ActivatePdpContext message. + // In GPRS, each PdpContext here corresponds to a Sndcp in the mSndcp[]. + PdpContext *mPdps[sNumPdps]; // Only 5..15 are used, but we allocate the whole array and index into + // it directly with the RB [Radio Bearer] index, aka NSAPI. + + public: + + // This points to the SgsnInfo we will use for downlink messages after registration. + // It is not used for the responses during the attach process. + // If msi does not already exist, it is allocated at the same time as GmmInfo, + // however, in that case where it did not pre-exist, it means we have not yet + // heard from the MS using the PTMSI we are going to assign to it, which means + // that there may be no corresponding MSInfo structure yet for this SgsnInfo, + // which case is handled by the TLLI change procecure in L2. + SgsnInfo *msi; + SgsnInfo *getSI() { return msi; } + + bool isRegistered() { + return mState == GmmState::GmmRegisteredNormal || + mState == GmmState::GmmRegisteredSuspsended; + } + + void setGmmState(GmmState::state newstate) { mState = newstate; } + GmmState::state getGmmState() { return mState; } + + bool isNSapiActive(unsigned nsapi); // True if the pdpcontext is not in state PDP-INACTIVE + // 3GPP 24.008 10.5.7.1: PDP Context Status. + // It is a bit-map of the allocated PdpContext for this ms. + // The result bytes for this IE are not in network order, so return it as two bytes. + PdpContextStatus getPdpContextStatus(); + void connectPdp(PdpContext *pdp, mg_con_t *mgp); + bool freePdp(unsigned nsapi); // Free both the PdpContext and the Sndcp. + unsigned freePdpAll(bool freeRabsToo); + PdpContext *getPdp(unsigned nsapi); + + GmmInfo(ByteVector &imsi); + ~GmmInfo(); +}; + + +// The data path through SGSN is different for GPRS and UMTS. +// For GPRS it includes LLC, LLE, and SNDCP. +// For UMTS it includes PDCP, which is a no-op, so it is nearly empty. +// Uplink Data Path: +// GPRS and UMTS have significantly different uplink paradigms in that: +// GPRS sends all packets to a single entry point in LLC, whence packets are +// directed based on information in the packet. +// But there is a wrinkle in that during the attach process there are multiple active TLLIs +// (the old one the ms uses to contact us, and the new one we are assigning) and they +// each have their own LLC state machine, but both must map to the same MS state info. +// TODO: The above is incorrect - there should be only one set of LLC. +// UMTS segregates data channels into RBs, so there is one entry point at the low end of the +// sgsn for each tuple of UE-identifier + RB-id, where the UE-identifier uniquely +// corresponds to an SgsnInfo, and RB-id is 3 is for L3 messages and 5..15 are for user data. +// The GPRS MS sends stuff to: +// sgsnWriteLowSide(ByteVector &payload,SgsnInfo *si); +// The UMTS UE sends stuff to: +// sgsnWriteLowSide(ByteVector &payload,RB????); +// At the high end of the uplink path, the PdpContext sends packets to: +// void SgsnInfo::sgsnSend2PdpLowSide(int nsapi, ByteVector &packet) +// which calls: +// PdpContext *pdp->pdpWriteLowSide(); +// Downlink Data Path: +// The pdp context sends stuff to: +// SgsnInfo *si->sgsnWriteHighSide(ByteVector &pdu,int mNSapi); +// At the low end of the downlink path, stuff goes to: +// void sgsnSend2MsHighSide(ByteVector &pdu,const char *descr, int rbid); +// which calls: +// SgsnInfo *si->getMS()->msDownlinkQueue.write(GPRS::DownlinkQPdu *dlpdu); + +// There is one SgsnInfo for each TLLI. +// This does not map one-to-one to MSInfo structs because MSInfo change TLLI when registered. +// During the attach process an MS may call in with several different TLLIs +// and/or PTMSIs, which will create a bunch of MSInfo structs in GPRS and corresponding +// SgsnInfos here. We dont know what MS these really are until we get an IMSI, +// or todo: see a known TMSI/PTSMI+RAI pair. +// Upon completion of a successful attach, sgsn assigns (or looks up) a new PTMSI +// for the MS, which it will use to talk back to us with yet another new TLLI based on +// the PTMSI that it sent us. +// The Session Management State is in the GmmInfo and is per-MS, not per-TLLI. +// There is a problem that during the attach procedure, uplink and downlink +// messages may be occuring simultaneously with different TLLIs, +// even though they are just one MS and we want to keep gprs straight about +// that so it does not try to start multiple TBFs for the same MS. +// This is how we resolve all that: +// The per-tlli info is in two structs: MSInfo in gprs and SgsnInfo here. +// The MSInfo has a tlli, an oldTlli, and an altTlli, see documentation at the class. +// In the sgsn, there is one SgsnInfo for each tlli. +// (It could have been organized like MSInfo, but is not just for historical reasons.) +// In the sgsn, before we have identified the MS we keep all the information +// in the SgsnInfo, which includes everything we need to remember from the +// RAUpdate or AttachRequest messages. +// After a successful AttachComplete, we will subsequently use only +// one TLLI, and therefore one SgsnInfo, for downlink messages, +// although we still accept uplink messages using the old TLLI, +// and the MS may continue to send messages using +// those old TLLIs well into the PdpContext activation procedures. +// So this process results in one SgsnInfo that is associated with the TLLI that +// finally successfully attached, one SgsnInfo for the TLLI the MS used to initiate +// the attach, and we also may end up with a bunch of ephemeral SgsnInfo +// associated with TLLIs that the MS tried but did not finish attaching. + +// Note that the ephemeral SgsnInfo must send send an L3 attach message +// (to assign the local TLLI) using the immensely over-bloated LLC machinery +// to get the MS attached to a semi-permanent SgsnInfo. +// I was really tempted just to hard-code the single LLC message that is required, +// but I didn't; we allocate a whole new LLC and send the L3 message through it. +// I think after the attach process we could just get rid of the SgsnInfo, and let them +// be created anew if the MS wants to talk to us again. +// ========== +// The LLC state machine is DEFINED as being per-TLLI. +// From 3GPP 04.64 [LLC] 4.5.1: "A logical link connection is identified by a DLCI +// consisting of two identifiers: a SAPI and a TLLI." +// However, during the Attach process we change the TLLI using the procedures +// defined in 3GPP 04.64 8.3. +// The GmmInfo holds the Session Management info, and is associated with one +// and only one MS identified by IMSI. +// There are two major types of SgsnInfo: +class SgsnInfo +{ + friend class MSUEAdapter; + + // The LlcEngine is used only by GPRS. + // UMTS uses PDCP, but it is a complete no-op, so is not represented here at all. + // The LLC sits between GPRS and SGSN and could have been combined with GPRS instead + // of being in the SGSN at all. Here, it is encapsulated entirely within this object. + // I left the LLC component in the SGSN for several reasons: + // in case we implement handover, the LLC state must be passed too; + // and just because that is the way things are partitioned in commercial systems. + private: + GmmInfo *mGmmp; + public: + GmmInfo *getGmm() const { return mGmmp; } + void setGmm(GmmInfo *gmm); + + LlcEngine *mLlcEngine; + time_t mLastUseTime; + + //GmmMobileIdentityIE mAttachMobileId; + + // For the local SGSN, this is the P-TMSI/TLLI that we [are attempting to] + // assign to this MS. In the old separated SGSN system, the SGSN would remember both old + // and new [being assigned] TLLI, and would try to change the TLLI in the BTS simultaneously, + // keeping everything in sync, but we dont do that any more. We let the GPRS directory create + // a separate MSInfo structure for each TLLI, each of which maps to an independent SgsnInfo struct + // for that TLLI, because each needs a unique LLC state machine. + // + // In L3 messages, we send P-TMSI to ms and it is supposed to convert that to + // a "TLLI" of some sort when talking back to us by twiddling the top bits + // depending on the Routing Area. In the same Routing Area, it sets the top 2 bits. + // Which is silly for us, so we just set the top 2 bits to start with and use + // the same 32-bit number for TLLI and P-TMSI. + uint32_t mMsHandle; // The single TLLI or URNTI associated with this SgsnInfo. + // If it is an assigned SgsnInfo, this is equal to the P-TMSI we allocated for the MS. + + // I tried to implement the TLLI Assign Procedure by adding an AltTlli to this + // SgsnInfo struct, but it does not work well, because after an attach, if + // the MS tries to attach again (happens if it is powercycled or confused) + // then we really need the two SgsnInfo separate again so we can communicate + // with the MS distinctly with the old TLLI for the attach accept message. + // Also it was easy to end up with two SgsnInfo with the same TLLI in that case, + // so its easier to just keep them distinct. + // So an SgsnInfo is synonymous with a single TLLI. +#if NEW_TLLI_ASSIGN_PROCEDURE + uint32_t mAltTlli; // For GPRS, this is the alternate TLLI before a TLLI assignment + // procedure that occurs at AttachComplete. This SgsnInfo must + // respond to both new and alt tllis, but only the tlli + // in mMsHandle is used for downlink communication. +#endif + SgsnInfo *changeTlli(bool now); + //uint32_t getTmsi() { return mMsHandle; } // NOT RIGHT! + //uint32_t getPTmsi() { return mMsHandle; } // NOT RIGHT! + + ByteVector mRAND; + // The information in the L3 AttachRequest is rightly part of the GmmInfo context, + // but when we receive the message, that does not exist yet, so we save the + // AttachRequest info in the SgsnInfo here. + // This is the type of attach the MS requested. It is saved from the AttachRequest so it can + // be used to send the correct attach accept message after the MS identity request handshake(s). + AttachInfo mtAttachInfo; + + AttachType attachResult() { + if (mtAttachInfo.mAttachReqType == AttachTypeGprsWhileImsiAttached) { + return AttachTypeCombined; + } + return mtAttachInfo.mAttachReqType; + } + void deactivateRabs(unsigned nsapiMask); + + + // Pass throughs to GmmInfo. All must be protected from mGmmp == NULL. + bool isRegistered() const { return mGmmp && mGmmp->isRegistered(); } + PdpContextStatus getPdpContextStatus() { return mGmmp ? mGmmp->getPdpContextStatus() : PdpContextStatus(); } + bool freePdp(unsigned nsapi) { return mGmmp ? mGmmp->freePdp(nsapi) : false; } + unsigned freePdpAll(bool freeRabsToo) { return mGmmp ? mGmmp->freePdpAll(freeRabsToo) : 0; } + PdpContext *getPdp(unsigned nsapi) { return mGmmp ? mGmmp->getPdp(nsapi) : 0; } + + // After packet has been processed by LLC or PDCP, this sends it to the correct PdpContext. + void sgsnSend2PdpLowSide(int nsapi, ByteVector &packet); + void sgsnSend2MsHighSide(ByteVector &pdu,const char *descr, int rbid); + + void sgsnWriteHighSide(ByteVector &sdu,int nsapi); + + // 24.008 11.2.2. T3310 is in the MS and is 15s. We are using it here similarly to place a limit + // on the Attach Request process, so when we receive an Identity Response we dont send + // an attach long afterward. + GSM::Z100Timer mT3310FinishAttach; + // T3370 is the ImsiRequest repeat at 6s, but the total time is only 15s, so its hardly worth bothering, + // so we dont; the MS will RACH again if necessary. But here is the timer anyway. + GSM::Z100Timer mT3370ImsiRequest; + + SgsnInfo(uint32_t wTlli); // May be a URNTI instead of TLLI. + ~SgsnInfo(); + void sirm(); // Remove si from list and delete it. + + MSUEAdapter *getMS() const; + + void sgsnReset(); + + // Downlink L3 Messages come here. + void sgsnWriteHighSideMsg(L3GprsDlMsg &msg); + + //GMMAttach *atp; // All the info from the attach or ra-update. + //GPRS::MSInfo *mMs; // The MSInfo associated with this SgsnInfo. + friend std::ostream& operator<<(std::ostream& os, const SgsnInfo*si); +}; +std::ostream& operator<<(std::ostream& os, const SgsnInfo*si); + +void handleL3Msg(SgsnInfo *si, ByteVector &payload); +void gmmDump(std::ostream&os); +void sgsnInfoDump(SgsnInfo *si,std::ostream&os); +void gmmInfoDump(GmmInfo *si,std::ostream&os,int options); +SgsnInfo *findSgsnInfoByHandle(uint32_t handle,bool create); +GmmInfo *findGmmByImsi(ByteVector&imsi,SgsnInfo *si); +bool cliSgsnInfoDelete(SgsnInfo *si); +void cliGmmDelete(GmmInfo *gmm); + + +}; // namespace +#endif diff --git a/SGSNGGSN/SgsnBase.h b/SGSNGGSN/SgsnBase.h new file mode 100644 index 00000000..e59e7267 --- /dev/null +++ b/SGSNGGSN/SgsnBase.h @@ -0,0 +1,144 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef SGSNBASE_H +#define SGSNBASE_H +#include "Logger.h" +#include "miniggsn.h" + +#define SNDCP_IN_PDP 1 + +namespace SGSN { +#ifdef WARNING +#undef WARNING +#endif + +extern bool sgsnDebug(); +//#define GGSNLOG2(level,who,stuff) {std::cout < +#include "Sgsn.h" +#include "Utils.h" +#include "Globals.h" +using namespace Utils; + +struct CliError { + string mMessage; + CliError(string wMessage) : mMessage(wMessage) {} +}; + +struct CliErrorBadNumArgs : CliError { + CliErrorBadNumArgs() : CliError("wrong number of arguments") {} +}; + +#define RN_CMD_OPTION(opt) (argi1 && 0==strncmp(argv[1],o,strlen(o)) ? argc--,argv++,1 : 0) +#define strmatch(what,pat) (0==strncmp(what,pat,strlen(pat))) + +namespace SGSN { + + +static void sgsnCliFind(char *what, char *idstr, GmmInfo **pgmm, SgsnInfo **psi, ostream&os) +{ + *pgmm = 0; + *psi = 0; + if (what == NULL || idstr == NULL) { + throw CliErrorBadNumArgs(); + } + if (strmatch(what,"imsi")) { + // The imsi bytevector is nibble packed. + ByteVector bimsi(strlen(idstr)/2 + 3); // Add some slop + bimsi.setAppendP(0); + for (char *cp = idstr; *cp; cp++) { + if (*cp >= '0' && *cp <= '9') { + bimsi.appendField(*cp - '0',4); + } else { + throw CliError(format("invalid imsi: %s",idstr)); + } + } + *pgmm = findGmmByImsi(bimsi,0); + if (*pgmm == NULL) { + throw CliError(format("SGSN record for imsi '%s' not found",idstr)); + } + } else if (strmatch(what,"tlli")) { + uint32_t tlli = strtoul(idstr,0,16); + if (tlli == 0) { + tlli = strtoul(idstr,0,10); // Try for 0x notation + } + if (tlli == 0) { + throw CliError(format("Invalid tlli argument: %s",idstr)); + } + *psi = findSgsnInfoByHandle(tlli,false); + if (*psi == NULL) { + throw CliError(format("SGSN record for tlli 0x%x not found.",tlli)); + } + } else { + throw CliError(format("unrecognized argument: %s",what)); + } +} + +static void sgsnCliList(int argc, char **argv, int argi, ostream&os) +{ + char *what = RN_CMD_ARG; + char *idstr = RN_CMD_ARG; + if (!what) { + gmmDump(os); // List all + } else { + GmmInfo *gmm; SgsnInfo *si; + sgsnCliFind(what,idstr,&gmm,&si,os); + if (gmm) { + gmmInfoDump(gmm,os,0); + } else if (si && si->getGmm()) { + gmmInfoDump(si->getGmm(),os,0); + } else if (si) { + sgsnInfoDump(si,os); + } + } +} + +static void sgsnCliFree(int argc, char **argv, int argi, ostream&os) +{ + char *what = RN_CMD_ARG; + char *idstr = RN_CMD_ARG; + if (!idstr) throw CliErrorBadNumArgs(); + GmmInfo *gmm; SgsnInfo *si; + sgsnCliFind(what,idstr,&gmm,&si,os); + if (gmm) { + cliGmmDelete(gmm); + } else if (si && si->getGmm()) { + cliGmmDelete(si->getGmm()); + } else if (si) { + if (! cliSgsnInfoDelete(si)) { + os << "Can not delete; it is in use. Instead, delete the GMM context by imsi."; + } + } +} + +static void sgsnCliHelp(int argc, char **argv, int argi, ostream&os); +static struct SgsnSubCmds { + const char *name; + void (*subcmd)(int argc, char **argv, int argi,std::ostream&os); + const char *syntax; +} sgsnSubCmds[] = { + { "list",sgsnCliList, "list [(imsi|tlli) id] # list all or specified MS" }, + { "free",sgsnCliFree, "free (imsi|tlli) id # Delete something" }, + { "help",sgsnCliHelp, "help # print this help" }, + //{ "stat",gprsStats, "stat # Show GPRS statistics" }, + //{ "debug",gprsDebug, "debug [level] # Set debug level; 0 turns off" }, + //{ "stop",gprsStop, "stop [-c] # stop gprs thread and if -c release channels" }, + { NULL,NULL } +}; + +static void sgsnCliHelp(int argc, char **argv, int argi, ostream&os) +{ + os << "sgsn sub-commands to control SGSN/GGSN sub-system. Syntax: sgsn subcommand \n"; + os << "subcommands are:\n"; + struct SgsnSubCmds *gscp; + for (gscp = sgsnSubCmds; gscp->name; gscp++) { + os << "\t" << gscp->syntax; + os << "\n"; + } +} + + +// For now, just do a list. +int sgsnCLI(int argc, char **argv, std::ostream &os) +{ + if (argc <= 1) { sgsnCliHelp(0,0,0,os); return 0; } + int argi = 1; // The number of arguments consumed so far; argv[0] was "sgsn" + char *subcmd = argv[argi++]; + + struct SgsnSubCmds *gscp; + for (gscp = sgsnSubCmds; gscp->name; gscp++) { + if (0 == strcasecmp(subcmd,gscp->name)) { + try { + gscp->subcmd(argc,argv,argi,os); + } catch (CliError &e) { + os << "sgsn:"< +#include // For RN_UMTS +#include "ByteVector.h" +#include "SgsnBase.h" // For SmCause +#include "LinkedLists.h" +#include "MemoryLeak.h" + +namespace SGSN { +class GmmInfo; +class SgsnInfo; + +// Printing option flags. +enum PrintOptions { + printVerbose = 1, + printCaps = 2, + printDebug = 4, + printNoMsId = 8 +}; + +// 24.008 4.1.3 GMM state machine picture and description. +class GmmState +{ public: + enum state { + //GmmNotOurTlli, // This tlli was not assigned by us. + GmmDeregistered, // Tlli was assigned by us, but not registered yet. + GmmRegistrationPending, + GmmRegisteredNormal, // aka "GPRS Attached" + GmmRegisteredSuspsended + }; + static const char *GmmState2Name(state); +}; + +// A class for downlink messages from the SGSN to GPRS/UMTS. +struct SgsnDownlinkMsg +{ + //enum { } msgType; + ByteVector mDlData; // The PDU itself. + std::string mDescr; // Description of this pdu. + SgsnDownlinkMsg(ByteVector a, std::string wDescr) : mDlData(a), mDescr(wDescr) { RN_MEMCHKNEW(SgsnDownlinkMsg) } + // The virtual keyword on a destructor indicates that both the base destructor (this one) + virtual ~SgsnDownlinkMsg() { RN_MEMCHKDEL(SgsnDownlinkMsg) } + //SgsnDownlinkPdu(SgsnDownlinkPdu&other) { *this = other; } +}; + +struct GprsSgsnDownlinkPdu : public SingleLinkListNode, public SgsnDownlinkMsg +{ + uint32_t mTlli; // For gprs: the TLLI of the MS to receive the message. + // (In gprs NSAPI is encoded in the LLC message in the data.) + uint32_t mAliasTlli;// Another TLLI that the SGSN knows refers to the same MS as the above. + Timeval mDlTime; + bool isKeepAlive() { return false; } // Is this is a dummy message? + unsigned size() { return mDlData.size(); } // Decl must exactly match SingleLinkListNode + GprsSgsnDownlinkPdu(ByteVector a, uint32_t wTlli, uint32_t wAliasTlli, std::string descr) : + SgsnDownlinkMsg(a,descr), mTlli(wTlli), mAliasTlli(wAliasTlli) + {} +}; + +struct UmtsSgsnDownlinkPdu : public SgsnDownlinkMsg +{ + int mRbid; // For UMTS, the rbid (if 3..4) or NSAPI if (5..15). + UmtsSgsnDownlinkPdu(ByteVector a, int wRbid, std::string descr) : + SgsnDownlinkMsg(a,descr), mRbid(wRbid) + {} +}; + +// This class is inherited by the MSInfo struct in GPRS and the UEInfo struct in UMTS +// to provide the necessary Sgsn linkage. +// The SgsnInfo class has the L3 information for the MS/UE and corresponds +// one-to-one with MSInfo or UEInfo classes (and their corresponding inherited MSUEAdapter) +// but the creation and destruction of SgsnInfo and UE or MSInfo are decoupled: +// the SgsnInfo is manufactured upon need (GPRS Attach Request) from the MSInfo or UEInfo, +// and either struct can be deleted without informing the other. +// They are identified by TLLI (in GPRS) or U-RNTI (in UMTS) which is the same in both. +// The MSInfo is very ephemeral, and is normally destroyed shortly after the +// MS ceases communication (sending TBFs); it could be as soon as the MS has certainly +// dropped back to GPRS Packet-Idle mode, because the MS will call back in if necessary +// to create a new MSInfo structure, although we keep it around a little longer than that. +// However, if the MS calls back in it will do so with the same TLLI, +// which will correspond to the SgsnInfo it used previously. +// According to the spec, the GPRS registration information, which is saved in GmmInfo +// should last about an hour. +class MSUEAdapter { + // Methods that begin with 'sgsn' are implemented by the SGSN side. + // Methods that begin with 'ms' are implemented by the UMTS RRC or GPRS L2 side. + + public: + // The rbid is used only by UMTS. + void sgsnWriteLowSide(ByteVector &payload,uint32_t handle, unsigned rbid=0); + GmmState::state sgsnGetRegistrationState(uint32_t mshandle); + void sgsnPrint(uint32_t mshandle, int options, std::ostream &os); + void sgsnFreePdpAll(uint32_t mshandle); + + //MSUEAdapter() {} + //virtual ~MSUEAdapter() {} + virtual std::string msid() const = 0; // A human readable name for the MS or UE. + +#if RN_UMTS + // This is the old interface to GPRS/UMTS; see saWriteHighSide() + // This sends a pdu from the sgsn (or anywhere) to the ms. + // For UMTS the rbid is the rbid; For GPRS the rbid is the TLLI. + // This is the previous interface: + virtual void msWriteHighSide(ByteVector &dlpdu, uint32_t rbidOrTlli, const char *descr) = 0; + // This is called when the RRC SecurityModeComplete or SecurityModeFailure is received. + void sgsnHandleSecurityModeComplete(bool success); + // When allocating Pdp Contexts, the Ggsn allocates RABs in the middle + // of the procedure, and waits for RRC to respond with this function: + void sgsnHandleRabSetupResponse(unsigned RabId,bool success); + // Deactivate all the rabs specified by rabMask. + virtual void msDeactivateRabs(unsigned rabMask) = 0; + // This is only externally visible for UMTS because in GPRS we have + // to send messages through LLC first, so this call is made by LLC to the SGSN. + void sgsnHandleL3Msg(uint32_t handle, ByteVector &msgFrame); + // For UMTS only, get the SgsnInfo that goes with this UE. + // Only used when called indirectly from RRC so we know the UEInfo still exists. + SGSN::SgsnInfo *sgsnGetSgsnInfo(); // Return SgsnInfo for this UE or NULL. +#else + // This sends a pdu from the MS/UE to the sgsn. + void sgsnSendKeepAlive(); + int sgsnGetMultislotClass(uint32_t mshandle); + bool sgsnGetGeranFeaturePackI(uint32_t mshandle); +#endif + private: + virtual uint32_t msGetHandle() = 0; // return URNTI or TLLI + friend std::ostream& operator<<(std::ostream&os,const SgsnInfo*); +}; + +// This is our information about the allocated channel passed from GPRS or UMTS back to GGSN. +// In a normal system, the whole QoS would be passed back and forth, but we +// are letting the GGSN module handle the QoS. +struct RabStatus { + enum Status { RabIdle, RabFailure, RabPending, RabAllocated, RabDeactPending } mStatus; + //Timeval mDeactivateTime; // If RabFreePending, when to kill it. + SmCauseType mFailCode; + unsigned mRateDownlink; // peak KByte/sec downlink of allocated channel + unsigned mRateUplink; // peak KByte/sec uplink of allocated channel + RabStatus(): mStatus(RabIdle), mFailCode((SmCauseType)0) {} + RabStatus(SmCauseType wFailCode): mStatus(RabFailure), mFailCode(wFailCode) {} + RabStatus(Status wStatus,SmCauseType wFailCode): mStatus(wStatus), mFailCode(wFailCode) {} + //void scheduleDeactivation() { + // mStatus = RabDeactPending; + // mDeactivationTime.future(gConfig.getNum("UMTS.Rab.DeactivationDelay",5000)); + //} + void text(std::ostream &os) const; +}; + +// This class is just a container to hold the callbacks from the SGSN to the external world. +// No objects of this class are created. +struct SgsnAdapter { + // Find the MS by TLLI or UE by U-RNTI. + // Return NULL if it no longer exists. + static MSUEAdapter *findMs(uint32_t); + static bool isUmts(); + // Send a PDU to GPRS/UMTS [not implemented for UMTS yet.] + static void saWriteHighSide(GprsSgsnDownlinkPdu *dlpdu); +#if RN_UMTS + // Return 0 on success or SmCause value on error. + static RabStatus allocateRabForPdp(uint32_t urnti, int rbid, ByteVector &qos); + static void startIntegrityProtection(uint32_t urnti, std::string Kcs); +#endif +}; + +// This is the external interface to the SGSN. +struct Sgsn { + public: + static bool isUmts() { return SgsnAdapter::isUmts(); } + + static bool handleGprsSuspensionRequest(uint32_t wTlli, const ByteVector &wRaId); + static void notifyGsmActivity(const char *imsi); +//#if RN_UMTS + // FIXME: make this work like gprs + //static void sgsnWriteLowSide(ByteVector &payload,SgsnInfo *si, unsigned rbid); // UMTS only +//#endif +}; + +}; // namespace SGSN +#endif diff --git a/SGSNGGSN/iputils.cpp b/SGSNGGSN/iputils.cpp new file mode 100644 index 00000000..979ae3aa --- /dev/null +++ b/SGSNGGSN/iputils.cpp @@ -0,0 +1,504 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // pat added for gethostbyname +#include // pat added for IPv4 iphdr +#include // pat added for tcphdr +#include // pat added for udphdr + +#include // pat added. +#include // pat added. +#include // pat added. This defines NCC, which is bad, because used in GSMConfig.h +#undef NCC // Just in case you want to include GSM files later. +#include // pat added +#include // pat added +#include // pat added. +#include // pat added. +#include +#include +#include +#include "miniggsn.h" +#include // for gConfig +#include + +namespace SGSN { +#define EXPORT +// Returns fractional seconds. +double pat_timef() +{ + struct timeval tv; + gettimeofday(&tv,NULL); + return tv.tv_usec / 1000000.0 + tv.tv_sec; +} + +double pat_elapsedf() +{ + static double start = 0; + double now = pat_timef(); + if (start == 0) start = now; + return now - start; +} + + +// ip address is in network order; return as dotted string. +EXPORT char *ip_ntoa(int32_t ip, char *buf) +{ + static char sbuf[30]; + if (buf == NULL) { buf = sbuf;} + ip = ntohl(ip); + sprintf(buf,"%d.%d.%d.%d",(ip>>24)&0xff,(ip>>16)&0xff,(ip>>8)&0xff,ip&0xff); + return buf; +} + +// Given an address like "192.168.1.0/24" return the base ip and the mask. +EXPORT bool ip_addr_crack(const char *input,uint32_t *paddr, uint32_t *pmask) +{ + char ip_buf[42]; + if (strlen(input) > 40) return false; + strcpy(ip_buf,input); + // Look for the '/'; + char *sl = strchr(ip_buf,'/'); + if (sl) *sl = 0; + *paddr = inet_addr(ip_buf); + if (sl == 0) { + // Indicates no mask specified: + *pmask = 0; + } else { + int maskbits = atoi(sl+1); + if (maskbits < 0 || maskbits >32) { + *pmask = 0; // be safe. + return false; // but return invalid. + } + *pmask = htonl(~ ( (1u<<(32-maskbits)) - 1 )); + } + return true; +} + +EXPORT char *ip_sockaddr2a(void * /*struct sockaddr * */ sap,char *buf) +{ + static char sbuf[100]; + if (buf == NULL) { buf = sbuf;} + struct sockaddr_in *to = (struct sockaddr_in*)sap; + sprintf(buf,"proto=%d%s port=%d ip=%s", + ntohs(to->sin_family), + to->sin_family == AF_INET ? "(AF_INET)" : "", + ntohs(to->sin_port), + ip_ntoa(to->sin_addr.s_addr,NULL)); + return buf; +} + +// Add an address to the interface. ifname is like "eth0" or "tun1" +// You have to be root to do this. +// Note that if you want to bind to a non-local address must set the ip_nonlocal_bind +// kernel option set in /proc. Update: that did not work, so use ip_addr_add() +// on the ethernet card if you want to send directly to the machine. +EXPORT int ip_add_addr(char *ifname, int32_t ipaddr, int maskbits) +{ + char fulladdr[100]; + sprintf(fulladdr,"%s/%d",ip_ntoa(ipaddr,NULL),maskbits); + if (fork() == 0) { + execl("/sbin/ip","ip","addr","add",fulladdr,"dev",ifname,NULL); + exit(0); // Just in case. + } + return 0; +} + +EXPORT const char *ip_proto_name(int ipproto) +{ + static char buf[10]; + switch(ipproto) { + case IPPROTO_TCP: return "tcp"; + case IPPROTO_UDP: return "udp"; + case IPPROTO_ICMP: return "icmp"; + default: sprintf(buf,"%d",ipproto); return buf; + } +} + +// IP standard checksum, see wikipedia "IPv4 Header" +// len is in bytes, will normally be 20 == sizeof(struct iphdr). +EXPORT unsigned int ip_checksum(void *ptr, unsigned len, void *dummyhdr) +{ + //if (len != 20) printf("WARNING: unexpected header length in ip_checksum\n"); + uint32_t i, sum = 0; + uint16_t *pp = (uint16_t*)ptr; + while (len > 1) { sum += *pp++; len -= 2; } + if (len == 1) { + uint16_t foo = 0; + unsigned char *cp = (unsigned char*)&foo; + *cp = *(unsigned char *)pp; + sum += foo; + } + if (dummyhdr) { // For TCP and UDP the dummy header is 3 words = 6 shorts. + pp = (uint16_t*)dummyhdr; + for (i = 0; i < 6; i++) { sum += pp[i]; } + } + //printf("intermediate sum=0x%x\n",sum); + // Convert from 2s complement to 1s complement: + //sum = ((sum >> 16)&0xffff) + (sum & 0xffff); /* add hi 16 to low 16 */ + sum = ((sum >> 16)) + (sum & 0xffff); /* add hi 16 to low 16 */ + sum += (sum >> 16); /* add carry */ + //printf("intermediate sum16=0x%x result=0x%x\n",sum,~sum); + return 0xffff & ~sum; +} + +#if 0 +// from ifconfig.c - skfd is undefined +static int set_ip_using(const char *name, int c, unsigned long ip) +{ + struct ifreq ifr; + struct sockaddr_in sin; + + /*safe_*/strncpy(ifr.ifr_name, name, IFNAMSIZ); + memset(&sin, 0, sizeof(struct sockaddr)); + sin.sin_family = AF_INET; + sin.sin_addr.s_addr = ip; + memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr)); + if (ioctl(skfd, c, &ifr) < 0) + return -1; + return 0; +} +#endif + +EXPORT void ip_hdr_dump(unsigned char *packet, const char *msg) +{ + struct iptcp { // This is only accurate if no ip options specified. + struct iphdr ip; + struct tcphdr tcp; + }; + struct iptcp *pp = (struct iptcp*)packet; + char nbuf[100]; + printf("%s: ",msg); + printf("%d bytes protocol=%d saddr=%s daddr=%s version=%d ihl=%d tos=%d id=%d\n", + ntohs(pp->ip.tot_len),pp->ip.protocol,ip_ntoa(pp->ip.saddr,nbuf),ip_ntoa(pp->ip.daddr,NULL), + pp->ip.version,pp->ip.ihl,pp->ip.tos, ntohs(pp->ip.id)); + printf("\tcheck=%d computed=%d frag=%d ttl=%d\n", + pp->ip.check,ip_checksum(packet,sizeof(struct iphdr),NULL), + ntohs(pp->ip.frag_off),pp->ip.ttl); + printf("\ttcp SYN=%d ACK=%d FIN=%d RES=%d sport=%u dport=%u\n", + pp->tcp.syn,pp->tcp.ack,pp->tcp.fin,pp->tcp.rst, + ntohs(pp->tcp.source),ntohs(pp->tcp.dest)); + printf("\t\tseq=%u ackseq=%u window=%u check=%u\n", + ntohl(pp->tcp.seq),ntohl(pp->tcp.ack_seq),htons(pp->tcp.window),htons(pp->tcp.check)); +} + +// Run the command. +// This is not ip specific, but used to call linux "ip" and "route" commands. +// If commands starts with '|', capture stdout and return the file descriptor to read it. +EXPORT int runcmd(const char *path, ...) +{ + int pipefd[2]; + int dopipe = 0; + if (*path == '|') { + path++; + dopipe = 1; + if (pipe(pipefd) == -1) { + MGERROR("could not create pipe: %s",strerror(errno)); + return -1; + } + } + int pid = fork(); + if (pid == -1) { + MGERROR("could not fork: %s",strerror(errno)); + return -1; + } + if (pid) { // This is the parent; wait for child to exit, then return childs status. + int status; + if (dopipe) { + close(pipefd[1]); // Close unused write end of pipe. + } + int result = 0; + for (int quartersecs = 0; quartersecs < 5*4; quartersecs++) { + if (waitpid(pid,&status,WNOHANG) == pid) { + // WARNING: This assumes the amount of info returned by the command + // is small enough to fit in the pipe. + return dopipe ? pipefd[0] : status; // Return read end of pipe, if piped. + } + usleep(250*1000); + } + MGERROR("sub-process did not complete in 5 seconds: %s",path); + return -1; + } + // This is the child process. + if (dopipe) { + close(pipefd[0]); // Close unused read end of pipe. + dup2(pipefd[1],1); // Capture stdout to pipe. + close(pipefd[1]); // Close now redundant fd. + } + + // Gather args into argc,argv; + int argc = 0; char *argv[100]; + va_list ap; + va_start(ap, path); + do { + argv[argc] = va_arg(ap,char*); + } while (argv[argc++]); + argv[argc] = NULL; + va_end(ap); + + // Print them out. + // But dont print if piped, because it goes into the pipe! + if (! dopipe) { + char buf[208], *bp = buf, *ep = &buf[200]; + int i; + for (i = 0; argv[i]; i++) { + int len = strlen(argv[i]); + if (bp + len > ep) { strcpy(bp,"..."); break; } + strcpy(bp,argv[i]); + bp += len; + *bp++ = ' '; + *bp = 0; + } + buf[200] = 0; + MGINFO("%s",buf); + } + + // exec them. + execv(path,argv); + _exit(2); // Just in case. + return 0; // This is never used. +} + + +// The addrstr is the tunnel address and must include the mask, eg: "192.168.2.0/24" +EXPORT int ip_tun_open(const char *tname, const char *addrstr) // int32_t ipaddr, int maskbits) +{ + struct ifreq ifr; + int fd; + const char *clonedev = "/dev/net/tun"; + if ((fd = open(clonedev,O_RDWR)) < 0) { + // Hmmph. Try to create the tunnel device. + runcmd("/sbin/modprobe","modprobe","tun",NULL); + runcmd("/sbin/modprobe","modprobe","ipip",NULL); + sleep(2); + } + if ((fd = open(clonedev,O_RDWR)) < 0) { + MGERROR("error: Could not open: %s\n",clonedev); + return -1; + } + + // This attaches to our existing mstun interface, if any, because + // of the magic TUNSETPERSIST flag. + memset(&ifr,0,sizeof(ifr)); + strcpy(ifr.ifr_name,tname); + ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // Disable packet info. + if (ioctl(fd,TUNSETIFF,&ifr) < 0) { + MGERROR("could not create tunnel %s: ioctl error: %s\n",tname,strerror(errno)); + return -1; + } + if (ioctl(fd,TUNSETPERSIST,1) < 0) { + MGERROR("could not setpersist tunnel %s: ioctl error: %s\n",tname,strerror(errno)); + } + + // This (and only this) magic works: + // We invoke with: + // ./miniggsn -v -t 192.168.1.75/32 -f 192.168.1.75 router + // or: ./miniggsn -v -t 192.168.2.0/24 -f 192.168.2.1 router + runcmd("/sbin/ip","ip","link","set",tname,"up",NULL); + // TODO: Instead of individual routes, we can also do: + // ip route add to 192.168.3.0/24 dev mstun # Note you must use .0, not .1 + //runcmd("/sbin/ip","ip","route","add","to",options.from,"dev",tname,NULL); + runcmd("/sbin/ip","ip","route","add","to",addrstr,"dev",tname,NULL); + // Establishing a "forwarding route" causes linux proxy-arp to automagically add an ARP + // for mstun, even though it has the "NOARP" option set. What a mess. + + // Now we can set the interface addr using ip command. + //ip_add_addr(tname,ipaddr,maskbits); + // The addrstr must include the correct maskbits, like /24, or it just doesnt work. + // If you call any of these, the output is no longer routed unless + // you called TUNSETPERSIST first. + //runcmd("/sbin/ifconfig","ifconfig",tname,"arp",NULL); + //runcmd("/sbin/ip","ip","addr","add",addrstr,"dev",tname,NULL); + //runcmd("/sbin/iptables","iptables","-A","FORWARD","-i",tname,"-j","ACCEPT", NULL); + + + // todo #include + // SIOCADDRT struct ifreq* /* add routing table entry */ + // SIOCGIFADDR get interface address. + // SIOCADDRT add interface address. + // SIOCSIFADDR, SIOCSIFNETMASK + /* + if (set_ip_using(buf, SIOCSIFADDR, ip) == -1) { + MGERROR("ioctl SIOCSIFADDR failed\n"); return 2; + } + if (set_ip_using(buf, SIOCSIFNETMASK , mask) == -1) { + MGERROR("ioctl SIOCSIFNETMASK failed\n"); return 2; + } + */ + // We wont set a broadcast address using SIOCSIFBRDADDR + + return fd; +} + +static int setprocoption(const char *procfn) +{ + int fd; + const char *str = "1\n"; + fd = open(procfn,1); + if (fd < 0) { MGERROR("Can not open file %s error: %s\n",procfn,strerror(errno)); return 2;} + int __attribute__((unused)) foobar=write(fd,str,2); // the foobar shuts up g++ + close(fd); + return 0; +} + +void ip_init() +{ + // Enable port forwarding and non-local bind + if (setprocoption("/proc/sys/net/ipv4/ip_forward")) /*exit(2)*/ ; + // This did not work: + if (setprocoption("/proc/sys/net/ipv4/ip_nonlocal_bind")) /*exit(2)*/ ; +} + +static int ip_getDnsDefault(uint32_t *dns) +{ + int nfnd = 0; + FILE *pf = fopen("/etc/resolv.conf","r"); + if (pf == NULL) { + MGERROR("GGSN: DNS servers: error: could not open /etc/resolv.conf\n"); + return 0; + } + char buf[300]; + while (fgets(buf,299,pf)) { + buf[299] = 0; + char *cp = buf; + while (*cp && isspace(*cp)) cp++; + if (0 == strncasecmp(cp,"nameserver",strlen("nameserver"))) { + cp += strlen("nameserver"); + while (*cp && isspace(*cp)) cp++; + uint32_t addr; + if (0 == inet_aton(cp,(struct in_addr*) &addr)) { + MGERROR("GGSN: Error: Invalid IP address in /etc/resolv.conf at: %s\n",buf); + continue; + } + dns[nfnd++] = addr; + if (nfnd == 2) break; + } + } + fclose(pf); + return nfnd; +} + +// If a DNS option was specified, put the addresses in dns[2] and return number found. +// The DNS may be a list of space separated servers. +static int ip_getDnsOption(uint32_t *dns) +{ + std::string sdns = gConfig.getStr("GGSN.DNS"); + int len = sdns.length(); + if (len <= 0) {return 0;} + + char *copy = strcpy((char*)alloca(len+1),sdns.c_str()); // make a copy of the option string. + // Insist on dotted notation to catch stupid errors like setting it to '1' by mistake + if (!strchr(copy,'.')) { + MGWARN("Invalid GGSN.DNS option ignored: '%s'",copy); + return 0; + } + char *argv[3]; + int argc = cstrSplit(copy,argv,3); // split into argv + if (argc > 2) { + MGWARN("GGSN: invalid GGSN.DNS option, more than 2 servers specified: '%s'\n",sdns.c_str()); + // But go ahead and use the first two. + argc = 2; + } + + for (int i = 0; i < argc; i++) { + if (0 == inet_aton(argv[i],(struct in_addr*) &dns[i])) { // is it an invalid IP address? + // failed. + MGERROR("GGSN: unrecognized GGSN.DNS option: %s\n",sdns.c_str()); + return i; + } + } + return argc; +} + +// Find the dns addresses. Return the number of dns addresses found. +// dns must point to uint32_t dns[2]; +EXPORT int ip_finddns(uint32_t *dns) +{ + dns[0] = dns[1] = 0; + int nfnd = ip_getDnsOption(dns); + if (!nfnd) { + nfnd = ip_getDnsDefault(dns); + } + + // Print out the dns servers whenever they change. + // If dns were not found, they will print as 0s, which is a good enough message. + static uint32_t prevdns[2] = {0,0}; + if (dns[0] == 0) { + // This is a disaster. + MGWARN("GGSN: No DNS servers found; GPRS service will not work"); + } else if (dns[0] != prevdns[0] || dns[1] != prevdns[1]) { + char bf[2][30]; + MGINFO("GGSN: DNS servers: %s %s",ip_ntoa(dns[0],bf[0]),ip_ntoa(dns[1],bf[1])); + prevdns[0] = dns[0]; + prevdns[1] = dns[1]; + } + return nfnd; +} + +// Return an array of ip addresses, terminated by a -1 address. +EXPORT uint32_t *ip_findmyaddr() +{ + const int maxaddrs = 5; + static uint32_t addrs[maxaddrs+1]; + int n = 0; + int fd = runcmd("|/bin/hostname","hostname","-I", NULL); + if (fd < 0) { + failed: + addrs[0] = (unsigned) -1; // converts to all 1s + return addrs; + } + //printf("ip_findmyaddr fd=%d\n",fd); + const int bufsize = 2000; + char buf[bufsize]; + int size = read(fd,buf,bufsize); + if (size < 0) { goto failed; } + buf[bufsize-1] = 0; + if (size < bufsize) { buf[size] = 0; } + char *cp = buf; + //printf("ip_findmyaddr buf=%s\n",buf); + while (n < maxaddrs) { + char *ep = strchr(cp,'\n'); + if (ep) *ep = 0; + if (strlen(cp)) { + uint32_t addr = inet_addr(cp); + //printf("ip_findmyaddr cp=%s = 0x%x\n",cp,addr); + if (addr != 0 && addr != (unsigned)-1) { + addrs[n++] = inet_addr(cp); + } + } + if (!ep) { + addrs[n] = (unsigned) -1; // terminate the list. + return addrs; + } + cp = ep+1; + } + addrs[maxaddrs] = (unsigned) -1; // converts to all 1s + return addrs; +} + +}; // namespace diff --git a/SGSNGGSN/miniggsn.cpp b/SGSNGGSN/miniggsn.cpp new file mode 100644 index 00000000..891fddcb --- /dev/null +++ b/SGSNGGSN/miniggsn.cpp @@ -0,0 +1,661 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +//TODO - include the TCP sequence number in the message, +// and identify the duplicate packets, +// then rerun a test downloading a single jpg +// with tossing off, to see what is happening. + +// iptables -t nat -A POSTROUTING -o eth0 -j SNAT --to 192.168.1.8 +// iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE +// iptables -t nat -F # Flush current tables +// iptables -t nat -L # List current tables +// --- + +// Options: +// o 2 tunnels. First is read/write by the ggsn. Second is configured via linux. +// This gives us a pseudo-'device' so we can use linux forwarding. +// iptables -t nat -A POSTROUTING -out eth0 -j MASQUERADE +// iptables -A FORWARD -i tun2 -j ACCEPT +// Another advantage is the tunnel could go to a remote machine. +// Or the first tunnel could terminate back in the OpenBTS. +// o Do NAT myself. +// o Use linux NAT. Write MS->Net packets to router. Bind raw socket on 192.168.1.8 for +// all packets, looking for our own. + +// NOTES: +// The ip tunnel local/remote specify the outer transport layer IP addresses in the IPIP +// protocol header prepended to packets sent on the tunnel. +// The ifconfig adds two local addresses +// +// Tunnel Example: +// Network A: net 10.0.1.0/24 router 10.0.1.1 public address 172.16.17.18 on public net C +// Network B: net 10.0.2.0/24 router 10.0.2.1 public address 172.19.20.21 on public net C +// Network A: +// ip tunnel add netb mode gre remote 172.19.20.21 local 172.16.17.18 +// # Apparently this tunnel is the router. +// ip addr add 10.0.1.1 dev netb +// ip route add 10.0.2.0/24 dev netb +// Or for ipip tunneling: +// ifconfig tunl0 10.0.1.1 pointopoint 172.19.20.21 +// route add -net 10.0.2.0 netmask 255.255.255.0 dev netb +// Network B: +// ip tunnel add neta mode gre remote 172.16.17.18 local 172.19.20.21 +// ip addr add 10.0.2.1 dev neta +// ip addr add 10.0.1.0/24 dev neta +// Or for ipip tunneling: +// ifconfig tunl0 10.0.2.1 pointopoint 172.19.20.21 +// route add -net 10.0.2.0 netmask 255.255.255.0 dev tunl0 +// +// +// Another Tunnel example from IPIPNotes: +// Router_1 eth0: 1.2.3.4 - Asterisk system +// Router_2 eth0: 4.3.2.1 eth1: 10.0.0.1 NAT private network and SIP phones. +// Router_1 +// ip tunnel add iptun mode ipip remote 4.3.2.1 local 10.0.1.1 +// route add -net 10.0.2.0/24 dev iptun +// Router_2 +// ip tunnel add iptun mode ipip remote 1.2.3.4 local 10.0.2.1 +// route add -net 10.0.1.0/24 dev iptun +// Router_1 +// route add -net 10.0.0.0/24 dev iptun +// route add -net 10.0.0.0/24 gw 10.0.0.1 +// Note: The comment is backwards. +// Allows you to ping any device on 10.0.0.0/24 from router_1 (not from router_2) +// Allows any 10.0.0.) device to ping router1 using 10.0.1.1 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // pat added for gethostbyname +#include // pat added for IPv4 iphdr +#include // pat added for tcphdr +#include // pat added for udphdr + +#include // pat added. +#include // pat added. +//#include // pat added, then removed because it defines NCC used in GSMConfig. +#include // pat added +#include // pat added +#include // pat added. +#include +#include +#include +#include "miniggsn.h" +#undef NCC // Make sure. This is defined in ioctl.h, but used as a name in GSMConfig.h. +#include "Ggsn.h" +#include + +// A mini-GGSN included inside the SGSN. +// Each MS will be assigned a dynamic IP address. +// We will use one 256-wide bank of IP address, eg: 192.168.99.1 - 192.68.99.254. +// This needs to be a config option. +// The addresses are be serviced by a NAT running on this box. +// An alternative architecture would be to get IP addresses from a DHCP server +// on whatever router is doing the NAT. +// The NAT could be tied to a static IP; a dynamic IP; or if the bts has two IP addresses, +// the NAT could be tied to the second static or dynamic IP address used solely for phones; +// or tied to a tunnel that goes elsewhere. +// Since there are so many possibilibies, the nat is configured externally. + +namespace SGSN { + +int pdpWriteHighSide(PdpContext *pdp, unsigned char *packet, unsigned len); +int tun_fd = -1; // This is the tunnel we use to talk with the MSs. +FILE *mg_log_fp = NULL; // Extra log file for IP traffic. +int mg_debug_level = 0; + + +// old: +//static char const *mg_base_ip_str = "192.168.99.1"; // This did not raw bind. +//static char const *mg_base_ip_route = "192.168.99.0/24"; +//static char const *mg_net_mask = "255.255.255.0"; // todo: get this from "/24" above. +static struct GgsnConfig { + unsigned mgMaxPduSize; + int mgMaxConnections; // The maximum number of PDP contexts, which is roughly + // the maximum number of simultaneous MS allowed. + unsigned mgIpTimeout; // Dont reuse a connection for this many seconds. + unsigned mgIpTossDup; // Toss duplicate packets. + +} ggConfig; + +// Mini-Firewall rules +struct GgsnFirewallRule { + GgsnFirewallRule *next; + uint32_t ipBasenl; + uint32_t ipMasknl; + GgsnFirewallRule(GgsnFirewallRule *wnext,uint32_t wipbasenl, uint32_t wmasknl) : + next(wnext),ipBasenl(wipbasenl),ipMasknl(wmasknl) {} +}; +GgsnFirewallRule *gFirewallRules = NULL; +void addFirewallRule(uint32_t ipbasenl,uint32_t masknl) { + gFirewallRules = new GgsnFirewallRule(gFirewallRules,ipbasenl,masknl); +} + + +static mg_con_t *mg_cons = 0; + + +// Now in Utils.cpp +//const char *timestr() +//{ +// struct timeval tv; +// struct tm tm; +// static char result[30]; +// gettimeofday(&tv,NULL); +// localtime_r(&tv.tv_sec,&tm); +// unsigned tenths = tv.tv_usec / 100000; // Rounding down is ok. +// sprintf(result," %02d:%02d:%02d.%1d",tm.tm_hour,tm.tm_min,tm.tm_sec,tenths); +// return result; +//} + + + +// Find a free IP connection or return NULL. +// Each PDP context activation gets a new IP address. +// The IP addresses are recycled after tiimeout. +// 7-10-2012: Each MS may ask for several IP addresses with different NSAPI. +// The IMSI+NSAPI now maps to an IP address in a semi-permanent way, so the MS +// effectively has a static IP address within the range assigned by the BTS, +// until the BTS is power cycled. The ptmsi is a unique id associated with the imsi. +mg_con_t *mg_con_find_free(uint32_t ptmsi, int nsapi) +{ + // Start by looking for this specific old connection: + // TODO: This should use a map for efficiency. + int i; + mg_con_t *mgp = &mg_cons[0]; + for (i=0; i < ggConfig.mgMaxConnections; i++, mgp++) { + if (mgp->mg_ptmsi == ptmsi && mgp->mg_nsapi == nsapi) { + return mgp; + } + } + + // Look for an unused IP address. + double now = pat_timef(); + static int mgnextindex = 0; + for (i=0; i < ggConfig.mgMaxConnections; i++) { + mgp = &mg_cons[mgnextindex]; + if (++mgnextindex == ggConfig.mgMaxConnections) { mgnextindex = 0; } + if (mgp->mg_pdp == NULL) { + // Dont reuse an ip address for mg_ip_timeout. + // TCP packets will continue to arrive for an IP address + // for quite some time after it becomes inactive. + if (mgp->mg_time_last_close && mgp->mg_time_last_close + ggConfig.mgIpTimeout > now) continue; + //mgp->mg_pdp = pctx; + mgp->mg_ptmsi = ptmsi; + mgp->mg_nsapi = nsapi; + return mgp; + } + } + return NULL; +} + +void mg_con_open(mg_con_t *mgp,PdpContext *pdp) +{ + mgp->mg_pdp = pdp; +} + +void mg_con_close(mg_con_t *mgp) +{ + mgp->mg_pdp = NULL; + mgp->mg_time_last_close = pat_timef(); +} + +#if 0 +static mg_con_t *mg_con_find_by_ctx(PdpContext *pctx) +{ + int i; mg_con_t *mgp = mg_cons; + for (i=0; i < ggConfig.mgMaxConnections; i++, mgp++) { + if (mgp->mg_pdp == pctx) { return mgp; } + } + return NULL; +} +#endif + +static mg_con_t *mg_con_find_by_ip(uint32_t addr) +{ + int i; mg_con_t *mgp = mg_cons; + for (i=0; i < ggConfig.mgMaxConnections; i++, mgp++) { + if (mgp->mg_ip == addr) { return mgp; } + } + return NULL; +} + +static bool verbose = true; + +static char *packettoa(char *result,unsigned char *packet, int len) +{ + struct iphdr *iph = (struct iphdr*)packet; + char nbuf1[40], nbuf2[40]; + if (verbose && iph->protocol == IPPROTO_TCP) { + struct tcphdr *tcph = (struct tcphdr*) (packet + 4 * iph->ihl); + sprintf(result,"proto=%s %d byte packet seq=%u ack=%u id=%u frag=%u from %s:%d to %s:%d", + ip_proto_name(iph->protocol), len, tcph->seq, tcph->ack_seq, + iph->id, iph->frag_off, + ip_ntoa(iph->saddr,nbuf1),tcph->source, + ip_ntoa(iph->daddr,nbuf2),tcph->dest); + } else { + sprintf(result,"proto=%s %d byte packet from %s to %s", + ip_proto_name(iph->protocol), len, + ip_ntoa(iph->saddr,nbuf1), + ip_ntoa(iph->daddr,nbuf2)); + } + return result; +} + + +unsigned char *miniggsn_rcv_npdu(int *plen, uint32_t *dstaddr) +{ + static unsigned char *recvbuf = NULL; + + if (recvbuf == NULL) { + recvbuf = (unsigned char*)malloc(ggConfig.mgMaxPduSize+2); + if (!recvbuf) { /**error = -ENOMEM;*/ return NULL; } + } + + // The O_NONBLOCK was set by default! Is not happening any more. + { + int flags = fcntl(tun_fd,F_GETFL,0); + if (flags & O_NONBLOCK) { + //MGDEBUG(4,"O_NONBLOCK = %d",O_NONBLOCK & flags); + flags &= ~O_NONBLOCK; // Want Blocking! + int fcntlstat = fcntl(tun_fd,F_SETFL,flags); + MGWARN("ggsn: WARNING: Turning off tun_fd blocking flag, fcntl=%d",fcntlstat); + } + } + + // We can just read from the tunnel. + int ret = read(tun_fd,recvbuf,ggConfig.mgMaxPduSize); + if (ret < 0) { + MGERROR("ggsn: error: reading from tunnel: %s", strerror(errno)); + //*error = ret; + return NULL; + } else if (ret == 0) { + MGERROR("ggsn: error: zero bytes reading from tunnel: %s", strerror(errno)); + //*error = ret; // huh? + return NULL; + } else { + struct iphdr *iph = (struct iphdr*)recvbuf; + { + char infobuf[200]; + MGINFO("ggsn: received %s at %s",packettoa(infobuf,recvbuf,ret), timestr().c_str()); + //MGLOGF("ggsn: received proto=%s %d byte npdu from %s for %s at %s", + //ip_proto_name(iph->protocol), ret, + //ip_ntoa(iph->saddr,nbuf), + //ip_ntoa(iph->daddr,NULL), timestr()); + } + + *dstaddr = iph->daddr; + // TODO: Do we have to allocate a new buffer? + *plen = ret; + // Zero terminate for the convenience of the pinger. + recvbuf[ret] = 0; + return recvbuf; + } +} + +// If this is a duplicate TCP packet, throw it away. +// The MS is so slow to respond that the servers often send dups +// which are unnecessary because we have reliable communication between here +// and the MS, so just toss them. +// Update 3-2012: Always do the check to print messages for dup packets even if not discarded. +static int mg_toss_dup_packet(mg_con_t*mgp,unsigned char *packet, int packetlen) +{ + struct iphdr *iph = (struct iphdr*)packet; + if (iph->protocol != IPPROTO_TCP) { return 0; } + struct tcphdr *tcph = (struct tcphdr*) (packet + 4 * iph->ihl); + if (tcph->rst | tcph->urg) { return 0; } + int i; + for (i = 0; i < MG_PACKET_HISTORY; i++) { + // 3-2012: Jpegs are not going through the system properly. + // I am adding some more checks here to see if we are tossing packets inappropriately. + // The tot_len includes headers, but if they are not the same in the duplicate packet, oh well. + // TODO: If the connection is reset we should zero out our history. + if (mgp->mg_packets[i].saddr == iph->saddr && + mgp->mg_packets[i].daddr == iph->daddr && + mgp->mg_packets[i].totlen == iph->tot_len && + //mgp->mg_packets[i].ipfragoff == iph->frag_off && + //mgp->mg_packets[i].ipid == iph->id && + mgp->mg_packets[i].seq == tcph->seq && + mgp->mg_packets[i].source == tcph->source && + mgp->mg_packets[i].dest == tcph->dest + ) { + const char *what = ggConfig.mgIpTossDup ? "discarding " : ""; + char buf1[40],buf2[40]; + MGINFO("ggsn: %sduplicate %d byte packet seq=%d frag=%d id=%d src=%s:%d dst=%s:%d",what, + packetlen,tcph->seq,iph->frag_off,iph->id, + ip_ntoa(iph->saddr,buf1),tcph->source, + ip_ntoa(iph->daddr,buf2),tcph->dest); + return ggConfig.mgIpTossDup; // Toss duplicate tcp packet if option set. + } + } + i = mgp->mg_oldest_packet; + if (++mgp->mg_oldest_packet >= MG_PACKET_HISTORY) { mgp->mg_oldest_packet = 0; } + mgp->mg_packets[i].saddr = iph->saddr; + mgp->mg_packets[i].daddr = iph->daddr; + mgp->mg_packets[i].totlen = iph->tot_len; + //mgp->mg_packets[i].ipfragoff = iph->frag_off; + //mgp->mg_packets[i].ipid = iph->id; + mgp->mg_packets[i].seq = tcph->seq; + mgp->mg_packets[i].source = tcph->source; + mgp->mg_packets[i].dest = tcph->dest; + return 0; // Do not toss. +} + +// There is data available on the socket. Go get it. +// see handle_nsip_read() +void miniggsn_handle_read() +{ + int packetlen; + uint32_t dstaddr; + unsigned char *packet = miniggsn_rcv_npdu(&packetlen, &dstaddr); + if (!packet) { return; } + + // We need to reassociate the packet with the PdpContext to which it belongs. + mg_con_t *mgp = mg_con_find_by_ip(dstaddr); + if (mgp == NULL || mgp->mg_pdp == NULL) { + MGERROR("ggsn: error: cannot find PDP context for incoming packet for IP dstaddr=%s", + ip_ntoa(dstaddr,NULL)); + return; // -1; + } + + if (mg_toss_dup_packet(mgp,packet,packetlen)) { return; } + + PdpContext *pdp = mgp->mg_pdp; + //MGDEBUG(2,"miniggsn_handle_read pdp=%p",pdp); + pdp->pdpWriteHighSide(packet,packetlen); +} + + +// The npdu is a raw packet including the ip header. +int miniggsn_snd_npdu_by_mgc(mg_con_t *mgp,unsigned char *npdu, unsigned len) +{ + // Verify the IP header. + struct iphdr *ipheader = (struct iphdr*)npdu; + // The return address must be the MS itself. + uint32_t ms_ip_address = mgp->mg_ip; + uint32_t packet_source_ip_addr = ipheader->saddr; + uint32_t packet_dest_ip_addr = ipheader->daddr; + + char infobuf[200]; + MGINFO("ggsn: writing %s at %s",packettoa(infobuf,npdu,len),timestr().c_str()); + //MGLOGF("ggsn: writing proto=%s %d byte npdu to %s from %s at %s", + //ip_proto_name(ipheader->protocol), + //len,ip_ntoa(packet_dest_ip_addr,NULL), + //ip_ntoa(packet_source_ip_addr,nbuf), timestr().c_str()); + +#define MUST_HAVE(assertion) \ + if (! (assertion)) { MGERROR("ggsn: Packet failed test, discarded: %s",#assertion); return -1; } + + if (mg_debug_level > 2) ip_hdr_dump(npdu,"npdu"); + MUST_HAVE(ipheader->version == 4); // 4 as in IPv4 + MUST_HAVE(ipheader->ihl >= 5); // Minimum header length is 5 words. + + int checksum = ip_checksum(ipheader,sizeof(*ipheader),NULL); + MUST_HAVE(checksum == 0); // If fails, packet is bad. + + MUST_HAVE(ipheader->ttl > 0); // Time to live - how many hops allowed. + + + // The blackberry sends ICMP packets, so we better support. + // I'm just going to allow any protocol through. + //MUST_HAVE(ipheader->protocol == IPPROTO_TCP || + // ipheader->protocol == IPPROTO_UDP || + // ipheader->protocol == IPPROTO_ICMP); + + MUST_HAVE(packet_source_ip_addr == ms_ip_address); + +#if OLD_FIREWALL + // The destination address may not be a local address on this machine + // as configured by the operator. + // Note these are all in network order, so be careful. + //uint32_t local_ip_addr = inet_addr(mg_base_ip_str); // probably "192.168.99.1" + uint32_t net_mask = inet_addr(mg_net_mask); // probably "255.255.255.0" + MUST_HAVE((packet_dest_ip_addr & net_mask) != (local_ip_addr & net_mask)); +#endif + + for (GgsnFirewallRule *rp = gFirewallRules; rp; rp = rp->next) { + MUST_HAVE((packet_dest_ip_addr & rp->ipMasknl) != (rp->ipBasenl & rp->ipMasknl)); + } + + // Decrement ttl and recompute checksum. We are doing this in place. + ipheader->ttl--; + ipheader->check = 0; + //ipheader->check = htons(ip_checksum(ipheader,sizeof(*ipheader),NULL)); + ipheader->check = ip_checksum(ipheader,sizeof(*ipheader),NULL); + + // Just write to the MS-side tunnel device. + + int result = write(tun_fd,npdu,len); + if (result != (int) len) { + MGERROR("ggsn: error: write(tun_fd,%d) result=%d %s",len,result,strerror(errno)); + } + return 0; +} + +#if 0 // not needed +// Route an n-pdu from the MS out to the internet. +// Called by SNDCP when it has received/re-assembled a N-PDU +int miniggsn_snd_npdu(PdpContext *pctx,unsigned char *npdu, unsigned len) +{ + // Find the fd from the pctx; We should put this in the pdp_ctx. + mg_con_t *mgp = mg_con_find_by_ctx(pctx); + if (mgp == NULL) { return -1; } // Whoops + return miniggsn_snd_npdu_by_mgc(mgp, npdu, len); +} +#endif + +time_t gGgsnInitTime; + +bool miniggsn_init() +{ + static int initstatus = -1; // -1: uninited; 0:init failed; 1: init succeeded. + if (initstatus >= 0) {return initstatus;} + initstatus = 0; // assume failure. + + + // We init config options at GGSN startup. + // They cannot be changed while running. + // To change an option, you would have to stop and restart the GGSN. + ggConfig.mgIpTimeout = gConfig.getNum("GGSN.IP.ReuseTimeout"); + ggConfig.mgMaxPduSize = gConfig.getNum("GGSN.IP.MaxPacketSize"); + ggConfig.mgMaxConnections = gConfig.getNum("GGSN.MS.IP.MaxCount"); + ggConfig.mgIpTossDup = gConfig.getBool("GGSN.IP.TossDuplicatePackets"); + + + string logfile = gConfig.getStr("GGSN.Logfile.Name"); + if (logfile.length()) { + mg_log_fp = fopen(logfile.c_str(),"w"); + if (mg_log_fp == 0) { + MGERROR("could not open %s log file:%s","GGSN.Logfile.Name",logfile.c_str()); + } + } + + // This is the first message in the newly opened file. + time(&gGgsnInitTime); + MGINFO("Initializing Mini GGSN %s",ctime(&gGgsnInitTime)); // ctime includes a newline. + + if (mg_log_fp) { + mg_debug_level = 1; + MGINFO("GGSN logging to file %s",logfile.c_str()); + } + + if (ggConfig.mgMaxConnections > 254) { + MGERROR("GGSN.MS.IP.MaxCount specifies too many connections (%d) specifed, using 254", + ggConfig.mgMaxConnections); + ggConfig.mgMaxConnections = 254; + } + + // We need three IP things: + // 1. the route expressed using "/maskbits" notation, + // 2. the base ip address, + // 3. the mask for the MS (which has very little to do with the netmask of the host we are running on.) + // All three can be derived from the ipRoute, if specfied. + // But conceivably the user might want to start their base ip address elsewhere. + + const char *ip_base_str = gConfig.getStr("GGSN.MS.IP.Base").c_str(); + uint32_t mgIpBasenl = inet_addr(ip_base_str); + if (mgIpBasenl == INADDR_NONE) { + MGERROR("miniggn: GGSN.MS.IP.Base address invalid:%s",ip_base_str); + return false; + } + + if ((ntohl(mgIpBasenl) & 0xff) == 0) { + MGERROR("miniggn: GGSN.MS.IP.Base address should not end in .0 but proceeding anyway: %s",ip_base_str); + } + + const char *route_str = 0; + char route_buf[40]; + string route_save; + if (gConfig.defines("GGSN.MS.IP.Route")) { + route_save = gConfig.getStr("GGSN.MS.IP.Route"); + route_str = route_save.c_str(); + } + + uint32_t route_basenl, route_masknl; + if (route_str && *route_str && *route_str != ' ') { + if (strlen(route_str) > strlen("aaa.bbb.ccc.ddd/yy") + 2) { // add some slop. + MGWARN("miniggn: GGSN.MS.IP.Route address is too long:%s",route_str); + // but use it anyway. + } + + if (! ip_addr_crack(route_str,&route_basenl,&route_masknl) || route_basenl == INADDR_NONE) { + MGWARN("miniggsn: GGSN.MS.IP.Route is not a valid ip address: %s",route_str); + // but use it anyway. + } + if (route_masknl == 0) { + MGWARN("miniggsn: GGSN.MS.IP.Route is not a valid route, /mask part missing or invalid: %sn", + route_str); + // but use it anyway. + } + + // We would like to check that the base ip is within the ip route range, + // which is tricky, but check the most common case: + if ((route_basenl&route_masknl) != (mgIpBasenl&route_masknl)) { + MGWARN("miniggsn: GGSN.MS.IP.Base = %s ip address does not appear to be in range of GGSN.MS.IP.Route = %s", + ip_base_str, route_str); + // but use it anyway. + } + } else { + // Manufacture a route string. Assume route is 24 bits. + route_masknl = inet_addr("255.255.255.0"); + route_basenl = mgIpBasenl & route_masknl; // Set low byte to 0. + ip_ntoa(route_basenl,route_buf); + strcat(route_buf,"/24"); + route_str = route_buf; + } + + // Firewall rules: + bool firewall_enable; + if ((firewall_enable = gConfig.getNum("GGSN.Firewall.Enable"))) { + // Block anything in the routed range: + addFirewallRule(route_basenl,route_masknl); + // Block local loopback: + uint32_t tmp_basenl,tmp_masknl; + if (ip_addr_crack("127.0.0.1/24",&tmp_basenl,&tmp_masknl)) { + addFirewallRule(tmp_basenl,tmp_masknl); + } + // Block the OpenBTS station itself: + uint32_t *myaddrs = ip_findmyaddr(); + for ( ; *myaddrs != (unsigned)-1; myaddrs++) { + addFirewallRule(*myaddrs,0xffffffff); + } + if (firewall_enable >= 2) { + // Block all private addresses: + // 16-bit block (/16 prefix, 256 × C) 192.168.0.0 192.168.255.255 65536 + uint32_t private_addrnl = inet_addr("192.168.0.0"); + uint32_t private_masknl = inet_addr("255.255.0.0"); + addFirewallRule(private_addrnl,private_masknl); + // 20-bit block (/12 prefix, 16 × B) 172.16.0.0 172.31.255.255 1048576 + private_addrnl = inet_addr("172.16.0.0"); + private_masknl = inet_addr("255.240.0.0"); + addFirewallRule(private_addrnl,private_masknl); + // 24-bit block (/8 prefix, 1 × A) 10.0.0.0 10.255.255.255 16777216 + private_addrnl = inet_addr("10.0.0.0"); + private_masknl = inet_addr("255.0.0.0"); + addFirewallRule(private_addrnl,private_masknl); + } + } + + MGINFO("GGSN Configuration:"); + MGINFO(" GGSN.MS.IP.Base=%s", ip_ntoa(mgIpBasenl,NULL)); + MGINFO(" GGSN.MS.IP.MaxCount=%d", ggConfig.mgMaxConnections); + MGINFO(" GGSN.MS.IP.Route=%s", route_str); + MGINFO(" GGSN.IP.MaxPacketSize=%d", ggConfig.mgMaxPduSize); + MGINFO(" GGSN.IP.ReuseTimeout=%d", ggConfig.mgIpTimeout); + MGINFO(" GGSN.Firewall.Enable=%d", firewall_enable); + MGINFO(" GGSN.IP.TossDuplicatePackets=%d", ggConfig.mgIpTossDup); + if (firewall_enable) { + MGINFO("GGSN Firewall Rules:"); + for (GgsnFirewallRule *rp = gFirewallRules; rp; rp = rp->next) { + char buf1[40], buf2[40]; + MGINFO(" block ip=%s mask=%s",ip_ntoa(rp->ipBasenl,buf1),ip_ntoa(rp->ipMasknl,buf2)); + } + } + uint32_t dns[2]; // We dont use the result, we just want to print out the DNS servers now. + ip_finddns(dns); // The dns servers are polled again later. + + const char *tun_if_name = gConfig.getStr("GGSN.TunName").c_str(); + + if (tun_fd == -1) { + ip_init(); + tun_fd = ip_tun_open(tun_if_name,route_str); + if (tun_fd < 0) { + MGERROR("ggsn: ERROR: Could not open tun device %s",tun_if_name); + return false; + } + } + + // DEBUG: Try it again. + //printf("DEBUG: Opening tunnel again: %d\n",ip_tun_open(tun_if_name,route_str)); + + if (mg_cons) free(mg_cons); + mg_cons = (mg_con_t*)calloc(ggConfig.mgMaxConnections,sizeof(mg_con_t)); + if (mg_cons == 0) { + MGERROR("ggsn: ERROR: out of memory"); + return false; + } + //memset(mg_cons,0,sizeof(mg_cons)); + + uint32_t base_iphl = ntohl(mgIpBasenl); + // 8-15: no dont do this. It subverts the purpose of the BASE ip address. + //base_iphl &= ~255; // In case they specify 192.168.2.1, make it 192.168.2.0 + int i; + // If the last digit is 0 (192.168.99.0), change it to 1 for the first IP addr served. + if ((base_iphl & 255) == 0) { base_iphl++; } + for (i=0; i < ggConfig.mgMaxConnections; i++) { + mg_cons[i].mg_ip = htonl(base_iphl + i); + //mg_cons[i].mg_ip = htonl(base_iphl + 1 + i); + // DEBUG!!!!! Use my own ip address. + //mg_cons[i].mg_ip = inet_addr("192.168.1.99"); + //printf("adding IP=%s\n",ip_ntoa(mg_cons[i].mg_ip,NULL)); + } + initstatus = 1; + return initstatus; +} + + +}; // namespace diff --git a/SGSNGGSN/miniggsn.h b/SGSNGGSN/miniggsn.h new file mode 100644 index 00000000..5db10f76 --- /dev/null +++ b/SGSNGGSN/miniggsn.h @@ -0,0 +1,106 @@ +/* +* Copyright 2011 Range Networks, Inc. +* All Rights Reserved. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#ifndef _MINIGGSN_H_ +#define _MINIGGSN_H_ +#include +#include "Logger.h" + +namespace SGSN { + +struct PdpContext; + +// MiniGGSN IP connections. +// This holds the IP address and is slightly different than a PDPContext: +// o The IP address is permanently allocated. +// o The PDPContext is strictly a handle for the MS; it is deleted when PDPContext is deactivated +// or when the MS is deleted. +// o There can be multiple PDPContext with the same IP address. +// o When a PDPContext is deallocated, there may still be messages for it in the message queue, +// so those messages point to the permanet mg_con_s instead of the PdpContext. +// o The IP address must remain reserved for a period of time after a PdpContext is deleted/deactivated. +// Note: This was written in C originally. +typedef struct mg_con_s { + PdpContext *mg_pdp; // Points back to the PDP context using this connection. + uint32_t mg_ptmsi; // The ptmsi that is using this IP connection. + int mg_nsapi; // The nsapi in this ptmsi that is using this IP connection. + uint32_t mg_ip; // The IP address used for this connection, in network order. + // Keep track of the last few tcp packets received: +#define MG_PACKET_HISTORY 60 + struct mg_packets { + uint16_t source, dest; // TCP source and dest ports. + uint16_t totlen; // IP length, which includes headers. + uint32_t seq; // TCP sequence number + uint32_t saddr, daddr; // source and destination IP addr + uint16_t ipid, ipfragoff; // Added 3-2012 + } mg_packets[MG_PACKET_HISTORY]; + int mg_oldest_packet; + double mg_time_last_close; +} mg_con_t; +#define MG_CON_DEFINED + +unsigned char *miniggsn_rcv_npdu(int *error, int *plen, uint32_t *dstaddr); +int miniggsn_snd_npdu(PdpContext *pctx,unsigned char *npdu, unsigned len); +int miniggsn_snd_npdu_by_mgc(mg_con_t *mgp,unsigned char *npdu, unsigned len); +void miniggsn_handle_read(); +bool miniggsn_init(); +mg_con_t *mg_con_find_free(uint32_t ptmsi, int nsapi); +void mg_con_close(mg_con_t *mgp); +void mg_con_open(mg_con_t *mgp,PdpContext *pdp); + +//extern int pinghttp(char *whoto,char *whofrom,mg_con_t *mgp); + +extern int tun_fd; + +// From iputils.h: +bool ip_addr_crack(const char *address,uint32_t *paddr, uint32_t *pmask); +char *ip_ntoa(int32_t ip, char *buf); +char *ip_sockaddr2a(void * /*struct sockaddr * */ sap,char *buf); +int ip_add_addr(char *ifname, int32_t ipaddr, int maskbits); +const char *ip_proto_name(int ipproto); +unsigned int ip_checksum(void *ptr, unsigned len, void *dummyhdr); +void ip_hdr_dump(unsigned char *packet, const char *msg); +int runcmd(const char *path, ...); +int ip_tun_open(const char *tname, const char *addrstr); +void ip_init(); +int ip_finddns(uint32_t*); +uint32_t *ip_findmyaddr(); +extern double pat_timef(), pat_elapsedf(); + + +// Logging. These date from when this was part of the SGSN, written in C. +extern FILE *mg_log_fp; +extern time_t gGgsnInitTime; + // formerly: fprintf(mg_log_fp,"%.1f:",pat_timef() - gGgsnInitTime); +#define MGLOGF(...) if (mg_log_fp) { \ + fprintf(mg_log_fp,"%s:",timestr().c_str()); \ + fprintf(mg_log_fp, __VA_ARGS__); \ + fputc('\n',mg_log_fp); \ + fflush(mg_log_fp); \ + } +#define MGLOG(stuff) {std::ostringstream ss; ss<0){LOG(DEBUG)<0){LOG(ERR)<0){LOG(WARNING)<0){LOG(INFO)<. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -106,6 +98,9 @@ const char* SIP::SIPStateString(SIPState s) case Canceled: return "Canceled"; case Cleared: return "Cleared"; case MessageSubmit: return "SMS-Submit"; + case HandoverInbound: return "HandoverInbound"; + case HandoverInboundReferred: return "HandoverInboundReferred"; + case HandoverOutbound: return "HandoverOutbound"; default: return NULL; } } @@ -214,7 +209,7 @@ void SIPEngine::saveResponse(osip_message_t *response) -void SIPEngine::saveBYE(const osip_message_t *BYE, bool /*mine*/) +void SIPEngine::saveBYE(const osip_message_t *BYE, bool mine) { // Instead of cloning, why not just keep the old one? // Because that doesn't work in all calling contexts. @@ -223,7 +218,7 @@ void SIPEngine::saveBYE(const osip_message_t *BYE, bool /*mine*/) osip_message_clone(BYE,&mBYE); } -void SIPEngine::saveCANCEL(const osip_message_t *CANCEL, bool /*mine*/) +void SIPEngine::saveCANCEL(const osip_message_t *CANCEL, bool mine) { // Instead of cloning, why not just keep the old one? // Because that doesn't work in all calling contexts. @@ -232,7 +227,7 @@ void SIPEngine::saveCANCEL(const osip_message_t *CANCEL, bool /*mine*/) osip_message_clone(CANCEL,&mCANCEL); } -void SIPEngine::saveERROR(const osip_message_t *ERROR, bool /*mine*/) +void SIPEngine::saveERROR(const osip_message_t *ERROR, bool mine) { // Instead of cloning, why not just keep the old one? // Because that doesn't work in all calling contexts. @@ -275,27 +270,43 @@ void SIPEngine::user( const char * wCallID, const char * IMSI, const char *origI mRemoteDomain = string(origHost); } +string randy401(osip_message_t *msg) +{ + if (msg->status_code != 401) return ""; + osip_www_authenticate_t *auth = (osip_www_authenticate_t*)osip_list_get(&msg->www_authenticates, 0); + if (auth == NULL) return ""; + char *rand = osip_www_authenticate_get_nonce(auth); + string rands = rand ? string(rand) : ""; + if (rands.length()!=32) { + LOG(WARNING) << "SIP RAND wrong length: " << rands; + return ""; + } + return rands; +} + -void SIPEngine::writePrivateHeaders(osip_message_t *msg, const GSM::LogicalChannel* chan) +void SIPEngine::writePrivateHeaders(osip_message_t *msg, const GSM::LogicalChannel *chan) { // P-PHY-Info // This is a non-standard private header in OpenBTS. - // TA= TE= UpRSSI= TxPwr= DnRSSIdBm= - // Get the values -#if 0 + // TA= TE= UpRSSI= TxPwr= + // DnRSSIdBm= time= + // Get the values. if (chan) { - char phy_info[200]; - sprintf(phy_info,"OpenBTS; TA=%d TE=%f UpRSSI=%f TxPwr=%d DnRSSIdBm=%d", + LOG(DEBUG); + char phy_info[400]; + sprintf(phy_info,"OpenBTS; TA=%d TE=%f UpRSSI=%f TxPwr=%d DnRSSIdBm=%d time=%9.3lf", chan->actualMSTiming(), chan->timingError(), chan->RSSI(), chan->actualMSPower(), - chan->measurementResults().RXLEV_FULL_SERVING_CELL_dBm()); + chan->measurementResults().RXLEV_FULL_SERVING_CELL_dBm(), + chan->timestamp()); + LOG(DEBUG) << "PHY-info: " << phy_info; osip_message_set_header(msg,"P-PHY-Info",phy_info); } -#endif // P-Access-Network-Info // See 3GPP 24.229 7.2. - char cgi_3gpp[50]; + char cgi_3gpp[256]; sprintf(cgi_3gpp,"3GPP-GERAN; cgi-3gpp=%s%s%04x%04x", gConfig.getStr("GSM.Identity.MCC").c_str(),gConfig.getStr("GSM.Identity.MNC").c_str(), (unsigned)gConfig.getNum("GSM.Identity.LAC"),(unsigned)gConfig.getNum("GSM.Identity.CI")); @@ -303,18 +314,25 @@ void SIPEngine::writePrivateHeaders(osip_message_t *msg, const GSM::LogicalChann // P-Preferred-Identity // See RFC-3325. - char pref_id[50]; + char pref_id[350]; sprintf(pref_id,"", mSIPUsername.c_str(), gConfig.getStr("SIP.Proxy.Speech").c_str()); osip_message_set_header(msg,"P-Preferred-Identity",pref_id); + // Check for illegal hostname length. 253 bytes for domain name + 6 bytes for port and colon. + // This isn't "pretty", but it should be fast, and gives us a ballpark. Their hostname will + // fail elsewhere if it is longer than 253 bytes (since this assumes a 5 byte port string). + if (gConfig.getStr("SIP.Proxy.Speech").length() > 259) { + LOG(ALERT) << "Configured SIP.Proxy.Speech hostname is great than 253 bytes!"; + } // FIXME -- Use the subscriber registry to look up the E.164 // and make a second P-Preferred-Identity header. } -bool SIPEngine::Register( Method wMethod ) + +bool SIPEngine::Register( Method wMethod , const GSM::LogicalChannel* chan, string *RAND, const char *IMSI, const char *SRES) { LOG(INFO) << "user " << mSIPUsername << " state " << mState << " " << wMethod << " callID " << mCallID; @@ -334,18 +352,20 @@ bool SIPEngine::Register( Method wMethod ) 60*gConfig.getNum("SIP.RegistrationPeriod"), mSIPPort, mSIPIP.c_str(), mProxyIP.c_str(), mMyTag.c_str(), - mViaBranch.c_str(), mCallID.c_str(), mCSeq + mViaBranch.c_str(), mCallID.c_str(), mCSeq, + RAND, IMSI, SRES ); } else if (wMethod == SIPUnregister ) { reg = sip_register( mSIPUsername.c_str(), 0, mSIPPort, mSIPIP.c_str(), mProxyIP.c_str(), mMyTag.c_str(), - mViaBranch.c_str(), mCallID.c_str(), mCSeq + mViaBranch.c_str(), mCallID.c_str(), mCSeq, + NULL, NULL, NULL ); } else { assert(0); } - //writePrivateHeaders(reg,chan); + writePrivateHeaders(reg,chan); gReports.incr("OpenBTS.SIP.REGISTER.Out"); LOG(DEBUG) << "writing registration " << reg; @@ -358,7 +378,7 @@ bool SIPEngine::Register( Method wMethod ) try { // SIPInterface::read will throw SIPTIimeout if it times out. // It should not return NULL. - msg = gSIPInterface.read(mCallID, gConfig.getNum("SIP.Timer.E")); + msg = gSIPInterface.read(mCallID, gConfig.getNum("SIP.Timer.E"),NULL); } catch (SIPTimeout) { // send again LOG(NOTICE) << "SIP REGISTER packet to " << mProxyIP << ":" << mProxyPort << " timeout; resending"; @@ -376,8 +396,18 @@ bool SIPEngine::Register( Method wMethod ) break; } if (status==401) { - LOG(INFO) << "REGISTER fail -- unauthorized"; - break; + string wRAND = randy401(msg); + // if rand is included on 401 unauthorized, then the challenge-response game is afoot + if (wRAND.length() != 0 && RAND != NULL) { + LOG(INFO) << "REGISTER challenge RAND=" << wRAND; + *RAND = wRAND; + osip_message_free(msg); + osip_message_free(reg); + return false; + } else { + LOG(INFO) << "REGISTER fail -- unauthorized"; + break; + } } if (status==404) { LOG(INFO) << "REGISTER fail -- not found"; @@ -403,91 +433,55 @@ bool SIPEngine::Register( Method wMethod ) } - -const char* geoprivTemplate = -"\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "%s\n" - "\n" - "\n" - "\n" - "\n" - "no\n" - "\n" - "\n" - "\n" - "\n" - "\n"; - -SIPState SIPEngine::SOSSendINVITE(short wRtp_port, unsigned wCodec, const GSM::LogicalChannel *chan) +float geodecode1(const char **p, int *err, bool colonExpected) { - LOG(INFO) << "user " << mSIPUsername << " state " << mState; - // Before start, need to add mCallID - gSIPInterface.addCall(mCallID); - gReports.incr("OpenBTS.SIP.INVITE-SOS.Out"); - - // Set Invite params. - // new CSEQ and codec - char tmp[50]; - make_branch(tmp); - mViaBranch = tmp; - mCodec = wCodec; - mCSeq++; - - mRemoteDomain = gConfig.getStr("Control.Emergency.Destination.Host"); - mRemoteUsername = gConfig.getStr("Control.Emergency.Destination.User"); - - mRTPPort= wRtp_port; - - LOG(DEBUG) << "To: " << mRemoteUsername << "@" << mRemoteDomain; - LOG(DEBUG) << "From: " << mSIPUsername << "@" << mSIPIP; - - osip_message_t *invite; - if (gConfig.defines("Control.Emergency.RFC5031")) { - invite = sip_invite5031( - mRTPPort, mSIPUsername.c_str(), - mSIPPort, mSIPIP.c_str(), mProxyIP.c_str(), - mMyTag.c_str(), mViaBranch.c_str(), mCallID.c_str(), mCSeq, mCodec); - } else { - invite = sip_invite( - mRemoteUsername.c_str(), mRTPPort, mSIPUsername.c_str(), - mSIPPort, mSIPIP.c_str(), mProxyIP.c_str(), - mMyTag.c_str(), mViaBranch.c_str(), mCallID.c_str(), mCSeq, mCodec); + float n = 0; + const char *q = *p; + while (**p >= '0' && **p <= '9') { + n = n * 10 + **p - '0'; + (*p)++; } + if (q == *p) *err = 1; + if (colonExpected) { + if (**p == ':') { + (*p)++; + } else { + *err = 1; + } + } + return n; +} - writePrivateHeaders(invite,chan); - // Add RFC-4119 geolocation XML to content area, if available. - if (gConfig.defines("Control.Emergency.Geolocation")) { - char xml[strlen(geoprivTemplate) + 100]; - sprintf(xml,geoprivTemplate, - mSIPUsername.c_str(), gConfig.getStr("Control.Emergency.GatewaySwitch").c_str(), - gConfig.getStr("Control.Emergency.Geolocation").c_str()); - osip_message_set_content_type(invite, strdup("application/pidf+xml")); - char tmp[20]; - sprintf(tmp,"%u", static_cast(strlen(xml))); - osip_message_set_content_length(invite, strdup(tmp)); - osip_message_set_body(invite,xml,strlen(xml)); +float geodecode(const char **p, int *err) +{ + float n = 0; + float m = 1; + while (**p == ' ') { + (*p)++; } - - // Send Invite to Asterisk. - gSIPInterface.write(&mProxyAddr,invite); - saveINVITE(invite,true); - osip_message_free(invite); - mState = Starting; - return mState; + if (**p == '-') { + m = -1; + (*p)++; + } + n = geodecode1(p, err, true); + n += geodecode1(p, err, true)/60.0; + n += geodecode1(p, err, false)/3600.0; + if (**p == ' ' || **p == 0) return n * m; + switch (**p) { + case 'N': + case 'E': + (*p)++; + return n * m; + case 'S': + case 'W': + (*p)++; + return n * m * -1.0; + } + *err = 1; + return 0; } - SIPState SIPEngine::MOCSendINVITE( const char * wCalledUsername, const char * wCalledDomain , short wRtp_port, unsigned wCodec, const GSM::LogicalChannel *chan) @@ -519,20 +513,21 @@ SIPState SIPEngine::MOCSendINVITE( const char * wCalledUsername, mMyTag.c_str(), mViaBranch.c_str(), mCallID.c_str(), mCSeq, mCodec); writePrivateHeaders(invite,chan); - + // Send Invite. gSIPInterface.write(&mProxyAddr,invite); saveINVITE(invite,true); osip_message_free(invite); mState = Starting; return mState; -} +}; SIPState SIPEngine::MOCResendINVITE() { assert(mINVITE); LOG(INFO) << "user " << mSIPUsername << " state " << mState; + LOG(NOTICE) << "SIP INVITE packet to " << mProxyIP << ":" << mProxyPort << " timedout; resending"; gSIPInterface.write(&mProxyAddr,mINVITE); return mState; } @@ -681,7 +676,6 @@ SIPState SIPEngine::MOCCheckForOK(Mutex *lock) return mState; } - SIPState SIPEngine::MOCSendACK() { assert(mLastResponse); @@ -730,17 +724,19 @@ SIPState SIPEngine::MODSendERROR(osip_message_t * cause, int code, const char * { LOG(INFO) << "user " << mSIPUsername << " state " << mState; if (NULL == cause){ - assert(mINVITE); + if (!mINVITE){ + LOG(WARNING) << "Sending ERROR without invite, probably a CLI generated message"; + return mState; + } cause = mINVITE; } - /* 480 is unavail */ - osip_message_t * unavail = sip_error(cause, mSIPIP.c_str(), - mSIPUsername.c_str(), mSIPPort, - code, reason); - gSIPInterface.write(&mProxyAddr,unavail); - saveERROR(unavail, true); - osip_message_free(unavail); + osip_message_t * error = sip_error(cause, mSIPIP.c_str(), + mSIPUsername.c_str(), mSIPPort, + code, reason); + gSIPInterface.write(&mProxyAddr,error); + saveERROR(error, true); + osip_message_free(error); if (cancel){ mState = MODCanceling; } @@ -760,7 +756,6 @@ SIPState SIPEngine::MODSendCANCEL() mState = MODCanceling; return mState; } - SIPState SIPEngine::MODResendBYE() { @@ -774,7 +769,7 @@ SIPState SIPEngine::MODResendBYE() SIPState SIPEngine::MODResendCANCEL() { LOG(INFO) << "user " << mSIPUsername << " state " << mState; - assert(mState==MODCanceling); + if (mState!=MODCanceling) LOG(ERR) << "incorrect state for this method"; assert(mCANCEL); gSIPInterface.write(&mProxyAddr,mCANCEL); return mState; @@ -893,7 +888,7 @@ SIPState SIPEngine::MODWaitForCANCELOK(Mutex *lock) static bool containsResponse(vector *validResponses, unsigned code) { - for (size_t i = 0; i < validResponses->size(); i++) { + for (int i = 0; i < validResponses->size(); i++) { if (validResponses->at(i) == code) return true; } @@ -999,7 +994,7 @@ SIPState SIPEngine::MTDCheckBYE() // If no messages, there is no change in state. if (fifoSize==0) return mState; - osip_message_t * msg = gSIPInterface.read(mCallID); + osip_message_t * msg = gSIPInterface.read(mCallID,0,NULL); if (msg->sip_method) { @@ -1011,7 +1006,7 @@ SIPState SIPEngine::MTDCheckBYE() } //repeated ACK, send OK //pretty sure this never happens, but someone else left a fixme before... -kurtis - if (strcmp(msg->sip_method,"ACK")==0) { + if (strcmp(msg->sip_method,"ACK")==0) { LOG(DEBUG) << "Not responding to repeated ACK. FIXME"; } } @@ -1109,13 +1104,12 @@ SIPState SIPEngine::MTCCheckForACK(Mutex *lock) // wait for ack,set this to timeout of // of call channel. If want a longer timeout // period, need to split into 2 handle situation - // like MOC where this fxn if called multiple times. + // like MOC where this fxn is called multiple times. LOG(INFO) << "user " << mSIPUsername << " state " << mState; //if (mState==Fail) return mState; //osip_message_t * ack = NULL; osip_message_t * ack; - // FIXME -- This is supposed to retransmit BYE on timer I. try { ack = gSIPInterface.read(mCallID, gConfig.getNum("SIP.Timer.H"), lock); } @@ -1181,8 +1175,7 @@ SIPState SIPEngine::MTCCheckForCancel() osip_message_t * msg; try { - //block for very small amount of time - msg = gSIPInterface.read(mCallID,1); + msg = gSIPInterface.read(mCallID,0,NULL); } catch (SIPTimeout& e) { gReports.incr("OpenBTS.SIP.ReadTimeout"); @@ -1393,7 +1386,7 @@ SIPState SIPEngine::MOSMSSendMESSAGE(const char * wCalledUsername, mSIPPort, mSIPIP.c_str(), mProxyIP.c_str(), mMyTag.c_str(), mViaBranch.c_str(), mCallID.c_str(), mCSeq, messageText, contentType); - + writePrivateHeaders(message,chan); // Send Invite to the SIP proxy. @@ -1466,6 +1459,7 @@ SIPState SIPEngine::MOSMSWaitForSubmit(Mutex *lock) } + SIPState SIPEngine::MTSMSSendOK(const GSM::LogicalChannel *chan) { LOG(INFO) << "user " << mSIPUsername << " state " << mState; @@ -1563,4 +1557,138 @@ bool SIPEngine::sameINVITE(osip_message_t * msg){ return n1==n2; } + +SIPState SIPEngine::inboundHandoverCheckForOK(Mutex *lock) +{ + return MOCCheckForOK(lock); +} + + + +SIPState SIPEngine::inboundHandoverSendACK() +{ + return MOCSendACK(); +} + +SIPState SIPEngine::inboundHandoverSendINVITE(TransactionEntry *transaction, unsigned wRTPPort) +{ + // We are "BS2" in the handover ladder diagram. + + LOG(INFO) << "user " << mSIPUsername << " state " << mState; + // Before start, need to add mCallID + mCallID = transaction->CallID(); + gSIPInterface.addCall(mCallID); + + // Set Invite params. + // New from tag + via branch + // new CSEQ and codec + char tmp[100]; + make_tag(tmp); + make_branch(tmp); + mViaBranch = tmp; + mCodec = transaction->Codec(); + mCSeq = transaction->CSeq(); + mCSeq++; + + mRemoteDomain = transaction->ToIP(); + mRemoteUsername = transaction->ToUsername(); + mRTPPort = wRTPPort; + mRTPRemPort = transaction->RTPRemPort(); + mRTPRemIP = transaction->RTPRemIP(); + mSIPUsername = transaction->FromUsername(); + string SIPDisplayname = transaction->FromUsername(); + mFromTag = transaction->FromTag(); + + if(mSession == NULL) { + mSession = rtp_session_new(RTP_SESSION_SENDRECV); + // do what we need to from InitRTP() without a message + bool rfc2833 = gConfig.defines("SIP.DTMF.RFC2833"); + if (rfc2833) { + RtpProfile* profile = rtp_session_get_send_profile(mSession); + int index = gConfig.getNum("SIP.DTMF.RFC2833.PayloadType"); + rtp_profile_set_payload(profile,index,&payload_type_telephone_event); + // Do we really need this next line? + rtp_session_set_send_profile(mSession,profile); + } + rtp_session_set_blocking_mode(mSession, TRUE); + rtp_session_set_scheduling_mode(mSession, TRUE); + rtp_session_set_connected_mode(mSession, TRUE); + rtp_session_set_symmetric_rtp(mSession, TRUE); + // Hardcode RTP session type to GSM full rate (GSM 06.10). + // FIXME -- Make this work for multiple vocoder types. + rtp_session_set_payload_type(mSession, 3); + rtp_session_set_local_addr(mSession, "0.0.0.0", mRTPPort ); + rtp_session_set_remote_addr(mSession, mRTPRemIP.c_str(), mRTPRemPort); + // Check for event support. + int code = rtp_session_telephone_events_supported(mSession); + if (code == -1) { + if (rfc2833) { LOG(ALERT) << "RTP session does not support selected DTMF method RFC-2833"; } + else { LOG(WARNING) << "RTP session does not support telephone events"; } + } + } + + // unpack RTP state and shove it into the session structure + char *items = strdup(transaction->RTPState().c_str()); + char *thisItem; + vector RTPState; + while ((thisItem=strsep(&items,","))!=NULL) { + RTPState.push_back(strtol(thisItem,NULL,10)); + } + free(items); + assert(RTPState.size() == 22); + /* Out of desperation, when the RTP refused to work, I transferred from BS1 to BS2 just about + * all the state in this struct. Well, it turns out NONE of it is necessary. Something else + * entirely was the problem. (Or, technically, state in a different struct.) Anyway, I'm + * leaving the transferring, and just not copying in anything here. So if any of it appears + * to be important some day, it will be easy to experiment. You're welcome. + mSession->rtp.snd_time_offset = RTPState[0]; + mSession->rtp.snd_ts_offset = RTPState[1]; + mSession->rtp.snd_rand_offset = RTPState[2]; + mSession->rtp.snd_last_ts = RTPState[3]; + mSession->rtp.rcv_time_offset = RTPState[4]; + mSession->rtp.rcv_ts_offset = RTPState[5]; + mSession->rtp.rcv_query_ts_offset = RTPState[6]; + mSession->rtp.rcv_last_ts = RTPState[7]; + mSession->rtp.rcv_last_app_ts = RTPState[8]; + mSession->rtp.rcv_last_ret_ts = RTPState[9]; + mSession->rtp.hwrcv_extseq = RTPState[10]; + mSession->rtp.hwrcv_seq_at_last_SR = RTPState[11]; + mSession->rtp.hwrcv_since_last_SR = RTPState[12]; + mSession->rtp.last_rcv_SR_ts = RTPState[13]; + mSession->rtp.last_rcv_SR_time.tv_sec = RTPState[14]; mSession->rtp.last_rcv_SR_time.tv_usec = RTPState[15]; + mSession->rtp.snd_seq = RTPState[16]; + mSession->rtp.last_rtcp_report_snt_r = RTPState[17]; + mSession->rtp.last_rtcp_report_snt_s = RTPState[18]; + mSession->rtp.rtcp_report_snt_interval = RTPState[19]; + mSession->rtp.last_rtcp_packet_count = RTPState[20]; + mSession->rtp.sent_payload_bytes = RTPState[21]; + */ + + osip_message_t * invite = sip_reinvite( + mRemoteUsername.c_str(), mRemoteDomain.c_str(), + SIPDisplayname.c_str(), mSIPUsername.c_str(), + transaction->FromTag().c_str(), transaction->FromUsername().c_str(), transaction->FromIP().c_str(), + transaction->ToTag().c_str(), transaction->ToUsername().c_str(), transaction->ToIP().c_str(), + mViaBranch.c_str(), mCallID.c_str(), transaction->CallIP().c_str(), + mCSeq, mCodec, mRTPPort, + transaction->SessionID().c_str(), transaction->SessionVersion().c_str()); + + // Send Invite to remote party. + struct sockaddr_in rmt; + if (!resolveAddress(&rmt, transaction->RmtIP().c_str(), transaction->RmtPort())) { + LOG(ALERT) << "unable to resolve IP address of remote party to send INVITE"; + mState = Fail; + gReports.incr("OpenBTS.SIP.Failed"); + gReports.incr("OpenBTS.SIP.LostProxy"); + return mState; + } + gSIPInterface.write(&rmt, invite); + saveINVITE(invite, true); + osip_message_free(invite); + // FIXME - is this the right state? + mState = Starting; + return mState; +} + + // vim: ts=4 sw=4 diff --git a/SIP/SIPEngine.h b/SIP/SIPEngine.h index 8765ad6b..5f47158b 100644 --- a/SIP/SIPEngine.h +++ b/SIP/SIPEngine.h @@ -3,24 +3,16 @@ * Copyright 2010 Kestrel Signal Processing, Inc. * Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -41,6 +33,15 @@ #include +namespace Control { +class TransactionEntry; +} + +namespace GSM { +class GSMLogicalChannel; +} + + namespace SIP { @@ -63,7 +64,10 @@ enum SIPState { Canceled, Cleared, Fail, - MessageSubmit + MessageSubmit, + HandoverInbound, + HandoverInboundReferred, + HandoverOutbound }; @@ -89,6 +93,8 @@ class SIPEngine std::string mViaBranch; std::string mSIPUsername; ///< our SIP username unsigned mCSeq; + std::string mAsteriskIP; + std::string mFromTag; /**@name Pre-formed headers. These point into mINVITE. */ //@{ osip_from_t* mMyToFromHeader; ///< To/From header for us @@ -119,6 +125,8 @@ class SIPEngine /**@name RTP state and parameters. */ //@{ short mRTPPort; + short mRTPRemPort; + string mRTPRemIP; unsigned mCodec; RtpSession * mSession; ///< RTP media session unsigned int mTxTime; ///< RTP transmission timestamp in 8 kHz samples @@ -126,7 +134,7 @@ class SIPEngine //@} SIPState mState; ///< current SIP call state - bool mInstigator; ///< true if this side initiated the call + bool mInstigator; ///< true if this side initiated the call /**@name RFC-2833 DTMF state. */ //@{ @@ -135,6 +143,7 @@ class SIPEngine unsigned mDTMFStartTime; ///< start time of the DTMF key event //@} + public: /** @@ -147,6 +156,7 @@ class SIPEngine ~SIPEngine(); const std::string& callID() const { return mCallID; } + void callID(const std::string wCallID) { mCallID = wCallID; } const std::string& proxyIP() const { return mProxyIP; } unsigned proxyPort() const { return mProxyPort; } @@ -154,16 +164,37 @@ class SIPEngine /** Return the current SIP call state. */ SIPState state() const { return mState; } + /** Return the from tag. */ + std::string FromTag() const { return mFromTag; } + + /** Return the INVITE. */ + osip_message_t * INVITE() const { return mINVITE; } + + /** Return the last response. */ + osip_message_t * LastResponse() const { return mLastResponse; } + + /** Return To/From header for us */ + osip_from_t * MyToFromHeader() const { return mMyToFromHeader; } + + /** Return To/From header for the remote party */ + osip_from_t * RemoteToFromHeader() const { return mRemoteToFromHeader; } + + /** Return RTP session */ + RtpSession * RTPSession() const { return mSession; } + + /** Force the state externally. */ + void state(SIPState wState) { mState=wState; } + /** Return the RTP Port being used. */ short RTPPort() const { return mRTPPort; } - /** Return if the call has successfully finished */ + /** Return if the call has finished, successful for not. */ bool finished() const { return (mState==Cleared || mState==Canceled || mState==Fail); } /** Return if the communication was started by us (true) or not (false) */ /* requires an mINVITE be established */ bool instigator() const { return mInstigator; } - + /** Set the user to IMSI and generate a call ID; for mobile-originated transactions. */ void user( const char * IMSI ); @@ -178,7 +209,13 @@ class SIPEngine Can throw SIPTimeout(). @return True on success. */ - bool Register(Method wMethod=SIPRegister); + bool Register(Method wMethod=SIPRegister, const GSM::LogicalChannel* chan = NULL, string *RAND = NULL, const char *IMSI = NULL, const char *SRES = NULL); + + // (pat) The UMTS code is still using the old function prototype without the chan arg. + // It would be better to add new arguments to the end of the list. + bool Register(Method wMethod, string *wRAND, const char *wIMSI=0, const char *wSRES=0) { + return Register(wMethod , NULL, wRAND, wIMSI, wSRES); + } /** Send sip unregister and look at return msg. @@ -190,20 +227,9 @@ class SIPEngine //@} - /**@name Messages associated with Emegency call (SOS) procedure. */ - //@{ - - /** - Send an invite message. - @param rtpPort UDP port to use for speech (will use this and next port) - @param codec Code for codec to be used. - @return New SIP call state. - */ - SIPState SOSSendINVITE(short rtpPort, unsigned codec, const GSM::LogicalChannel *chan = NULL); - //SIPState SOSResendINVITE(); - //SIPState SOSWaitForOK(); + //SIPState SOSCheckForOK(); //SIPState SOSSendACK(); @@ -273,12 +299,10 @@ class SIPEngine /**@name Messages associated with MTSMS procedure. */ //@{ - SIPState MTCSendOK(); + SIPState MTCSendOK(const GSM::LogicalChannel *chan = NULL); //@} - - /**@name Messages for MOD procedure. */ //@{ SIPState MODSendBYE(); @@ -302,7 +326,6 @@ class SIPEngine SIPState MODWaitFor487(Mutex *lock=NULL); SIPState MODWaitForResponse(vector *validResponses, Mutex *lock=NULL); - //@} @@ -315,6 +338,13 @@ class SIPEngine SIPState MTDSendCANCELOK(); //@} + /**@name Messages for Handover procedure. */ + //@{ + SIPState inboundHandoverSendINVITE(Control::TransactionEntry*, unsigned int); + SIPState inboundHandoverCheckForOK(Mutex *lock); + SIPState inboundHandoverSendACK(); + //@} + /** Set up to start sending RFC2833 DTMF event frames in the RTP stream. */ bool startDTMF(char key); @@ -381,7 +411,6 @@ class SIPEngine Generate a standard set of private headers on initiating messages. */ void writePrivateHeaders(osip_message_t *msg, const GSM::LogicalChannel *chan); - }; diff --git a/SIP/SIPInterface.cpp b/SIP/SIPInterface.cpp index 0c41346d..55badb48 100644 --- a/SIP/SIPInterface.cpp +++ b/SIP/SIPInterface.cpp @@ -266,8 +266,11 @@ void SIPInterface::drive() // heroic efforts to get it to parse the www-authenticate header failed, // so we'll just crowbar that sucker in. char *p = strcasestr(mReadBuffer, "nonce"); - if (p) { - string RAND = string(mReadBuffer, p-mReadBuffer+6, 32); + if (p && p[-1] != 'c') { // nonce but not cnonce + p += 6; + char *q = p; + while (isalnum(*q)) { q++; } + string RAND = string(mReadBuffer, p-mReadBuffer, q-p); LOG(INFO) << "crowbar www-authenticate " << RAND; osip_www_authenticate_t *auth; osip_www_authenticate_init(&auth); @@ -278,6 +281,23 @@ void SIPInterface::drive() if (k < 0) LOG(ERR) << "problem adding www_authenticate"; } + // The parser doesn't seem to be interested in authentication info either. + // Get kc from there and put it in tmsi table. + char *pp = strcasestr(mReadBuffer, "cnonce"); + if (pp) { + pp += 7; + char *qq = pp; + while (isalnum(*qq)) { qq++; } + string kc = string(mReadBuffer, pp-mReadBuffer, qq-pp); + LOG(INFO) << "storing kc in TMSI table"; // mustn't display kc in log + const char *imsi = osip_uri_get_username(msg->to->url); + if (imsi && strlen(imsi) > 0) { + gTMSITable.putKc(imsi+4, kc); + } else { + LOG(ERR) << "can't find imsi to store kc"; + } + } + if (msg->sip_method) LOG(DEBUG) << "read method " << msg->sip_method; // Must check if msg is an invite. @@ -380,7 +400,7 @@ bool SIPInterface::checkInvite( osip_message_t * msg) if (strcmp(method,"INVITE") == 0) { // INVITE is for MTC. // Set the required channel type to match the assignment style. - if (gConfig.defines("Control.VEA")) { + if (gConfig.getBool("Control.VEA")) { // Very early assignment. requiredChannel = GSM::TCHFType; channelAvailable = gBTS.TCHAvailable(); diff --git a/SIP/SIPMessage.cpp b/SIP/SIPMessage.cpp index 3ccb5ad5..d5587eae 100644 --- a/SIP/SIPMessage.cpp +++ b/SIP/SIPMessage.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -46,8 +36,8 @@ using namespace SIP; void openbts_message_init(osip_message_t ** msg){ osip_message_init(msg); //I think it's like 40 characters - char tag[60]; - sprintf(tag, "OpenBTS %s Build Date %s", VERSION, __DATE__); + static const char* userAgent = "OpenBTS " VERSION " Build Date " __DATE__; + const char *tag = userAgent; osip_message_set_user_agent(*msg, strdup(tag)); } @@ -170,7 +160,7 @@ int openbts_message_set_rr(osip_message_t *response, osip_message_t *orig) return MSG_NO_ERROR; } -osip_message_t * SIP::sip_register( const char * sip_username, short timeout, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq) { +osip_message_t * SIP::sip_register( const char * sip_username, short timeout, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, string *RAND, const char *IMSI, const char *SRES) { char local_port[10]; sprintf(local_port,"%i",wlocal_port); @@ -250,6 +240,17 @@ osip_message_t * SIP::sip_register( const char * sip_username, short timeout, sh // add contact osip_list_add(&request->contacts, con, -1); + if (SRES) { + // add authentication + osip_authorization_t *auth; + osip_authorization_init(&auth); + osip_authorization_set_auth_type(auth, osip_strdup("Digest")); + osip_authorization_set_nonce(auth, osip_strdup(RAND->c_str())); + osip_authorization_set_uri(auth, osip_strdup(IMSI)); + osip_authorization_set_response(auth, osip_strdup(SRES)); + osip_list_add(&request->authorizations, auth, -1); + } + return request; } @@ -328,7 +329,7 @@ osip_message_t * SIP::sip_message( const char * dialed_number, const char * sip_ } // Content-Length - sprintf(temp_buf,"%u", static_cast(strlen(message))); + sprintf(temp_buf,"%u",strlen(message)); osip_message_set_content_length(request, strdup(temp_buf)); // Payload. @@ -337,9 +338,8 @@ osip_message_t * SIP::sip_message( const char * dialed_number, const char * sip_ return request; } +osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec) { -osip_message_t * SIP::sip_invite5031(short rtp_port, const char * sip_username, short wlocal_port, const char * local_ip, const char* proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec) -{ char local_port[10]; sprintf(local_port, "%i", wlocal_port); @@ -350,9 +350,8 @@ osip_message_t * SIP::sip_invite5031(short rtp_port, const char * sip_username, request->sip_method = strdup("INVITE"); osip_message_set_version(request, strdup("SIP/2.0")); osip_uri_init(&request->req_uri); - osip_uri_set_scheme(request->req_uri, strdup("sip")); - osip_uri_set_username(request->req_uri, strdup("sos")); osip_uri_set_host(request->req_uri, strdup(proxy_ip)); + osip_uri_set_username(request->req_uri, strdup(dialed_number)); // VIA osip_via_t * via; @@ -379,15 +378,15 @@ osip_message_t * SIP::sip_invite5031(short rtp_port, const char * sip_username, osip_from_set_tag(request->from, strdup(from_tag)); osip_uri_init(&request->from->url); - osip_uri_set_host(request->from->url, strdup(local_ip)); + osip_uri_set_host(request->from->url, strdup(proxy_ip)); osip_uri_set_username(request->from->url, strdup(sip_username)); // TO osip_to_init(&request->to); osip_to_set_displayname(request->to, strdup("")); osip_uri_init(&request->to->url); - osip_uri_set_host(request->to->url, strdup(gConfig.getStr("Emergency.Destination.Host").c_str())); - osip_uri_set_username(request->to->url, strdup(gConfig.getStr("Emergency.Destination.User").c_str())); + osip_uri_set_host(request->to->url, strdup(proxy_ip)); + osip_uri_set_username(request->to->url, strdup(dialed_number)); // If response, we need a to tag. //osip_uri_param_t * to_tag_param; @@ -453,37 +452,64 @@ osip_message_t * SIP::sip_invite5031(short rtp_port, const char * sip_username, * to turn it into a string first. */ openbts_message_set_sdp(request, sdp); - // TODO: In the very unlikely event that sdp_str is null, we should probably do some nice cleanup. + // TODO: In the unlikely event that sdp_str is null, we should probably do some nice cleanup. return request; } -osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec) { +osip_message_t * SIP::sip_reinvite( + const char* RemoteUsername, + const char* RemoteIP, - char local_port[10]; - sprintf(local_port, "%i", wlocal_port); + const char* SIPDisplayname, + const char* SIPUsername, + + const char* fromTag, + const char* fromUsername, + const char* fromIP, + + const char* toTag, + const char* toUsername, + const char* toIP, + + const char* viaBranch, + const char* callID, + const char* callIP, + + int cseq, + unsigned codec, + short wRTPPort, + + const char* SessionID, + const char* SessionVersion) +{ + + char RTPPort[10]; + sprintf(RTPPort, "%i", wRTPPort); + + const char * BS2port = gConfig.getStr("SIP.Local.Port").c_str(); + const char * BS2IP = gConfig.getStr("SIP.Local.IP").c_str(); osip_message_t * request; - openbts_message_init(&request); - // FIXME -- Should use the "force_update" function. + osip_message_init(&request); request->message_property = 2; request->sip_method = strdup("INVITE"); osip_message_set_version(request, strdup("SIP/2.0")); osip_uri_init(&request->req_uri); - osip_uri_set_host(request->req_uri, strdup(proxy_ip)); - osip_uri_set_username(request->req_uri, strdup(dialed_number)); + osip_uri_set_host(request->req_uri, strdup(RemoteIP)); + osip_uri_set_username(request->req_uri, strdup(RemoteUsername)); // VIA osip_via_t * via; osip_via_init(&via); via_set_version(via, strdup("2.0")); via_set_protocol(via, strdup("UDP")); - via_set_host(via, strdup(local_ip)); - via_set_port(via, strdup(local_port)); + via_set_host(via, strdup(BS2IP)); + via_set_port(via, strdup(BS2port)); // VIA BRANCH - osip_via_set_branch(via, strdup(via_branch)); + osip_via_set_branch(via, strdup(viaBranch)); // MAX FORWARDS osip_message_set_max_forwards(request, strdup(gConfig.getStr("SIP.MaxForwards").c_str())); @@ -493,30 +519,24 @@ osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, co // FROM osip_from_init(&request->from); - osip_from_set_displayname(request->from, strdup(sip_username)); - - // FROM TAG - osip_from_set_tag(request->from, strdup(from_tag)); - + // osip_from_set_displayname(request->from, strdup(fromDisplayname)); + osip_from_set_tag(request->from, strdup(fromTag)); osip_uri_init(&request->from->url); - osip_uri_set_host(request->from->url, strdup(proxy_ip)); - osip_uri_set_username(request->from->url, strdup(sip_username)); + osip_uri_set_host(request->from->url, strdup(fromIP)); + osip_uri_set_username(request->from->url, strdup(fromUsername)); // TO osip_to_init(&request->to); - osip_to_set_displayname(request->to, strdup("")); + // osip_to_set_displayname(request->to, strdup(toDisplayname)); + osip_to_set_tag(request->to, strdup(toTag)); osip_uri_init(&request->to->url); - osip_uri_set_host(request->to->url, strdup(proxy_ip)); - osip_uri_set_username(request->to->url, strdup(dialed_number)); - - // If response, we need a to tag. - //osip_uri_param_t * to_tag_param; - //osip_from_get_tag(rsp->to, &to_tag_param); + osip_uri_set_host(request->to->url, strdup(toIP)); + osip_uri_set_username(request->to->url, strdup(toUsername)); // CALL ID osip_call_id_init(&request->call_id); - osip_call_id_set_host(request->call_id, strdup(local_ip)); - osip_call_id_set_number(request->call_id, strdup(call_id)); + if (*callIP) osip_call_id_set_host(request->call_id, strdup(callIP)); + osip_call_id_set_number(request->call_id, strdup(callID)); // CSEQ osip_cseq_init(&request->cseq); @@ -531,9 +551,9 @@ osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, co // CONTACT URI osip_uri_init(&con->url); - osip_uri_set_host(con->url, strdup(local_ip)); - osip_uri_set_port(con->url, strdup(local_port)); - osip_uri_set_username(con->url, strdup(sip_username)); + osip_uri_set_host(con->url, strdup(BS2IP)); + osip_uri_set_port(con->url, strdup(BS2port)); + osip_uri_set_username(con->url, strdup(SIPUsername)); osip_contact_param_add(con, strdup("expires"), strdup("3600") ); // add contact @@ -542,17 +562,16 @@ osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, co sdp_message_t * sdp; sdp_message_init(&sdp); sdp_message_v_version_set(sdp, strdup("0")); - sdp_message_o_origin_set(sdp, strdup(sip_username), strdup("0"), - strdup("0"), strdup("IN"), strdup("IP4"), strdup(local_ip)); + sdp_message_o_origin_set(sdp, strdup(SIPDisplayname), strdup(SessionID), + strdup(SessionVersion), strdup("IN"), strdup("IP4"), strdup(BS2IP)); sdp_message_s_name_set(sdp, strdup("Talk Time")); sdp_message_t_time_descr_add(sdp, strdup("0"), strdup("0") ); - sprintf(temp_buf,"%i",rtp_port); sdp_message_m_media_add(sdp, strdup("audio"), - strdup(temp_buf), NULL, strdup("RTP/AVP")); + strdup(RTPPort), NULL, strdup("RTP/AVP")); sdp_message_c_connection_add - (sdp, 0, strdup("IN"), strdup("IP4"), strdup(local_ip),NULL, NULL); + (sdp, 0, strdup("IN"), strdup("IP4"), strdup(BS2IP),NULL, NULL); // FIXME -- This should also be inside the switch? sdp_message_m_payload_add(sdp,0,strdup("3")); @@ -573,7 +592,7 @@ osip_message_t * SIP::sip_invite( const char * dialed_number, short rtp_port, co * to turn it into a string first. */ openbts_message_set_sdp(request, sdp); - // TODO: In the very unlikely event that sdp_str is null, we should probably do some nice cleanup. + // TODO: In the unlikely event that sdp_str is null, we should probably do some nice cleanup. return request; } @@ -645,7 +664,7 @@ osip_message_t * SIP::sip_ack(const char * req_uri, const char * dialed_number, } -osip_message_t * SIP::sip_bye(const char * req_uri, const char * dialed_number, const char * sip_username, short wlocal_port, const char * local_ip, const char * /*proxy_ip*/, short wproxy_port, const osip_from_t* from_header, const osip_to_t* to_header, const char * via_branch, const osip_call_id_t* call_id_header, int cseq) { +osip_message_t * SIP::sip_bye(const char * req_uri, const char * dialed_number, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, short wproxy_port, const osip_from_t* from_header, const osip_to_t* to_header, const char * via_branch, const osip_call_id_t* call_id_header, int cseq) { // FIXME -- We really need some NULL-value error checking in here. @@ -758,7 +777,7 @@ osip_message_t * SIP::sip_error(osip_message_t * invite, const char * host, con return unavail; } -/* Cancel a previously sent invite */ +/* Cancel a previous invite */ osip_message_t * SIP::sip_cancel( osip_message_t * invite, const char * host, const char * username, short port) { @@ -767,7 +786,7 @@ osip_message_t * SIP::sip_cancel( osip_message_t * invite, const char * host, c osip_message_t * cancel; openbts_message_init(&cancel); //clone doesn't work -kurtis - //osip_message_clone(invite, &cancel); + //osip_message_clone(invite, &cancel) // FIXME -- Should use the "force_update" function. cancel->message_property = 2; //header stuff first @@ -939,7 +958,7 @@ osip_message_t * SIP::sip_trying( osip_message_t * invite, const char * sip_user // Get Via openbts_message_set_via(trying, invite); - + // from/to header osip_from_clone(invite->from, &trying->from); osip_to_clone(invite->to, &trying->to); @@ -1005,7 +1024,7 @@ osip_message_t * SIP::sip_ringing( osip_message_t * invite, const char * sip_use } -osip_message_t * SIP::sip_okay( osip_message_t * inv, const char* /*sip_username*/, const char* /*local_ip*/, short wlocal_port) +osip_message_t * SIP::sip_okay( osip_message_t * inv, const char * sip_username, const char * local_ip, short wlocal_port) { // Check for consistency. @@ -1048,7 +1067,7 @@ osip_message_t * SIP::sip_okay( osip_message_t * inv, const char* /*sip_username } -osip_message_t * SIP::sip_info(unsigned info, const char *dialed_number, short /*rtp_port*/, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const osip_call_id_t *call_id_header, int cseq) { +osip_message_t * SIP::sip_info(unsigned info, const char *dialed_number, short rtp_port, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const osip_call_id_t *call_id_header, int cseq) { char local_port[10]; sprintf(local_port, "%i", wlocal_port); diff --git a/SIP/SIPMessage.h b/SIP/SIPMessage.h index 0f607512..232f8e84 100644 --- a/SIP/SIPMessage.h +++ b/SIP/SIPMessage.h @@ -1,24 +1,15 @@ /* * Copyright 2008 Free Software Foundation, Inc. +* Copyright 2011 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -33,7 +24,8 @@ namespace SIP { osip_message_t * sip_register( const char * sip_username, short timeout, short local_port, const char * local_ip, -const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq); +const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, +string *RAND, const char *IMSI, const char *SRES); @@ -41,8 +33,9 @@ osip_message_t * sip_message( const char * dialed_number, const char * sip_usern osip_message_t * sip_invite( const char * dialed_number, short rtp_port,const char * sip_username, short local_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec); -osip_message_t * sip_invite5031(short rtp_port,const char * sip_username, short local_port, const char * local_ip, const char* proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec); +osip_message_t * sip_reinvite( const char* RemoteUsername, const char* RemoteIP, const char* SIPDisplayname, const char* SIPUsername, const char* fromTag, const char* fromUsername, const char* fromIP, const char* toTag, const char* toUsername, const char* toIP, const char* viaBranch, const char* callID, const char* callIP, int cseq, unsigned codec, short wRTPPort, const char* wSessionID, const char* wSessionVersion); +osip_message_t * sip_invite_referred( const char * dialed_number, short rtp_port,const char * sip_username, short local_port, const char * local_ip, const char * proxy_ip, const char * from_tag, const char * via_branch, const char * call_id, int cseq, unsigned codec); osip_message_t * sip_ack( const char * req_uri, const char * dialed_number, const char * sip_username, short wlocal_port, const char * local_ip, const char * proxy_ip, const osip_from_t* from_header, const osip_to_t* to_header, const char * via_branch, const osip_call_id_t* call_id_header, int cseq); @@ -69,3 +62,4 @@ osip_message_t * sip_ringing( osip_message_t * invite, const char * sip_username }; #endif + diff --git a/SIP/SIPUtility.cpp b/SIP/SIPUtility.cpp index 0b31866d..513eb0d9 100644 --- a/SIP/SIPUtility.cpp +++ b/SIP/SIPUtility.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -99,7 +89,7 @@ void SIP::make_branch( char * branch ) uint64_t r1 = random(); uint64_t r2 = random(); uint64_t val = (r1<<32) + r2; - sprintf(branch,"z9hG4bKobts28%llx", static_cast(val)); + sprintf(branch,"z9hG4bKobts28%llx", val); } /* get the return address from the SIP VIA header diff --git a/SIP/SIPUtility.h b/SIP/SIPUtility.h index e599a17a..310851fd 100644 --- a/SIP/SIPUtility.h +++ b/SIP/SIPUtility.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/SMS/SMSMessages.cpp b/SMS/SMSMessages.cpp index b8ec77e1..b3198cd5 100644 --- a/SMS/SMSMessages.cpp +++ b/SMS/SMSMessages.cpp @@ -1,25 +1,15 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - */ #include diff --git a/SMS/SMSMessages.h b/SMS/SMSMessages.h index eed8a88f..b9c2f761 100644 --- a/SMS/SMSMessages.h +++ b/SMS/SMSMessages.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -251,7 +241,7 @@ class TLMessage { - 7 RP (9.2.3.17) */ //@{ - bool mMMS; ///< more messages to send (reversed-sense) + bool mMMS; ///< more messages to send (reversed-sense) bool mRD; ///< reject duplicates unsigned mVPF; ///< validity period format bool mSRR; ///< status report request @@ -350,7 +340,6 @@ class TLSubmit : public TLMessage { int MTI() const { return SUBMIT; } - const unsigned PI() const { return mPI; } const TLAddress& DA() const { return mDA; } const TLUserData& UD() const { return mUD; } @@ -406,11 +395,6 @@ class TLDeliver : public TLMessage { int MTI() const { return DELIVER; } - const unsigned PID() const { return mPID; } - const TLAddress& OA() const { return mOA; } - const TLTimestamp& SCTS() const { return mSCTS; } - const TLUserData& UD() const { return mUD; } - size_t l2BodyLength() const; void writeBody( TLFrame& frame, size_t& wp ) const; void parseBody(const TLFrame&, size_t&) { assert(0); } diff --git a/SMS/SMSTransfer.cpp b/SMS/SMSTransfer.cpp index 2ea7cdc6..ad89fc96 100644 --- a/SMS/SMSTransfer.cpp +++ b/SMS/SMSTransfer.cpp @@ -1,25 +1,15 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - */ diff --git a/SMS/SMSTransfer.h b/SMS/SMSTransfer.h index 08a1e871..b0158ef4 100644 --- a/SMS/SMSTransfer.h +++ b/SMS/SMSTransfer.h @@ -1,25 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - */ diff --git a/TRXManager/TRXManager.cpp b/TRXManager/TRXManager.cpp index 33a58be2..d4b7253e 100644 --- a/TRXManager/TRXManager.cpp +++ b/TRXManager/TRXManager.cpp @@ -1,39 +1,33 @@ /* * Copyright 2008, 2010 Free Software Foundation, Inc. +* Copyright 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -#include + +#include #include "TRXManager.h" -#include "GSMCommon.h" -#include "GSMTransfer.h" -#include "GSMLogicalChannel.h" -#include "GSMConfig.h" -#include "GSML1FEC.h" -#include +#include +#include +#include +#include +#include +#include + +#include #include #include @@ -74,9 +68,24 @@ void TransceiverManager::start() void* ClockLoopAdapter(TransceiverManager *transceiver) { + // This loop checks the clock messages from the transceiver. + // These messages keep the BTS clock in sync with the hardware, + // and also serve as a heartbeat for the radiomodem. + + // This is a convenient place for other periodic housekeeping as well. + + // This loop has a period of about 3 seconds. + + gResetWatchdog(); Timeval nextContact; while (1) { transceiver->clockHandler(); + LOG(DEBUG) << "watchdog timer expires in " << gWatchdogRemaining() << " seconds"; + if (gWatchdogExpired()) { + LOG(ALERT) << "restarting OpenBTS on expiration of watchdog timer"; + gReports.incr("OpenBTS.Exit.Error.Watchdog"); + exit(-2); + } } return NULL; } @@ -90,9 +99,15 @@ void TransceiverManager::clockHandler() // Did the transceiver die?? if (msgLen<0) { - LOG(ALERT) << "TRX clock interface timed out, assuming TRX is dead."; + LOG(EMERG) << "TRX clock interface timed out, assuming TRX is dead."; gReports.incr("OpenBTS.Exit.Error.TransceiverHeartbeat"); +#ifdef RN_DEVELOPER_MODE + // (pat) Added so you can keep debugging without the radio. + static int foo = 0; + pthread_exit(&foo); +#else abort(); +#endif } if (msgLen==0) { @@ -103,7 +118,7 @@ void TransceiverManager::clockHandler() if (strncmp(buffer,"IND CLOCK",9)==0) { uint32_t FN; sscanf(buffer,"IND CLOCK %u", &FN); - LOG(DEBUG) << "CLOCK indication, clock="<writeLowSide(inBurst); - mTableLock.unlock(); + proc->writeLowSideRx(inBurst); } diff --git a/TRXManager/TRXManager.h b/TRXManager/TRXManager.h index 4cce1516..505edddc 100644 --- a/TRXManager/TRXManager.h +++ b/TRXManager/TRXManager.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -149,7 +139,11 @@ class ARFCNManager { unsigned ARFCN() const { return mARFCN; } - void writeHighSide(const GSM::TxBurst& burst); + // (pat) This passes the message through to UDPSocket::write(), + // which maps to DatagramSocket::write() which does an immediate sendto() on the socket. + // (pat) Renamed overloaded function to clarify code. + // Culprit says who called us, for debugging. + void writeHighSideTx(const GSM::TxBurst& burst,const char *culprit); /**@name Transceiver controls. */ @@ -172,9 +166,11 @@ class ARFCNManager { /** Turn off the transceiver. */ bool powerOff(); - /** - Turn on the transceiver. - @param warn Warn if the transceiver fails to start + /** Turn on the transceiver. */ + + /** + Turn on the transceiver. + @param warn Warn if the transceiver fails to start */ bool powerOn(bool warn); @@ -192,12 +188,33 @@ class ARFCNManager { */ signed setRxGain(signed dB); + /** + Set radio transmit attenuation + @param new desired attenuation in dB. + @return new attenuation in dB. + */ + signed setTxAtten(signed dB); + + /** + Set radio frequency offset + @param new desired freq. offset + @return new freq. offset + */ + signed setFreqOffset(signed offset); + + /** Get noise level as RSSI. @return current noise level. */ signed getNoiseLevel(void); + /** + Get factory calibration values. + @return current eeprom values. + */ + signed getFactoryCalibration(const char * param); + /** Set power wrt full scale. @param dB Power level wrt full power. @@ -227,6 +244,20 @@ class ARFCNManager { */ bool setSlot(unsigned TN, unsigned combo); + /** + Set the given slot to run the handover burst correlator. + @param TN The timeslot number. + @return true on succes. + */ + bool setHandover(unsigned TN); + + /** + Clear the given slot to run the handover burst correlator. + @param TN The timeslot number. + @return true on success. + */ + bool clearHandover(unsigned TN); + //@} @@ -254,6 +285,15 @@ class ARFCNManager { */ int sendCommandPacket(const char* command, char* response); + /** + Send a command with a parameter. + @param command The command name. + @param param The parameter for the command. + @param responseParam Optional parameter returned + @return The status code, 0 on success, -1 on local failure. + */ + int sendCommand(const char*command, const char*param, int *responseParam); + /** Send a command with a parameter. @param command The command name. diff --git a/Transceiver52M/Transceiver.cpp b/Transceiver52M/Transceiver.cpp index bdc8affe..eff519ca 100644 --- a/Transceiver52M/Transceiver.cpp +++ b/Transceiver52M/Transceiver.cpp @@ -105,6 +105,7 @@ Transceiver::Transceiver(int wBasePort, mPower = -10; mEnergyThreshold = INIT_ENERGY_THRSHD; prevFalseDetectionTime = startTime; + } Transceiver::~Transceiver() @@ -114,20 +115,22 @@ Transceiver::~Transceiver() mTransmitPriorityQueue.clear(); } - -void Transceiver::addRadioVector(BitVector &burst, +radioVector *Transceiver::fixRadioVector(BitVector &burst, int RSSI, GSM::Time &wTime) { + // modulate and stick into queue signalVector* modBurst = modulateBurst(burst,*gsmPulse, 8 + (wTime.TN() % 4 == 0), mSamplesPerSymbol); scaleVector(*modBurst,txFullScale * pow(10,-RSSI/10)); + radioVector *newVec = new radioVector(*modBurst,wTime); - mTransmitPriorityQueue.write(newVec); + //fillerActive[ARFCN][wTime.TN()] = (ARFCN==0) || (RSSI != 255); delete modBurst; + return newVec; } #ifdef TRANSMIT_LOGGING @@ -153,6 +156,26 @@ void Transceiver::unModulateVector(signalVector wVector) } #endif +// If force, set the FillerTable regardless of channel. +// If allocate, must allocate a copy of the incoming vector. +void Transceiver::setFiller(radioVector *rv, bool allocate, bool force) +{ + int TN = rv->getTime().TN() & 0x07; // (pat) Changed to 0x7 from 0x3. + if (!force && (IGPRS == mChanType[TN])) { + LOG(INFO) << "setFiller ignored"<getTime().FN() % fillerModulus[TN]; + delete fillerTable[modFN][TN]; + if (allocate) { + fillerTable[modFN][TN] = new signalVector(*rv); + } else { + fillerTable[modFN][TN] = rv; + } +} + void Transceiver::pushRadioVector(GSM::Time &nowTime) { @@ -161,37 +184,49 @@ void Transceiver::pushRadioVector(GSM::Time &nowTime) // Even if the burst is stale, put it in the fillter table. // (It might be an idle pattern.) LOG(NOTICE) << "dumping STALE burst in TRX->USRP interface"; - const GSM::Time& nextTime = staleBurst->getTime(); - int TN = nextTime.TN(); - int modFN = nextTime.FN() % fillerModulus[TN]; - delete fillerTable[modFN][TN]; - fillerTable[modFN][TN] = staleBurst; + setFiller(staleBurst,false,false); } - + + // Everything from this point down operates in one TN period, int TN = nowTime.TN(); - int modFN = nowTime.FN() % fillerModulus[nowTime.TN()]; + radioVector *sendVec = NULL; // if queue contains data at the desired timestamp, stick it into FIFO - if (radioVector *next = (radioVector*) mTransmitPriorityQueue.getCurrentBurst(nowTime)) { - LOG(DEBUG) << "transmitFIFO: wrote burst " << next << " at time: " << nowTime; - delete fillerTable[modFN][TN]; - fillerTable[modFN][TN] = new signalVector(*(next)); - mRadioInterface->driveTransmitRadio(*(next),(mChanType[TN]==NONE)); //fillerTable[modFN][TN])); - delete next; -#ifdef TRANSMIT_LOGGING - if (nowTime.TN()==TRANSMIT_LOGGING) { - unModulateVector(*(fillerTable[modFN][TN])); + bool addFiller = true; + while (radioVector *next = (radioVector*) mTransmitPriorityQueue.getCurrentBurst(nowTime)) { + //LOG(DEBUG) << "transmitFIFO: wrote burst " << next << " at time: " << nowTime; + LOG(DEBUG) << (sendVec?"adding":"sending")<<" burst " << next << " at time: " << nowTime; + setFiller(next,true,false); + addFiller = false; + if (!sendVec) { + sendVec = next; + } else { + addVector(*sendVec,*next); + delete next; } -#endif - return; } - // otherwise, pull filler data, and push to radio FIFO - mRadioInterface->driveTransmitRadio(*(fillerTable[modFN][TN]),(mChanType[TN]==NONE)); -#ifdef TRANSMIT_LOGGING - if (nowTime.TN()==TRANSMIT_LOGGING) - unModulateVector(*fillerTable[modFN][TN]); -#endif + // pull filler data, and set it up to be transmitted + if (addFiller){ + int modFN = nowTime.FN() % fillerModulus[TN]; + radioVector *tmpVec = new radioVector(*fillerTable[modFN][TN],nowTime); + if (IGPRS == mChanType[TN]) { + LOG(DEBUG) << (sendVec?"adding":"setting")<<" GPRS filler burst on T" << TN << " FN " << nowTime.FN(); + } + if (!sendVec) { + sendVec = tmpVec; + } else { + addVector(*sendVec,*tmpVec); + delete tmpVec; + } + } + + //LOG(DEBUG) << "sendVec size: " << sendVec->size(); + + // What if sendVec is still NULL? + // It can't be if there are no NULLs in the filler table. + mRadioInterface->driveTransmitRadio(*sendVec,false); + delete sendVec; } @@ -203,6 +238,7 @@ void Transceiver::setModulus(int timeslot) case II: case III: case FILL: + case IGPRS: fillerModulus[timeslot] = 26; break; case IV: @@ -214,9 +250,6 @@ void Transceiver::setModulus(int timeslot) case VII: fillerModulus[timeslot] = 102; break; - case XIII: - fillerModulus[timeslot] = 52; - break; default: break; } @@ -236,6 +269,7 @@ Transceiver::CorrType Transceiver::expectedCorrType(GSM::Time currTime) case FILL: return IDLE; break; + case IGPRS: case I: return TSC; /*if (burstFN % 26 == 25) @@ -271,16 +305,6 @@ Transceiver::CorrType Transceiver::expectedCorrType(GSM::Time currTime) else return TSC; break; - case XIII: { - int mod52 = burstFN % 52; - if ((mod52 == 12) || (mod52 == 38)) - return RACH; - else if ((mod52 == 25) || (mod52 == 51)) - return IDLE; - else - return TSC; - break; - } case LOOPBACK: if ((burstFN % 51 <= 50) && (burstFN % 51 >=48)) return IDLE; @@ -591,6 +615,36 @@ void Transceiver::driveControl() sprintf(response,"RSP SETTSC 0 %d",TSC); } } + else if (strcmp(command,"HANDOVER")==0) { + int timeslot; + sscanf(buffer,"%3s %s %d",cmdcheck,command,×lot); + //sscanf(buffer,"%3s %s %d %d %d",cmdcheck,command,×lot,&corrCode,&ARFCN); + if ((timeslot < 0) || (timeslot > 7)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP HANDOVER 1 %d",timeslot); + } + else { + //mHandoverActive[ARFCN][timeslot] = true; + //sprintf(response,"RSP HANDOVER 0 %d",timeslot); + //handover fails! -kurtis + sprintf(response,"RSP HANDOVER 1 %d",timeslot); + } + } + else if (strcmp(command,"NOHANDOVER")==0) { + int timeslot; + sscanf(buffer,"%3s %s %d",cmdcheck,command,×lot); + //sscanf(buffer,"%3s %s %d %d %d",cmdcheck,command,×lot,&corrCode,&ARFCN); + if ((timeslot < 0) || (timeslot > 7)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP NOHANDOVER 1 %d",timeslot); + } + else { + //mHandoverActive[ARFCN][timeslot] = false; + //sprintf(response,"RSP NOHANDOVER 0 %d",timeslot); + //hanover fails! -kurtis + sprintf(response,"RSP NOHANDOVER 1 %d",timeslot); + } + } else if (strcmp(command,"SETSLOT")==0) { // set TSC int corrCode; @@ -606,6 +660,13 @@ void Transceiver::driveControl() sprintf(response,"RSP SETSLOT 0 %d %d",timeslot,corrCode); } + else if (strcmp(command,"READFACTORY")==0) { + // TODO: Actually support reading data from various USRPs + int ret = 0; //fail everything -kurtis + //sprintf(response,"RSP READFACTORY 0 %d", ret); + // READFACTORY FAILS + sprintf(response,"RSP READFACTORY 1 %d", ret); + } else { LOG(WARNING) << "bogus command " << command << " on control interface."; } @@ -628,10 +689,13 @@ bool Transceiver::driveTransmitPriorityQueue() } int timeSlot = (int) buffer[0]; + int fillerFlag = timeSlot & SET_FILLER_FRAME; // Magic flag says this is a filler burst. + timeSlot = timeSlot & 0x7; uint64_t frameNum = 0; for (int i = 0; i < 4; i++) frameNum = (frameNum << 8) | (0x0ff & buffer[i+1]); - + + /* if (GSM::Time(frameNum,timeSlot) > mTransmitDeadlineClock + GSM::Time(51,0)) { // stale burst @@ -649,13 +713,12 @@ bool Transceiver::driveTransmitPriorityQueue() */ // periodically update GSM core clock - LOG(DEBUG) << "mTransmitDeadlineClock " << mTransmitDeadlineClock - << " mLastClockUpdateTime " << mLastClockUpdateTime; + //LOG(DEBUG) << "mTransmitDeadlineClock " << mTransmitDeadlineClock + // << " mLastClockUpdateTime " << mLastClockUpdateTime; if (mTransmitDeadlineClock > mLastClockUpdateTime + GSM::Time(216,0)) writeClockInterface(); - - LOG(DEBUG) << "rcvd. burst at: " << GSM::Time(frameNum,timeSlot); + LOG(DEBUG) << "rcvd. burst at: " << GSM::Time(frameNum,timeSlot) < templates, these inline most operations for speed /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/TransceiverRAD1/DummyLoad.cpp b/TransceiverRAD1/DummyLoad.cpp index 056fbf7f..38718a07 100644 --- a/TransceiverRAD1/DummyLoad.cpp +++ b/TransceiverRAD1/DummyLoad.cpp @@ -86,10 +86,10 @@ bool DummyLoad::stop() // NOTE: Assumes sequential reads -int DummyLoad::readSamples(short *buf, int len, bool* /*overrun*/, +int DummyLoad::readSamples(short *buf, int len, bool *overrun, TIMESTAMP timestamp, bool *wUnderrun, - unsigned* /*RSSI*/) + unsigned *RSSI) { updateTime(); underrunLock.lock(); diff --git a/TransceiverRAD1/FactoryCalibration.cpp b/TransceiverRAD1/FactoryCalibration.cpp new file mode 100644 index 00000000..c34d0021 --- /dev/null +++ b/TransceiverRAD1/FactoryCalibration.cpp @@ -0,0 +1,256 @@ +/* +* Copyright 2013 Range Networks, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + + +#include + + +int FactoryCalibration::hexval (char ch) +{ + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } + + if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 10; + } + + if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 10; + } + + return -1; +} + +unsigned char * FactoryCalibration::hex_string_to_binary(const char *string, int *lenptr) +{ + int sl = strlen (string); + if (sl & 0x01){ +// fprintf (stderr, "%s: odd number of chars in \n", prog_name); + return 0; + } + + int len = sl / 2; + *lenptr = len; + unsigned char *buf = new unsigned char [len]; + + for (int i = 0; i < len; i++){ + int hi = hexval (string[2 * i]); + int lo = hexval (string[2 * i + 1]); + if (hi < 0 || lo < 0){ +// fprintf (stderr, "%s: invalid char in \n", prog_name); + delete [] buf; + return 0; + } + buf[i] = (hi << 4) | lo; + } + return buf; +} + +bool FactoryCalibration::i2c_write(int i2c_addr, char *hex_string) +{ + int len = 0; + unsigned char *buf = hex_string_to_binary (hex_string, &len); + if (buf == 0) { + return false; + } + return core->writeI2c(i2c_addr, buf, len); +} + +std::string FactoryCalibration::i2c_read(int i2c_addr, int len) +{ + unsigned char *buf = new unsigned char [len]; + bool result = core->readI2c(i2c_addr, buf, len); + if (!result) { + return ""; + } + + char hex[64]; + for (int i = 0; i < len; i++){ + sprintf (hex+(2*i), "%02x", buf[i]); + } + + return std::string(hex); +} + +unsigned int FactoryCalibration::hex2dec(std::string hex) +{ + unsigned int dec; + std::stringstream tempss; + + tempss << std::hex << hex; + tempss >> dec; + + return dec; +} + +unsigned int FactoryCalibration::getValue(std::string name) { + if (name.compare("sdrsn")==0) { + return sdrsn; + + } else if (name.compare("rfsn")==0) { + return rfsn; + + // TODO : (mike) I thought these should be DEC comparisons but only HEX vals are matching, + // too rushed for 3.1 to figure out why I'm dumb + } else if (name.compare("band")==0) { + // BAND_85="85" # dec 133 + if (band == 85) { + return 850; + + // BAND_90="90" # dec 144 + } else if (band == 90) { + return 900; + + // BAND_18="18" # dec 24 + } else if (band == 18) { + return 1800; + + // BAND_19="19" # dec 25 + } else if (band == 19) { + return 1900; + + // BAND_21="21" # dec 33 + } else if (band == 21) { + return 2100; + + // BAND_MB="ab" # dec 171 + } else if (band == 0xab) { + return 0; + + // TODO : anything to handle here? for now pretend they have a multi-band + } else { + return 0; + } + + } else if (name.compare("freq")==0) { + return freq; + + } else if (name.compare("rxgain")==0) { + return rxgain; + + } else if (name.compare("txgain")==0) { + return txgain; + + // TODO : need a better error condition here + } else { + return 0; + } +} + +void FactoryCalibration::readEEPROM() { + + core = new rnrad1Core( + 0, + RAD1_CMD_INTERFACE, + RAD1_CMD_ALTINTERFACE, + "fpga.rbf", + "ezusb.ihx", + false + ); + + bool ret; + std::string temp; + std::string temp1; + + /* + SDR_ADDR="0x50" + BASEST="df" + MKR_ST="ff" + ./RAD1Cmd i2c_write $SDR_ADDR $BASEST$MKR_ST + sleep 1 + */ + ret = i2c_write(0x50, "dfff"); + sleep(1); +// std::cout << "i2c_write = " << ret << std::endl; + + /* + TEMP=$( ./RAD1Cmd i2c_read $SDR_ADDR 16 ) + sleep 1 + */ + temp = i2c_read(0x50, 16); + sleep(1); +// std::cout << "i2c_read 16 = " << temp << std::endl; + + /* + TEMP=$( ./RAD1Cmd i2c_read $SDR_ADDR 32 ) + */ + temp = i2c_read(0x50, 32); +// std::cout << "i2c_read 32 = " << temp << std::endl; + + /* + # parse SDR serial number + TEMP1=$( echo "$TEMP" | cut -c 1-4 ) + SDRSN=$( printf '%d' 0x$TEMP1 ) + echo "SDR Serial Number [$SDRSN] hex was [$TEMP1]" + */ + temp1 = temp.substr(0, 4); + sdrsn = hex2dec(temp1); +// std::cout << "SDR Serial Number [" << sdrsn << "] hex was [" << temp1 << "]" << std::endl; + + /* + # parse RF serial number + TEMP1=$( echo "$TEMP" | cut -c 5-8 ) + RFSN=$( printf '%d' 0x$TEMP1 ) + echo "RF Serial Number [$RFSN] hex was [$TEMP1]" + */ + temp1 = temp.substr(4, 4); + rfsn = hex2dec(temp1); +// std::cout << "RF Serial Number [" << rfsn << "] hex was [" << temp1 << "]" << std::endl; + + /* + # BAND + BAND=$( echo "$TEMP" | cut -c 9-10 ) + echo "RF BAND [$BAND]" + */ + temp1 = temp.substr(8, 2); + band = atoi(temp1.c_str()); +// std::cout << "RF BAND [" << band << "]" << std::endl; + + /* + # Frequency Setting + TEMP1=$( echo "$TEMP" | cut -c 11-12 ) + FREQ=$( printf '%d' 0x$TEMP1 ) + echo "FREQ [$FREQ]" + */ + temp1 = temp.substr(10, 2); + freq = hex2dec(temp1); +// std::cout << "FREQ [" << freq << "] hex was [" << temp1 << "]" << std::endl; + + /* + # RxGAIN + TEMP1=$( echo "$TEMP" | cut -c 13-14 ) + RXGN=$( printf '%d' 0x$TEMP1 ) + echo "RxGAIN [$RXGN]" + */ + temp1 = temp.substr(12, 2); + rxgain = hex2dec(temp1); +// std::cout << "RxGAIN [" << rxgain << "] hex was [" << temp1 << "]" << std::endl; + + /* + # TxGAIN + TEMP1=$( echo "$TEMP" | cut -c 15-16 ) + TXGN=$( printf '%d' 0x$TEMP1 ) + echo "ATTEN [$TXGN]" + */ + temp1 = temp.substr(14, 2); + txgain = hex2dec(temp1); +// std::cout << "TxGAIN/ATTEN [" << txgain << "] hex was [" << temp1 << "]" << std::endl; + + core->~rnrad1Core(); + + return; +} diff --git a/TransceiverRAD1/FactoryCalibration.h b/TransceiverRAD1/FactoryCalibration.h new file mode 100644 index 00000000..f8127397 --- /dev/null +++ b/TransceiverRAD1/FactoryCalibration.h @@ -0,0 +1,45 @@ +/* +* Copyright 2013 Range Networks, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + +#include "rnrad1Core.h" + +/* + TODO : lots of copied bits from RAD1CMD and RAD1SN, could definitely be improved + by simplifying the hex<->dec parsing. Comments from rad1_setup.sh inline. +*/ +class FactoryCalibration { + + private: + + rnrad1Core * core; + unsigned int sdrsn; + unsigned int rfsn; + unsigned int band; + unsigned int freq; + unsigned int rxgain; + unsigned int txgain; + + static int hexval (char ch); + static unsigned char * hex_string_to_binary(const char *string, int *lenptr); + bool i2c_write(int i2c_addr, char *hex_string); + std::string i2c_read(int i2c_addr, int len); + unsigned int hex2dec(std::string hex); + + public: + + unsigned int getValue(std::string name); + void readEEPROM(); +}; diff --git a/TransceiverRAD1/Makefile.am b/TransceiverRAD1/Makefile.am index 859975ba..eadfabf8 100644 --- a/TransceiverRAD1/Makefile.am +++ b/TransceiverRAD1/Makefile.am @@ -20,7 +20,7 @@ include $(top_srcdir)/Makefile.common -DESTDIR = +DESTDIR := AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(USB_INCLUDES) $(WITH_INCLUDES) AM_CXXFLAGS = -DOMNITHREAD_POSIX=1 -lusb-1.0 -ldl -lpthread @@ -40,6 +40,7 @@ libtransceiver_la_SOURCES = \ sigProcLib.cpp \ Transceiver.cpp \ RAD1Device.cpp \ + FactoryCalibration.cpp \ DummyLoad.cpp @@ -49,6 +50,7 @@ noinst_PROGRAMS = \ sigProcLibTest \ RAD1Cmd \ RAD1SN \ + RAD1RxRawPowerSweep \ RAD1RxRawPower noinst_HEADERS = \ @@ -69,6 +71,7 @@ noinst_HEADERS = \ sigProcLib.h \ Transceiver.h \ RAD1Device.h \ + FactoryCalibration.h \ DummyLoad.h RAD1ping_SOURCES = RAD1ping.cpp @@ -81,6 +84,11 @@ RAD1RxRawPower_LDADD = \ libtransceiver.la \ $(COMMON_LA) $(SQLITE_LA) +RAD1RxRawPowerSweep_SOURCES = RAD1RxRawPowerSweep.cpp +RAD1RxRawPowerSweep_LDADD = \ + libtransceiver.la \ + $(COMMON_LA) $(SQLITE_LA) + RAD1Cmd_SOURCES = RAD1Cmd.cpp RAD1Cmd_LDADD = \ libtransceiver.la \ @@ -108,15 +116,15 @@ sigProcLibTest_LDADD = \ MOSTLYCLEANFILES += -install: transceiver - @mkdir -p "$(DESTDIR)/OpenBTS/" - install transceiver "$(DESTDIR)/OpenBTS/" - install ezusb.ihx "$(DESTDIR)/OpenBTS/" - install fpga.rbf "$(DESTDIR)/OpenBTS/" - #radioInterface.cpp #ComplexTest.cpp #sigProcLibTest.cpp #sweepGenerator.cpp #testRadio.cpp +install: transceiver + @mkdir -p "$(DESTDIR)/OpenBTS/" + install transceiver "$(DESTDIR)/OpenBTS/" + install ezusb.ihx "$(DESTDIR)/OpenBTS/" + install fpga.rbf "$(DESTDIR)/OpenBTS/" + diff --git a/TransceiverRAD1/RAD1Cmd.cpp b/TransceiverRAD1/RAD1Cmd.cpp index 543f97a7..827e41b4 100644 --- a/TransceiverRAD1/RAD1Cmd.cpp +++ b/TransceiverRAD1/RAD1Cmd.cpp @@ -2,8 +2,7 @@ /* * USRP - Universal Software Radio Peripheral * - * Copyright (C) 2003,2004,2009,2011 Free Software Foundation, Inc. - * Copyright 2011 Range Networks, Inc. + * Copyright (C) 2003,2004,2009 Free Software Foundation, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -197,7 +196,7 @@ main (int argc, char **argv) const char *cmd = argv[optind++]; nopts--; - gLogInit("openbts",argv[1],LOG_LOCAL7); + gLogInit("openbts",NULL,LOG_LOCAL7); rnrad1Core *core = new rnrad1Core(which_board, RAD1_CMD_INTERFACE, diff --git a/TransceiverRAD1/RAD1Device.cpp b/TransceiverRAD1/RAD1Device.cpp index 14d3e2b1..6bbbb227 100644 --- a/TransceiverRAD1/RAD1Device.cpp +++ b/TransceiverRAD1/RAD1Device.cpp @@ -1,6 +1,5 @@ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * * This software is distributed under the terms of the GNU Public License. * See the COPYING file in the main directory for details. @@ -50,7 +49,8 @@ unsigned char* write_it(unsigned v, unsigned char *s) { } -const float RAD1Device::LO_OFFSET = 6.0e6; +const double RAD1Device::LO_OFFSET = 6.0e6; + const double RAD1Device::masterClockRate = (double) 52.0e6; bool RAD1Device::compute_regs(double freq, @@ -68,7 +68,9 @@ bool RAD1Device::compute_regs(double freq, else { DIV2 = 0; freq_mult = 1; + // This default value matches the RAD1r3. CP1 = 7; + // This default value matches the RAD1r3. CP2 = 7; } @@ -78,8 +80,12 @@ bool RAD1Device::compute_regs(double freq, float B = floor(desired_n/16); float A = desired_n - 16*B; + unsigned B_DIV = int(B); unsigned A_DIV = int(A); + + //printf("B: %f, A: %f, B_DIV: %u, A_DIV: %u\n",B,A,B_DIV,A_DIV); + if (B < A) return false; *R = (R_RSV<<22) | (BSC << 20) | @@ -164,6 +170,8 @@ RAD1Device::RAD1Device (double _desiredSampleRate) actualSampleRate = masterClockRate/decimRate; rxGain = 0; + // This default value matches the RAD1r3. + PC = 0; // bits 3,2 Core power 15mA #ifdef SWLOOPBACK samplePeriod = 1.0e6/actualSampleRate; loopbackBufferSize = 0; @@ -205,6 +213,10 @@ bool RAD1Device::make(bool wSkipRx) m_uRx->writeIO((POWER_UP|RX_TXN),(POWER_UP|RX_TXN|ENABLE)); + // FIXME -- This should be configurable. + setRxGain(47); //maxRxGain()); + + } try { @@ -281,8 +293,6 @@ bool RAD1Device::start() m_uRx->setPga(1,m_uRx->pgaMax()); m_uRx->setMux(0x00000010); writeLock.unlock(); - // FIXME -- This should be configurable. - setRxGain(47); //maxRxGain()); } data = new short[currDataSize]; @@ -396,7 +406,7 @@ int RAD1Device::readSamples(short *buf, int len, bool *overrun, if (underrun) *underrun = false; - uint32_t readBuf[2000]; + uint32_t readBuf[20000]; while (1) { //guestimate USB read size @@ -606,6 +616,9 @@ bool RAD1Device::updateAlignment(TIMESTAMP timestamp) #endif } +bool RAD1Device::setVCTCXO(unsigned int freq_cal) { + m_uRx->writeAuxDac(2,freq_cal << 4); +} #ifndef SWLOOPBACK bool RAD1Device::setTxFreq(double wFreq, double wAdjFreq) { @@ -637,6 +650,7 @@ bool RAD1Device::setRxFreq(double wFreq, double wAdjFreq) { m_uRx->writeAuxDac(2,freq_cal << 4); if (!rx_setFreq(wFreq-2*LO_OFFSET,&actFreq)) return false; bool retVal = m_uRx->setRxFreq(wFreq-actFreq); + //printf("set RX: %f, actual RX: %f\n",wFreq-actFreq,m_uRx->rxFreq()); LOG(DEBUG) << "set RX: " << wFreq-actFreq << " actual RX: " << m_uRx->rxFreq(); return retVal; diff --git a/TransceiverRAD1/RAD1Device.h b/TransceiverRAD1/RAD1Device.h index e1c5d754..c5d23ef1 100644 --- a/TransceiverRAD1/RAD1Device.h +++ b/TransceiverRAD1/RAD1Device.h @@ -38,6 +38,10 @@ #include "bytesex.h" +#include + +extern ConfigurationTable gConfig; + /** A class to handle a USRP rev 4, with a two RFX900 daughterboards */ class RAD1Device: public RadioDevice { @@ -107,7 +111,7 @@ class RAD1Device: public RadioDevice { static const unsigned SPI_FMT_HDR_0 = (0 << 5); */ - static const float LO_OFFSET; + static const double LO_OFFSET; //static const float LO_OFFSET = 4.0e6; static const unsigned R_DIV = 13; @@ -124,21 +128,21 @@ class RAD1Device: public RadioDevice { static const unsigned BSC = 3; // bits 21,20 Div by 8 to be safe static const unsigned TEST = 0; // bit 19 static const unsigned LDP = 1; // bit 18 - static const unsigned ABP = 2; // bit 17,16 6ns, 0 = 3ns + static const unsigned ABP = 0; // bit 17,16 3ns // N-Register Common Values static const unsigned N_RSV = 0; // bit 7 // Control Register Common Values static const unsigned PD = 0; // bits 21,20 Normal operation - static const unsigned PL = 1; // bits 13,12 7.5mA, -6dbm + static const unsigned PL = 2; // bits 13,12 7.5mA, -6dbm static const unsigned MTLD = 1; // bit 11 enabled static const unsigned CPG = 0; // bit 10 CP setting 1 static const unsigned CP3S = 0; // bit 9 Normal static const unsigned PDP = 1; // bit 8 Positive static const unsigned MUXOUT = 1;// bits 7:5 Digital Lock Detect static const unsigned CR = 0; // bit 4 Normal - static const unsigned PC = 0; // bits 3,2 Core power 15mA + unsigned PC; // bits 3,2 Core power 15mA // ATR register value //static const int FR_ATR_MASK_0 = 20; @@ -201,7 +205,10 @@ class RAD1Device: public RadioDevice { /** Update the alignment between the read and write timestamps */ bool updateAlignment(TIMESTAMP timestamp); - + + /** Set the VCTCXO offset*/ + bool setVCTCXO(unsigned int wAdjFreq = 0); + /** Set the transmitter frequency */ bool setTxFreq(double wFreq, double wAdjFreq = 0); diff --git a/TransceiverRAD1/RAD1RxRawPower.cpp b/TransceiverRAD1/RAD1RxRawPower.cpp index e1dadf2f..5b60e665 100644 --- a/TransceiverRAD1/RAD1RxRawPower.cpp +++ b/TransceiverRAD1/RAD1RxRawPower.cpp @@ -1,6 +1,5 @@ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. -* Copyright 2011 Range Newtworks, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * * This software is distributed under the terms of the GNU Public License. * See the COPYING file in the main directory for details. @@ -31,7 +30,8 @@ #include #include "RAD1Device.h" -ConfigurationTable gConfig; +/* tomr had to add direct path to OpenBTS.db */ +ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db"); using namespace std; @@ -43,44 +43,67 @@ int main(int argc, char *argv[]) { if (argc > 1) whichBoard = atoi(argv[1]); //if (argc>2) gSetLogFile(argv[2]); - RAD1Device *rad1 = new RAD1Device(52.0e6/192.0); + RAD1Device *usrp = new RAD1Device(52.0e6/192.0); - rad1->make(); + usrp->make(); - double freq = 0.0; - if (argc > 2) freq = (double) atoi(argv[2]); + double freqkHz = 0.0; + if (argc > 2) freqkHz = (double) atoi(argv[2]); TIMESTAMP timestamp; - if (!rad1->setRxFreq(freq*1.0e6,118)) printf("RX failed!"); + if (!usrp->setRxFreq(freqkHz*1.0e3,108)) printf("RX failed!"); - rad1->start(); + usrp->start(); - rad1->setRxGain(47); + /* tomr added default gain value to 53 and 3rd arg for setting an alt value */ + unsigned int rxgain = 53; + + if (argc > 3) { + rxgain = atoi(argv[3]); + printf("Updated RxGain = %d\n", rxgain); + } else + printf("Deafult RxGain Setting = %d\n", rxgain); + + usrp->setRxGain(rxgain); + + bool movingAverage = false; + if (argc > 4) { + movingAverage = (atoi(argv[4])!=0); + } + printf("Moving average = %d\n",movingAverage); bool underrun; - rad1->updateAlignment(20000); - rad1->updateAlignment(21000); + usrp->updateAlignment(20000); + usrp->updateAlignment(21000); timestamp = 30000; double sum = 0.0; unsigned long num = 0; - double rcvCeil = rad1->fullScaleOutputValue()*rad1->fullScaleOutputValue(); + double rcvCeil = usrp->fullScaleOutputValue()*usrp->fullScaleOutputValue(); while (1) { short readBuf[512*2]; - printf("reading data...\n"); - int rd = rad1->readSamples(readBuf,512,&underrun,timestamp); + int rd = usrp->readSamples(readBuf,512,&underrun,timestamp); if (rd) { LOG(INFO) << "rcvd. data@:" << timestamp; - float pwr = 0; for (int i = 0; i < 512; i++) { + uint32_t *wordPtr = (uint32_t *) &readBuf[2*i]; + *wordPtr = usrp_to_host_u32(*wordPtr); + //printf ("%llu: %d %d\n", timestamp+i,readBuf[2*i],readBuf[2*i+1]); sum += (readBuf[2*i+1]*readBuf[2*i+1] + readBuf[2*i]*readBuf[2*i]); - pwr += (readBuf[2*i+1]*readBuf[2*i+1] + readBuf[2*i]*readBuf[2*i]); num++; - if (num % 10000 == 0) printf("RSSI: %f\n",10*log10(rcvCeil/(sum/(double) num))); + if (num % 10000 == 0) { + printf("RSSI: %f\n",10*log10(rcvCeil/(sum/(double) num))); + usrp->setRxGain(rxgain); + + if (movingAverage) { + sum = 0; + num = 0; + } + } } timestamp += rd; } diff --git a/TransceiverRAD1/RAD1RxRawPowerSweep.cpp b/TransceiverRAD1/RAD1RxRawPowerSweep.cpp new file mode 100644 index 00000000..e14f1693 --- /dev/null +++ b/TransceiverRAD1/RAD1RxRawPowerSweep.cpp @@ -0,0 +1,108 @@ +/* +* Copyright 2008, 2009 Free Software Foundation, Inc. +* +* This software is distributed under the terms of the GNU Public License. +* See the COPYING file in the main directory for details. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +*/ + + + +#include +#include +#include +#include +#include "RAD1Device.h" + +/* tomr had to add direct path to OpenBTS.db */ +ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db"); + +using namespace std; + +int main(int argc, char *argv[]) { + + gLogInit("openbts","INFO",LOG_LOCAL7); + + int whichBoard = 0; + if (argc > 1) whichBoard = atoi(argv[1]); + //if (argc>2) gSetLogFile(argv[2]); + + RAD1Device *usrp = new RAD1Device(52.0e6/192.0); + + usrp->make(); + + double startFreqkHz = 0.0; + double endFreqkHz = 0.0; + if (argc > 2) startFreqkHz = (double) atoi(argv[2]); + if (argc > 3) endFreqkHz = (double) atoi(argv[3]); + + TIMESTAMP timestamp; + + //if (!usrp->setRxFreq(freqkHz*1.0e3,108)) printf("RX failed!"); + + usrp->start(); + + /* tomr added default gain value to 53 and 3rd arg for setting an alt value */ + unsigned int rxgain = 53; + + if (argc > 4) { + rxgain = atoi(argv[4]); + printf("Updated RxGain = %d\n", rxgain); + } else + printf("Deafult RxGain Setting = %d\n", rxgain); + + usrp->setRxGain(rxgain); + + bool underrun; + + usrp->updateAlignment(20000); + usrp->updateAlignment(21000); + + timestamp = 30000; + double sum = 0.0; + unsigned long num = 0; + + double rcvCeil = usrp->fullScaleOutputValue()*usrp->fullScaleOutputValue(); + + for (double freqkHz = startFreqkHz; freqkHz <= endFreqkHz; freqkHz += 200) { + double sum = 0.0; + unsigned long num = 0; + if (!usrp->setRxFreq(freqkHz*1.0e3,108)) printf("RX failed!"); + for (int j = 0; j < 250; j++) { + short readBuf[512*2]; + int rd = usrp->readSamples(readBuf,512,&underrun,timestamp); + if (rd) { + LOG(INFO) << "rcvd. data@:" << timestamp; + for (int i = 0; i < 512; i++) { + uint32_t *wordPtr = (uint32_t *) &readBuf[2*i]; + *wordPtr = usrp_to_host_u32(*wordPtr); + //printf ("%llu: %d %d\n", timestamp+i,readBuf[2*i],readBuf[2*i+1]); + if (j >= 50) { + sum += (readBuf[2*i+1]*readBuf[2*i+1] + readBuf[2*i]*readBuf[2*i]); + num++; + } + } + timestamp += rd; + } + } + printf("RSSI is %f at %fkHz\n",10*log10(rcvCeil/(sum/(double) num)),freqkHz); + + } + +} diff --git a/TransceiverRAD1/RAD1SN.cpp b/TransceiverRAD1/RAD1SN.cpp index 3eb166de..f1045408 100644 --- a/TransceiverRAD1/RAD1SN.cpp +++ b/TransceiverRAD1/RAD1SN.cpp @@ -1,7 +1,8 @@ /* -*- c++ -*- */ /* + * USRP - Universal Software Radio Peripheral + * * Copyright (C) 2003,2004,2009 Free Software Foundation, Inc. - * Copyright 2011 Range Networks, Inc. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/TransceiverRAD1/RAD1ping.cpp b/TransceiverRAD1/RAD1ping.cpp index 315aff12..05b29876 100644 --- a/TransceiverRAD1/RAD1ping.cpp +++ b/TransceiverRAD1/RAD1ping.cpp @@ -1,6 +1,5 @@ /* -* Copyright 2008, 2009, 2011 Free Software Foundation, Inc. -* Copyright 2011 Range Networks, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. * * This software is distributed under the terms of the GNU Public License. * See the COPYING file in the main directory for details. @@ -31,7 +30,7 @@ #include #include "RAD1Device.h" -ConfigurationTable gConfig; +ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db"); using namespace std; @@ -52,8 +51,8 @@ int main(int argc, char *argv[]) { TIMESTAMP timestamp; - if (!usrp->setTxFreq(1825.2e6,113)) printf("TX failed!"); - if (!usrp->setRxFreq(1825.2e6,113)) printf("RX failed!"); + if (!usrp->setTxFreq(925.2e6,113)) printf("TX failed!"); + if (!usrp->setRxFreq(925.2e6,113)) printf("RX failed!"); usrp->start(); @@ -98,7 +97,7 @@ int main(int argc, char *argv[]) { } printf("For %llu to %llu, power is %f\n",timestamp,timestamp+511,pwr); timestamp += rd; - usrp->writeSamples((short*) data2,512*numpkts,&underrun,timestamp+1000); + //usrp->writeSamples((short*) data2,512*numpkts,&underrun,timestamp+1000); } } diff --git a/TransceiverRAD1/README.DFEsymbolspaced b/TransceiverRAD1/README.DFEsymbolspaced new file mode 100644 index 00000000..f1ab4791 --- /dev/null +++ b/TransceiverRAD1/README.DFEsymbolspaced @@ -0,0 +1,14 @@ +signalVectors G0, G1. i.e. G0(D) = 1 +2D + 3D^2 = [1 2 3] +G0(D) = 1/sqrt(SNR). +G1(D) = [h0 h1D .. h_(N-1)D^(N-1)] +for i = 0,1,...,N_f-1, + d = |G0(0)|^2+|G1(0)|^2 + l_i(D) = D^i ( G0(D)*G0'(0) + G1(D)*G1'(0) )/d + k = G1(0)/G0(0) + G0n(D) = G0(D)+k'*G1(D) + G1n(D) = (-G0(D)*k+G1(D))/D + G0(D) = G0n(D)/sqrt(1+k*k') + G1(D) = G1n(D)/sqrt(1+k*k') +end + + diff --git a/TransceiverRAD1/Transceiver.cpp b/TransceiverRAD1/Transceiver.cpp index 1cd6b934..59a319f4 100644 --- a/TransceiverRAD1/Transceiver.cpp +++ b/TransceiverRAD1/Transceiver.cpp @@ -1,24 +1,15 @@ /* -* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2008, 2009 Free Software Foundation, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -32,8 +23,10 @@ #include "Transceiver.h" #include #include +#include extern ConfigurationTable gConfig; +extern FactoryCalibration gFactoryCalibration; Transceiver::Transceiver(int wBasePort, const char *TRXAddress, @@ -83,62 +76,67 @@ Transceiver::Transceiver(int wBasePort, txFullScale = mRadioInterface->fullScaleInputValue(); rxFullScale = mRadioInterface->fullScaleOutputValue(); - // initialize filler tables with dummy bursts, initialize other per-timeslot variables - for (int i = 0; i < 8; i++) { - signalVector* modBurst = modulateBurst(gDummyBurst,*gsmPulse, - 8 + (i % 4 == 0), - mSamplesPerSymbol); - scaleVector(*modBurst,txFullScale); - fillerModulus[i] = 26; - for (int j = 0; j < 102; j++) { - fillerTable[j][i] = new signalVector(*modBurst); - } - delete modBurst; - for (int j = 0; j < mNumARFCNs; j++) { - mChanType[j][i] = NONE; - } - } - mFreqOffset = 0.0; mMultipleARFCN = (mNumARFCNs > 1); - if (mMultipleARFCN) { - //mOversamplingRate = mNumARFCNs/2 + mNumARFCNs; - //mOversamplingRate = 15; //mOversamplingRate*4; - //if (mOversamplingRate % 2) mOversamplingRate++; - double beaconFreq = -1.0*(mNumARFCNs-1)*200e3; - for (int j = 0; j < mNumARFCNs; j++) { - frequencyShifter[j] = new signalVector(157*mOversamplingRate); - frequencyShifter[j]->fill(complex(1.0,0.0)); - frequencyShifter[j]->isRealOnly(false); - frequencyShift(frequencyShifter[j],frequencyShifter[j],2.0*M_PI*(beaconFreq+j*400e3)/(1625.0e3/6.0*mOversamplingRate)); - } - int filtLen = 6*mOversamplingRate; - decimationFilter = createLPF(0.5/mOversamplingRate,filtLen,1); - interpolationFilter = createLPF(0.5/mOversamplingRate,filtLen,1); - scaleVector(*interpolationFilter,mOversamplingRate); - mFreqOffset = -beaconFreq; - mRadioInterface->setSamplesPerSymbol(SAMPSPERSYM*mOversamplingRate); - - // refill filler table - for (int i = 0; i < 8; i++) { - signalVector* modBurst = modulateBurst(gDummyBurst,*gsmPulse, - 8 + (i % 4 == 0), - mSamplesPerSymbol); - scaleVector(*modBurst,txFullScale/mNumARFCNs); - signalVector *interpVec = polyphaseResampleVector(*modBurst,mOversamplingRate,1,interpolationFilter); - //signalVector *interpVec = new signalVector(modBurst->size()*mOversamplingRate); - //interpVec->fill(txFullScale); - multVector(*interpVec,*frequencyShifter[0]); - for (int j = 0; j < 102; j++) { - delete fillerTable[j][i]; - fillerTable[j][i] = new signalVector(*interpVec); - } - delete modBurst; - delete interpVec; - } + // initialize other per-timeslot variables + for (int tn = 0; tn < 8; tn++) { + for (int arfcn = 0; arfcn < mNumARFCNs; arfcn++) { + mFillerModulus[arfcn][tn] = 26; + mChanType[arfcn][tn] = NONE; + //fillerActive[arfcn][tn] = (arfcn==0); + mHandoverActive[arfcn][tn] = false; + } } + if (mMultipleARFCN) { + // Create the "tones" for sub-band tuning multiple ARFCNs. + //mOversamplingRate = mNumARFCNs/2 + mNumARFCNs; + //mOversamplingRate = 15; //mOversamplingRate*4; + //if (mOversamplingRate % 2) mOversamplingRate++; + double beaconFreq = -1.0*(mNumARFCNs-1)*200e3; + for (int j = 0; j < mNumARFCNs; j++) { + mFrequencyShifter[j] = new signalVector(157*mOversamplingRate); + mFrequencyShifter[j]->fill(complex(1.0,0.0)); + mFrequencyShifter[j]->isRealOnly(false); + frequencyShift(mFrequencyShifter[j],mFrequencyShifter[j],2.0*M_PI*(beaconFreq+j*400e3)/(1625.0e3/6.0*mOversamplingRate)); + } + + int filtLen = 6*mOversamplingRate; + decimationFilter = createLPF(0.5/mOversamplingRate,filtLen,1); + interpolationFilter = createLPF(0.5/mOversamplingRate,filtLen,1); + scaleVector(*interpolationFilter,mOversamplingRate); + mFreqOffset = -beaconFreq; + mRadioInterface->setSamplesPerSymbol(SAMPSPERSYM*mOversamplingRate); + } // if (mMultipleARFCN) + + // initialize filler tables with dummy bursts. + for (int cn = 0; cn < mNumARFCNs; cn++) { + for (int tn = 0; tn < 8; tn++) { + signalVector* modBurst = modulateBurst(gDummyBurst,*gsmPulse, + 8 + (tn % 4 == 0), + mSamplesPerSymbol); + // Power-scale, resample and frequency-shift. + // Note that these are zero-power bursts on cn other than c0. + // FIXME -- It would be cleaner to handle cn>0 in a different loop. + // Otherwise, we as wasting a lot of computation here. + if (cn==0) scaleVector(*modBurst,txFullScale/mNumARFCNs); + else scaleVector(*modBurst,0.0); + if (mMultipleARFCN) { + signalVector *interpVec = polyphaseResampleVector(*modBurst,mOversamplingRate,1,interpolationFilter); + //signalVector *interpVec = new signalVector(modBurst->size()*mOversamplingRate); + //interpVec->fill(txFullScale); + multVector(*interpVec,*mFrequencyShifter[cn]); + delete modBurst; + modBurst = interpVec; + } + for (int fn = 0; fn < MAXMODULUS; fn++) { + mFillerTable[cn][fn][tn] = new signalVector(*modBurst); + } + delete modBurst; + } + } + mOn = false; mTxFreq = 0.0; mRxFreq = 0.0; @@ -146,7 +144,6 @@ Transceiver::Transceiver(int wBasePort, mControlLock.unlock(); mTransmitPriorityQueueLock.unlock(); - } Transceiver::~Transceiver() @@ -157,11 +154,12 @@ Transceiver::~Transceiver() } -void Transceiver::addRadioVector(BitVector &burst, +radioVector *Transceiver::fixRadioVector(BitVector &burst, int RSSI, GSM::Time &wTime, int ARFCN) { + // modulate and stick into queue signalVector* modBurst = modulateBurst(burst,*gsmPulse, 8 + (wTime.TN() % 4 == 0), @@ -169,9 +167,11 @@ void Transceiver::addRadioVector(BitVector &burst, /*complex rScale = complex(2*M_PI*((float) rand()/(float) RAND_MAX),(2*M_PI*((float) rand()/(float) RAND_MAX))); rScale = rScale/rScale.abs(); scaleVector(*modBurst,rScale);*/ + float headRoom = (mNumARFCNs > 1) ? 0.5 : 1.0; scaleVector(*modBurst,txFullScale * headRoom * pow(10,-RSSI/10)/mNumARFCNs); radioVector *newVec = new radioVector(*modBurst,wTime,ARFCN); + //fillerActive[ARFCN][wTime.TN()] = (ARFCN==0) || (RSSI != 255); // upsample and filter and freq shift if (mMultipleARFCN) { @@ -180,15 +180,13 @@ void Transceiver::addRadioVector(BitVector &burst, delete newVec; //if (ARFCN!=0) printf("ARFCN: %d\n",ARFCN); - multVector(*interpVec,*frequencyShifter[ARFCN]); + multVector(*interpVec,*mFrequencyShifter[ARFCN]); newVec = new radioVector(*interpVec,wTime,ARFCN); delete interpVec; } - - mTransmitPriorityQueue.write(newVec); - delete modBurst; + return newVec; } #ifdef TRANSMIT_LOGGING @@ -214,55 +212,90 @@ void Transceiver::unModulateVector(signalVector wVector) } #endif +// If force, set the mFillerTable regardless of channel. +// If allocate, must allocate a copy of the incoming vector. +void Transceiver::setFiller(radioVector *rv, bool allocate, bool force) +{ + int CN = rv->ARFCN(); + int TN = rv->time().TN() & 0x07; // (pat) Changed to 0x7 from 0x3. + if (!force && (IGPRS == mChanType[CN][TN])) { + LOG(INFO) << "setFiller ignored"<time().FN() % mFillerModulus[CN][TN]; + delete mFillerTable[CN][modFN][TN]; + if (allocate) { + mFillerTable[CN][modFN][TN] = new signalVector(*rv); + } else { + mFillerTable[CN][modFN][TN] = rv; + } +} + void Transceiver::pushRadioVector(GSM::Time &nowTime) { + // Transmit any enqueued bursts in this timeslot, on all ARFCNs. + // dump stale bursts, if any while (radioVector* staleBurst = mTransmitPriorityQueue.getStaleBurst(nowTime)) { - // Even if the burst is stale, put it in the fillter table. + // Even if the burst is stale, put it in the filler table. // (It might be an idle pattern.) - LOG(NOTICE) << "dumping STALE burst in TRX->USRP interface"; - if (staleBurst->ARFCN()==0) { - const GSM::Time& nextTime = staleBurst->time(); - int TN = nextTime.TN(); - int modFN = nextTime.FN() % fillerModulus[TN]; - // FIXME!!!!!! - delete fillerTable[modFN][TN]; - fillerTable[modFN][TN] = staleBurst; - } - else - delete staleBurst; + LOG(NOTICE) << "dumping STALE burst in TRX->USRP interface cn="<ARFCN() + <<" at "<time(); + setFiller(staleBurst,false,false); } + + // Everything from this point down operates in one TN period, + // across multiple ARFCNs in freq. int TN = nowTime.TN(); - int modFN = nowTime.FN() % fillerModulus[nowTime.TN()]; - bool addFiller = true; + // Sum up ARFCNs that are ready to transmit in this FN and TN + bool addFiller[mNumARFCNs]; + for (int CN=0; CNARFCN(); + if (CN >= mNumARFCNs) { + LOG(ERR) << "attempt to send burst on illegal ARFCN. C" << CN << "T" << TN << " FN " << nowTime.FN(); + delete next; + continue; + } + if (addFiller[CN] == false) { + LOG(ERR) << "attempt to send multiple bursts on C" << CN << "T" << TN << " FN " << nowTime.FN(); + delete next; + continue; + } //LOG(DEBUG) << "transmitFIFO: wrote burst " << next << " at time: " << nowTime; - if (next->ARFCN() == 0) { - delete fillerTable[modFN][TN]; - fillerTable[modFN][TN] = new signalVector(*(next)); - addFiller = false; - } - if (!sendVec) + LOG(DEBUG) << (sendVec?"adding":"sending")<<" burst " << next << " at time: " << nowTime; + setFiller(next,true,false); + addFiller[CN] = false; + if (!sendVec) { sendVec = next; - else { + } else { addVector(*sendVec,*next); delete next; } } - if (addFiller) { - // pull filler data, and push to radio FIFO - int dummy = 0; - radioVector *tmpVec = new radioVector(*fillerTable[modFN][TN],nowTime,dummy); - if (!sendVec) + // generate filler on ARFCNs where we didn't get anything. + for (int CN=0; CNsize(); - // otherwise, pull filler data, and push to radio FIFO - mRadioInterface->driveTransmitRadio(*sendVec,(mChanType[0][TN]==NONE)); - + // What if sendVec is still NULL? + // It can't be if there are no NULLs in the filler table. + mRadioInterface->driveTransmitRadio(*sendVec,false); delete sendVec; } -void Transceiver::setModulus(int timeslot) +void Transceiver::setModulus(int arfcn, int timeslot) { - switch (mChanType[0][timeslot]) { + switch (mChanType[arfcn][timeslot]) { case NONE: case I: case II: case III: case FILL: - fillerModulus[timeslot] = 26; + case IGPRS: + mFillerModulus[arfcn][timeslot] = 26; break; case IV: case VI: case V: - fillerModulus[timeslot] = 51; + mFillerModulus[arfcn][timeslot] = 51; break; //case V: case VII: - fillerModulus[timeslot] = 102; + mFillerModulus[arfcn][timeslot] = 102; break; default: break; @@ -303,19 +337,22 @@ void Transceiver::setModulus(int timeslot) } -CorrType Transceiver::expectedCorrType(GSM::Time currTime, int ARFCN) +CorrType Transceiver::expectedCorrType(GSM::Time currTime, int arfcn) { unsigned burstTN = currTime.TN(); unsigned burstFN = currTime.FN(); - switch (mChanType[ARFCN][burstTN]) { + if (mHandoverActive[arfcn][burstTN]) return RACH; + + switch (mChanType[arfcn][burstTN]) { case NONE: return OFF; break; case FILL: return IDLE; break; + case IGPRS: case I: return TSC; /*if (burstFN % 26 == 25) @@ -376,14 +413,14 @@ void Transceiver::pullRadioVector() //LOG(DEBUG) << "receiveFIFO: read radio vector " << rxBurst << " at time: " << rxBurst->time() << ", new size: " << mReceiveFIFO->size(); GSM::Time theTime = rxBurst->time(); - int timeslot = rxBurst->time().TN(); + int timeslot = rxBurst->time().TN() & 0x03; for (int i = 0; i < mNumARFCNs; i++) { CorrType corrType = expectedCorrType(rxBurst->time(),i); if ((corrType == OFF) || (corrType == IDLE)) continue; radioVector *ARFCNVec = new radioVector(*(signalVector *)rxBurst,theTime,i); if (mMultipleARFCN) { - multVector(*ARFCNVec,*frequencyShifter[mNumARFCNs-1-i]); + multVector(*ARFCNVec,*mFrequencyShifter[mNumARFCNs-1-i]); signalVector *rcvVec = polyphaseResampleVector(*ARFCNVec,1,mOversamplingRate,decimationFilter); delete ARFCNVec; ARFCNVec = new radioVector(*rcvVec,theTime,i); @@ -401,7 +438,7 @@ void Transceiver::start() for(int i = 0; i < mNumARFCNs; i++) { ThreadStruct *cs = new ThreadStruct; cs->trx = this; - cs->ARFCN = i; + cs->CN = i; mControlServiceLoopThread[i]->start((void * (*)(void*))ControlServiceLoopAdapter,(void*) cs); } } @@ -473,7 +510,7 @@ void Transceiver::driveControl(unsigned ARFCN) for (int i = 0; i < mNumARFCNs; i++) { ThreadStruct *cs = new ThreadStruct; cs->trx = this; - cs->ARFCN = i; + cs->CN = i; mTransmitPriorityQueueServiceLoopThread[i]->start((void * (*)(void*))TransmitPriorityQueueServiceLoopAdapter,(void*) cs); Demodulator *demod = new Demodulator(i,this,mStartTime); mDemodulators[i] = demod; @@ -498,17 +535,44 @@ void Transceiver::driveControl(unsigned ARFCN) } else if (strcmp(command,"SETRXGAIN")==0) { // FIXME -- Use the configuration table instead. - //set expected maximum time-of-arrival int newGain; sscanf(buffer,"%3s %s %d",cmdcheck,command,&newGain); newGain = mRadioInterface->setRxGain(newGain); sprintf(response,"RSP SETRXGAIN 0 %d",newGain); } + else if (strcmp(command,"SETTXATTEN")==0) { + // set output power in dB + int dbPwr; + sscanf(buffer,"%3s %s %d",cmdcheck,command,&dbPwr); + if (!mOn) + sprintf(response,"RSP SETTXATTEN 1 %d",dbPwr); + else { + if (ARFCN==0) { + mRadioInterface->setPowerAttenuation(mPower + dbPwr); + } + sprintf(response,"RSP SETTXATTEN 0 %d",dbPwr); + } + } + else if (strcmp(command,"SETFREQOFFSET")==0) { + // set output power in dB + int tuneVoltage; + sscanf(buffer,"%3s %s %d",cmdcheck,command,&tuneVoltage); + if (!mOn) + sprintf(response,"RSP SETFREQOFFSET 1 %d",tuneVoltage); + else { + if (ARFCN==0) { + mRadioInterface->setVCTCXO(tuneVoltage); + } + sprintf(response,"RSP SETFREQOFFSET 0 %d",tuneVoltage); + } + } + + else if (strcmp(command,"NOISELEV")==0) { // FIXME -- Use the status table instead. if (mOn) { sprintf(response,"RSP NOISELEV 0 %d", - (int) round(20.0*log10(rxFullScale/mDemodulators[0]->getEnergyThreshold()))); + (int) mDemodulators[0]->mNoiseFloorRSSI); } else { sprintf(response,"RSP NOISELEV 1 0"); @@ -580,6 +644,40 @@ void Transceiver::driveControl(unsigned ARFCN) sprintf(response,"RSP SETTSC 0 %d",TSC); } } + else if (strcmp(command,"HANDOVER")==0) { + int timeslot; + sscanf(buffer,"%3s %s %d",cmdcheck,command,×lot); + //sscanf(buffer,"%3s %s %d %d %d",cmdcheck,command,×lot,&corrCode,&ARFCN); + if ((timeslot < 0) || (timeslot > 7)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP HANDOVER 1 %d",timeslot); + } + else if ((ARFCN < 0) || (ARFCN >= MAXARFCN)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP HANDOVER 1 %d",timeslot); + } + else { + mHandoverActive[ARFCN][timeslot] = true; + sprintf(response,"RSP HANDOVER 0 %d",timeslot); + } + } + else if (strcmp(command,"NOHANDOVER")==0) { + int timeslot; + sscanf(buffer,"%3s %s %d",cmdcheck,command,×lot); + //sscanf(buffer,"%3s %s %d %d %d",cmdcheck,command,×lot,&corrCode,&ARFCN); + if ((timeslot < 0) || (timeslot > 7)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP NOHANDOVER 1 %d",timeslot); + } + else if ((ARFCN < 0) || (ARFCN >= MAXARFCN)) { + LOG(ERR) << "bogus message on control interface"; + sprintf(response,"RSP NOHANDOVER 1 %d",timeslot); + } + else { + mHandoverActive[ARFCN][timeslot] = false; + sprintf(response,"RSP NOHANDOVER 0 %d",timeslot); + } + } else if (strcmp(command,"SETSLOT")==0) { // set TSC int corrCode; @@ -596,10 +694,17 @@ void Transceiver::driveControl(unsigned ARFCN) } else { mChanType[ARFCN][timeslot] = (ChannelCombination) corrCode; - setModulus(timeslot); + setModulus(ARFCN,timeslot); sprintf(response,"RSP SETSLOT 0 %d %d",timeslot,corrCode); } } + else if (strcmp(command,"READFACTORY")==0) { + char param[16]; + sscanf(buffer,"%3s %s %s",cmdcheck,command,¶m); + int ret = gFactoryCalibration.getValue(std::string(param)); + // TODO : this should actually return the param name requested + sprintf(response,"RSP READFACTORY 0 %d", ret); + } else { LOG(ERR) << "bogus command " << command << " on control interface."; } @@ -614,26 +719,26 @@ void Transceiver::driveControl(unsigned ARFCN) bool Transceiver::driveTransmitPriorityQueue(unsigned ARFCN) { - -#if 1 char buffer[gSlotLen+50]; // check data socket size_t msgLen = mDataSocket[ARFCN]->read(buffer); - mTransmitPriorityQueueLock.lock(); + ScopedLock lock(mTransmitPriorityQueueLock); if (msgLen!=gSlotLen+1+4+1) { LOG(ERR) << "badly formatted packet on GSM->TRX interface"; - mTransmitPriorityQueueLock.unlock(); return false; } int timeSlot = (int) buffer[0]; + int fillerFlag = timeSlot & SET_FILLER_FRAME; // Magic flag says this is a filler burst. + timeSlot = timeSlot & 0x7; uint64_t frameNum = 0; for (int i = 0; i < 4; i++) frameNum = (frameNum << 8) | (0x0ff & buffer[i+1]); - + + /* if (GSM::Time(frameNum,timeSlot) > mTransmitDeadlineClock + GSM::Time(51,0)) { // stale burst @@ -657,7 +762,7 @@ bool Transceiver::driveTransmitPriorityQueue(unsigned ARFCN) writeClockInterface(); - //LOG(DEBUG) << "rcvd. burst at: " << GSM::Time(frameNum,timeSlot); + LOG(DEBUG) << "rcvd. burst at: " << GSM::Time(frameNum,timeSlot) <getClock()); - - if (mOn) { - radioClock->wait(); // wait until clock updates - // time to push burst to transmit FIFO - mTransmitPriorityQueueLock.lock(); - for (int i = 0; i < 4; i++) - { - int iSlot = radioClock->get().TN(); //mTransmitDeadlineClock.TN(); - static BitVector newBurst(gSlotLen); - GSM::Time currTime = GSM::Time(mTransmitDeadlineClock.FN()+50,(iSlot+i)%8); - addRadioVector(newBurst,0,currTime,ARFCN); - //printf("adding %d %d\n",mTransmitDeadlineClock.FN(),iSlot); - } - mTransmitPriorityQueueLock.unlock(); - } -#endif - - return true; @@ -732,7 +821,7 @@ void Transceiver::driveTransmitFIFO() // only do latency update every 10 frames, so we don't over update if (radioClock->get() > mLatencyUpdateTime + GSM::Time(100,0)) { mTransmitLatency = mTransmitLatency + GSM::Time(1,0); - LOG(NOTICE) << "new latency: " << mTransmitLatency; + LOG(INFO) << "new latency: " << mTransmitLatency; mLatencyUpdateTime = radioClock->get(); } } @@ -742,7 +831,7 @@ void Transceiver::driveTransmitFIFO() if (mTransmitLatency > GSM::Time(1,1)) { if (radioClock->get() > mLatencyUpdateTime + GSM::Time(216,0)) { mTransmitLatency.decTN(); - LOG(NOTICE) << "reduced latency: " << mTransmitLatency; + LOG(INFO) << "reduced latency: " << mTransmitLatency; mLatencyUpdateTime = radioClock->get(); } } @@ -801,9 +890,9 @@ void *RFIFOServiceLoopAdapter(Transceiver *transceiver) void *ControlServiceLoopAdapter(ThreadStruct *ts) { Transceiver *transceiver = ts->trx; - unsigned ARFCN = ts->ARFCN; + unsigned CN = ts->CN; while (1) { - transceiver->driveControl(ARFCN); + transceiver->driveControl(CN); pthread_testcancel(); } return NULL; @@ -812,7 +901,7 @@ void *ControlServiceLoopAdapter(ThreadStruct *ts) void *DemodServiceLoopAdapter(Demodulator *demodulator) { while(1) { - demodulator->driveDemod(); + demodulator->driveDemod(false); pthread_testcancel(); } return NULL; @@ -821,16 +910,18 @@ void *DemodServiceLoopAdapter(Demodulator *demodulator) void *TransmitPriorityQueueServiceLoopAdapter(ThreadStruct *ts) { Transceiver *transceiver = ts->trx; - unsigned ARFCN = ts->ARFCN; + unsigned CN = ts->CN; while (1) { bool stale = false; // Flush the UDP packets until a successful transfer. - while (!transceiver->driveTransmitPriorityQueue(ARFCN)) { + while (!transceiver->driveTransmitPriorityQueue(CN)) { stale = true; } if (stale) { // If a packet was stale, remind the GSM stack of the clock. transceiver->writeClockInterface(); + LOG(NOTICE) << "dumping STALE bursts at UDP interface cn="<getClock()); //radioClock->wait(); + demodBurst = mDemodFIFO->get(); if (!wSingleARFCN) { while (!demodBurst) { @@ -902,8 +997,8 @@ void Demodulator::driveDemod(bool wSingleARFCN) if (rxBurst) { - LOG(DEBUG) << "burst parameters: " - << " ARFCN: " << mARFCN + DEMOD_DEBUG << "burst parameters: " + << " CN: " << mCN << " time: " << burstTime << " RSSI: " << RSSI << " TOA: " << TOA @@ -939,24 +1034,24 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, int timeslot = rxBurst->time().TN(); - CorrType corrType = mTRX->expectedCorrType(rxBurst->time(),mARFCN); + CorrType corrType = mTRX->expectedCorrType(rxBurst->time(),mCN); - //LOG(INFO) << "Demoding ptr " << rxBurst << " at " << rxBurst->time() << " for ARFCN " << mARFCN; + //LOG(INFO) << "Demoding ptr " << rxBurst << " at " << rxBurst->time() << " for CN " << mCN; if ((corrType==OFF) || (corrType==IDLE)) { - //LOG(DEBUG) << "Illegal burst"; + //DEMOD_DEBUG << "Illegal burst"; delete rxBurst; return NULL; } // check to see if received burst has sufficient signalVector *vectorBurst = rxBurst; - //LOG(DEBUG) << "vectorBurst: " << vectorBurst << " rxBurst: " << rxBurst; + //DEMOD_DEBUG << "vectorBurst: " << vectorBurst << " rxBurst: " << rxBurst; complex amplitude = 0.0; float TOA = 0.0; float avgPwr = 0.0; /*if (!energyDetect(*vectorBurst,20*mSamplesPerSymbol,mEnergyThreshold,&avgPwr)) { - LOG(DEBUG) << "Estimated Energy: " << sqrt(avgPwr) << ", at time " << rxBurst->time(); + DEMOD_DEBUG << "Estimated Energy: " << sqrt(avgPwr) << ", at time " << rxBurst->time(); double framesElapsed = rxBurst->time()-prevFalseDetectionTime; if (framesElapsed > 50) { // if we haven't had any false detections for a while, lower threshold //mEnergyThreshold -= 1.0; @@ -967,16 +1062,16 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, LOG(INFO) << "Deleting " << rxBurst; return NULL; }*/ - LOG(DEBUG) << "Estimated Energy: " << sqrt(avgPwr) << ", at time " << rxBurst->time(); + DEMOD_DEBUG << "Estimated Energy: " << sqrt(avgPwr) << ", at time " << rxBurst->time(); // run the proper correlator bool success = false; if (corrType==TSC) { - LOG(DEBUG) << "looking for TSC at time: " << rxBurst->time(); + DEMOD_DEBUG << "looking for TSC at time: " << rxBurst->time(); signalVector *channelResp; double framesElapsed = rxBurst->time()-channelEstimateTime[timeslot]; bool estimateChannel = false; - //if ((framesElapsed > 50) || (channelResponse[timeslot]==NULL)) { + //if ((framesElapsed > 50) || (channelResponse[timeslot]==NULL)) { if (channelResponse[timeslot]) delete channelResponse[timeslot]; if (DFEForward[timeslot]) delete DFEForward[timeslot]; @@ -1002,24 +1097,24 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, &chanOffset); if (success) { - LOG(DEBUG) << "FOUND TSC!!!!!! " << amplitude << " " << TOA; + DEMOD_DEBUG << "FOUND TSC!!!!!! " << amplitude << " " << TOA; //mEnergyThreshold -= 0.1F; if (mEnergyThreshold < 0.0) mEnergyThreshold = 0.0; SNRestimate[timeslot] = amplitude.norm2()/(mEnergyThreshold*mEnergyThreshold+1.0); // this is not highly accurate if (estimateChannel) { - LOG(DEBUG) << "estimating channel..."; + DEMOD_DEBUG << "estimating channel..."; channelResponse[timeslot] = channelResp; chanRespOffset[timeslot] = chanOffset; chanRespAmplitude[timeslot] = amplitude; scaleVector(*channelResp, complex(1.0,0.0)/amplitude); designDFE(*channelResp, SNRestimate[timeslot], 7, &DFEForward[timeslot], &DFEFeedback[timeslot]); channelEstimateTime[timeslot] = rxBurst->time(); - LOG(DEBUG) << "SNR: " << SNRestimate[timeslot] << ", DFE forward: " << *DFEForward[timeslot] << ", DFE backward: " << *DFEFeedback[timeslot]; + DEMOD_DEBUG << "SNR: " << SNRestimate[timeslot] << ", DFE forward: " << *DFEForward[timeslot] << ", DFE backward: " << *DFEFeedback[timeslot]; } } else { double framesElapsed = rxBurst->time()-prevFalseDetectionTime; - LOG(DEBUG) << "wTime: " << rxBurst->time() << ", pTime: " << prevFalseDetectionTime << ", fElapsed: " << framesElapsed; + DEMOD_DEBUG << "wTime: " << rxBurst->time() << ", pTime: " << prevFalseDetectionTime << ", fElapsed: " << framesElapsed; //mEnergyThreshold += 0.1F*exp(-framesElapsed); prevFalseDetectionTime = rxBurst->time(); channelResponse[timeslot] = NULL; @@ -1033,7 +1128,7 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, &litude, &TOA); if (success) { - LOG(DEBUG) << "FOUND RACH!!!!!! " << amplitude << " " << TOA; + DEMOD_DEBUG << "FOUND RACH!!!!!! " << amplitude << " " << TOA; //mEnergyThreshold -= 0.1F; if (mEnergyThreshold < 0.0) mEnergyThreshold = 0.0; channelResponse[timeslot] = NULL; @@ -1042,9 +1137,12 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, double framesElapsed = rxBurst->time()-prevFalseDetectionTime; //mEnergyThreshold += 0.1F*exp(-framesElapsed); prevFalseDetectionTime = rxBurst->time(); + float avgPwr; + energyDetect(*vectorBurst,20*mSamplesPerSymbol,0.0,&avgPwr); + mNoiseFloorRSSI = (int) floor(20.0*log10(rxFullScale/sqrt(avgPwr))); } } - LOG(DEBUG) << "energy Threshold = " << mEnergyThreshold; + DEMOD_DEBUG << "energy Threshold = " << mEnergyThreshold; // demodulate burst SoftVector *burst = NULL; @@ -1066,12 +1164,12 @@ SoftVector *Demodulator::demodRadioVector(radioVector *rxBurst, wTime = rxBurst->time(); // FIXME: what is full scale for the USRP? we get more that 12 bits of resolution... RSSI = (int) floor(20.0*log10(rxFullScale/amplitude.abs())); - LOG(DEBUG) << "RSSI: " << RSSI; + DEMOD_DEBUG << "RSSI: " << RSSI; timingOffset = (int) round(TOA*256.0/mSamplesPerSymbol); } //if (burst) LOG(DEEPDEBUG) << "burst: " << *burst << '\n'; - LOG(DEBUG) << "Deleting rxBurst"; + DEMOD_DEBUG << "Deleting rxBurst"; delete rxBurst; return burst; diff --git a/TransceiverRAD1/Transceiver.h b/TransceiverRAD1/Transceiver.h index e480c81a..1e67e4e8 100644 --- a/TransceiverRAD1/Transceiver.h +++ b/TransceiverRAD1/Transceiver.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -29,6 +19,9 @@ TRANSMIT_LOGGING write every burst on the given slot to a log */ +#ifdef TIMESTAMP +#undef TIMESTAMP +#endif #include "radioInterface.h" #include "Interthread.h" #include "GSMCommon.h" @@ -41,6 +34,7 @@ //#define TRANSMIT_LOGGING 1 #define MAXARFCN 5 +#define MAXMODULUS 102 /** Codes for burst types of received bursts*/ @@ -56,7 +50,7 @@ class Transceiver; typedef struct ThreadStruct { Transceiver *trx; - unsigned ARFCN; + unsigned CN; } ThreadStruct; /** The Transceiver class, responsible for physical layer of basestation */ @@ -96,6 +90,7 @@ class Transceiver { bool mLoadTest; /** Codes for channel combinations */ + public: typedef enum { FILL, ///< Channel is transmitted, but unused I, ///< TCH/FS @@ -106,20 +101,19 @@ class Transceiver { VI, ///< CCCH+BCCH, uplink RACH VII, ///< SDCCH/8 + SACCH/8 NONE, ///< Channel is inactive, default - LOOPBACK ///< similar go VII, used in loopback testing + LOOPBACK, ///< similar go VII, used in loopback testing + IGPRS ///< GPRS channel, like I but static filler frames. } ChannelCombination; - + private: /** unmodulate a modulated burst */ #ifdef TRANSMIT_LOGGING void unModulateVector(signalVector wVector); #endif + void setFiller(radioVector *rv, bool allocate, bool force); /** modulate and add a burst to the transmit queue */ - void addRadioVector(BitVector &burst, - int RSSI, - GSM::Time &wTime, - int ARFCN); + radioVector *fixRadioVector(BitVector &burst, int RSSI, GSM::Time &wTime, int CN); /** Push modulated burst into transmit FIFO corresponding to a particular timestamp */ void pushRadioVector(GSM::Time &nowTime); @@ -128,7 +122,7 @@ class Transceiver { void pullRadioVector(void); /** Set modulus for specific timeslot */ - void setModulus(int timeslot); + void setModulus(int carrier, int timeslot); /** send messages over the clock socket */ void writeClockInterface(void); @@ -143,15 +137,18 @@ class Transceiver { double mRxFreq; ///< the receive frequency int mPower; ///< the transmit power in dB unsigned mTSC; ///< the midamble sequence code - int fillerModulus[8]; ///< modulus values of all timeslots, in frames - signalVector *fillerTable[102][8]; ///< table of modulated filler waveforms for all timeslots + int mFillerModulus[MAXARFCN][8]; ///< modulus values of all timeslots, in frames + signalVector *mFillerTable[MAXARFCN][MAXMODULUS][8]; ///< table of modulated filler waveforms for all timeslots + // Pat thinks fillerActive is left over from when the filler was only implemented on ARFCN 0. + //bool fillerActive[MAXARFCN][8]; ///< indicates if filler burst is to be transmitted + bool mHandoverActive[MAXARFCN][8]; unsigned mMaxExpectedDelay; ///< maximum expected time-of-arrival offset in GSM symbols unsigned int mNumARFCNs; bool mMultipleARFCN; unsigned char mOversamplingRate; double mFreqOffset; - signalVector *frequencyShifter[MAXARFCN]; + signalVector *mFrequencyShifter[MAXARFCN]; signalVector *decimationFilter; signalVector *interpolationFilter; @@ -184,10 +181,10 @@ class Transceiver { /** start the Transceiver */ void start(); - bool multiARFCN() {return mMultipleARFCN;} + bool multiARFCN() { return mMultipleARFCN; } /** return the expected burst type for the specified timestamp */ - CorrType expectedCorrType(GSM::Time currTime, int ARFCN); + CorrType expectedCorrType(GSM::Time currTime, int CN); /** attach the radioInterface receive FIFO */ void receiveFIFO(VectorFIFO *wFIFO) { mReceiveFIFO = wFIFO;} @@ -195,13 +192,13 @@ class Transceiver { /** attach the radioInterface transmit FIFO */ void transmitFIFO(VectorFIFO *wFIFO) { mTransmitFIFO = wFIFO;} - VectorFIFO *demodFIFO(unsigned ARFCN) { return mDemodFIFO[ARFCN]; } + VectorFIFO *demodFIFO(unsigned CN) { return mDemodFIFO[CN]; } RadioInterface *radioInterface(void) { return mRadioInterface; } unsigned samplesPerSymbol(void) { return mSamplesPerSymbol; } - UDPSocket *dataSocket(int ARFCN) { return mDataSocket[ARFCN]; } + UDPSocket *dataSocket(int CN) { return mDataSocket[CN]; } signalVector *GSMPulse(void) { return gsmPulse; } @@ -209,6 +206,13 @@ class Transceiver { unsigned getTSC(void) { return mTSC; } + // This magic flag is ORed with the TN TimeSlot in vectors passed to the transceiver + // to indicate the radio block is a filler frame instead of a radio frame. + // Must be higher than any possible TN. + enum TransceiverFlags { + SET_FILLER_FRAME = 0x10 + }; + protected: /** drive reception and demodulation of GSM bursts */ @@ -218,13 +222,13 @@ class Transceiver { void driveTransmitFIFO(); /** drive handling of control messages from GSM core */ - void driveControl(unsigned ARFCN); + void driveControl(unsigned CN); /** drive modulation and sorting of GSM bursts from GSM core @return true if a burst was transferred successfully */ - bool driveTransmitPriorityQueue(unsigned ARFCN); + bool driveTransmitPriorityQueue(unsigned CN); friend void *FIFOServiceLoopAdapter(Transceiver *); @@ -252,7 +256,7 @@ class Demodulator { private: - int mARFCN; + int mCN; Transceiver *mTRX; RadioInterface *mRadioInterface; VectorFIFO *mDemodFIFO; @@ -268,7 +272,6 @@ class Demodulator { signalVector *gsmPulse; unsigned mTSC; unsigned mSamplesPerSymbol; - UDPSocket *mTRXDataSocket; unsigned mMaxExpectedDelay; @@ -282,14 +285,14 @@ class Demodulator { public: - Demodulator(int wARFCN, + Demodulator(int wCN, Transceiver *wTRX, GSM::Time wStartTime); double getEnergyThreshold() {return mEnergyThreshold;} - - //protected: + int mNoiseFloorRSSI; +//protected: void driveDemod(bool wSingleARFCN = true); protected: diff --git a/TransceiverRAD1/burn-rnrad1-eeprom.sh b/TransceiverRAD1/burn-rnrad1-eeprom.sh new file mode 100755 index 00000000..53b6bdde --- /dev/null +++ b/TransceiverRAD1/burn-rnrad1-eeprom.sh @@ -0,0 +1,79 @@ +#!/bin/sh + +# comments tomr 12_14_11 + +# General procedure for a virgin eeprom (new SDR board or a corrupted one), note all LEDs will be off +# +# first use lsusb to find the RAD1 USB device, it should display the Cypress FX2 development string +# this comes up when the FX2 boot but sees no valid data from the eeprom +# Bus 001 Device 102: ID 04b4:8613 Cypress Semiconductor Corp. CY7C68013 EZ-USB FX2 USB 2.0 Development Kit +# +# In the above example the usb bus is --> 001 and the device --> 102 +# you need to provide those args in the path below for -D arg +# used here vvv vvv +# sudo fxload -t fx2 -D /dev/bus/usb/001/102 -I /usr/local/share/usrp/rev4/std.ihx +# once you have successfully loaded the firmware the LED on the SDR board will be blinking rapidly +# now you can execute this programming script +# we had planned to make this more so this is more automatic + +# note on the programming data string, the first byte is the starting offset, the next 8 bytes are data + +lsusb | grep Cypress +# echo "sudo fxload -t fx2 -D /dev/bus/usb/xxx/xxx -I ezusb.ihx" +# exit 0 +#./RAD1Cmd -x load_firmware ezusb.ihx +sleep 1 +./RAD1Cmd i2c_write 0x50 00c2feff0200040000 +sleep 1 +./RAD1Cmd i2c_write 0x50 0800c300000200b312 +sleep 1 +./RAD1Cmd i2c_write 0x50 10002d80feaa825380 +sleep 1 +./RAD1Cmd i2c_write 0x50 18cf8a8212001c8508 +sleep 1 +./RAD1Cmd i2c_write 0x50 208212001c43803022 +sleep 1 +./RAD1Cmd i2c_write 0x50 28aa827b08ea23fa13 +sleep 1 +./RAD1Cmd i2c_write 0x50 309281d280c280dbf4 +sleep 1 +./RAD1Cmd i2c_write 0x50 382275803875b23b75 +sleep 1 +./RAD1Cmd i2c_write 0x50 40a0c075b4cf75b1f0 +sleep 1 +./RAD1Cmd i2c_write 0x50 4875b6f890e68ae4f0 +sleep 1 +./RAD1Cmd i2c_write 0x50 500090e680e4f053a0 +sleep 1 +./RAD1Cmd i2c_write 0x50 58fe43a001c2817508 +sleep 1 +./RAD1Cmd i2c_write 0x50 600175820112000875 +sleep 1 +./RAD1Cmd i2c_write 0x50 68080f758208120008 +sleep 1 +./RAD1Cmd i2c_write 0x50 707508007582141200 +sleep 1 +./RAD1Cmd i2c_write 0x50 78087a00ea24e0f582 +sleep 1 +./RAD1Cmd i2c_write 0x50 80e434e1f583e4f00a +sleep 1 +./RAD1Cmd i2c_write 0x50 88ba10f07a007b000a +sleep 1 +./RAD1Cmd i2c_write 0x50 90ba00010beb30e7f7 +sleep 1 +./RAD1Cmd i2c_write 0x50 9863a04080f27800e8 +sleep 1 +./RAD1Cmd i2c_write 0x50 a04400600c79009018 +sleep 1 +./RAD1Cmd i2c_write 0x50 a800e4f0a3d8fcd9fa +sleep 1 +./RAD1Cmd i2c_write 0x50 b0d083d082f6d8fdc0 +sleep 1 +./RAD1Cmd i2c_write 0x50 b882c0837582002275 +sleep 1 +./RAD1Cmd i2c_write 0x50 c08108120091e58260 +sleep 1 +./RAD1Cmd i2c_write 0x50 c80302000302000380 +sleep 1 +./RAD1Cmd i2c_write 0x50 d001e60000 +sleep 1 diff --git a/TransceiverRAD1/fpga_regs.h b/TransceiverRAD1/fpga_regs.h index 11080983..3ac1a3ce 100644 --- a/TransceiverRAD1/fpga_regs.h +++ b/TransceiverRAD1/fpga_regs.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #ifndef INCLUDED_FPGA_REGS_H #define INCLUDED_FPGA_REGS_H diff --git a/TransceiverRAD1/fusb.cpp b/TransceiverRAD1/fusb.cpp index e34e40a7..b88d5584 100644 --- a/TransceiverRAD1/fusb.cpp +++ b/TransceiverRAD1/fusb.cpp @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #ifdef HAVE_CONFIG_H #include "config.h" #endif diff --git a/TransceiverRAD1/fusb.h b/TransceiverRAD1/fusb.h index 9b3c2462..5e0da02d 100644 --- a/TransceiverRAD1/fusb.h +++ b/TransceiverRAD1/fusb.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #ifndef _FUSB_H_ #define _FUSB_H_ diff --git a/TransceiverRAD1/i2c.h b/TransceiverRAD1/i2c.h index a7723dd9..56f4c88f 100644 --- a/TransceiverRAD1/i2c.h +++ b/TransceiverRAD1/i2c.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - // I2C addresses #define I2C_DEV_EEPROM 0x50 // 24LC02[45]: 7-bits 1010xxx diff --git a/TransceiverRAD1/ids.h b/TransceiverRAD1/ids.h index db0a5889..d3b15ba8 100644 --- a/TransceiverRAD1/ids.h +++ b/TransceiverRAD1/ids.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #define USB_VID_CYPRESS 0x04b4 #define USB_PID_CYPRESS_FX2 0x8613 diff --git a/TransceiverRAD1/inband-signaling-usb b/TransceiverRAD1/inband-signaling-usb new file mode 100644 index 00000000..14f83479 --- /dev/null +++ b/TransceiverRAD1/inband-signaling-usb @@ -0,0 +1,314 @@ +This file specifies the format of USB packets used for in-band data +transmission and signaling on the USRP. All packets are 512-byte long, +and are transfered using USB "bulk" transfers. + +IN packets are sent towards the host. +OUT packets are sent away from the host. + +The layout is 32-bits wide. All data is transmitted in little-endian +format across the USB. + + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |O|U|D|S|E| RSSI | Chan | mbz | Tag | Payload Len | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Timestamp | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | Payload | + . . + . . + . . + | | + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ... | . + +-+-+-+-+-+-+-+ . + . . + . Padding . + . . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + mbz Must be Zero: these bits must be zero in both IN and OUT packets. + + O Overrun Flag: set in an IN packet if an overrun condition was + detected. Must be zero in OUT packets. Overrun occurs when + the FPGA has data to transmit to the host and there is no + buffer space available. This generally indicates a problem on + the host. Either it is not keeping up, or it has configured + the FPGA to transmit data at a higher rate than the transport + (USB) can support. + + U Underrun Flag: set in an IN packet if an underrun condition + was detected. Must be zero in OUT packets. Underrun occurs + when the FPGA runs out of samples, and it's not between + bursts. See the "End of Burst flag" below. + + D Dropped Packet Flag: Set in an IN packet if the FPGA + discarded an OUT packet because its timestamp had already + passed. + + S Start of Burst Flag: Set in an OUT packet if the data is the + first segment of what is logically a continuous burst of data. + Must be zero in IN packets. + + E End of Burst Flag: Set in an OUT packet if the data is the + last segment of what is logically a continuous burst of data. + Must be zero in IN packets. Underruns are not reported + when the FPGA runs out of samples between bursts. + + + RSSI 6-bit Received Strength Signal Indicator: Must be zero in OUT + packets. In IN packets, indicates RSSI as reported by front end. + FIXME The format and interpretation are to be determined. + + Chan 5-bit logical channel number. Channel number 0x1f is reserved + for control information. See "Control Channel" below. Other + channels are "data channels." Each data channel is logically + independent of the others. A data channel payload field + contains a sequence of homogeneous samples. The format of the + samples is determined by the configuration associated with the + given channel. It is often the case that the payload field + contains 32-bit complex samples, each containing 16-bit real + and imaginary components. + + Tag 4-bit tag for matching IN packets with OUT packets. + [FIXME, write more...] + + Payload Len: 9-bit field that specifies the length of the payload + field in bytes. Must be in the range 0 to 504 inclusive. + + Timestamp: 32-bit timestamp. + On IN packets, the timestamp indicates the time at which the + first sample of the packet was produced by the A/D converter(s) + for that channel. On OUT packets, the timestamp specifies the + time at which the first sample in the packet should go out the + D/A converter(s) for that channel. If a packet reaches the + head of the transmit queue, and the current time is later than + the timestamp, an error is assumed to have occurred and the + packet is discarded. As a special case, the timestamp + 0xffffffff is interpreted as "Now". + + The time base is a free running 32-bit counter that is + incremented by the A/D sample-clock. + + Payload: Variable length field. Length is specified by the + Payload Len field. + + Padding: This field is 504 - Payload Len bytes long, and its content + is unspecified. This field pads the packet out to a constant + 512 bytes. + + + +"Data Channel" payload format: +------------------------------- + +If Chan != 0x1f, the packet is a "data packet" and the payload is a +sequence of homogeneous samples. The format of the samples is +determined by the configuration associated with the given channel. +It is often the case that the payload field contains 32-bit complex +samples, each containing 16-bit real and imaginary components. + + +"Control Channel" payload format: +--------------------------------- + +If Chan == 0x1f, the packet is a "control packet". The control channel +payload consists of a sequence of 0 or more sub-packets. + +Each sub-packet starts on a 32-bit boundary, and consists of an 8-bit +Opcode field, an 8-bit Length field, Length bytes of arguments, and 0, +1, 2 or 3 bytes of padding to align the tail of the sub-packet to +a 32-bit boundary. + +Control channel packets shall be processed at the head of the queue, +and shall observe the timestamp semantics described above. + + +General sub-packet format: +-------------------------- + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-//-+-+-+-+-+-+-+-+ + | Opcode | Length | ... | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-//-+-+-+-+-+-+-+-+ + + +Specific sub-packet formats: +---------------------------- + + RID: 6-bit Request-ID. Copied from request sub-packet into corresponding + reply sub-packet. RID allows the host to match requests and replies. + + Reg Number: 10-bit Register Number. + + + +Ping Fixed Length: + + Opcode: OP_PING_FIXED + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 2 | RID | Ping Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Ping Fixed Length Reply: + + Opcode: OP_PING_FIXED_REPLY + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 2 | RID | Ping Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Write Register: + + Opcode: OP_WRITE_REG + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 6 | mbz | Reg Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Register Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Write Register Masked: + + Opcode: OP_WRITE_REG_MASKED + + REG[Num] = (REG[Num] & ~Mask) | (Value & Mask) + + That is, only the register bits that correspond to 1's in the + mask are written with the new value. + + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 10 | mbz | Reg Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Register Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Mask Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Read Register: + + Opcode: OP_READ_REG + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 2 | RID | Reg Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Read Register Reply: + + Opcode: OP_READ_REG_REPLY + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 6 | RID | Reg Number | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Register Value | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +I2C Write: + + Opcode: OP_I2C_WRITE + I2C Addr: 7-bit I2C address + Data: The bytes to write to the I2C bus + Length: Length of Data + 2 + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | Length | mbz | I2C Addr | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +I2C Read: + + Opcode: OP_I2C_READ + I2C Addr: 7-bit I2C address + Nbytes: Number of bytes to read from I2C bus + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 3 | RID | mbz | I2C Addr | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Nbytes | unspecified padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +I2C Read Reply: + + Opcode: OP_I2C_READ_REPLY + I2C Addr: 7-bit I2C address + Data: Length - 2 bytes of data read from I2C bus. + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | Length | RID | mbz | I2C Addr | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +SPI Write: + + Opcode: OP_SPI_WRITE + Enables: Which SPI enables to assert (mask) + Format: Specifies format of SPI data and Opt Header Bytes + Opt Header Bytes: 2-byte field containing optional Tx bytes; see Format + Data: The bytes to write to the SPI bus + Length: Length of Data + 6 + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | Length | mbz | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Enables | Format | Opt Header Bytes | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +SPI Read: + + Opcode: OP_SPI_READ + Enables: Which SPI enables to assert (mask) + Format: Specifies format of SPI data and Opt Header Bytes + Opt Header Bytes: 2-byte field containing optional Tx bytes; see Format + Nbytes: Number of bytes to read from SPI bus. + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 7 | RID | mbz | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Enables | Format | Opt Header Bytes | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Nbytes | unspecified padding | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +SPI Read Reply: + + Opcode: OP_SPI_READ_REPLY + Data: Length - 2 bytes of data read from SPI bus. + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | Length | RID | mbz | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Data ... . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +Delay: + + Opcode: OP_DELAY + Ticks: 16-bit unsigned delay count + + Delay Ticks clock ticks before executing next operation. + + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Opcode | 2 | Ticks | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + diff --git a/TransceiverRAD1/interfaces.h b/TransceiverRAD1/interfaces.h index 10546e48..4fd76f1d 100644 --- a/TransceiverRAD1/interfaces.h +++ b/TransceiverRAD1/interfaces.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - /* * We've now split the RAD1 into 3 separate interfaces. * diff --git a/TransceiverRAD1/pulseApproximate.m b/TransceiverRAD1/pulseApproximate.m new file mode 100644 index 00000000..2ff92348 --- /dev/null +++ b/TransceiverRAD1/pulseApproximate.m @@ -0,0 +1,15 @@ +pp = [0 0 0.015 0.18 0.7 0.96 0.7 0.18 0.015 0 0]; +t = -2.5:0.5:2.5; + +v = -0.000:-0.001:-1.999; + + +for ix1 = 1:length(v), + disp(ix1); + for ix2 = 1:length(v), + p = exp(v(ix1)*t.^2+v(ix2)*t.^4); + r(ix1,ix2) = norm(p./max(abs(p)) - pp./max(abs(pp))); + end; +end; + + diff --git a/TransceiverRAD1/radioDevice.h b/TransceiverRAD1/radioDevice.h index 9ccad888..8f9257f5 100644 --- a/TransceiverRAD1/radioDevice.h +++ b/TransceiverRAD1/radioDevice.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -31,6 +21,11 @@ #endif /** a 64-bit virtual timestamp for radio data */ +// (pat) TIMESTAMP is defined in someones include file somewheres, so get rid of that. +// This should be decapitalized. +#ifdef TIMESTAMP +#undef TIMESTAMP +#endif typedef unsigned long long TIMESTAMP; /** A class to handle a USRP rev 4, with a two RFX900 daughterboards */ @@ -73,6 +68,9 @@ class RadioDevice { /** Update the alignment between the read and write timestamps */ virtual bool updateAlignment(TIMESTAMP timestamp)=0; + + /** Set the VCTCXO input voltage */ + virtual bool setVCTCXO(unsigned int wAdjFreq)=0; /** Set the transmitter frequency */ virtual bool setTxFreq(double wFreq, double wAdjFreq)=0; diff --git a/TransceiverRAD1/radioInterface.cpp b/TransceiverRAD1/radioInterface.cpp index 6866ab2f..2748c42c 100644 --- a/TransceiverRAD1/radioInterface.cpp +++ b/TransceiverRAD1/radioInterface.cpp @@ -1,25 +1,14 @@ /* * Copyright 2008, 2009 Free Software Foundation, Inc. * -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -213,6 +202,12 @@ void RadioInterface::pullBuffer(void) } +bool RadioInterface::setVCTCXO(unsigned int tuneVoltage) +{ + return mRadio->setVCTCXO(tuneVoltage); + +} + bool RadioInterface::tuneTx(double freq, double adjFreq) { return mRadio->setTxFreq(freq, adjFreq); diff --git a/TransceiverRAD1/radioInterface.h b/TransceiverRAD1/radioInterface.h index 491fec38..f632ce23 100644 --- a/TransceiverRAD1/radioInterface.h +++ b/TransceiverRAD1/radioInterface.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -46,7 +36,7 @@ class radioVector : public signalVector { /** constructor */ radioVector(const signalVector& wVector, GSM::Time& wTime, - int& wARFCN): signalVector(wVector),mTime(wTime),mARFCN(wARFCN){}; + int& wARFCN): signalVector(wVector),mTime(wTime),mARFCN(wARFCN) {}; /** timestamp read and write operators */ GSM::Time time() const { return mTime;} @@ -117,12 +107,12 @@ class RadioClock { public: /** Set clock */ - void set(const GSM::Time& wTime) { ScopedLock lock(mLock); mClock = wTime; updateSignal.signal();} - //void set(const GSM::Time& wTime) { ScopedLock lock(mLock); mClock = wTime; updateSignal.broadcast();;} + //void set(const GSM::Time& wTime) { ScopedLock lock(mLock); mClock = wTime; updateSignal.signal();} + void set(const GSM::Time& wTime) { ScopedLock lock(mLock); mClock = wTime; updateSignal.broadcast();;} /** Increment clock */ - void incTN() { ScopedLock lock(mLock); mClock.incTN(); updateSignal.signal();} - //void incTN() { ScopedLock lock(mLock); mClock.incTN(); updateSignal.broadcast();} + //void incTN() { ScopedLock lock(mLock); mClock.incTN(); updateSignal.signal();} + void incTN() { ScopedLock lock(mLock); mClock.incTN(); updateSignal.broadcast();} /** Get clock value */ GSM::Time get() { ScopedLock lock(mLock); return mClock; } @@ -223,6 +213,8 @@ class RadioInterface { /** get receive gain */ double getRxGain(void) {if (mRadio) return mRadio->getRxGain(); else return -1;} + /** tune VCTCXO */ + bool setVCTCXO(unsigned int tuneVoltage); /** set transmit frequency */ bool tuneTx(double freq, double adjFreq); diff --git a/TransceiverRAD1/rnrad1.h b/TransceiverRAD1/rnrad1.h index f17394f8..fe464857 100644 --- a/TransceiverRAD1/rnrad1.h +++ b/TransceiverRAD1/rnrad1.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #ifndef RNRAD1_H #define RNRAD1_H diff --git a/TransceiverRAD1/rnrad1Core.cpp b/TransceiverRAD1/rnrad1Core.cpp index 828629ec..54d15eb5 100644 --- a/TransceiverRAD1/rnrad1Core.cpp +++ b/TransceiverRAD1/rnrad1Core.cpp @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #include "rnrad1Core.h" #ifdef HAVE_CONFIG_H @@ -74,7 +49,7 @@ int usbMsg (struct libusb_device_handle *udh, if (ret < 0) { // we get EPIPE if the firmware stalls the endpoint. if (ret != LIBUSB_ERROR_PIPE) { - LOG(ERR) << "libusb_control_transfer failed: " << _get_usb_error_str(ret); + LOG(ALERT) << "libusb_control_transfer failed: " << _get_usb_error_str(ret); } } @@ -261,7 +236,7 @@ bool rad1LoadFirmware (libusb_device_handle *udh, const char *filename, int i; while (!feof(f)){ - fgets(s, sizeof (s), f); /* we should not use more than 263 bytes normally */ + char *shut_up_gcc = fgets(s, sizeof (s), f); /* we should not use more than 263 bytes normally */ if(s[0]!=':'){ LOG(ERR) "File " << filename << " has invalid line: " << s; goto fail; @@ -368,7 +343,7 @@ bool rad1LoadFpga (libusb_device_handle *udh, const char *filename, ok &= (usbMsg(udh, VRQ_FPGA_SET_RX_RESET , 0, 0, 0, 0)!=0); if (!ok) - LOG(ERR) << "Failed to reset TX/RX path"; + LOG(NOTICE) << "Failed to reset TX/RX path"; // Manually reset all regs except master control to zero. // FIXME may want to remove this when we rework FPGA reset strategy. diff --git a/TransceiverRAD1/rnrad1Core.h b/TransceiverRAD1/rnrad1Core.h index acda4bac..93d328b8 100644 --- a/TransceiverRAD1/rnrad1Core.h +++ b/TransceiverRAD1/rnrad1Core.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #ifndef __RNRAD1CORE_H__ #define __RNRAD1CORE_H__ diff --git a/TransceiverRAD1/rnrad1Rx.cpp b/TransceiverRAD1/rnrad1Rx.cpp index dc5b14f8..fc0a126b 100644 --- a/TransceiverRAD1/rnrad1Rx.cpp +++ b/TransceiverRAD1/rnrad1Rx.cpp @@ -1,28 +1,3 @@ -/* -* Copyright 2008, 2009 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #include "rnrad1.h" using namespace ad9862; diff --git a/TransceiverRAD1/rnrad1Tx.cpp b/TransceiverRAD1/rnrad1Tx.cpp index 8c351f6f..9e9969be 100644 --- a/TransceiverRAD1/rnrad1Tx.cpp +++ b/TransceiverRAD1/rnrad1Tx.cpp @@ -1,28 +1,3 @@ -/* -* Copyright 2008, 2009 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - #include "rnrad1.h" #include "ad9862.h" diff --git a/TransceiverRAD1/runTransceiver.cpp b/TransceiverRAD1/runTransceiver.cpp index 542ede7e..38823c1e 100644 --- a/TransceiverRAD1/runTransceiver.cpp +++ b/TransceiverRAD1/runTransceiver.cpp @@ -2,24 +2,14 @@ * Copyright 2008, 2009 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -36,11 +26,13 @@ #include #include #include +#include using namespace std; -ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db"); - +ConfigurationKeyMap getConfigurationKeys(); +ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db", 0, getConfigurationKeys()); +FactoryCalibration gFactoryCalibration; volatile bool gbShutdown = false; static void ctrlCHandler(int signo) @@ -66,6 +58,8 @@ int main(int argc, char *argv[]) // Configure logger. gLogInit("transceiver",gConfig.getStr("Log.Level").c_str(),LOG_LOCAL7); + gFactoryCalibration.readEEPROM(); + int numARFCN=1; if (argc>1) numARFCN = atoi(argv[1]); @@ -149,3 +143,38 @@ int main(int argc, char *argv[]) delete trx; // delete radio; } + +ConfigurationKeyMap getConfigurationKeys() +{ + ConfigurationKeyMap map; + ConfigurationKey *tmp; + + tmp = new ConfigurationKey("TRX.RadioFrequencyOffset","128", + "~170Hz steps", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "96:160",// educated guess + true, + "Fine-tuning adjustment for the transceiver master clock. " + "Roughly 170 Hz/step. " + "Set at the factory. " + "Do not adjust without proper calibration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.TxAttenOffset","0", + "dB of attenuation", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "0:100",// educated guess + true, + "Hardware-specific gain adjustment for transmitter, matched to the power amplifier, expessed as an attenuationi in dB. " + "Set at the factory. " + "Do not adjust without proper calibration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + return map; +} diff --git a/TransceiverRAD1/sigProcLib.cpp b/TransceiverRAD1/sigProcLib.cpp index f020fdd6..4504d9e9 100644 --- a/TransceiverRAD1/sigProcLib.cpp +++ b/TransceiverRAD1/sigProcLib.cpp @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -599,7 +589,6 @@ signalVector *modulateBurst(const BitVector &wBurst, modBurst.isRealOnly(true); //memset(staticBurst,0,sizeof(complex)*burstSize); modBurst.fill(0.0); - //modBurst.fill(1.0); signalVector::iterator modBurstItr = modBurst.begin(); #if 0 @@ -621,6 +610,15 @@ signalVector *modulateBurst(const BitVector &wBurst, modBurstItr += samplesPerSymbol; } + // power ramping, empirically determined via CMD57 until spec is met + for (unsigned int i = 0; i < guardPeriodLength; i++) { + if (i < 5-2) + *modBurstItr = sqrt(0.25/2.0); + else + *modBurstItr = sqrt(0.001/2.0); + modBurstItr += samplesPerSymbol; + } + // shift up pi/2 // ignore starting phase, since spec allows for discontinuous phase GMSKRotate(modBurst); diff --git a/TransceiverRAD1/sigProcLib.h b/TransceiverRAD1/sigProcLib.h index e99904aa..9e9c3872 100644 --- a/TransceiverRAD1/sigProcLib.h +++ b/TransceiverRAD1/sigProcLib.h @@ -1,24 +1,14 @@ /* * Copyright 2008 Free Software Foundation, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ @@ -35,7 +25,7 @@ using namespace GSM; typedef enum Symmetry { NONE = 0, ABSSYM = 1 -}; +} Symmetry; /** Convolution type indicator */ typedef enum ConvType { @@ -46,7 +36,7 @@ typedef enum ConvType { NO_DELAY = 4, CUSTOM = 5, UNDEFINED = 255 -}; +} ConvType; /** the core data structure of the Transceiver */ class signalVector: public Vector diff --git a/TransceiverRAD1/sigProcLibTest.cpp b/TransceiverRAD1/sigProcLibTest.cpp index 475e1940..2255dc96 100644 --- a/TransceiverRAD1/sigProcLibTest.cpp +++ b/TransceiverRAD1/sigProcLibTest.cpp @@ -1,25 +1,11 @@ /* -* Copyright 2010 Free Software Foundation, Inc. * Copyright (c) 2008, 2010 Kestrel Signal Processing, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. +* This software is distributed under multiple licenses; see the COPYING file in the main directory for licensing information for this specific distribuion. - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ diff --git a/TransceiverRAD1/spi.h b/TransceiverRAD1/spi.h index 6afc058d..38ee890c 100644 --- a/TransceiverRAD1/spi.h +++ b/TransceiverRAD1/spi.h @@ -1,28 +1,3 @@ -/* -* Copyright 2011 Free Software Foundation, Inc. -* -* -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. -* -* This use of this software may be subject to additional restrictions. -* See the LEGAL file in the main directory for details. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - /* * defines for the VRQ_SPI_READ and VRQ_SPI_WRITE commands * diff --git a/apps/GetConfigurationKeys.cpp b/apps/GetConfigurationKeys.cpp new file mode 100644 index 00000000..14b2eb2f --- /dev/null +++ b/apps/GetConfigurationKeys.cpp @@ -0,0 +1,2761 @@ +/* +* Copyright 2008, 2009, 2010 Free Software Foundation, Inc. +* Copyright 2010 Kestrel Signal Processing, Inc. +* Copyright 2011, 2012 Range Networks, Inc. +* +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. +* +* This use of this software may be subject to additional restrictions. +* See the LEGAL file in the main directory for details. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + +*/ + +#include +#include +#include +#include + +#include + +ConfigurationKeyMap getConfigurationKeys() +{ + + //VALIDVALUES NOTATION: + // * A:B : range from A to B in steps of 1 + // * A:B(C) : range from A to B in steps of C + // * A:B,D:E : range from A to B and from D to E + // * A,B : multiple choices of value A and B + // * X|A,Y|B : multiple choices of string "A" with value X and string "B" with value Y + // * ^REGEX$ : string must match regular expression + + /* + TODO : double check: sometimes symbol periods == 0.55km and other times 1.1km? + TODO : .defines() vs sql audit + TODO : Optional vs *_OPT audit + TODO : configGetNumQ() + TODO : description contains "if specified" == key is optional + TODO : crossCheck possibility here: GSM.CellOptions.RADIO-LINK-TIMEOUT should be coordinated with T3109. + TODO : These things exist but aren't defined as keys. + - GSM.SI3RO + - GSM.SI3RO.CBQ + - GSM.SI3RO.CRO + - GSM.SI3RO.TEMPORARY_OFFSET + - GSM.SI3RO.PENALTY_TIME + - GPRS.SGSN.External + - GPRS.SGSN.Host + - 'GPRS.TBF.Downlink.NStuck','250',0,0,'Maximum number of blocks sent to non-advancing TBF' + - 'GPRS.MS.ResponseTime','4',0,0,'Allow the MS this much processing time to respond to an assignment message, specified as a count of RLC blocks. The MS must send its response this many blocks after receiving the message. Can also add an arbitrary additional delay if necessary.' + - ... + */ + + ConfigurationKeyMap map; + ConfigurationKey *tmp; + + tmp = new ConfigurationKey("CLI.SocketPath","/var/run/OpenBTS/command", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + false, + "Path for Unix domain datagram socket used for the OpenBTS console interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Call.QueryRRLP.Early","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for its location via RRLP during the setup of a call." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Call.QueryRRLP.Late","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for its location via RRLP during the teardown of a call." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.GSMTAP.GPRS","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Capture GPRS signaling and traffic at L1/L2 interface via GSMTAP." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.GSMTAP.GSM","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Capture GSM signaling at L1/L2 interface via GSMTAP." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.GSMTAP.TargetIP","127.0.0.1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPADDRESS, + "", + false, + "Target IP address for GSMTAP packets; the IP address of Wireshark, if you use it for real time traces." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.AttachDetach","1", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Use attach/detach procedure. " + "This will make initial LUR more prompt. " + "It will also cause an un-regstration if the handset powers off and really heavy LUR loads in areas with spotty coverage." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.FailedRegistration.Message","Your handset is not provisioned for this network. ", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING_OPT,// audited + "^[[:print:]]+$", + false, + "Send this text message, followed by the IMSI, to unprovisioned handsets that are denied registration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.FailedRegistration.ShortCode","1000", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[0-9]+$", + false, + "The return address for the failed registration message." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.NormalRegistration.Message","", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING_OPT,// audited + "^[[:print:]]+$", + false, + "The text message (followed by the IMSI) to be sent to provisioned handsets when they attach on Um. " + "By default, no message is sent. " + "To have a message sent, specify one. " + "To stop sending messages again, execute \"unconfig Control.LUR.NormalRegistration.Message\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.NormalRegistration.ShortCode","0000", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[0-9]+$", + false, + "The return address for the normal registration message." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.OpenRegistration","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::REGEX_OPT,// audited + "", + false, + "This is value is a regular expression. " + "Any handset with an IMSI matching the regular expression is allowed to register, even if it is not provisioned. " + "By default, this feature is disabled. " + "To enable open registration, specify a regular expression e.g. ^460 (which matches any IMSI starting with 460, the MCC for China). " + "To disable open registration again, execute \"unconfig Control.LUR.OpenRegistration\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.OpenRegistration.Message","Welcome to the test network. Your IMSI is ", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[[:print:]]+$", + false, + "Send this text message, followed by the IMSI, to unprovisioned handsets when they attach on Um due to open registration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.OpenRegistration.Reject","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::REGEX_OPT,// audited + "", + false, + "This is value is a regular expression. " + "Any unprovisioned handset with an IMSI matching the regular expression is rejected for registration, even if it matches Control.LUR.OpenRegistration. " + "By default, this feature is disabled. " + "To enable this filter, specify a regular expression e.g. ^460 (which matches any IMSI starting with 460, the MCC for China). " + "To disable this filter again, execute \"unconfig Control.LUR.OpenRegistration.Reject\". " + "If Control.LUR.OpenRegistration is disabled, this parameter has no effect." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.OpenRegistration.ShortCode","101", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[0-9]+$", + false, + "The return address for the open registration message." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.QueryClassmark","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for classmark during LUR." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.QueryIMEI","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for IMEI during LUR." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.QueryRRLP","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for its location via RRLP during LUR." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.SendTMSIs","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Send new TMSI assignments to handsets that are allowed to attach." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.UnprovisionedRejectCause","0x04", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "0x02|IMSI unknown in HLR," + //"0x03|Illegal MS," + "0x04|IMSI unknown in VLR," + "0x05|IMEI not accepted," + //"0x06|Illegal ME," + "0x0B|PLMN not allowed," + "0x0C|Location Area not allowed," + "0x0D|Roaming not allowed in this location area," + "0x11|Network failure," + "0x16|Congestion," + "0x20|Service option not supported," + "0x21|Requested service option not subscribed," + "0x22|Service option temporarily out of order," + "0x26|Call cannot be identified," + "0x30|Retry upon entry into a new cell," + "0x5F|Semantically incorrect message," + "0x60|Invalid mandatory information," + "0x61|Message type non-existent or not implemented," + "0x62|Message type not compatible with the protocol state," + "0x63|Information element non-existent or not implemented," + "0x64|Conditional IE error," + "0x65|Message not compatible with the protocol state," + "0x6F|Unspecified protocol error", + false, + "Reject cause for location updating failures for unprovisioned phones. " + "Reject causes come from GSM 04.08 10.5.3.6. " + "Reject cause 0x04, IMSI not in VLR, is usually the right one." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.WhiteList","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Whitelist checking is performed." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.WhiteListing.Message","Your handset needs to be added to the whitelist.", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[[:print:]]+$", + false, + "The whitelisting notification message." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.WhiteListing.RejectCause","0x04", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "0x02|IMSI unknown in HLR," + //"0x03|Illegal MS," + "0x04|IMSI unknown in VLR," + "0x05|IMEI not accepted," + //"0x06|Illegal ME," + "0x0B|PLMN not allowed," + "0x0C|Location Area not allowed," + "0x0D|Roaming not allowed in this location area," + "0x11|Network failure," + "0x16|Congestion," + "0x20|Service option not supported," + "0x21|Requested service option not subscribed," + "0x22|Service option temporarily out of order," + "0x26|Call cannot be identified," + "0x30|Retry upon entry into a new cell," + "0x5F|Semantically incorrect message," + "0x60|Invalid mandatory information," + "0x61|Message type non-existent or not implemented," + "0x62|Message type not compatible with the protocol state," + "0x63|Information element non-existent or not implemented," + "0x64|Conditional IE error," + "0x65|Message not compatible with the protocol state," + "0x6F|Unspecified protocol error", + false, + "Reject cause for handset not in the whitelist, when whitelisting is enforced. " + "Reject causes come from GSM 04.08 10.5.3.6. " + "Reject cause 0x04, IMSI not in VLR, is usually the right one." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.LUR.WhiteListing.ShortCode","1000", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[0-9]+$", + false, + "The return address for the whitelisting notificiation message." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Reporting.PhysStatusTable","/var/run/OpenBTS/ChannelTable.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + true, + "File path for channel status reporting database." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Reporting.StatsTable","/var/log/OpenBTSStats.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + true, + "File path for statistics reporting database." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Reporting.TMSITable","/var/run/OpenBTS/TMSITable.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + true, + "File path for TMSITable database." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.Reporting.TransactionTable","/var/run/OpenBTS/TransactionTable.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + true, + "File path for transaction table database." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.SACCHTimeout.BumpDown","1", + "dB", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1:3",// educated guess + false, + "Decrease the RSSI by this amount to induce more power in the MS each time we fail to receive a response from it." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.SMS.QueryRRLP","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Query every MS for its location via RRLP during an SMS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.SMSCB.Table","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH_OPT,// audited + "", + true, + "File path for SMSCB scheduling database. " + "By default, this feature is disabled. " + "To enable, specify a file path for the database e.g. /var/run/OpenBTS/SMSCB.db. " + "To disable again, execute \"unconfig Control.SMSCB.Table\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.TestCall.AutomaticModeChange","0", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::BOOLEAN, + "", + false, + "Automatically change the channel mode of a TCH/FACCH from signaling-only to speech-V1 before starting the fuzzing interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.TestCall.LocalPort","24020", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::PORT, + "", + false, + "Port number part of source for L3 payloads received from the handset in fuzzing interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.TestCall.PollTime","100", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "50:200(10)",// educated guess + false, + "Polling time of the fuzzing interface in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.TestCall.RemoteHost","127.0.0.1", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::IPADDRESS, + "", + false, + "Host part of destination for L3 payloads received from the handset in fuzzing interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.TestCall.RemotePort","24021", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::PORT, + "", + false, + "Port number part of destination for L3 payloads received from the handset in fuzzing interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.VEA","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Use very early assignment for speech call establishment. " + "See GSM 04.08 Section 7.3.2 for a detailed explanation of assignment types. " + "If VEA is selected, GSM.CellSelection.NECI should be set to 1. " + "See GSM 04.08 Sections 9.1.8 and 10.5.2.4 for an explanation of the NECI bit. " + "Note that some handset models exhibit bugs when VEA is used and these bugs may affect performance." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Control.WatchdogMinutes","60", + "minutes", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "45:75",// educated guess + false, + "Number of minutes before the radio watchdog expires and OpenBTS is restarted." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.DNS","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::MIPADDRESS_OPT,// audited + "", + true, + "The list of DNS servers to be used by downstream clients. " + "By default, DNS servers are taken from the host system. " + "To override, specify a space-separated list of the DNS servers, in IP dotted notation, eg: 1.2.3.4 5.6.7.8. " + "To use the host system DNS servers again, execute \"unconfig GGSN.DNS\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.Firewall.Enable","1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "0|Disable Firewall," + "1|Block MS Access to OpenBTS and Other MS," + "2|Block All Private IP Addresses", + true, + "0=no firewall; 1=block MS attempted access to OpenBTS or other MS; 2=block all private IP addresses." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.IP.MaxPacketSize","1520", + "bytes", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1492:9000",// educated guess + true, + "Maximum size of an IP packet. " + "Should normally be 1520." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.IP.ReuseTimeout","180", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "120:240",// educated guess, + true, + "How long IP addresses are reserved after a session ends." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.IP.TossDuplicatePackets","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + true, + "Toss duplicate TCP/IP packets to prevent unnecessary traffic on the radio." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.Logfile.Name","", + "", + ConfigurationKey::FACTORY, + ConfigurationKey::FILEPATH_OPT, + "", + true, + "If specified, internet traffic is logged to this file e.g. ggsn.log" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.MS.IP.Base","192.168.99.1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::IPADDRESS, + "", + true, + "Base IP address assigned to MS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.MS.IP.MaxCount","254", + "addresses", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:254",// educated guess + true, + "Number of IP addresses to use for MS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.MS.IP.Route","", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CIDR_OPT,// audited + "", + true, + "A route address to be used for downstream clients. " + "By default, OpenBTS manufactures this value from the GGSN.MS.IP.Base assuming a 24 bit mask. " + "To override, specify a route address in the form xxx.xxx.xxx.xxx/yy. " + "The address must encompass all MS IP addresses. " + "To use the auto-generated value again, execute \"unconfig GGSN.MS.IP.Route\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.ShellScript","", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::FILEPATH_OPT,// audited + "", + false, + "A shell script to be invoked when MS devices attach or create IP connections. " + "By default, this feature is disabled. " + "To enable, specify an absolute path to the script you wish to execute e.g. /usr/bin/ms-attach.sh. " + "To disable again, execute \"unconfig GGSN.ShellScript\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GGSN.TunName","sgsntun", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::STRING, + "^[a-z0-9]+$", + true, + "Tunnel device name for GGSN." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.advanceblocks","10", + "blocks", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5:15",// educated guess + false, + "Number of advance blocks to use in the CCCH reservation." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.CellOptions.T3168Code","5", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|500ms," + "1|1000ms," + "2|1500ms," + "3|2000ms," + "4|2500ms," + "5|3000ms," + "6|3500ms," + "7|4000ms", + true, + "Timer 3168 in the MS controls the wait time after sending a Packet Resource Request to initiate a TBF before giving up or reattempting a Packet Access Procedure, which may imply sending a new RACH. " + "This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. " + "See GSM 04.60 12.24. " + "Range 0..7 to represent 0.5sec to 4sec in 0.5sec steps." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.CellOptions.T3192Code","0", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|500ms," + "1|1000ms," + "2|1500ms," + "3|0ms," + "4|80ms," + "5|120ms," + "6|160ms," + "7|200ms", + true, + "Timer 3192 in the MS specifies the time MS continues to listen on PDCH after all downlink TBFs are finished, and is used to reduce unnecessary RACH traffic. " + "This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. " + "The value must be one of the codes described in GSM 04.60 12.24. " + "Value 0 implies 500msec; 2 implies 1500msec; 3 imples 0msec." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.ChannelCodingControl.RSSI","-40", + "dB", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "-65:-15",// educated guess + false, + "If the initial unlink signal strength is less than this amount in DB GPRS uses a lower bandwidth but more robust encoding CS-1. " + "This value should normally be GSM.Radio.RSSITarget + 10 dB." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Channels.Congestion.Threshold","200", + "probability in %", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "100:300(5)",// educated guess + false, + "The GPRS channel is considered congested if the desired bandwidth exceeds available bandwidth by this amount, specified in percent." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Channels.Congestion.Timer","60", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "30:90(5)",// educated guess + false, + "How long in seconds GPRS congestion exceeds the Congestion.Threshold before we attempt to allocate another channel for GPRS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Channels.Min.C0","2", + "channels", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:7", + false, + "Minimum number of channels allocated for GPRS service on ARFCN C0." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Channels.Min.CN","0", + "channels", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:100", + false, + "Minimum number of channels allocated for GPRS service on ARFCNs other than C0." + ); + map[tmp->getName()] = *tmp; + delete tmp; + +#if GPRS_CHANNELS_MAX_SUPPORTED + tmp = new ConfigurationKey("GPRS.Channels.Max","4", + "channels", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:10",// educated guess + false, + "Maximum number of channels allocated for GPRS service." + ); + map[tmp->getName()] = *tmp; + delete tmp; +#endif + + tmp = new ConfigurationKey("GPRS.Codecs.Downlink","14", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::STRING, + "^1{0,1}2{0,1}3{0,1}4{0,1}$",// "1234" with each number optional + false, + "List of allowed GPRS downlink codecs 1..4 for CS-1..CS-4. Currently, only 1 and 4 are supported e.g. 14." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Codecs.Uplink","14", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::STRING, + "^1{0,1}2{0,1}3{0,1}4{0,1}$",// "1234" with each number optional + false, + "List of allowed GPRS uplink codecs 1..4 for CS-1..CS-4. Currently, only 1 and 4 are supported e.g. 14." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.Assign","10", + "messages", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5:15",// educated guess + false, + "Maximum number of assign messages sent" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.N3101","20", + "responses", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "8:32",// educated guess + false, + "Counts unused USF responses to detect nonresponsive MS. " + "Should be > 8. " + "See GSM04.60 sec 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.N3103","8", + "attempts", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "4:12",// educated guess + false, + "Counts ACK/NACK attempts to detect nonresponsive MS. " + "See GSM04.60 sec 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.N3105","12", + "responses", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "6:18",// educated guess + false, + "Counts unused RRBP responses to detect nonresponsive MS. " + "See GSM04.60 sec 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.Reassign","6", + "messages", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3:9",// educated guess + false, + "Maximum number of reassign messages sent" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Counters.TbfRelease","5", + "messages", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3:8",// educated guess + false, + "Maximum number of TBF release messages sent" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Debug","0", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::BOOLEAN, + "", + false, + "Toggle GPRS debugging." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Downlink.KeepAlive","300", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "200:5000(100)",// educated guess + false, + "How often to send keep-alive messages for persistent TBFs in milliseconds; must be long enough to avoid simultaneous in-flight duplicates, and short enough that MS gets one every 5 seconds. " + "GSM 5.08 10.2.2 indicates MS must get a block every 360ms" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Downlink.Persist","0", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:10000(100)",// educated guess + false, + "After completion, downlink TBFs are held open for this time in milliseconds. " + "If non-zero, must be greater than GPRS.Downlink.KeepAlive." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Enable","1", + + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "If enabled, GPRS service is advertised in the C0T0 beacon, and GPRS service may be started on demand. " + "See also GPRS.Channels.*." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.LocalTLLI.Enable","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::BOOLEAN, + "", + false, + "Enable recognition of local TLLI" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.MS.KeepExpiredCount","20", + "structs", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "10:30",// eduated guess + false, + "How many expired MS structs to retain; they can be viewed with gprs list ms -x" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.MS.Power.Alpha","10", + "alpha", + ConfigurationKey::DEVELOPER, + ConfigurationKey::CHOICE, + "0|0.0," + "1|0.1," + "2|0.2," + "3|0.3," + "4|0.4," + "5|0.5," + "6|0.6," + "7|0.7," + "8|0.8," + "9|0.9," + "10|1.0", + false, + "MS power control parameter, unitless, in steps of 0.1, so a parameter of 5 is an alpha value of 0.5. " + "Determines sensitivity of handset to variations in downlink RSSI. " + "Valid range is 0...10 for alpha values of 0...1.0. " + "See GSM 05.08 10.2.1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.MS.Power.Gamma","31", + "2 dB steps", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:31", + false, + "MS power control parameter, in 2 dB steps. " + "Determines baseline of handset uplink power relative to downlink RSSI. " + "The optimum value will tend to be lower for BTS units with higher power output. " + "This default assumes a balanced link with a BTS output of 2-4 W/ARFCN. " + "Valid range is 0...31 for gamma values of 0...62 dB. " + "See GSM 05.08 10.2.1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.MS.Power.T_AVG_T","15", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:25", + true, + "MS power control parameter; see GSM 05.08 10.2.1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.MS.Power.T_AVG_W","15", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:25", + true, + "MS power control parameter; see GSM 05.08 10.2.1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Multislot.Max.Downlink","3", + "channels", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:10",// educated guess + false, + "Maximum number of channels used for a single MS in downlink." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Multislot.Max.Uplink","2", + "channels", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:10",// educated guess + false, + "Maximum number of channels used for a single MS in uplink." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.NC.NetworkControlOrder","2", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:3", + true, + "Controls measurement reports and cell reselection mode (MS autonomous or under network control); should not be changed. " + "See GSM 5.08 10.1.4." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.NMO","2", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "1|Mode I," + "2|Mode II," + "3|Mode III", + false, + "Network Mode of Operation. " + "See GSM 03.60 Section 6.3.3.1 and 24.008 4.7.1.6. " + "Allowed values are 1, 2, 3 for modes I, II, III. " + "Mode II (2) is recommended. " + "Mode I implies combined routing updating procedures." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.PRIORITY-ACCESS-THR","6", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::CHOICE, + "0|Packet access not allowed in the cell," + "3|Packet access allowed for priority level 1," + "4|Packet access allowed for priority level 1 to 2," + "5|Packet access allowed for priority level 1 to 3," + "6|Packet access allowed for priority level 1 to 4", + true, + "Code contols GPRS packet access priorities allowed. " + "See GSM04.08 table 10.5.76." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.RAC","0", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:255", + true, + "GPRS Routing Area Code, advertised in the C0T0 beacon." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.RA_COLOUR","0", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:7", + false, + "GPRS Routing Area Color as advertised in the C0T0 beacon." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.RRBP.Min","0", + "?", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:3", + false, + "Minimum value for RRBP reservations, range 0..3. " + "Should normally be 0. " + "A non-zero value gives the MS more time to respond to the RRBP request." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Reassign.Enable","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::BOOLEAN, + "", + false, + "Enable TBF Reassignment" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.SendIdleFrames","0", + "", + ConfigurationKey::FACTORY, + ConfigurationKey::BOOLEAN, + "", + false, + "Should be 0 for current transceiver or 1 for deprecated version of transceiver." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.SGSN.port","1920", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::PORT, + "", + false, + "Port number of the SGSN required for GPRS service. This must match the port specified in the SGSN config file, currently osmo_sgsn.cfg." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.TBF.Downlink.Poll1","10", + "blocks", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5:15",// educated guess + false, + "When the first poll is sent for a downlink tbf, measured in blocks sent." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.TBF.EST","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::BOOLEAN, + "", + false, + "Allow MS to request another uplink assignment at end up of uplink TBF. " + "See GSM 4.60 9.2.3.4" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.TBF.Expire","30000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "20000:40000",// educated guess + false, + "How long to try before giving up on a TBF." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.TBF.KeepExpiredCount","20", + "structs", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "15:25",// educated guess + false, + "How many expired TBF structs to retain; they can be viewed with gprs list tbf -x" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.TBF.Retry","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|Do Not Retry," + "1|Codec 1," + "2|Codec 2," + "3|Codec 3," + "4|Codec 4", + false, + "If 0, no tbf retry, otherwise if a tbf fails it will be retried with this codec, numbered 1..4" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.Channels.Idle","6000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3000:6000(100)",// educated guess + false, + "How long in milliseconds a GPRS channel is idle before being returned to the pool of channels. " + "Also depends on Channels.Min. " + "Currently the channel cannot be returned to the pool while there is any GPRS activity on any channel." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.MS.Idle","600", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "300:900(10)",// educated guess + false, + "How long in seconds an MS is idle before the BTS forgets about it." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.MS.NonResponsive","6000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3000:9000(100)",// educated guess + false, + "How long in milliseconds a TBF is non-responsive before the BTS kills it." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.T3169","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + false, + "Nonresponsive uplink TBF resource release timer, in milliseconds. " + "See GSM04.60 sec 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.T3191","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + false, + "Nonresponsive downlink TBF resource release timer, in milliseconds. " + "See GSM04.60 Section 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.T3193","0", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:5000(100)",// educated guess + false, + "Timer T3193 (in milliseconds) in the base station corresponds to T3192 in the MS, which is set by GPRS.CellOptions.T3192Code. " + "The T3193 value should be slightly longer than that specified by the T3192Code. " + "If 0, the BTS will fill in a default value based on T3192Code." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Timers.T3195","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + false, + "Nonresponsive downlink TBF resource release timer, in milliseconds. " + "See GSM04.60 Section 13." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Uplink.KeepAlive","300", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "200:5000(100)",// educated guess + false, + "How often to send keep-alive messages for persistent TBFs in milliseconds; must be long enough to avoid simultaneous in-flight duplicates, and short enough that MS gets one every 5 seconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GPRS.Uplink.Persist","4000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:6000(100)",// educated guess + true, + "After completion, uplink TBFs are held open for this time in milliseconds. " + "If non-zero, must be greater than GPRS.Uplink.KeepAlive. " + "This is broadcast in the beacon and it cannot be changed once BTS is started." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CCCH.AGCH.QMax","5", + "queued access grants", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "3:8",// educated guess + false, + "Maximum number of access grants to be queued for transmission on AGCH before declaring congestion." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CCCH.CCCH-CONF","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "1|C-V Beacon," + "2|C-IV Beacon", + true, + "CCCH configuration type. " + "See GSM 10.5.2.11 for encoding. " + "Value of 1 means we are using a C-V beacon. " + "Any other value selects a C-IV beacon. " + "In C2.9 and earlier, the only allowed value is 1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellOptions.RADIO-LINK-TIMEOUT","15", + "seconds", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "10:20",// educated guess + true, + "Seconds before declaring a physical link dead." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellSelection.CELL-RESELECT-HYSTERESIS","3", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|0dB," + "1|2dB," + "2|4dB," + "3|6dB," + "4|8dB," + "5|10dB," + "6|12dB," + "7|14dB", + false, + "Cell Reselection Hysteresis. " + "See GSM 04.08 10.5.2.4, Table 10.5.23 for encoding. " + "Encoding is $2N$ dB, values of $N$ are 0...7 for 0...14 dB." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellSelection.MS-TXPWR-MAX-CCH","0", + "dB", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:31", + false, + "Cell selection parameters. " + "See GSM 04.08 10.5.2.4." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellSelection.NCCsPermitted","1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::VALRANGE, + "0:255", + false, + "NCCs Permitted. " + "An 8-bit mask of allowed NCCs. " + "Unless you are coordinating with another carrier, this should probably just select your own NCC." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellSelection.NECI","1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "0|New establishment causes are not supported," + "1|New establishment causes are supported", + false, + "NECI, New Establishment Causes. " + "This must be set to 1 if you want to support very early assignment (VEA). " + "It can be set to 1 even if you do not use VEA, so you might as well leave it as 1. " + "See GSM 04.08 10.5.2.4, Table 10.5.23 and 04.08 9.1.8, Table 9.9 and the Control.VEA parameter." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.CellSelection.RXLEV-ACCESS-MIN","0", + "dB", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:63", + false, + "Cell selection parameters. " + "See GSM 04.08 10.5.2.4." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Channels.C1sFirst","0", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::BOOLEAN, + "", + true, + "Allocate C-I slots first, starting at C0T1. " + "Otherwise, allocate C-VII slots first." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Channels.NumC1s","7", + "timeslots", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:100",// this is crap, calc from arfcns + true, + "Number of Combination-I timeslots to configure. " + "The C-I slot carries a single full-rate TCH, used for speech calling." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Channels.NumC7s","0", + "timeslots", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:100",// this is crap, calc from arfcns + true, + "Number of Combination-VII timeslots to configure. " + "The C-VII slot carries 8 SDCCHs, useful to handle high registration loads or SMS. " + "If C0T0 is C-IV, which it always is in C2.9 and earlier,, you must have at least one C-VII also." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Channels.SDCCHReserve","0", + "SDCCHs", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:10",// educated guess + false, + "Number of SDCCHs to reserve for non-LUR operations. " + "This can be used to force LUR transactions into a lower priority." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Cipher.CCHBER","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::VALRANGE, + "0.0:1.0", + false, + "Probability of a bit getting toggled in a control channel burst for cracking protection." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Cipher.Encrypt","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Encrypt traffic between phone and OpenBTS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Cipher.RandomNeighbor","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::VALRANGE, + "0.0:1.0", + false, + "Probability of a random neighbor being added to SI5 for cracking protection." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Cipher.ScrambleFiller","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Scramble filler in layer 2 for cracking protection." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Control.GPRSMaxIgnore","5", + "messages", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3:8",// educated guess + false, + "Ignore GPRS messages on GSM control channels. " + "Value is number of consecutive messages to ignore." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Handover.FailureHoldoff","5", + "seconds", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "2:7",// educated guess + false, + "The number of seconds to wait before attempting another handover with a given neighbor BTS." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + // (pat) This value interacts with "GPRS.ChannelCodingControl.RSSI" which selects the GPRS codec. + // In my opinion, if GPRS is enabled, we should handover to try to get the RSSI above GPRS.ChannelCodingControl.RSSI. + tmp = new ConfigurationKey("GSM.Handover.LocalRSSIMin","-80", + "dBm", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "-100:-60",// educated guess + false, + "Do not handover if downlink RSSI is above this level (in dBm), regardless of power difference." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Handover.ThresholdDelta","10", + "dB", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "5:20",// educated guess + false, + "A neighbor downlink signal must be this much stronger (in dB) than this downlink signal for handover to occur." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.BSIC.BCC","2", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "0:7", + false, + "GSM basestation color code; lower 3 bits of the BSIC. " + "BCC values in a multi-BTS network should be assigned so that BTS units with overlapping coverage do not share a BCC. " + "This value will also select the training sequence used for all slots on this unit." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.BSIC.NCC","0", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "0:7", + false, + "GSM network color code; upper 3 bits of the BSIC. " + "Assigned by your national regulator. " + "Must be distinct from NCCs of other GSM operators in your area." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.CI","10", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "0:65535", + false, + "Cell ID, 16 bits. " + "Should be unique." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.LAC","1000", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "0:65280", + false, + "Location area code, 16 bits, values 0xFFxx are reserved. " + "For multi-BTS networks, assign a unique LAC to each BTS unit. " + "(That is not the normal procedure in conventional GSM networks, but is the correct procedure in OpenBTS networks.)" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.MCC","001", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::STRING, + "^[0-9]{3}$", + false, + "Mobile country code; must be three digits. " + "Defined in ITU-T E.212. 001 for test networks." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.MNC","01", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::STRING, + "^[0-9]{2,3}$", + false, + "Mobile network code, two or three digits. " + "Assigned by your national regulator. " + "01 for test networks." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Identity.ShortName","Range", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::STRING, + "^[[:alnum:]]+$", + false, + "Network short name, displayed on some phones. " + "Optional but must be defined if you also want the network to send time-of-day." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MS.Power.Damping","50", + "?damping value", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "25:75",// educated guess + false, + "Damping value for MS power control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MS.Power.Max","33", + "dBm", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "5:100",// educated guess + false, + "Maximum commanded MS power level in dBm." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MS.Power.Min","5", + "dBm", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "5:100",// educated guess + false, + "Minimum commanded MS power level in dBm." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MS.TA.Damping","50", + "?damping value", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "25:75",// educated guess + false, + "Damping value for timing advance control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MS.TA.Max","62", + "symbol periods", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:62", + false, + "Maximum allowed timing advance in symbol periods. " + "One symbol period of round-trip delay is about 0.55 km of distance. " + "Ignore RACH bursts with delays greater than this. " + "Can be used to limit service range. " + "Valid range is 1..62." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.MaxSpeechLatency","2", + "20 millisecond frames", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:5",// educated guess + false, + "Maximum allowed speech buffering latency, in 20 millisecond frames. " + "If the jitter is larger than this delay, frames will be lost." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Neighbors","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::MIPADDRESS_OPT,// audited + "", + false, + "A list of IP addresses of neighbor BTSs available for handover. " + "By default, this feature is disabled. " + "To enable, specify a space-separated list of the BTS IP addresses, in IP dotted notation, eg: 1.2.3.4 5.6.7.8. " + "To disable again, execute \"unconfig GSM.Neighbors\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Neighbors.NumToSend","8", + "neighbors", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:10",// educated guess + false, + "Maximum number of neighbors to send to handset in a neighbor list." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Ny1","5", + "repeats", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:10",// educated guess + true, + "Maximum number of repeats of the Physical Information Message during handover procedure, GSM 04.08 11.1.3." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RACH.MaxRetrans","1", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|1 retransmission," + "1|2 retransmissions," + "2|4 retransmissions," + "3|7 retransmissions", + false, + "Maximum RACH retransmission attempts. " + "This is the raw parameter sent on the BCCH. " + "See GSM 04.08 10.5.2.29 for encoding." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RACH.TxInteger","14", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::CHOICE, + "0|3 slots," + "1|4 slots," + "2|5 slots," + "3|6 slots," + "4|7 slots," + "5|8 slots," + "6|9 slots," + "7|10 slots," + "8|11 slots," + "9|12 slots," + "10|14 slots," + "11|16 slots," + "12|20 slots," + "13|25 slots," + "14|32 slots," + "15|50 slots", + false, + "Parameter to spread RACH busts over time. " + "This is the raw parameter sent on the BCCH. " + "See GSM 04.08 10.5.2.29 for encoding." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.ACCURACY","40", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "20:60",// educated guess + false, + "Requested accuracy of location request. " + "K in r=10(1.1**K-1), where r is the accuracy in meters. " + "See 3GPP 03.32, sect 6.2" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.ALMANAC.ASSIST.PRESENT","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Send almanac info to mobile" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.ALMANAC.REFRESH.TIME","24.0", + "hours", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "18.0:30.0(0.1)",// educated guess + false, + "How often the almanac is refreshed, in hours" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.ALMANAC.URL","http://www.navcen.uscg.gov/?pageName=currentAlmanac&format=yuma", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::STRING, + "^(http|ftp)://[[:alnum:]_.-]", + false, + "URL of almanac source." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.EPHEMERIS.ASSIST.COUNT","9", + "satellites", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "6:12",// educated guess + false, + "number of satellites to include in navigation model" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.EPHEMERIS.REFRESH.TIME","1.0", + "hours", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0.5:1.5(0.1)",// educated guess + false, + "How often the ephemeris is refreshed, in hours." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.EPHEMERIS.URL","ftp://ftp.trimble.com/pub/eph/CurRnxN.nav", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::STRING, + "^(http|ftp)://[[:alnum:]_.-]", + false, + "URL of ephemeris source." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.RESPONSETIME","4", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "2:6",// educated guess + false, + "Mobile timeout. " + "(OpenBTS timeout is 130 sec = max response time + 2.) N in 2**N. " + "See 3GPP 04.31 sect A.2.2.1" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.SEED.ALTITUDE","0", + "meters", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "-420:8850(5)", + false, + "Seed altitude in meters wrt geoidal surface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.SEED.LATITUDE","37.777423", + "degrees", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "-90.000000:90.000000", + false, + "Seed latitude in degrees. " + "-90 (south pole) .. +90 (north pole)" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.SEED.LONGITUDE","-122.39807", + "degrees", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::VALRANGE, + "-180.000000:180.000000", + false, + "Seed longitude in degrees. " + "-180 (west of greenwich) .. 180 (east)" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.RRLP.SERVER.URL","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::STRING_OPT,// audited + "^(http|ftp)://[[:alnum:]_.-]", + false, + "URL of RRLP server. " + "By default, this feature is disabled. " + "To enable, specify a server URL eg: http://localhost/cgi/rrlpserver.cgi. " + "To disable again, execute \"unconfig GSM.RRLP.SERVER.URL\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.ARFCNs","1", + "ARFCNs", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::VALRANGE, + "1:10",// educated guess + true, + "The number of ARFCNs to use. " + "The ARFCN set will be C0, C0+2, C0+4, etc." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.Band","900", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "850|GSM850," + "900|PGSM900," + "1800|DCS1800," + "1900|PCS1900", + true, + "The GSM operating band. " + "Valid values are 850 (GSM850), 900 (PGSM900), 1800 (DCS1800) and 1900 (PCS1900). " + "For non-multiband units, this value is dictated by the hardware and should not be changed." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.C0","51", + "", + ConfigurationKey::CUSTOMERSITE, + ConfigurationKey::CHOICE, + ConfigurationKey::getARFCNsString(), + true, + "The C0 ARFCN. " + "Also the base ARFCN for a multi-ARFCN configuration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.MaxExpectedDelaySpread","4", + "symbol periods", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "1:4", + false, + "Expected worst-case delay spread in symbol periods, roughly 3.7 us or 1.1 km per unit. " + "This parameter is dependent on the terrain type in the installation area. " + "Typical values are 1 for open terrain and small coverage areas. " + "For large coverage areas, a value of 4 is strongly recommended. " + "This parameter has a large effect on computational requirements of the software radio; values greater than 4 should be avoided." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.NeedBSIC","0", + "", + ConfigurationKey::FACTORY, + ConfigurationKey::BOOLEAN, + "", + false, + "Does the Radio type require the full BSIC" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.MaxAttenDB","10", + "dB", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:80",// educated guess + false, + "Maximum transmitter attenuation level, in dB wrt full scale on the D/A output. " + "This sets the minimum power output level in the output power control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.MinAttenDB","0", + "dB", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "0:80",// educated guess + false, + "Minimum transmitter attenuation level, in dB wrt full scale on the D/A output. " + "This sets the maximum power output level in the output power control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.NumSamples","10", + "sample count", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5:20",// educated guess + false, + "Number of samples averaged by the output power control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.Period","6000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "4500:7500(100)",// educated guess + false, + "Power manager control loop master period, in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.SamplePeriod","2000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1500:2500(100)",// educated guess + false, + "Sample period for the output power control loopm in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.PowerManager.TargetT3122","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "3750:6250(100)",// educated guess + false, + "Target value for T3122, the random access hold-off timer, for the power control loop." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.RSSITarget","-50", + "dB", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "-75:-25",// educated guess + false, + "Target uplink RSSI for MS power control loop, in dB wrt to A/D full scale. " + "Should be 6-10 dB above the noise floor." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Radio.RxGain","47", + "dB", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "25:75",// educated guess + true, + "Receiver gain setting in dB. " + "Ideal value is dictated by the hardware; 47 dB for RAD1. " + "This database parameter is static but the receiver gain can be modified in real time with the CLI rxgain command." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.ShowCountry","0", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::BOOLEAN, + "", + false, + "Tell the phone to show the country name based on the MCC." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3103","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + true, + "Handover timeout in milliseconds, GSM 04.08 11.1.2. " + "This is the timeout for a handset to sieze a channel during handover." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3105","50", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "25:75(5)",// educated guess + true, + "Milliseconds for handset to respond to physical information. " + "GSM 04.08 11.1.2." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3113","10000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5000:15000(500)",// educated guess + false, + "Paging timer T3113 in milliseconds. " + "This is the timeout for a handset to respond to a paging request. " + "This should usually be the same as SIP.Timer.B in your VoIP network." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3122Max","255000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "127500:382500(1000)",// educated guess + false, + "Maximum allowed value for T3122, the RACH holdoff timer, in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3122Min","2000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1000:3000(100)",// educated guess + false, + "Minimum allowed value for T3122, the RACH holdoff timer, in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("GSM.Timer.T3212","30", + "minutes", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:1530(6)", + false, + "Registration timer T3212 period in minutes. " + "Should be a factor of 6. " + "Set to 0 to disable periodic registration. " + "Should be smaller than SIP registration period." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Peering.Neighbor.RefreshAge","60000", + "milliseconds", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "30000:90000(1000)",// educated guess + false, + "Milliseconds before refreshing parameters from a neighbor." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Peering.NeighborTable.Path","/var/run/OpenBTS/NeighborTable.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + true, + "File path for neighbor information database." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Peering.Port","16001", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::PORT, + "", + true, + "The UDP port used by the peer interface for handover." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Peering.ResendCount","5", + "attempts", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "3:8",// educated guess + false, + "Number of tries to send message over the peer interface before giving up" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Peering.ResendTimeout","100", + "milliseconds", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "50:100(10)",// educated guess + false, + "Milliseconds before resending a message on the peer interface" + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("RTP.Range","98", + "ports", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::VALRANGE, + "25:200",// educated guess + true, + "Range of RTP port pool. " + "Pool is RTP.Start to RTP.Range-1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("RTP.Start","16484", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::PORT, + "", + true, + "Base of RTP port pool. " + "Pool is RTP.Start to RTP.Range-1." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SGSN.Debug","0", + "", + ConfigurationKey::DEVELOPER, + ConfigurationKey::BOOLEAN, + "", + false, + "Add layer-3 messages to the GGSN.Logfile, if any." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SGSN.Timer.ImplicitDetach","3480", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2000:4000(10)",// educated guess + false, + "3GPP 24.008 11.2.2. " + "GPRS attached MS is implicitly detached in seconds. " + "Should be at least 240 seconds greater than SGSN.Timer.RAUpdate."); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SGSN.Timer.MS.Idle","600", + "?seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "300:900(10)",// educated guess + false, + "How long an MS is idle before the SGSN forgets TLLI specific information." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SGSN.Timer.RAUpdate","3240", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:11160(2)", // This is the allowed range of times expressed by the dopey GPRS Timer IE. Max is 31 deci-hours. + false, + "Also known as T3312, 3GPP 24.008 4.7.2.2. " + "How often the MS reports into the SGSN when it is idle, in seconds. " + "Setting to 0 or >12000 deactivates entirely, i.e., sets the timer to effective infinity. " + "Note: to prevent GPRS Routing Area Updates you must set both this and GSM.Timer.T3212 to 0. " + + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SGSN.Timer.Ready","44", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "30:90",// educated guess + false, + "Also known as T3314, 3GPP 24.008 4.7.2.1. " + "Inactivity period required before MS may perform another routing area or cell update, in seconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.DTMF.RFC2833","1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Use RFC-2833 (RTP event signalling) for in-call DTMF." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.DTMF.RFC2833.PayloadType","101", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::VALRANGE, + "96:127", + false, + "Payload type to use for RFC-2833 telephone event packets." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.DTMF.RFC2967","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Use RFC-2967 (SIP INFO method) for in-call DTMF." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Local.IP","127.0.0.1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPADDRESS, + "", + true, + "IP address of the OpenBTS machine as seen by its proxies. " + "If these are all local, this can be localhost." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Local.Port","5062", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::PORT, + "", + true, + "IP port that OpenBTS uses for its SIP interface." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.MaxForwards","70", + "referrals", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1:100", + false, + "Maximum allowed number of referrals." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Proxy.Registration","127.0.0.1:5064", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPANDPORT, + "", + false, + "The IP host and port of the proxy to be used for registration and authentication. " + "This should normally be the subscriber registry SIP interface, not Asterisk." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Proxy.SMS","127.0.0.1:5063", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPANDPORT, + "", + false, + "The IP host and port of the proxy to be used for text messaging. " + "This is smqueue, for example." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Proxy.Speech","127.0.0.1:5060", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPANDPORT, + "", + false, + "The IP host and port of the proxy to be used for normal speech calls. " + "This is Asterisk, for example." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.RegistrationPeriod","90", + "minutes", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "6:2298",// educated guess + false, + "Registration period in minutes for MS SIP users. " + "Should be longer than GSM T3212." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.RFC3428.NoTrying","0", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::BOOLEAN, + "", + false, + "Send 100 Trying response to SIP MESSAGE, even though that violates RFC-3428." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.SMSC","smsc", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE_OPT,// audited + "smsc", + false, + "The SMSC handler in smqueue. " + "This is the entity that handles full 3GPP MIME-encapsulted TPDUs. " + "If not defined, use direct numeric addressing. " + "The value should be disabled with \"unconfig SIP.SMSC\" if SMS.MIMEType is \"text/plain\" or set to \"smsc\" if SMS.MIMEType is \"application/vnd.3gpp\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Timer.A","2000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1500:2500(100)",// educated guess + false, + "SIP timer A, the INVITE retry period, RFC-3261 Section 17.1.1.2, in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Timer.B","10000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5000:15000(100)",// educated guess + false, + "INVITE transaction timeout in milliseconds. " + "This value should usually match GSM.Timer.T3113." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Timer.E","500", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "250:750(10)",// educated guess + false, + "Non-INVITE initial request retransmit period in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Timer.F","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + false, + "Non-INVITE initial request timeout in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SIP.Timer.H","5000", + "milliseconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "2500:7500(100)",// educated guess + false, + "ACK timeout period in milliseconds." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SMS.FakeSrcSMSC","0000", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::STRING, + "^[0-9]+$", + false, + "Use this to fill in L4 SMSC address in SMS delivery." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SMS.MIMEType","application/vnd.3gpp.sms", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::CHOICE, + "application/vnd.3gpp.sms," + "text/plain", + false, + "This is the MIME Type that OpenBTS will use for RFC-3428 SIP MESSAGE payloads. " + "Valid values are \"application/vnd.3gpp.sms\" and \"text/plain\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SubscriberRegistry.A3A8","/OpenBTS/comp128", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + false, + "Path to the program that implements the A3/A8 algorithm." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SubscriberRegistry.db","/var/lib/asterisk/sqlite3dir/sqlite3.db", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::FILEPATH, + "", + false, + "The location of the sqlite3 database holding the subscriber registry." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SubscriberRegistry.Manager.Title","Subscriber Registry", + "", + ConfigurationKey::CUSTOMER, + ConfigurationKey::STRING, + "^[[:print:]]+$", + false, + "Title text to be displayed on the subscriber registry manager." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SubscriberRegistry.Manager.VisibleColumns","name username type context host", + "", + ConfigurationKey::CUSTOMERTUNE, + ConfigurationKey::STRING, + "^(name){0,1} (username){0,1} (type){0,1} (context){0,1} (host){0,1}$", + false, + "A space separated list of columns to display in the subscriber registry manager." + ); + + tmp = new ConfigurationKey("SubscriberRegistry.Port","5064", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::PORT, + "", + false, + "Port used by the SIP Authentication Server. NOTE: In some older releases (pre-2.8.1) this is called SIP.myPort." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("SubscriberRegistry.UpstreamServer","", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::STRING_OPT,// audited + "", + false, + "URL of the subscriber registry HTTP interface on the upstream server. " + "By default, this feature is disabled. " + "To enable, specify a server URL eg: http://localhost/cgi/subreg.cgi. " + "To disable again, execute \"unconfig SubscriberRegistry.UpstreamServer\"." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.IP","127.0.0.1", + "", + ConfigurationKey::CUSTOMERWARN, + ConfigurationKey::IPADDRESS, + "", + true, + "IP address of the transceiver application." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.MinimumRxRSSI","-63", + "dB", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "-90:90",// educated guess + false, + "Bursts received at the physical layer below this threshold are automatically ignored. " + "Values in dB. " + "Set at the factory. " + "Do not adjust without proper calibration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.Port","5700", + "", + ConfigurationKey::FACTORY, + ConfigurationKey::PORT, + "", + true, + "IP port of the transceiver application." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.RadioFrequencyOffset","128", + "~170Hz steps", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "64:192", + true, + "Fine-tuning adjustment for the transceiver master clock. " + "Roughly 170 Hz/step. " + "Set at the factory. " + "Do not adjust without proper calibration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.Timeout.Clock","10", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "5:15",// educated guess + false, + "How long to wait during a read operation from the transceiver before giving up." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + // unused? + tmp = new ConfigurationKey("TRX.Timeout.Start","2", + "seconds", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "1:3",// educated guess + false, + "How long to wait during system startup before checking to see if the transceiver can be reached." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("TRX.TxAttenOffset","0", + "dB of attenuation", + ConfigurationKey::FACTORY, + ConfigurationKey::VALRANGE, + "0:100",// educated guess + true, + "Hardware-specific gain adjustment for transmitter, matched to the power amplifier, expessed as an attenuationi in dB. " + "Set at the factory. " + "Do not adjust without proper calibration." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Test.GSM.SimulatedFER.Downlink","0", + "probability in %", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:100(5)",// educated guess + true, + "Probability (0-100) of dropping any downlink frame to test robustness." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Test.GSM.SimulatedFER.Uplink","0", + "probability in %", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:100(5)",// educated guess + true, + "Probability (0-100) of dropping any uplink frame to test robustness." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Test.GSM.UplinkFuzzingRate","0", + "probability in %", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:100(5)",// educated guess + true, + "Probability (0-100) of flipping a bit in any uplink frame to test robustness." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + tmp = new ConfigurationKey("Test.SIP.SimulatedPacketLoss","0", + "probability in %", + ConfigurationKey::DEVELOPER, + ConfigurationKey::VALRANGE, + "0:100(5)",// educated guess + true, + "Probability (0-100) of dropping any inbound or outbound SIP packet to test robustness." + ); + map[tmp->getName()] = *tmp; + delete tmp; + + return map; +} diff --git a/apps/Makefile.am b/apps/Makefile.am index 3f246a0b..3aacf45c 100644 --- a/apps/Makefile.am +++ b/apps/Makefile.am @@ -21,21 +21,26 @@ include $(top_srcdir)/Makefile.common -AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) $(USB_INCLUDES) $(WITH_INCLUDES) $(OPENBTS_CPPFLAGS) -AM_CXXFLAGS = -Wall -pthread -ldl $(OPENBTS_CXXFLAGS) +DESTDIR := -DESTDIR = +# These variables are defined in ../Makefile.common +# AM_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) +# AM_CXXFLAGS = -Wall -ldl -pthread noinst_PROGRAMS = \ OpenBTS \ OpenBTSDo \ OpenBTSCLI -OpenBTS_SOURCES = OpenBTS.cpp -OpenBTS_LDADD = \ +OpenBTS_SOURCES = OpenBTS.cpp GetConfigurationKeys.cpp +OpenBTS_CPPFLAGS = $(STD_DEFINES_AND_INCLUDES) + +ourlibs = \ $(CLI_LA) \ $(SIP_LA) \ $(GSM_LA) \ + $(GPRS_LA) \ + $(SGSNGGSN_LA) \ $(TRX_LA) \ $(GLOBALS_LA) \ $(CONTROL_LA) \ @@ -43,14 +48,25 @@ OpenBTS_LDADD = \ $(COMMON_LA) \ $(SQLITE_LA) \ $(SMS_LA) \ + $(PEERING_LA) \ $(OSIP_LIBS) \ $(ORTP_LIBS) +OpenBTS_LDADD = $(ourlibs) -ldl -losipparser2 -losip2 -lortp -la53 + OpenBTSCLI_SOURCES = OpenBTSCLI.cpp +# RedHat RHEL5 needs to have ncurses included +OpenBTSCLI_LDADD = -lreadline -lncurses OpenBTSDo_SOURCES = OpenBTSDo.cpp EXTRA_DIST = \ - OpenBTS.example.sql + OpenBTS.example.sql \ + setUpFiles.sh \ + generateConfigTable.sh \ + exportConfigTable.sh \ + importConfigTable.sh \ + generateKeys.sh \ + verifyKey.sh install: OpenBTS OpenBTSCLI OpenBTSDo mkdir -p "$(DESTDIR)/OpenBTS/" @@ -58,4 +74,13 @@ install: OpenBTS OpenBTSCLI OpenBTSDo install OpenBTSCLI "$(DESTDIR)/OpenBTS/" install OpenBTSDo "$(DESTDIR)/OpenBTS/" mkdir -p "$(DESTDIR)/etc/OpenBTS/" + install iptables.rules "$(DESTDIR)/etc/OpenBTS/" install OpenBTS.example.sql "$(DESTDIR)/etc/OpenBTS/" + mkdir -p "$(DESTDIR)/etc/rsyslog.d/" + mkdir -p "$(DESTDIR)/etc/logrotate.d/" + install rsyslogd.OpenBTS.conf "$(DESTDIR)/etc/rsyslog.d/OpenBTS.conf" + install OpenBTS.logrotate "$(DESTDIR)/etc/logrotate.d/OpenBTS" + mkdir -p "$(DESTDIR)/home/openbts/" + install CLI "$(DESTDIR)/home/openbts/" + install openbtsconfig "$(DESTDIR)/home/openbts/" + diff --git a/apps/OpenBTS.cpp b/apps/OpenBTS.cpp index 592e24b8..be26ae4d 100644 --- a/apps/OpenBTS.cpp +++ b/apps/OpenBTS.cpp @@ -1,35 +1,33 @@ /* * Copyright 2008, 2009, 2010 Free Software Foundation, Inc. * Copyright 2010 Kestrel Signal Processing, Inc. -* Copyright 2011,2012 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * -* This software is distributed under the terms of the GNU Affero Public License. -* See the COPYING file in the main directory for details. +* This software is distributed under multiple licenses; +* see the COPYING file in the main directory for licensing +* information for this specific distribuion. * * This use of this software may be subject to additional restrictions. * See the LEGAL file in the main directory for details. - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ #include #include +#include +#include #include +std::vector configurationCrossCheck(const std::string& key); +static const char *cOpenBTSConfigEnv = "OpenBTSConfigFile"; // Load configuration from a file. -ConfigurationTable gConfig("/etc/OpenBTS/OpenBTS.db"); +ConfigurationTable gConfig(getenv(cOpenBTSConfigEnv)?getenv(cOpenBTSConfigEnv):"/etc/OpenBTS/OpenBTS.db","OpenBTS", getConfigurationKeys()); +#include +Log dummy("openbts",gConfig.getStr("Log.Level").c_str(),LOG_LOCAL7); // Set up the performance reporter. #include @@ -48,12 +46,13 @@ ReportingTable gReports(gConfig.getStr("Control.Reporting.StatsTable").c_str()); #include #include -#include #include #include #include #include #include +#include "NeighborTable.h" +#include #include @@ -62,9 +61,16 @@ ReportingTable gReports(gConfig.getStr("Control.Reporting.StatsTable").c_str()); #include #include +// (pat) mcheck.h is for mtrace, which permits memory leak detection. +// Set env MALLOC_TRACE=logfilename +// Call mtrace() in the program. +// post-process the logfilename with mtrace (a perl script.) +// #include + using namespace std; using namespace GSM; +int gBtsXg = 0; // Enable gprs const char* gDateTime = __DATE__ " " __TIME__; @@ -88,12 +94,24 @@ SIP::SIPInterface gSIPInterface; // So don't create this until AFTER loading the config file. GSMConfig gBTS; +// Note to all from pat: +// It is inadvisable to statically initialize any non-trivial entity here because +// the underlying dependencies may not yet have undergone their static initialization. +// For example, if any of these throw an alarm, the system will crash because +// the Logger may not have been initialized yet. + // Our interface to the software-defined radio. TransceiverManager gTRX(gConfig.getNum("GSM.Radio.ARFCNs"), gConfig.getStr("TRX.IP").c_str(), gConfig.getNum("TRX.Port")); -// Subscriber registry +// Subscriber registry and http authentication SubscriberRegistry gSubscriberRegistry; +/** The global peering interface. */ +Peering::PeerInterface gPeerInterface; + +/** The global neighbor table. */ +Peering::NeighborTable gNeighborTable; + /** Define a function to call any time the configuration database changes. */ void purgeConfig(void*,int,char const*, char const*, sqlite3_int64) @@ -101,6 +119,7 @@ void purgeConfig(void*,int,char const*, char const*, sqlite3_int64) LOG(INFO) << "purging configuration cache"; gConfig.purge(); gBTS.regenerateBeacon(); + gResetWatchdog(); } @@ -116,14 +135,14 @@ void startTransceiver() // Start the transceiver binary, if the path is defined. // If the path is not defined, the transceiver must be started by some other process. - char TRXnumARFCN[16]; - sprintf(TRXnumARFCN,"%1d", static_cast(gConfig.getNum("GSM.Radio.ARFCNs"))); + char TRXnumARFCN[4]; + sprintf(TRXnumARFCN,"%1d",(int)gConfig.getNum("GSM.Radio.ARFCNs")); LOG(NOTICE) << "starting transceiver " << transceiverPath << " " << TRXnumARFCN; gTransceiverPid = vfork(); LOG_ASSERT(gTransceiverPid>=0); if (gTransceiverPid==0) { // Pid==0 means this is the process that starts the transceiver. - execlp(transceiverPath,transceiverPath,TRXnumARFCN,NULL); + execlp(transceiverPath,transceiverPath,TRXnumARFCN,(void*)NULL); LOG(EMERG) << "cannot find " << transceiverPath; _exit(1); } else { @@ -142,8 +161,6 @@ void createStats() // count of OpenBTS start events gReports.create("OpenBTS.Starts"); - // count of exit events driven from the CLI - gReports.create("OpenBTS.Exit.Normal.CLI"); // count of watchdog restarts gReports.create("OpenBTS.Exit.Error.Watchdog"); // count of aborts due to problems with CLI socket @@ -218,8 +235,6 @@ void createStats() //gReports.create("OpenBTS.GSM.MM.TMSI.Unknown"); // count of CM Service requests for MOC gReports.create("OpenBTS.GSM.MM.CMServiceRequest.MOC"); - // count of CM Service requests for emergency calls - gReports.create("OpenBTS.GSM.MM.CMServiceRequest.SOS"); // count of CM Service requests for MOSMS gReports.create("OpenBTS.GSM.MM.CMServiceRequest.MOSMS"); // count of CM Service requests for services we don't support @@ -244,6 +259,8 @@ void createStats() gReports.create("OpenBTS.GSM.CC.MOD.Disconnect"); // total number of minutes of carried calls gReports.create("OpenBTS.GSM.CC.CallMinutes"); + // count of dropped calls + gReports.create("OpenBTS.GSM.CC.DroppedCalls"); // count of CS (non-GPRS) channel assignments gReports.create("OpenBTS.GSM.RR.ChannelAssignment"); @@ -270,6 +287,14 @@ void createStats() //gReports.create("OpenBTS.TRX.Command.Failed"); //gReports.create("OpenBTS.TRX.FailedStart"); //gReports.create("OpenBTS.TRX.LostLink"); + + // GPRS + // number of RACH bursts processed for GPRS + gReports.create("GPRS.RACH"); + // number of TBFs assigned + gReports.create("GPRS.TBF"); + // number of MSInfo records generated + gReports.create("GPRS.MSInfo"); } @@ -277,22 +302,58 @@ void createStats() int main(int argc, char *argv[]) { + // mtrace(); // Enable memory leak detection. Unfortunately, huge amounts of code have been started in the constructors above. // TODO: Properly parse and handle any arguments if (argc > 1) { - for (int argi = 0; argi < argc; argi++) { + for (int argi = 1; argi < argc; argi++) { // Skip argv[0] which is the program name. if (!strcmp(argv[argi], "--version") || !strcmp(argv[argi], "-v")) { cout << gVersionString << endl; + continue; } + if (!strcmp(argv[argi], "--gensql")) { + cout << gConfig.getDefaultSQL(string(argv[0]), gVersionString) << endl; + continue; + } + if (!strcmp(argv[argi], "--gentex")) { + cout << gConfig.getTeX(string(argv[0]), gVersionString) << endl; + continue; + } + + // (pat) Adding support for specified sql file. + // Unfortunately, the Config table was inited quite some time ago, + // so stick this arg in the environment, whence the ConfigurationTable can find it, and then reboot. + if (!strcmp(argv[argi],"--config")) { + if (++argi == argc) { + LOG(ALERT) <<"Missing argument to -sql option"; + exit(2); + } + setenv(cOpenBTSConfigEnv,argv[argi],1); + execl(argv[0],"OpenBTS",NULL); + LOG(ALERT) <<"execl failed? Exiting..."; + exit(0); + } + if (!strcmp(argv[argi],"--help")) { + printf("OpenBTS [--version --gensql --genex] [--config file.db]\n"); + printf("OpenBTS exiting...\n"); + exit(0); + } + + printf("OpenBTS: unrecognized argument: %s\nexiting...\n",argv[argi]); } return 0; } createStats(); - + + gConfig.setCrossCheckHook(&configurationCrossCheck); + gReports.incr("OpenBTS.Starts"); + gNeighborTable.NeighborTableInit( + gConfig.getStr("Peering.NeighborTable.Path").c_str()); + int sock = socket(AF_UNIX,SOCK_DGRAM,0); if (sock<0) { perror("creating CLI datagram socket"); @@ -306,8 +367,7 @@ int main(int argc, char *argv[]) srandom(time(NULL)); gConfig.setUpdateHook(purgeConfig); - gLogInit("openbts",gConfig.getStr("Log.Level").c_str()); - LOG(ALERT) << "OpenBTS starting, ver " << VERSION << " build date " << __DATE__; + LOG(ALERT) << "OpenBTS (re)starting, ver " << VERSION << " build date " << __DATE__; COUT("\n\n" << gOpenBTSWelcome << "\n"); gTMSITable.open(gConfig.getStr("Control.Reporting.TMSITable").c_str()); @@ -323,7 +383,7 @@ int main(int argc, char *argv[]) // Start the transceiver interface. LOG(INFO) << "checking transceiver"; //gTRX.ARFCN(0)->powerOn(); - //sleep(gConfig.getNum("TRX.Timeout.Start",2)); + //sleep(gConfig.getNum("TRX.Timeout.Start")); bool haveTRX = gTRX.ARFCN(0)->powerOn(false); Thread transceiverThread; @@ -339,6 +399,34 @@ int main(int argc, char *argv[]) // Start the SIP interface. gSIPInterface.start(); + // Start the peer interface + gPeerInterface.start(); + + // Sync factory calibration as defaults from radio EEPROM + signed sdrsn = gTRX.ARFCN(0)->getFactoryCalibration("sdrsn"); + if (sdrsn != 0 && sdrsn != 65535) { + signed val; + + val = gTRX.ARFCN(0)->getFactoryCalibration("band"); + if (gConfig.isValidValue("GSM.Radio.Band", val)) { + gConfig.mSchema["GSM.Radio.Band"].updateDefaultValue(val); + } + + val = gTRX.ARFCN(0)->getFactoryCalibration("freq"); + if (gConfig.isValidValue("TRX.RadioFrequencyOffset", val)) { + gConfig.mSchema["TRX.RadioFrequencyOffset"].updateDefaultValue(val); + } + + val = gTRX.ARFCN(0)->getFactoryCalibration("rxgain"); + if (gConfig.isValidValue("GSM.Radio.RxGain", val)) { + gConfig.mSchema["GSM.Radio.RxGain"].updateDefaultValue(val); + } + + val = gTRX.ARFCN(0)->getFactoryCalibration("txgain"); + if (gConfig.isValidValue("TRX.TxAttenOffset", val)) { + gConfig.mSchema["TRX.TxAttenOffset"].updateDefaultValue(val); + } + } // // Configure the radio. @@ -428,12 +516,25 @@ int main(int argc, char *argv[]) SDCCHLogicalChannel(0,0,gSDCCH_4_3), }; Thread C0T0SDCCHControlThread[4]; + // Subchannel 2 used for CBCH if SMSCB enabled. + bool SMSCB = (gConfig.getStr("Control.SMSCB.Table").length() != 0); + CBCHLogicalChannel CBCH(gSDCCH_4_2); + Thread CBCHControlThread; for (int i=0; i<4; i++) { + if (SMSCB && (i==2)) continue; C0T0SDCCH[i].downstream(C0radio); C0T0SDCCHControlThread[i].start((void*(*)(void*))Control::DCCHDispatcher,&C0T0SDCCH[i]); C0T0SDCCH[i].open(); gBTS.addSDCCH(&C0T0SDCCH[i]); } + // Install CBCH if used. + if (SMSCB) { + LOG(INFO) << "creating CBCH for SMSCB"; + CBCH.downstream(C0radio); + CBCH.open(); + gBTS.addCBCH(&CBCH); + CBCHControlThread.start((void*(*)(void*))Control::SMSCBSender,NULL); + } // @@ -443,7 +544,19 @@ int main(int argc, char *argv[]) // Count configured slots. unsigned sCount = 1; - if (gConfig.defines("GSM.Channels.C1sFirst")) { + + if (!gConfig.defines("GSM.Channels.NumC1s")) { + int numChan = numARFCNs*7; + LOG(CRIT) << "GSM.Channels.NumC1s not defined. Defaulting to " << numChan << "."; + gConfig.set("GSM.Channels.NumC1s",numChan); + } + if (!gConfig.defines("GSM.Channels.NumC7s")) { + int numChan = numARFCNs-1; + LOG(CRIT) << "GSM.Channels.NumC7s not defined. Defaulting to " << numChan << "."; + gConfig.set("GSM.Channels.NumC7s",numChan); + } + + if (gConfig.getBool("GSM.Channels.C1sFirst")) { // Create C-I slots. for (int i=0; i configurationCrossCheck(const string& key) { + vector warnings; + ostringstream warning; + + // GSM.Timer.T3113 should equal SIP.Timer.B + if (key.compare("GSM.Timer.T3113") == 0 || key.compare("SIP.Timer.B") == 0) { + string gsm = gConfig.getStr("GSM.Timer.T3113"); + string sip = gConfig.getStr("SIP.Timer.B"); + if (gsm.compare(sip) != 0) { + warning << "GSM.Timer.T3113 (" << gsm << ") and SIP.Timer.B (" << sip << ") should usually have the same value"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // Control.VEA depends on GSM.CellSelection.NECI + } else if (key.compare("Control.VEA") == 0 || key.compare("GSM.CellSelection.NECI") == 0) { + if (gConfig.getBool("Control.VEA") && gConfig.getStr("GSM.CellSelection.NECI").compare("1") != 0) { + warning << "Control.VEA is enabled but will not be functional until GSM.CellSelection.NECI is set to \"1\""; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // GSM.Timer.T3212 should be a factor of six and shorter than SIP.RegistrationPeriod + } else if (key.compare("GSM.Timer.T3212") == 0 || key.compare("SIP.RegistrationPeriod") == 0) { + int gsm = gConfig.getNum("GSM.Timer.T3212"); + int sip = gConfig.getNum("SIP.RegistrationPeriod"); + if (key.compare("GSM.Timer.T3212") == 0 && gsm % 6) { + warning << "GSM.Timer.T3212 should be a factor of 6"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + if (gsm >= sip) { + warning << "GSM.Timer.T3212 (" << gsm << ") should be shorter than SIP.RegistrationPeriod (" << sip << ")"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // GPRS.ChannelCodingControl.RSSI should normally be 10db more than GSM.Radio.RSSITarget + } else if (key.compare("GPRS.ChannelCodingControl.RSSI") == 0 || key.compare("GSM.Radio.RSSITarget") == 0) { + int gprs = gConfig.getNum("GPRS.ChannelCodingControl.RSSI"); + int gsm = gConfig.getNum("GSM.Radio.RSSITarget"); + if ((gprs - gsm) != 10) { + warning << "GPRS.ChannelCodingControl.RSSI (" << gprs << ") should normally be 10db higher than GSM.Radio.RSSITarget (" << gsm << ")"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // TODO : This NEEDS to be an error not a warning. OpenBTS will fail to start because of an assert if an invalid value is used. + // GSM.Radio.C0 needs to be inside the valid range of ARFCNs for GSM.Radio.Band + } else if (key.compare("GSM.Radio.C0") == 0 || key.compare("GSM.Radio.Band") == 0) { + int c0 = gConfig.getNum("GSM.Radio.C0"); + string band = gConfig.getStr("GSM.Radio.Band"); + string range; + if (band.compare("850") == 0 && (c0 < 128 || 251 < c0)) { + range = "128-251"; + } else if (band.compare("900") == 0 && (c0 < 1 || 124 < c0)) { + range = "1-124"; + } else if (band.compare("1800") == 0 && (c0 < 512 || 885 < c0)) { + range = "512-885"; + } else if (band.compare("1900") == 0 && (c0 < 512 || 810 < c0)) { + range = "512-810"; + } + if (range.length()) { + warning << "GSM.Radio.C0 (" << c0 << ") falls outside the valid range of ARFCNs " << range << " for GSM.Radio.Band (" << band << ")"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // SGSN.Timer.ImplicitDetach should be at least 240 seconds greater than SGSN.Timer.RAUpdate" + } else if (key.compare("SGSN.Timer.ImplicitDetach") == 0 || key.compare("SGSN.Timer.RAUpdate") == 0) { + int detach = gConfig.getNum("SGSN.Timer.ImplicitDetach"); + int update = gConfig.getNum("SGSN.Timer.RAUpdate"); + if ((detach - update) < 240) { + warning << "SGSN.Timer.ImplicitDetach (" << detach << ") should be at least 240 seconds greater than SGSN.Timer.RAUpdate (" << update << ")"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // Control.LUR.WhiteList depends on Control.WhiteListing.Message, Control.LUR.WhiteListing.RejectCause and Control.WhiteListing.ShortCode + } else if (key.compare("Control.LUR.WhiteList") == 0 || key.compare("Control.WhiteListing.Message") == 0 || + key.compare("Control.LUR.WhiteListing.RejectCause") == 0 || key.compare("Control.WhiteListing.ShortCode") == 0) { + if (gConfig.getBool("Control.LUR.WhiteList")) { + if (!gConfig.getStr("Control.WhiteListing.Message").length()) { + warning << "Control.LUR.WhiteList is enabled but will not be functional until Control.WhiteListing.Message is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } else if (!gConfig.getStr("Control.LUR.WhiteListing.RejectCause").length()) { + warning << "Control.LUR.WhiteList is enabled but will not be functional until Control.WhiteListing.RejectCause is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } else if (!gConfig.getStr("Control.WhiteListing.ShortCode").length()) { + warning << "Control.LUR.WhiteList is enabled but will not be functional until Control.WhiteListing.ShortCode is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + } + + // GSM.CellSelection.NCCsPermitted needs to contain our own GSM.Identity.BSIC.NCC + } else if (key.compare("GSM.CellSelection.NCCsPermitted") == 0 || key.compare("GSM.Identity.BSIC.NCC") == 0) { + int ourNCCMask = gConfig.getNum("GSM.CellSelection.NCCsPermitted"); + int NCCMaskBit = 1 << gConfig.getNum("GSM.Identity.BSIC.NCC"); + if ((NCCMaskBit & ourNCCMask) == 0) { + warning << "GSM.CellSelection.NCCsPermitted is not set to a mask which contains the local network color code defined in GSM.Identity.BSIC.NCC. "; + warning << "Set GSM.CellSelection.NCCsPermitted to " << NCCMaskBit; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // Control.LUR.FailedRegistration.Message depends on Control.LUR.FailedRegistration.ShortCode + } else if (key.compare("Control.LUR.FailedRegistration.Message") == 0 || key.compare("Control.LUR.FailedRegistration.ShortCode") == 0) { + if (gConfig.getStr("Control.LUR.FailedRegistration.Message").length() && !gConfig.getStr("Control.LUR.FailedRegistration.ShortCode").length()) { + warning << "Control.LUR.FailedRegistration.Message is enabled but will not be functional until Control.LUR.FailedRegistration.ShortCode is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // Control.LUR.NormalRegistration.Message depends on Control.LUR.NormalRegistration.ShortCode + } else if (key.compare("Control.LUR.NormalRegistration.Message") == 0 || key.compare("Control.LUR.NormalRegistration.ShortCode") == 0) { + if (gConfig.getStr("Control.LUR.NormalRegistration.Message").length() && !gConfig.getStr("Control.LUR.NormalRegistration.ShortCode").length()) { + warning << "Control.LUR.NormalRegistration.Message is enabled but will not be functional until Control.LUR.NormalRegistration.ShortCode is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // Control.LUR.OpenRegistration depends on Control.LUR.OpenRegistration.ShortCode + } else if (key.compare("Control.LUR.OpenRegistration") == 0 || key.compare("Control.LUR.OpenRegistration.ShortCode") == 0) { + if (gConfig.getStr("Control.LUR.OpenRegistration").length() && !gConfig.getStr("Control.LUR.OpenRegistration.ShortCode").length()) { + warning << "Control.LUR.OpenRegistration is enabled but will not be functional until Control.LUR.OpenRegistration.ShortCode is set"; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + + // TODO : SIP.SMSC is actually broken with the verification bits, no way to set value as null + // SIP.SMSC should normally be NULL if SMS.MIMIEType is "text/plain" and "smsc" if SMS.MIMEType is "application/vnd.3gpp". + } else if (key.compare("SMS.MIMEType") == 0 || key.compare("SIP.SMSC") == 0) { + string sms = gConfig.getStr("SMS.MIMEType"); + string sip = gConfig.getStr("SIP.SMSC"); + if (sms.compare("application/vnd.3gpp.sms") == 0 && sip.compare("smsc") != 0) { + warning << "SMS.MIMEType is set to \"application/vnc.3gpp.sms\", SIP.SMSC should usually be set to \"smsc\""; + warnings.push_back(warning.str()); + warning.str(std::string()); + } + } + + return warnings; +} + // vim: ts=4 sw=4 diff --git a/apps/OpenBTS.example.sql b/apps/OpenBTS.example.sql index e17f4001..b20e12e5 100644 --- a/apps/OpenBTS.example.sql +++ b/apps/OpenBTS.example.sql @@ -1,134 +1,234 @@ +-- +-- This file was generated using: ./OpenBTS --gensql +-- binary version: release TRUNK C built Jul 1 2013 rev5843 +-- +-- Future changes should not be put in this file directly but +-- rather in the program's ConfigurationKey schema. +-- PRAGMA foreign_keys=OFF; -PRAGMA journal_mode=WAL; BEGIN TRANSACTION; -CREATE TABLE CONFIG ( KEYSTRING TEXT UNIQUE NOT NULL, VALUESTRING TEXT, STATIC INTEGER DEFAULT 0, OPTIONAL INTEGER DEFAULT 0, COMMENTS TEXT DEFAULT ''); +CREATE TABLE IF NOT EXISTS CONFIG ( KEYSTRING TEXT UNIQUE NOT NULL, VALUESTRING TEXT, STATIC INTEGER DEFAULT 0, OPTIONAL INTEGER DEFAULT 0, COMMENTS TEXT DEFAULT ''); INSERT OR IGNORE INTO "CONFIG" VALUES('CLI.SocketPath','/var/run/command',0,0,'Path for Unix domain datagram socket used for the OpenBTS console interface.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.PhysStatusTable','/var/run/OpenBTSChannelTable.db',1,0,'File path for channel status reporting database. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.TransactionTable','/var/run/TransactionTable.db',1,0,'File path for transaction table database. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.StatsTable','/var/log/OpenBTSStats.db',1,0,'File path for statistics reporting database. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.TMSITable','/var/run/OpenBTSTMSITable.db',1,0,'File path for TMSITable database. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Call.QueryRRLP.Early',NULL,0,1,'If not NULL, query every MS for its location via RRLP during the setup of a call.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Call.QueryRRLP.Late',NULL,0,1,'If not NULL, query every MS for its location via RRLP during the teardown of a call.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.GSMTAP.TargetIP',NULL,0,1,'Target IP address for GSMTAP packets; the IP address of Wireshark, if you use it for GSM.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.AttachDetach',1,0,0,'Attach/detach flag. Set to 1 to use attach/detach procedure, 0 otherwise. This will make initial LUR more prompt. It will also cause an un-regstration if the handset powers off and really heavy LUR loads in areas with spotty coverage.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.FailedRegistration.Message','Your handset is not provisioned for this network. ',0,1,'If defined, send this text message, followed by the IMSI, to unprovisioned handsets that are denied registration.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.FailedRegistration.ShortCode','1000',0,1,'The return address for the failed registration message. If the message is defined, this must also be defined.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.NormalRegistration.Message',NULL,0,1,'If defined, send this text message, followed by the IMSI, to provisioned handsets when they attach on Um.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.NormalRegistration.ShortCode','0000',0,1,'The return address for the normal registration message. If the message is defined, this must also be defined.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration',NULL,0,1,'If not NULL, allow unprovisioned handsets to attach in Um.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration.Message','Welcome to the GSM test network. Your IMSI is ',0,1,'If defined, send this text message, followed by the IMSI, to unprovisioned handsets when they attach on Um due to open registration.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration.ShortCode','101',0,1,'The return address for the open registration message. If the message is defined, this must also be defined.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryClassmark',NULL,0,1,'If not NULL, query every MS for classmark during LUR.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryIMEI',NULL,0,1,'If not NULL, query every MS for IMSI during LUR.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryRRLP',NULL,0,1,'If not NULL, query every MS for its location via RRLP during LUR.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.SendTMSIs',NULL,0,1,'If not NULL, send new TMSI assignments to handsets that are allowed to attach.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Call.QueryRRLP.Early','0',0,0,'1=enabled, 0=disabled - Query every MS for its location via RRLP during the setup of a call.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Call.QueryRRLP.Late','0',0,0,'1=enabled, 0=disabled - Query every MS for its location via RRLP during the teardown of a call.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.GSMTAP.GPRS','0',0,0,'1=enabled, 0=disabled - Capture GPRS signaling and traffic at L1/L2 interface via GSMTAP.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.GSMTAP.GSM','0',0,0,'1=enabled, 0=disabled - Capture GSM signaling at L1/L2 interface via GSMTAP.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.GSMTAP.TargetIP','127.0.0.1',0,0,'Target IP address for GSMTAP packets; the IP address of Wireshark, if you use it for real time traces.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.AttachDetach','1',0,0,'1=enabled, 0=disabled - Use attach/detach procedure. This will make initial LUR more prompt. It will also cause an un-regstration if the handset powers off and really heavy LUR loads in areas with spotty coverage.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.FailedRegistration.Message','Your handset is not provisioned for this network. ',0,0,'Send this text message, followed by the IMSI, to unprovisioned handsets that are denied registration.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.FailedRegistration.ShortCode','1000',0,0,'The return address for the failed registration message.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.NormalRegistration.Message','',0,0,'The text message (followed by the IMSI) to be sent to provisioned handsets when they attach on Um. By default, no message is sent. To have a message sent, specify one. To stop sending messages again, execute "unconfig Control.LUR.NormalRegistration.Message".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.NormalRegistration.ShortCode','0000',0,0,'The return address for the normal registration message.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration','',0,0,'This is value is a regular expression. Any handset with an IMSI matching the regular expression is allowed to register, even if it is not provisioned. By default, this feature is disabled. To enable open registration, specify a regular expression e.g. ^460 (which matches any IMSI starting with 460, the MCC for China). To disable open registration again, execute "unconfig Control.LUR.OpenRegistration".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration.Message','Welcome to the test network. Your IMSI is ',0,0,'Send this text message, followed by the IMSI, to unprovisioned handsets when they attach on Um due to open registration.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration.Reject','',0,0,'This is value is a regular expression. Any unprovisioned handset with an IMSI matching the regular expression is rejected for registration, even if it matches Control.LUR.OpenRegistration. By default, this feature is disabled. To enable this filter, specify a regular expression e.g. ^460 (which matches any IMSI starting with 460, the MCC for China). To disable this filter again, execute "unconfig Control.LUR.OpenRegistration.Reject". If Control.LUR.OpenRegistration is disabled, this parameter has no effect.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.OpenRegistration.ShortCode','101',0,0,'The return address for the open registration message.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryClassmark','0',0,0,'1=enabled, 0=disabled - Query every MS for classmark during LUR.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryIMEI','0',0,0,'1=enabled, 0=disabled - Query every MS for IMEI during LUR.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.QueryRRLP','0',0,0,'1=enabled, 0=disabled - Query every MS for its location via RRLP during LUR.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.SendTMSIs','0',0,0,'1=enabled, 0=disabled - Send new TMSI assignments to handsets that are allowed to attach.'); INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.UnprovisionedRejectCause','0x04',0,0,'Reject cause for location updating failures for unprovisioned phones. Reject causes come from GSM 04.08 10.5.3.6. Reject cause 0x04, IMSI not in VLR, is usually the right one.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.WhiteList','0',0,0,'1=enabled, 0=disabled - Whitelist checking is performed.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.WhiteListing.Message','Your handset needs to be added to the whitelist.',0,0,'The whitelisting notification message.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.WhiteListing.RejectCause','0x04',0,0,'Reject cause for handset not in the whitelist, when whitelisting is enforced. Reject causes come from GSM 04.08 10.5.3.6. Reject cause 0x04, IMSI not in VLR, is usually the right one.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.LUR.WhiteListing.ShortCode','1000',0,0,'The return address for the whitelisting notificiation message.'); INSERT OR IGNORE INTO "CONFIG" VALUES('Control.NumSQLTries','3',0,0,'Number of times to retry SQL queries before declaring a database access failure.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.SMS.QueryRRLP',NULL,0,1,'If not NULL, query every MS for its location via RRLP during an SMS.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TMSITable.MaxAge','72',0,0,'Maximum allowed age for a TMSI in hours.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TMSITable.MaxSize','100000',0,0,'Maximum size of TMSI table before oldest TMSIs are discarded.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Control.VEA',1,0,1,'If not NULL, user very early assignment for speech call establishment. See GSM 04.08 Section 7.3.2 for a detailed explanation of assignment types. If VEA is selected, GSM.CellSelection.NECI should be set to 1. See GSM 04.08 Sections 9.1.8 and 10.5.2.4 for an explanation of the NECI bit.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CCCH.AGCH.QMax','5',0,0,'Maximum number of access grants to be queued for transmission on AGCH before declaring congrestion.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CCCH.CCCH-CONF','1',0,0,'CCCH configuration type. See GSM 10.5.2.11 for encoding. Value of 1 means we are using a C-V beacon. Any other value selects a C-IV beacon.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.SDCCHReserve','0',0,0,'Number of SDCCHs to reserve for non-LUR operations. This can be used to force LUR transactions into a lower priority.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.PhysStatusTable','/var/run/ChannelTable.db',1,0,'File path for channel status reporting database. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.StatsTable','/var/log/OpenBTSStats.db',1,0,'File path for statistics reporting database. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.TMSITable','/var/run/TMSITable.db',1,0,'File path for TMSITable database. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.Reporting.TransactionTable','/var/run/TransactionTable.db',1,0,'File path for transaction table database. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.SACCHTimeout.BumpDown','1',0,0,'Decrease the RSSI by this amount to induce more power in the MS each time we fail to receive a response from it.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.SMS.QueryRRLP','0',0,0,'1=enabled, 0=disabled - Query every MS for its location via RRLP during an SMS.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.SMSCB.Table','',1,0,'File path for SMSCB scheduling database. By default, this feature is disabled. To enable, specify a file path for the database e.g. /var/run/SMSCB.db. To disable again, execute "unconfig Control.SMSCB.Table". Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TestCall.AutomaticModeChange','0',0,0,'1=enabled, 0=disabled - Automatically change the channel mode of a TCH/FACCH from signaling-only to speech-V1 before starting the fuzzing interface.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TestCall.LocalPort','24020',0,0,'Port number part of source for L3 payloads received from the handset in fuzzing interface.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TestCall.PollTime','100',0,0,'Polling time of the fuzzing interface in milliseconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TestCall.RemoteHost','127.0.0.1',0,0,'Host part of destination for L3 payloads received from the handset in fuzzing interface.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.TestCall.RemotePort','24021',0,0,'Port number part of destination for L3 payloads received from the handset in fuzzing interface.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.VEA','0',0,0,'1=enabled, 0=disabled - Use very early assignment for speech call establishment. See GSM 04.08 Section 7.3.2 for a detailed explanation of assignment types. If VEA is selected, GSM.CellSelection.NECI should be set to 1. See GSM 04.08 Sections 9.1.8 and 10.5.2.4 for an explanation of the NECI bit. Note that some handset models exhibit bugs when VEA is used and these bugs may affect performance.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Control.WatchdogMinutes','60',0,0,'Number of minutes before the radio watchdog expires and OpenBTS is restarted.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.DNS','',1,0,'The list of DNS servers to be used by downstream clients. By default, DNS servers are taken from the host system. To override, specify a space-separated list of the DNS servers, in IP dotted notation, eg: 1.2.3.4 5.6.7.8. To use the host system DNS servers again, execute "unconfig GGSN.DNS". Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.Firewall.Enable','1',1,0,'0=no firewall; 1=block MS attempted access to OpenBTS or other MS; 2=block all private IP addresses. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.IP.MaxPacketSize','1520',1,0,'Maximum size of an IP packet. Should normally be 1520. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.IP.ReuseTimeout','180',1,0,'How long IP addresses are reserved after a session ends. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.IP.TossDuplicatePackets','0',1,0,'1=enabled, 0=disabled - Toss duplicate TCP/IP packets to prevent unnecessary traffic on the radio. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.Logfile.Name','/var/log/openbts-ggsn.log',1,0,'If specified, internet traffic is logged to this file e.g. ggsn.log Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.MS.IP.Base','192.168.99.1',1,0,'Base IP address assigned to MS. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.MS.IP.MaxCount','254',1,0,'Number of IP addresses to use for MS. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.MS.IP.Route','',1,0,'A route address to be used for downstream clients. By default, OpenBTS manufactures this value from the GGSN.MS.IP.Base assuming a 24 bit mask. To override, specify a route address in the form xxx.xxx.xxx.xxx/yy. The address must encompass all MS IP addresses. To use the auto-generated value again, execute "unconfig GGSN.MS.IP.Route". Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.ShellScript','',0,0,'A shell script to be invoked when MS devices attach or create IP connections. By default, this feature is disabled. To enable, specify an absolute path to the script you wish to execute e.g. /usr/bin/ms-attach.sh. To disable again, execute "unconfig GGSN.ShellScript".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GGSN.TunName','sgsntun',1,0,'Tunnel device name for GGSN. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.CellOptions.T3168Code','5',1,0,'Timer 3168 in the MS controls the wait time after sending a Packet Resource Request to initiate a TBF before giving up or reattempting a Packet Access Procedure, which may imply sending a new RACH. This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. See GSM 04.60 12.24. Range 0..7 to represent 0.5sec to 4sec in 0.5sec steps. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.CellOptions.T3192Code','0',1,0,'Timer 3192 in the MS specifies the time MS continues to listen on PDCH after all downlink TBFs are finished, and is used to reduce unnecessary RACH traffic. This code is broadcast to the MS in the C0T0 beacon in the GPRS Cell Options IE. The value must be one of the codes described in GSM 04.60 12.24. Value 0 implies 500msec; 2 implies 1500msec; 3 imples 0msec. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.ChannelCodingControl.RSSI','-40',0,0,'If the initial unlink signal strength is less than this amount in DB GPRS uses a lower bandwidth but more robust encoding CS-1. This value should normally be GSM.Radio.RSSITarget + 10 dB.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Channels.Congestion.Threshold','200',0,0,'The GPRS channel is considered congested if the desired bandwidth exceeds available bandwidth by this amount, specified in percent.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Channels.Congestion.Timer','60',0,0,'How long in seconds GPRS congestion exceeds the Congestion.Threshold before we attempt to allocate another channel for GPRS.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Channels.Min.C0','2',0,0,'Minimum number of channels allocated for GPRS service on ARFCN C0.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Channels.Min.CN','0',0,0,'Minimum number of channels allocated for GPRS service on ARFCNs other than C0.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Codecs.Downlink','14',0,0,'List of allowed GPRS downlink codecs 1..4 for CS-1..CS-4. Currently, only 1 and 4 are supported e.g. 14.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Codecs.Uplink','14',0,0,'List of allowed GPRS uplink codecs 1..4 for CS-1..CS-4. Currently, only 1 and 4 are supported e.g. 14.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.Assign','10',0,0,'Maximum number of assign messages sent'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.N3101','20',0,0,'Counts unused USF responses to detect nonresponsive MS. Should be > 8. See GSM04.60 sec 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.N3103','8',0,0,'Counts ACK/NACK attempts to detect nonresponsive MS. See GSM04.60 sec 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.N3105','12',0,0,'Counts unused RRBP responses to detect nonresponsive MS. See GSM04.60 sec 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.Reassign','6',0,0,'Maximum number of reassign messages sent'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Counters.TbfRelease','5',0,0,'Maximum number of TBF release messages sent'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Debug','0',0,0,'1=enabled, 0=disabled - Toggle GPRS debugging.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Downlink.KeepAlive','300',0,0,'How often to send keep-alive messages for persistent TBFs in milliseconds; must be long enough to avoid simultaneous in-flight duplicates, and short enough that MS gets one every 5 seconds. GSM 5.08 10.2.2 indicates MS must get a block every 360ms'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Downlink.Persist','0',0,0,'After completion, downlink TBFs are held open for this time in milliseconds. If non-zero, must be greater than GPRS.Downlink.KeepAlive.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Enable','0',0,0,'1=enabled, 0=disabled - If enabled, GPRS service is advertised in the C0T0 beacon, and GPRS service may be started on demand. See also GPRS.Channels.*.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.LocalTLLI.Enable','1',0,0,'1=enabled, 0=disabled - Enable recognition of local TLLI'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.MS.KeepExpiredCount','20',0,0,'How many expired MS structs to retain; they can be viewed with gprs list ms -x'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.MS.Power.Alpha','10',0,0,'MS power control parameter, unitless, in steps of 0.1, so a parameter of 5 is an alpha value of 0.5. Determines sensitivity of handset to variations in downlink RSSI. Valid range is 0...10 for alpha values of 0...1.0. See GSM 05.08 10.2.1.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.MS.Power.Gamma','31',0,0,'MS power control parameter, in 2 dB steps. Determines baseline of handset uplink power relative to downlink RSSI. The optimum value will tend to be lower for BTS units with higher power output. This default assumes a balanced link with a BTS output of 2-4 W/ARFCN. Valid range is 0...31 for gamma values of 0...62 dB. See GSM 05.08 10.2.1.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.MS.Power.T_AVG_T','15',1,0,'MS power control parameter; see GSM 05.08 10.2.1. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.MS.Power.T_AVG_W','15',1,0,'MS power control parameter; see GSM 05.08 10.2.1. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Multislot.Max.Downlink','3',0,0,'Maximum number of channels used for a single MS in downlink.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Multislot.Max.Uplink','2',0,0,'Maximum number of channels used for a single MS in uplink.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.NC.NetworkControlOrder','2',1,0,'Controls measurement reports and cell reselection mode (MS autonomous or under network control); should not be changed. See GSM 5.08 10.1.4. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.NMO','2',0,0,'Network Mode of Operation. See GSM 03.60 Section 6.3.3.1 and 24.008 4.7.1.6. Allowed values are 1, 2, 3 for modes I, II, III. Mode II (2) is recommended. Mode I implies combined routing updating procedures.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.PRIORITY-ACCESS-THR','6',1,0,'Code contols GPRS packet access priorities allowed. See GSM04.08 table 10.5.76. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.RAC','0',1,0,'GPRS Routing Area Code, advertised in the C0T0 beacon. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.RA_COLOUR','0',0,0,'GPRS Routing Area Color as advertised in the C0T0 beacon.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.RRBP.Min','0',0,0,'Minimum value for RRBP reservations, range 0..3. Should normally be 0. A non-zero value gives the MS more time to respond to the RRBP request.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Reassign.Enable','1',0,0,'1=enabled, 0=disabled - Enable TBF Reassignment'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.SGSN.port','1920',0,0,'Port number of the SGSN required for GPRS service. This must match the port specified in the SGSN config file, currently osmo_sgsn.cfg.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.SendIdleFrames','0',0,0,'1=enabled, 0=disabled - Should be 0 for current transceiver or 1 for deprecated version of transceiver.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.TBF.Downlink.Poll1','10',0,0,'When the first poll is sent for a downlink tbf, measured in blocks sent.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.TBF.EST','1',0,0,'1=enabled, 0=disabled - Allow MS to request another uplink assignment at end up of uplink TBF. See GSM 4.60 9.2.3.4'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.TBF.Expire','30000',0,0,'How long to try before giving up on a TBF.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.TBF.KeepExpiredCount','20',0,0,'How many expired TBF structs to retain; they can be viewed with gprs list tbf -x'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.TBF.Retry','1',0,0,'If 0, no tbf retry, otherwise if a tbf fails it will be retried with this codec, numbered 1..4'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.Channels.Idle','6000',0,0,'How long in milliseconds a GPRS channel is idle before being returned to the pool of channels. Also depends on Channels.Min. Currently the channel cannot be returned to the pool while there is any GPRS activity on any channel.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.MS.Idle','600',0,0,'How long in seconds an MS is idle before the BTS forgets about it.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.MS.NonResponsive','6000',0,0,'How long in milliseconds a TBF is non-responsive before the BTS kills it.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.T3169','5000',0,0,'Nonresponsive uplink TBF resource release timer, in milliseconds. See GSM04.60 sec 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.T3191','5000',0,0,'Nonresponsive downlink TBF resource release timer, in milliseconds. See GSM04.60 Section 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.T3193','0',0,0,'Timer T3193 (in milliseconds) in the base station corresponds to T3192 in the MS, which is set by GPRS.CellOptions.T3192Code. The T3193 value should be slightly longer than that specified by the T3192Code. If 0, the BTS will fill in a default value based on T3192Code.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Timers.T3195','5000',0,0,'Nonresponsive downlink TBF resource release timer, in milliseconds. See GSM04.60 Section 13.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Uplink.KeepAlive','300',0,0,'How often to send keep-alive messages for persistent TBFs in milliseconds; must be long enough to avoid simultaneous in-flight duplicates, and short enough that MS gets one every 5 seconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.Uplink.Persist','4000',1,0,'After completion, uplink TBFs are held open for this time in milliseconds. If non-zero, must be greater than GPRS.Uplink.KeepAlive. This is broadcast in the beacon and it cannot be changed once BTS is started. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GPRS.advanceblocks','10',0,0,'Number of advance blocks to use in the CCCH reservation.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CCCH.AGCH.QMax','5',0,0,'Maximum number of access grants to be queued for transmission on AGCH before declaring congestion.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CCCH.CCCH-CONF','1',1,0,'CCCH configuration type. See GSM 10.5.2.11 for encoding. Value of 1 means we are using a C-V beacon. Any other value selects a C-IV beacon. In C2.9 and earlier, the only allowed value is 1. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellOptions.RADIO-LINK-TIMEOUT','15',1,0,'Seconds before declaring a physical link dead. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.CELL-RESELECT-HYSTERESIS','3',0,0,'Cell Reselection Hysteresis. See GSM 04.08 10.5.2.4, Table 10.5.23 for encoding. Encoding is $2N$ dB, values of $N$ are 0...7 for 0...14 dB.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.MS-TXPWR-MAX-CCH','0',0,0,'Cell selection parameters. See GSM 04.08 10.5.2.4.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.NCCsPermitted','1',0,0,'NCCs Permitted. An 8-bit mask of allowed NCCs. Unless you are coordinating with another carrier, this should probably just select your own NCC.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.NECI','1',0,0,'NECI, New Establishment Causes. This must be set to "1" if you want to support very early assignment (VEA). It can be set to "1" even if you do not use VEA, so you might as well leave it as "1". See GSM 04.08 10.5.2.4, Table 10.5.23 and 04.08 9.1.8, Table 9.9 and the Control.VEA parameter.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.Neighbors','39 41 43',0,0,'ARFCNs of neighboring cells.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.NECI','1',0,0,'NECI, New Establishment Causes. This must be set to 1 if you want to support very early assignment (VEA). It can be set to 1 even if you do not use VEA, so you might as well leave it as 1. See GSM 04.08 10.5.2.4, Table 10.5.23 and 04.08 9.1.8, Table 9.9 and the Control.VEA parameter.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.CellSelection.RXLEV-ACCESS-MIN','0',0,0,'Cell selection parameters. See GSM 04.08 10.5.2.4.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.C1sFirst',NULL,1,0,'If not NULL, allocate C-I slots first, starting at C0T1. Otherwise, allocate C-VII slots first. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.C1sFirst','0',1,0,'1=enabled, 0=disabled - Allocate C-I slots first, starting at C0T1. Otherwise, allocate C-VII slots first. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.NumC1s','7',1,0,'Number of Combination-I timeslots to configure. The C-I slot carries a single full-rate TCH, used for speech calling. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.NumC7s','0',1,0,'Number of Combination-VII timeslots to configure. The C-VII slot carries 8 SDCCHs, useful to handle high registration loads or SMS. If C0T0 is C-IV, you must have at least one C-VII also. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Control.GPRSMaxIgnore','5',0,1,'The maximum number of suspension requests to ignore before aborting a transaction.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.NumC7s','0',1,0,'Number of Combination-VII timeslots to configure. The C-VII slot carries 8 SDCCHs, useful to handle high registration loads or SMS. If C0T0 is C-IV, which it always is in C2.9 and earlier,, you must have at least one C-VII also. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Channels.SDCCHReserve','0',0,0,'Number of SDCCHs to reserve for non-LUR operations. This can be used to force LUR transactions into a lower priority.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Cipher.CCHBER','0',0,0,'Probability of a bit getting toggled in a control channel burst for cracking protection.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Cipher.Encrypt','0',0,0,'1=enabled, 0=disabled - Encrypt traffic between phone and OpenBTS.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Cipher.RandomNeighbor','0',0,0,'Probability of a random neighbor being added to SI5 for cracking protection.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Cipher.ScrambleFiller','0',0,0,'1=enabled, 0=disabled - Scramble filler in layer 2 for cracking protection.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Control.GPRSMaxIgnore','5',0,0,'Ignore GPRS messages on GSM control channels. Value is number of consecutive messages to ignore.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Handover.FailureHoldoff','5',0,0,'The number of seconds to wait before attempting another handover with a given neighbor BTS.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Handover.LocalRSSIMin','-80',0,0,'Do not handover if downlink RSSI is above this level (in dBm), regardless of power difference.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Handover.ThresholdDelta','10',0,0,'A neighbor downlink signal must be this much stronger (in dB) than this downlink signal for handover to occur.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.BSIC.BCC','2',0,0,'GSM basestation color code; lower 3 bits of the BSIC. BCC values in a multi-BTS network should be assigned so that BTS units with overlapping coverage do not share a BCC. This value will also select the training sequence used for all slots on this unit.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.BSIC.NCC','0',0,0,'GSM network color code; upper 3 bits of the BSIC. Assigned by your national regulator. Must be distinct from NCCs of other GSM operators in your area.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.CI','10',0,0,'Cell ID, 16 bits. Should be unique.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.LAC','1000',0,0,'Location area code, 16 bits, values 0xFFxx are reserved. For multi-BTS networks, assign a unique LAC to each BTS unit. (That is not the normal procedure in conventional GSM networks, but is the correct procedure in OpenBTS networks.)'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.MCC','001',0,0,'Mobile country code, 2 or 3 digits. Defined in ITU-T E.212.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.MNC','01',0,0,'Mobile network code; Must be 3 dgits. Assigned by your national regulator.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.ShortName','Range',0,1,'Network short name, displayed on some phones. Optional but must be defined if you also want the network to send time-of-day.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.ShowCountry',1,0,0,'If not NULL, tell the phone to show the country name based on the MCC.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.MCC','001',0,0,'Mobile country code; must be three digits. Defined in ITU-T E.212. 001 for test networks.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.MNC','01',0,0,'Mobile network code, two or three digits. Assigned by your national regulator. 01 for test networks.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Identity.ShortName','Range',0,0,'Network short name, displayed on some phones. Optional but must be defined if you also want the network to send time-of-day.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.Power.Damping','50',0,0,'Damping value for MS power control loop.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.Power.Max','33',0,0,'Maximum commanded MS power level in dBm.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.Power.Min','5',0,0,'Minimum commanded MS power level in dBm.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.TA.Damping','50',0,0,'Damping value for timing advance control loop.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.TA.Max','5',0,0,'Maximum allowed timing advance in symbol periods. Ignore RACH bursts with delays greater than this. Can be used to limit service range.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MaxSpeechLatency','2',0,0,'Maximum allowed speech buffering latency, in 20 ms frames. If the jitter is larger than this delay, frames will be lost.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RACH.AC','1024',0,0,'Access class flags. This is the raw parameter sent on the BCCH. See GSM 04.08 10.5.2.29 for encoding. Set to 0 to allow full access. If you do not have proper PSAP integration, set to 0x0400 to indicate no support for emergency calls.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MS.TA.Max','62',0,0,'Maximum allowed timing advance in symbol periods. One symbol period of round-trip delay is about 0.55 km of distance. Ignore RACH bursts with delays greater than this. Can be used to limit service range. Valid range is 1..62.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.MaxSpeechLatency','2',0,0,'Maximum allowed speech buffering latency, in 20 millisecond frames. If the jitter is larger than this delay, frames will be lost.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Neighbors','',0,0,'A list of IP addresses of neighbor BTSs available for handover. By default, this feature is disabled. To enable, specify a space-separated list of the BTS IP addresses, in IP dotted notation, eg: 1.2.3.4 5.6.7.8. To disable again, execute "unconfig GSM.Neighbors".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Neighbors.NumToSend','8',0,0,'Maximum number of neighbors to send to handset in a neighbor list.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Ny1','5',1,0,'Maximum number of repeats of the Physical Information Message during handover procedure, GSM 04.08 11.1.3. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RACH.MaxRetrans','1',0,0,'Maximum RACH retransmission attempts. This is the raw parameter sent on the BCCH. See GSM 04.08 10.5.2.29 for encoding.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RACH.TxInteger','14',0,0,'Parameter to spread RACH busts over time. This is the raw parameter sent on the BCCH. See GSM 04.08 10.5.2.29 for encoding.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.ARFCNs','1 ',1,0,'The number of ARFCNs to use. The ARFCN set will be C0, C0+2, C0+4, etc. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RADIO-LINK-TIMEOUT','15',0,0,' L1 radio link timeout. This is the raw parameter sent on the BCCH; see GSM 10.5.2.3 for encoding. Should be coordinated with T3109.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ACCURACY','40',0,0,'Requested accuracy of location request. K in 10(1.1**K-1). See 3GPP 03.32, sect 6.2'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ACCURACY','40',0,0,'Requested accuracy of location request. K in r=10(1.1**K-1), where r is the accuracy in meters. See 3GPP 03.32, sect 6.2'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ALMANAC.ASSIST.PRESENT','0',0,0,'1=enabled, 0=disabled - Send almanac info to mobile'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ALMANAC.REFRESH.TIME','24.0',0,0,'How often the almanac is refreshed, in hours'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ALMANAC.URL','http://www.navcen.uscg.gov/?pageName=currentAlmanac&format=yuma',0,0,'URL of almanac source.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.EPHEMERIS.ASSIST.COUNT','9',0,0,'number of satellites to include in navigation model'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.EPHEMERIS.REFRESH.TIME','1.0',0,0,'How often the ephemeris is refreshed, in hours.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.EPHEMERIS.URL','ftp://ftp.trimble.com/pub/eph/CurRnxN.nav',0,0,'URL of ephemeris source.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.RESPONSETIME','4',0,0,'Mobile timeout. (OpenBTS timeout is 130 sec = max response time + 2.) N in 2**N. See 3GPP 04.31 sect A.2.2.1'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SEED.ALTITUDE','0',0,0,'Seed altitude in meters wrt geoidal surface.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SEED.LATITUDE','37.8720708',0,0,'Seed latitude in degrees. -90 (south pole) .. +90 (north pole)'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SEED.LONGITUDE','-122.2578337',0,0,'Seed longitude in degrees. -180 (west of greenwich) .. 180 (east)'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SERVER.URL','http://localhost/cgi-bin/rrlpserver.cgi',0,0,'URL of RRLP server.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.ALMANAC.ASSIST.PRESENT','0',0,0,'1=send almanac info to mobile; 0=do not'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.EPHEMERIS.ASSIST.COUNT','9',0,0,'number of satellites to include in navigation model'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.Band','900',1,0,'The GSM operating band. Valid values are 850 (GSM850), 900 (PGSM900), 1800 (DCS1800) and 1900 (PCS1900). For most Range models, this value is dictated by the hardware and should not be changed. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.C0','51 ',1,0,'The C0 ARFCN. Also the base ARFCN for a multi-ARFCN configuration. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.MaxExpectedDelaySpread','1 ',0,0,'Expected worst-case delay spread in symbol periods, roughly 3.7 us or 1.1 km per unit.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SEED.LATITUDE','37.777423',0,0,'Seed latitude in degrees. -90 (south pole) .. +90 (north pole)'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SEED.LONGITUDE','-122.39807',0,0,'Seed longitude in degrees. -180 (west of greenwich) .. 180 (east)'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.RRLP.SERVER.URL','',0,0,'URL of RRLP server. By default, this feature is disabled. To enable, specify a server URL eg: http://localhost/cgi/rrlpserver.cgi. To disable again, execute "unconfig GSM.RRLP.SERVER.URL".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.ARFCNs','1',1,0,'The number of ARFCNs to use. The ARFCN set will be C0, C0+2, C0+4, etc. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.Band','900',1,0,'The GSM operating band. Valid values are 850 (GSM850), 900 (PGSM900), 1800 (DCS1800) and 1900 (PCS1900). For non-multiband units, this value is dictated by the hardware and should not be changed. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.C0','51',1,0,'The C0 ARFCN. Also the base ARFCN for a multi-ARFCN configuration. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.MaxExpectedDelaySpread','4',0,0,'Expected worst-case delay spread in symbol periods, roughly 3.7 us or 1.1 km per unit. This parameter is dependent on the terrain type in the installation area. Typical values are 1 for open terrain and small coverage areas. For large coverage areas, a value of 4 is strongly recommended. This parameter has a large effect on computational requirements of the software radio; values greater than 4 should be avoided.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.NeedBSIC','0',0,0,'1=enabled, 0=disabled - Does the Radio type require the full BSIC'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.MaxAttenDB','10',0,0,'Maximum transmitter attenuation level, in dB wrt full scale on the D/A output. This sets the minimum power output level in the output power control loop.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.MinAttenDB','0',0,0,'Minimum transmitter attenuation level, in dB wrt full scale on the D/A output. This sets the maximum power output level in the output power control loop.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.NumSamples','10',0,0,'Number of samples averaged by the output power control loop.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.Period','6000',0,0,''); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.SamplePeriod','2000',0,0,'Sample period for the output power control loop.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.Period','6000',0,0,'Power manager control loop master period, in milliseconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.SamplePeriod','2000',0,0,'Sample period for the output power control loopm in milliseconds.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.PowerManager.TargetT3122','5000',0,0,'Target value for T3122, the random access hold-off timer, for the power control loop.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.RxGain','47',1,0,'Receiver gain setting in dB. Ideal value is dictacted by the hardware. This database parameter is static but the receiver gain can be modified in real time with the CLI rxgain command. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.RSSITarget','-50',0,0,'Target uplink RSSI for MS power control loop, in dB wrt to A/D full scale. Should be 6-10 dB above the noise floor.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.NeedBSIC','0',0,0,'Does the Radio type require the full BSIC'); -INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3113','10000',0,0,'Paging timer T3113 in ms. This is the timeout for a handset to respond to a paging request. This should usually be the same as SIP.Timer.B in your VoIP network.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Radio.RxGain','47',1,0,'Receiver gain setting in dB. Ideal value is dictated by the hardware; 47 dB for RAD1. This database parameter is static but the receiver gain can be modified in real time with the CLI rxgain command. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.ShowCountry','0',0,0,'1=enabled, 0=disabled - Tell the phone to show the country name based on the MCC.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3103','5000',1,0,'Handover timeout in milliseconds, GSM 04.08 11.1.2. This is the timeout for a handset to sieze a channel during handover. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3105','50',1,0,'Milliseconds for handset to respond to physical information. GSM 04.08 11.1.2. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3113','10000',0,0,'Paging timer T3113 in milliseconds. This is the timeout for a handset to respond to a paging request. This should usually be the same as SIP.Timer.B in your VoIP network.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3122Max','255000',0,0,'Maximum allowed value for T3122, the RACH holdoff timer, in milliseconds.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3122Min','2000',0,0,'Minimum allowed value for T3122, the RACH holdoff timer, in milliseconds.'); INSERT OR IGNORE INTO "CONFIG" VALUES('GSM.Timer.T3212','30',0,0,'Registration timer T3212 period in minutes. Should be a factor of 6. Set to 0 to disable periodic registration. Should be smaller than SIP registration period.'); INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Alarms.Max','20',0,0,'Maximum number of alarms to remember inside the application.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level','WARNING',0,0,'Default logging level when no other level is defined for a file.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level.CallControl.cpp','INFO',0,1,'Default configuration logs a trace at L3.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level.MobilityManagement.cpp','INFO',0,1,'Default configuration logs a trace at L3.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level.RadioResource.cpp','INFO',0,1,'Default configuration logs a trace at L3.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level.SMSControl.cpp','INFO',0,1,'Default configuration logs a trace at L3.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('NTP.Server','pool.ntp.org',0,1,'NTP server(s) for time-of-day clock syncing. For multiple servers, use a space-delimited list. If left undefined, NTP will not be used, but it is strongly recommended.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Log.File','',0,0,'Path to use for textfile based logging. By default, this feature is disabled. To enable, specify an absolute path to the file you wish to use, eg: /tmp/my-debug.log. To disable again, execute "unconfig Log.File".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Log.Level','NOTICE',0,0,'Default logging level when no other level is defined for a file.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Peering.Neighbor.RefreshAge','60000',0,0,'Milliseconds before refreshing parameters from a neighbor.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Peering.NeighborTable.Path','/var/run/NeighborTable.db',1,0,'File path for neighbor information database. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Peering.Port','16001',1,0,'The UDP port used by the peer interface for handover. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Peering.ResendCount','5',0,0,'Number of tries to send message over the peer interface before giving up'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Peering.ResendTimeout','100',0,0,'Milliseconds before resending a message on the peer interface'); INSERT OR IGNORE INTO "CONFIG" VALUES('RTP.Range','98',1,0,'Range of RTP port pool. Pool is RTP.Start to RTP.Range-1. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('RTP.Start','16484',1,0,'Base of RTP port pool. Pool is RTP.Start to RTP.Range-1. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.RFC3428.NoTrying','0',0,1,'If NULL or 0, send 100 Trying response to SIP MESSAGE, even though that violates RFC-3428. In other words, to actually comply with the RFC, set this to something other than NULL or 0'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2833','1',0,1,'If not NULL, use RFC-2833 (RTP event signalling) for in-call DTMF.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2833.PayloadType','101',0,1,'Payload type to use for RFC-2833 telephone event packets. If SIP.DTMF.2833 is defined, this must also be defined.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2967',NULL,0,1,'If not NULL, use RFC-2967 (SIP INFO method) for in-call DTMF.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SGSN.Debug','0',0,0,'1=enabled, 0=disabled - Add layer-3 messages to the GGSN.Logfile, if any.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SGSN.Timer.ImplicitDetach','3480',0,0,'3GPP 24.008 11.2.2. GPRS attached MS is implicitly detached in seconds. Should be at least 240 seconds greater than SGSN.Timer.RAUpdate.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SGSN.Timer.MS.Idle','600',0,0,'How long an MS is idle before the SGSN forgets TLLI specific information.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SGSN.Timer.RAUpdate','3240',0,0,'Also known as T3312, 3GPP 24.008 4.7.2.2. How often MS polls routing info from the BTS when it is idle, in seconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SGSN.Timer.Ready','44',0,0,'Also known as T3314, 3GPP 24.008 4.7.2.1. Inactivity period required before MS may perform another routing area or cell update, in seconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2833','1',0,0,'1=enabled, 0=disabled - Use RFC-2833 (RTP event signalling) for in-call DTMF.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2833.PayloadType','101',0,0,'Payload type to use for RFC-2833 telephone event packets.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.DTMF.RFC2967','0',0,0,'1=enabled, 0=disabled - Use RFC-2967 (SIP INFO method) for in-call DTMF.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Local.IP','127.0.0.1',1,0,'IP address of the OpenBTS machine as seen by its proxies. If these are all local, this can be localhost. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Local.Port','5062',1,0,'IP port that OpenBTS uses for its SIP interface. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.MaxForwards','5',0,0,'Maximum allowed number of referrals.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.MaxForwards','70',0,0,'Maximum allowed number of referrals.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Proxy.Registration','127.0.0.1:5064',0,0,'The IP host and port of the proxy to be used for registration and authentication. This should normally be the subscriber registry SIP interface, not Asterisk.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Proxy.SMS','127.0.0.1:5063',0,0,'The IP host and port of the proxy to be used for text messaging. This is smqueue, for example.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Proxy.Speech','127.0.0.1:5060',0,0,'The IP host and port of the proxy to be used for normal speech calls. This is Asterisk, for example.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.RFC3428.NoTrying','0',0,0,'1=enabled, 0=disabled - Send 100 Trying response to SIP MESSAGE, even though that violates RFC-3428.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.RegistrationPeriod','90',0,0,'Registration period in minutes for MS SIP users. Should be longer than GSM T3212.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.SMSC','smsc',0,1,'The SMSC handler in smqueue. This is the entity that handles full 3GPP MIME-encapsulted TPDUs. If not defined, use direct numeric addressing. Normally the value is NULL if SMS.MIMIEType is "text/plain" or "smsc" if SMS.MIMEType is "application/vnd.3gpp".'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.A','500',0,0,'INVITE retransmit period in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.B','10000',0,0,'INVITE transaction timeout in ms. This value should usually match GSM.Timer.T3113.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.E','500',0,0,'Non-INVITE initial request retransmit period in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.F','5000',0,0,'Non-INVITE initial request timeout in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.J','500',0,0,'Non-INVITE non-initial request retransmit period in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.H','5000',0,0,'ACK timeout period in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.I','500',0,0,'ACK retransmit period in ms.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SMS.DefaultDestSMSC','0000',0,0,'Use this to fill in L4 SMSC address in SMS submission.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.SMSC','smsc',0,0,'The SMSC handler in smqueue. This is the entity that handles full 3GPP MIME-encapsulted TPDUs. If not defined, use direct numeric addressing. The value should be disabled with "unconfig SIP.SMSC" if SMS.MIMEType is "text/plain" or set to "smsc" if SMS.MIMEType is "application/vnd.3gpp".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.A','2000',0,0,'SIP timer A, the INVITE retry period, RFC-3261 Section 17.1.1.2, in milliseconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.B','10000',0,0,'INVITE transaction timeout in milliseconds. This value should usually match GSM.Timer.T3113.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.E','500',0,0,'Non-INVITE initial request retransmit period in milliseconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.F','5000',0,0,'Non-INVITE initial request timeout in milliseconds.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SIP.Timer.H','5000',0,0,'ACK timeout period in milliseconds.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SMS.FakeSrcSMSC','0000',0,0,'Use this to fill in L4 SMSC address in SMS delivery.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SMS.MIMEType','application/vnd.3gpp.sms',0,0,'This is the MIME Type that OpenBTS will use for RFC-3428 SIP MESSAGE payloads. Valid values are "application/vnd.3gpp.sms" and "text/plain".'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.Manager.Title','Subscriber Registry',0,0,'Title of subscriber registry database manager web page.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.Manager.Url','http://127.0.0.1/cgi/srmanager.cgi',0,0,'URL of the subscriber registry database manager.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.Manager.VisibleColumns','name username type context host',0,0,'Field names in subscriber registry visible in the database manager.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.db','/var/lib/asterisk/sqlite3dir/sqlite3.db',0,0,'The location of the sqlite3 database holding the subscriber registry.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.A3A8','/OpenBTS/comp128',0,0,'Path to the program that implements the A3/A8 algorithm.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.Manager.Title','Subscriber Registry',0,0,'Title text to be displayed on the subscriber registry manager.'); INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.Port','5064',0,0,'Port used by the SIP Authentication Server. NOTE: In some older releases (pre-2.8.1) this is called SIP.myPort.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.UpstreamServer','',0,0,'URL of the subscriber registry HTTP interface on the upstream server. By default, this feature is disabled. To enable, specify a server URL eg: http://localhost/cgi/subreg.cgi. To disable again, execute "unconfig SubscriberRegistry.UpstreamServer".'); +INSERT OR IGNORE INTO "CONFIG" VALUES('SubscriberRegistry.db','/var/lib/asterisk/sqlite3dir/sqlite3.db',0,0,'The location of the sqlite3 database holding the subscriber registry.'); INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.IP','127.0.0.1',1,0,'IP address of the transceiver application. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.MinimumRxRSSI','-63',0,0,'Bursts received at the physical layer below this threshold are automatically ignored. Values in dB. Set at the factory. Do not adjust without proper calibration.'); INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.Port','5700',1,0,'IP port of the transceiver application. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.RadioFrequencyOffset','128',1,0,'Fine-tuning adjustment for the transceiver master clock. Roughly 170 Hz/step. Set at the factory. Do not adjust without proper calibration. Static.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.Timeout.Clock','10',0,1,'How long to wait during a read operation from the transceiver before giving up.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.Timeout.Start','2',0,1,'How long to wait during system startup before checking to see if the transceiver can be reached.'); -INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.TxAttenOffset','2',1,0,'Hardware-specific gain adjustment for transmitter, matched to the power amplifier, expessed as an attenuationi in dB. Set at the factory. Do not adjust without proper calibration. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.Timeout.Clock','10',0,0,'How long to wait during a read operation from the transceiver before giving up.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.Timeout.Start','2',0,0,'How long to wait during system startup before checking to see if the transceiver can be reached.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('TRX.TxAttenOffset','0',1,0,'Hardware-specific gain adjustment for transmitter, matched to the power amplifier, expessed as an attenuationi in dB. Set at the factory. Do not adjust without proper calibration. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Test.GSM.SimulatedFER.Downlink','0',1,0,'Probability (0-100) of dropping any downlink frame to test robustness. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Test.GSM.SimulatedFER.Uplink','0',1,0,'Probability (0-100) of dropping any uplink frame to test robustness. Static.'); +INSERT OR IGNORE INTO "CONFIG" VALUES('Test.GSM.UplinkFuzzingRate','0',1,0,'Probability (0-100) of flipping a bit in any uplink frame to test robustness. Static.'); INSERT OR IGNORE INTO "CONFIG" VALUES('Test.SIP.SimulatedPacketLoss','0',1,0,'Probability (0-100) of dropping any inbound or outbound SIP packet to test robustness. Static.'); COMMIT; + + diff --git a/apps/OpenBTS.logrotate b/apps/OpenBTS.logrotate new file mode 100755 index 00000000..4c92102e --- /dev/null +++ b/apps/OpenBTS.logrotate @@ -0,0 +1,6 @@ +/var/log/OpenBTS.log { + sizem 20 + rotate 10 + compress + notifempty +} diff --git a/apps/OpenBTSCLI.cpp b/apps/OpenBTSCLI.cpp index 516e2c16..3e6d43e6 100644 --- a/apps/OpenBTSCLI.cpp +++ b/apps/OpenBTSCLI.cpp @@ -1,5 +1,5 @@ /* -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * * This software is distributed under the terms of the GNU Affero Public License. * See the COPYING file in the main directory for details. @@ -8,8 +8,8 @@ * See the LEGAL file in the main directory for details. This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, @@ -22,6 +22,9 @@ */ + +// KEEP THIS FILE CLEAN FOR GPL PUBLIC RELEASE. + #include #include #include @@ -32,6 +35,9 @@ #include #include +#define HAVE_LIBREADLINE + + #ifdef HAVE_LIBREADLINE # include # include @@ -44,6 +50,13 @@ int main(int argc, char *argv[]) { + + printf("OpenBTS Commnd Line Interface (CLI) utility\n"); + printf("Copyright 2012, 2013 Range Networks, Inc.\n"); + printf("Licensed under GPLv2.\n"); +#ifdef HAVE_LIBREADLINE + printf("Includes libreadline, GPLv2.\n"); +#endif const char* cmdPath = DEFAULT_CMD_PATH; if (argc!=1) { cmdPath = argv[1]; @@ -73,8 +86,8 @@ int main(int argc, char *argv[]) // locally bound address struct sockaddr_un rspSockName; rspSockName.sun_family = AF_UNIX; - char rmcmd[strlen(rspPath)+5]; - sprintf(rmcmd,"rm %s",rspPath); + char rmcmd[strlen(rspPath)+10]; + sprintf(rmcmd,"rm -f %s",rspPath); system(rmcmd); strcpy(rspSockName.sun_path,rspPath); if (bind(sock, (struct sockaddr *) &rspSockName, sizeof(struct sockaddr_un))) { @@ -106,12 +119,10 @@ int main(int argc, char *argv[]) read_history(history_name); } } - - printf("readline installed\n"); #endif - printf("Remote Interface Ready.\nType:\n \"help\" to see commands,\n \"version\" for version information,\n \"notices\" for licensing information.\n\"quit\" to exit console interface\n"); + printf("Remote Interface Ready.\nType:\n \"help\" to see commands,\n \"version\" for version information,\n \"notices\" for licensing information.\n \"quit\" to exit console interface\n"); while (1) { @@ -135,13 +146,19 @@ int main(int argc, char *argv[]) printf("closing remote console\n"); break; } + // shell escape? + if (cmd[0]=='!') { + system(cmd+1); + continue; + } + // use the socket if (sendto(sock,cmd,strlen(cmd)+1,0,(struct sockaddr*)&cmdSockName,sizeof(cmdSockName))<0) { perror("sending datagram"); printf("Is the remote application running?\n"); continue; } free(cmd); - const int bufsz = 10000; + const int bufsz = 100000; char resbuf[bufsz]; int nread = recv(sock,resbuf,bufsz-1,0); if (nread<0) { @@ -166,4 +183,8 @@ int main(int argc, char *argv[]) close(sock); + + // Delete the path to limit clutter in /tmp. + sprintf(rmcmd,"rm -f %s",rspPath); + system(rmcmd); } diff --git a/apps/OpenBTSDo.cpp b/apps/OpenBTSDo.cpp index 71dfb9d5..4109040b 100644 --- a/apps/OpenBTSDo.cpp +++ b/apps/OpenBTSDo.cpp @@ -1,5 +1,5 @@ /* -* Copyright 2011 Range Networks, Inc. +* Copyright 2011, 2012 Range Networks, Inc. * * This software is distributed under the terms of the GNU Affero Public License. * See the COPYING file in the main directory for details. @@ -34,7 +34,7 @@ #include -#define DEFAULT_CMD_PATH "/var/run/command" +#define DEFAULT_CMD_PATH "/var/run/OpenBTS/command" int main(int argc, char *argv[]) { @@ -61,6 +61,9 @@ int main(int argc, char *argv[]) // locally bound address struct sockaddr_un rspSockName; rspSockName.sun_family = AF_UNIX; + char rmcmd[strlen(rspPath)+10]; + sprintf(rmcmd,"rm -f %s",rspPath); + system(rmcmd); strcpy(rspSockName.sun_path,rspPath); if (bind(sock, (struct sockaddr *) &rspSockName, sizeof(struct sockaddr_un))) { perror("binding name to datagram socket"); @@ -71,13 +74,15 @@ int main(int argc, char *argv[]) char *inbuf = (char*)malloc(200); char *cmd = fgets(inbuf,199,stdin); if (!cmd) exit(0); + cmd[strlen(cmd)-1] = '\0'; if (sendto(sock,cmd,strlen(cmd)+1,0,(struct sockaddr*)&cmdSockName,sizeof(cmdSockName))<0) { perror("sending datagram"); exit(1); } - const int bufsz = 1500; + // buffer to be sized as necessary to accomodate config data length + const int bufsz = 8500; char resbuf[bufsz]; int nread = recv(sock,resbuf,bufsz-1,0); if (nread<0) { @@ -88,4 +93,8 @@ int main(int argc, char *argv[]) printf("%s\n",resbuf); close(sock); + + // Delete the path to limit clutter in /tmp. + sprintf(rmcmd,"rm -f %s",rspPath); + system(rmcmd); } diff --git a/apps/exportConfigTable.sh b/apps/exportConfigTable.sh new file mode 100755 index 00000000..6492acc1 --- /dev/null +++ b/apps/exportConfigTable.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sqlite3 /etc/OpenBTS/OpenBTS.db ".dump CONFIG" > OpenBTS.export.sql diff --git a/apps/generateConfigTable.sh b/apps/generateConfigTable.sh new file mode 100755 index 00000000..0e3ba25d --- /dev/null +++ b/apps/generateConfigTable.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +CONFIG_DIR=/etc/OpenBTS + +# Create the dir if it dos not exist +if [ ! -d "$CONFIG_DIR" ]; then + mkdir $CONFIG_DIR +fi + +# Create the dir if it dos not exist +if [ ! -d "$CONFIG_DIR/saved" ]; then + mkdir $CONFIG_DIR/saved +fi + +# backup any exsisting DB before we create the default +if [ -e $CONFIG_DIR/OpenBTS.db ]; then + mv -f $CONFIG_DIR/OpenBTS.db $CONFIG_DIR/saved/OpenBTS.db +fi + +sqlite3 $CONFIG_DIR/OpenBTS.db ".read $1" diff --git a/apps/generateTeX.sh b/apps/generateTeX.sh new file mode 100755 index 00000000..06ad85ca --- /dev/null +++ b/apps/generateTeX.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sqlite3 -separator "" $1 "select '\item ',KEYSTRING,' -- ',COMMENTS from CONFIG order by KEYSTRING;" diff --git a/apps/importConfigTable.sh b/apps/importConfigTable.sh new file mode 100755 index 00000000..6e837f9f --- /dev/null +++ b/apps/importConfigTable.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +CONFIG_DIR=/etc/OpenBTS + +# Create the config dir if it dos not exist +if [ ! -d "$CONFIG_DIR" ]; then + mkdir $CONFIG_DIR +fi + +# Create the config/saved dir if it dos not exist +if [ ! -d "$CONFIG_DIR/saved" ]; then + mkdir $CONFIG_DIR/saved +fi + +# backup any exsisting DB before we create the default +if [ -e $CONFIG_DIR/OpenBTS.db ]; then + mv -f $CONFIG_DIR/OpenBTS.db $CONFIG_DIR/saved/OpenBTS.db +fi +sqlite3 $CONFIG_DIR/OpenBTS.db ".read OpenBTS.export.sql" diff --git a/apps/iptables.rules b/apps/iptables.rules new file mode 100644 index 00000000..b02ca954 --- /dev/null +++ b/apps/iptables.rules @@ -0,0 +1,13 @@ +# Generated by iptables-save v1.4.4 +*nat +:PREROUTING ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +-A POSTROUTING -o wlan0 -j MASQUERADE +COMMIT +# Generated by iptables-save v1.4.4 +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +COMMIT diff --git a/apps/openbtsconfig b/apps/openbtsconfig new file mode 100755 index 00000000..5de462bb --- /dev/null +++ b/apps/openbtsconfig @@ -0,0 +1,7 @@ +#!/bin/bash +if [ $# -eq '1' ] +then + sqlite3 /etc/OpenBTS/OpenBTS.db "select KEYSTRING,VALUESTRING from CONFIG where KEYSTRING like '%$1%'"; +else + sqlite3 /etc/OpenBTS/OpenBTS.db "insert or replace into CONFIG (VALUESTRING,KEYSTRING) values ('$2','$1')"; +fi diff --git a/apps/rsyslogd.OpenBTS.conf b/apps/rsyslogd.OpenBTS.conf new file mode 100644 index 00000000..c756a118 --- /dev/null +++ b/apps/rsyslogd.OpenBTS.conf @@ -0,0 +1,4 @@ +# OpenBTS logging controls + +local7.* /var/log/OpenBTS.log + diff --git a/apps/runloop.OpenBTS.sh b/apps/runloop.OpenBTS.sh deleted file mode 100755 index 41419119..00000000 --- a/apps/runloop.OpenBTS.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -# A script to restart and just keep OpenBTS running. -while true; do killall transceiver; killall wget; sleep 2; ./OpenBTS; done diff --git a/configure.ac b/configure.ac index b1f99b11..a8f25e08 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,7 @@ dnl You should have received a copy of the GNU General Public License dnl along with this program. If not, see . dnl -AC_INIT(openbts,P2.8TRUNK) +AC_INIT(openbts-public,TRUNK) AC_PREREQ(2.57) AC_CONFIG_SRCDIR([config/Makefile.am]) AC_CONFIG_AUX_DIR([.]) @@ -30,9 +30,6 @@ AC_CANONICAL_TARGET AM_INIT_AUTOMAKE -dnl Linux kernel KBuild style compile messages -m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) - AM_PROG_AS AC_PROG_CXX AC_PROG_LN_S @@ -118,18 +115,10 @@ AM_CONDITIONAL(RESAMPLE, [test "x$with_resamp" = "xyes"]) AM_CONDITIONAL(UHD, [test "x$with_uhd" = "xyes"]) AM_CONDITIONAL(USRP1, [test "x$with_usrp1" = "xyes"]) -# Defines OSIP_CFLAGS, OSIP_INCLUDEDIR, and OSIP_LIBS -PKG_CHECK_MODULES(OSIP, libosip2) - -# Defines ORTP_CFLAGS, ORTP_INCLUDEDIR, and ORTP_LIBS -PKG_CHECK_MODULES(ORTP, ortp) # Defines LIBUSB_TRANSFER_CANCELLED, LIBUSB_TRANSFER_COMPLETED, LIBUSB_SUCCESS, LIBUSB_ERROR_* PKG_CHECK_MODULES(LIBUSB, libusb-1.0) -# Prepends -lreadline to LIBS and defines HAVE_LIBREADLINE in config.h -AC_CHECK_LIB(readline, readline) - # Check for glibc-specific network functions AC_CHECK_FUNC(gethostbyname_r, [AC_DEFINE(HAVE_GETHOSTBYNAME_R, 1, Define if libc implements gethostbyname_r)]) AC_CHECK_FUNC(gethostbyname2_r, [AC_DEFINE(HAVE_GETHOSTBYNAME2_R, 1, Define if libc implements gethostbyname2_r)]) @@ -140,23 +129,23 @@ AC_CONFIG_FILES([\ apps/Makefile \ config/Makefile \ doc/Makefile \ - tools/Makefile \ - AsteriskConfig/Makefile \ CommonLibs/Makefile \ Globals/Makefile \ Control/Makefile \ GSM/Makefile \ + GPRS/Makefile \ + SGSNGGSN/Makefile \ SIP/Makefile \ SMS/Makefile \ + TransceiverDYN/Makefile \ TransceiverRAD1/Makefile \ + TransceiverRAD1-DYN65-Hack/Makefile \ + Transceiver52M/Makefile \ TRXManager/Makefile \ CLI/Makefile \ + Peering/Makefile \ SR/Makefile \ sqlite3/Makefile \ -]) - -AS_IF([test "x$with_usrp1" = "xyes" -o "x$with_uhd" = "xyes"], - [AC_CONFIG_FILES(Transceiver52M/Makefile) ]) AC_OUTPUT diff --git a/ctags.sh b/ctags.sh new file mode 100644 index 00000000..1ddd21ef --- /dev/null +++ b/ctags.sh @@ -0,0 +1,14 @@ +#DIRS="AsteriskConfig tools" +DIRS="CLI CommonLibs Control GPRS GSM Globals Peering + SGSNGGSN SIP SMS SR TRXManager TransceiverRAD1 apps" +# sqlite3 doc pat + +files="" +for dir in $DIRS;do + files="$files $dir/*.h $dir/*.cpp" +done + +eval echo $files +# Ignore PACKED keyword +eval ctags -I PACKED --extra=+fq $files + diff --git a/debian/changelog b/debian/changelog index 01795ad7..7af74f26 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,11 @@ -openbts-public (2.8) UNRELEASED; urgency=low +openbts-public (3.2) UNRELEASED; urgency=low - [ Donald C. Kirker ] - * Test release + * Public sync - [ Kurtis Heimerl ] - * Release of debian scripts into public + -- Kurtis Thu, 01 Aug 2013 20:37:03 -0700 - -- Kurtis Heimerl Sat, 06 Jul 2013 17:19:05 -0700 +openbts-public (3.2) UNRELEASED; urgency=low + + * Test + + -- Donald C. Kirker Mon, 22 Apr 2013 00:11:00 -0700 diff --git a/debian/control b/debian/control index 60e0d412..17c47c67 100755 --- a/debian/control +++ b/debian/control @@ -1,17 +1,18 @@ Source: openbts-public +Provides: openbts Section: comm Priority: optional Maintainer: Range Networks, Inc. Homepage: http://www.rangenetworks.com/ -Build-Depends: build-essential, debhelper (>= 7), libosip2-dev, pkg-config, autoconf, uhd +Build-Depends: build-essential, debhelper (>= 7), libosip2-dev, pkg-config, autoconf Standards-Version: 3.7.3 Package: openbts-public -Version: P2.8TRUNK +Provides: openbts +Version: TRUNK Section: comm Priority: optional Architecture: any Essential: no -Depends: sqlite3, libosip2-4, libusb-1.0-0, libortp8, libglib2.0-0, libgl1-mesa-glx, libc6, libasound2, pkg-config, libpcre3, gawk, sipauthserve-public, uhd -Conflicts: openbts -Description: OpenBTS Public Release. +Depends: sqlite3 (>= 3.7), libosip2-4, libusb-1.0-0, libortp8, libglib2.0-0, libgl1-mesa-glx, libc6, libasound2, pkg-config, libpcre3, gawk, screen +Description: OpenBTS Public software. diff --git a/debian/postinst b/debian/postinst index f887e4ec..e8ad1e06 100755 --- a/debian/postinst +++ b/debian/postinst @@ -24,7 +24,7 @@ DB_LOC=/etc/OpenBTS/OpenBTS.db DATE=$(date +'%Y-%m-%d.%H:%M:%S') CONFIG_BACKUP=$DB_LOC.dump-$DATE -if [ -e $DB_LOC ]; then +if [ ! -e $CONFIG_BACKUP ]; then sqlite3 $DB_LOC ".dump" > $CONFIG_BACKUP fi diff --git a/debian/preinst b/debian/preinst index db743824..836976bf 100755 --- a/debian/preinst +++ b/debian/preinst @@ -13,9 +13,27 @@ set -e # for details, see http://www.debian.org/doc/debian-policy/ or # the debian-policy package +install() +{ + +INSTALL_DIR=/OpenBTS +DATE=$(date +'%Y-%m-%d.%H:%M:%S') +BACKUP_DIR=$INSTALL_DIR/backup_$DATE + +if [ -f $INSTALL_DIR/OpenBTS -a -f $INSTALL_DIR/transceiver ]; then + if [ ! -e $BACKUP_DIR ]; then + mkdir -p $BACKUP_DIR/ + + mv $INSTALL_DIR/OpenBTS $BACKUP_DIR/ + mv $INSTALL_DIR/transceiver $BACKUP_DIR/ + fi +fi + +} case "$1" in install|upgrade) + install ;; abort-upgrade) diff --git a/debian/rules b/debian/rules index ec5e34e6..34295555 100755 --- a/debian/rules +++ b/debian/rules @@ -108,6 +108,7 @@ binary-common: # dh_perl dh_makeshlibs dh_installdeb +# dh_shlibdeps dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info dh_gencontrol dh_md5sums diff --git a/doc/Makefile.am b/doc/Makefile.am index 6d373b76..887b2d92 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -21,6 +21,5 @@ include $(top_srcdir)/Makefile.common EXTRA_DIST = \ - CodingStandard \ - SoftwareP2.8Manual.pdf + CodingStandard diff --git a/doc/SoftwareP2.8Manual.pdf b/doc/SoftwareP2.8Manual.pdf deleted file mode 100644 index 2ec3f8d860a89ae3ded677c1dff4b974b38ad66b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 465576 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%0DcgTBU~ArSmj~IfsnHgJN(G!x?f``Ck-Y@c}s8{ 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

b&!qrr$=gOz{@$FZvfIF8;=UND$i1jQs;!& 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
    QrdQ_5B7nW+!ev*`RLE_d!5&5A`(Z~%c$&!IRX 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
    k~7@zY)97t{=1z^WNu{(TPjC&By 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 L85vCjErI_79N+>J diff --git a/doxconfig b/doxconfig new file mode 100644 index 00000000..396db641 --- /dev/null +++ b/doxconfig @@ -0,0 +1,1078 @@ +# Doxyfile 1.3.4 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project +# +# All text after a hash (#) is considered a comment and will be ignored +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (" ") + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded +# by quotes) that should identify the project. + +PROJECT_NAME = OpenBTS + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = 2.7 + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = dox + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, +# Finnish, French, German, Greek, Hungarian, Italian, Japanese, Japanese-en +# (Japanese with English messages), Korean, Norwegian, Polish, Portuguese, +# Romanian, Russian, Serbian, Slovak, Slovene, Spanish, Swedish, and Ukrainian. + +OUTPUT_LANGUAGE = English + +# This tag can be used to specify the encoding used in the generated output. +# The encoding is not always determined by the language that is chosen, +# but also whether or not the output is meant for Windows or non-Windows users. +# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES +# forces the Windows encoding (this is the default for the Windows binary), +# whereas setting the tag to NO uses a Unix-style encoding (the default for +# all platforms other than Windows). + +USE_WINDOWS_ENCODING = NO + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all inherited +# members of a class in the documentation of that class as if those members were +# ordinary class members. Constructors, destructors and assignment operators of +# the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = NO + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. It is allowed to use relative paths in the argument list. + +STRIP_FROM_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful is your file systems +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like the Qt-style comments (thus requiring an +# explict @brief command for a brief description. + +JAVADOC_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the DETAILS_AT_TOP tag is set to YES then Doxygen +# will output the detailed description near the top, like JavaDoc. +# If set to NO, the detailed description appears after the member +# documentation. + +DETAILS_AT_TOP = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# reimplements. + +INHERIT_DOCS = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name=value". +# For example adding "sideeffect=\par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources +# only. Doxygen will then generate output that is more tailored for Java. +# For instance, namespaces will be presented as packages, qualified scopes +# will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = YES + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = YES + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if sectionname ... \endif. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or define consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and defines in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp +# *.h++ *.idl *.odl *.cs *.php *.php3 *.inc + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories +# that are symbolic links (a Unix filesystem feature) are excluded from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. + +EXCLUDE_PATTERNS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. + +INPUT_FILTER = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. + +SOURCE_BROWSER = YES + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C and C++ comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES (the default) +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = YES + +# If the REFERENCES_RELATION tag is set to YES (the default) +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = YES + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If the tag is left blank doxygen +# will generate a default style sheet + +HTML_STYLESHEET = + +# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, +# files or namespaces will be aligned in HTML using tables. If set to +# NO a bullet list will be used. + +HTML_ALIGN_MEMBERS = YES + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output dir. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index at +# top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. + +DISABLE_INDEX = NO + +# This tag can be used to set the number of enum values (range [1..20]) +# that doxygen will group on one line in the generated HTML documentation. + +ENUM_VALUES_PER_LINE = 4 + +# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be +# generated containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, +# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are +# probably better off using the HTML help feature. + +GENERATE_TREEVIEW = YES + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = YES + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, a4wide, letter, legal and +# executive. If left blank a4wide will be used. + +PAPER_TYPE = a4wide + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = NO + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = NO + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimised for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assigments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. This is useful +# if you want to understand what is going on. On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_PREDEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# in the INCLUDE_PATH (see below) will be search if a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name=definition (no spaces). If the definition and the = are +# omitted =1 is assumed. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all function-like macros that are alone +# on a line, have an all uppercase name, and do not end with a semicolon. Such +# function macros are typically used for boiler-plate code, and will confuse the +# parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::addtions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. +# Optionally an initial location of the external documentation +# can be added for each tagfile. The format of a tag file without +# this location is as follows: +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# TAGFILES = file1=loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths or +# URLs. If a location is present for each tag, the installdox tool +# does not have to be run to correct the links. +# Note that each tag file must have a unique name +# (where the name does NOT include the path) +# If a tag file is not located in the directory in which doxygen +# is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base or +# super classes. Setting the tag to NO turns the diagrams off. Note that this +# option is superceded by the HAVE_DOT option below. This is only a fallback. It is +# recommended to install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# the CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similiar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will +# generate a call dependency graph for every global function or class method. +# Note that enabling this option will significantly increase the time of a run. +# So in most cases it will be better to enable call graphs for selected +# functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are png, jpg, or gif +# If left blank png will be used. + +DOT_IMAGE_FORMAT = png + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found on the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width +# (in pixels) of the graphs generated by dot. If a graph becomes larger than +# this value, doxygen will try to truncate the graph, so that it fits within +# the specified constraint. Beware that most browsers cannot cope with very +# large images. + +MAX_DOT_GRAPH_WIDTH = 1024 + +# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height +# (in pixels) of the graphs generated by dot. If a graph becomes larger than +# this value, doxygen will try to truncate the graph, so that it fits within +# the specified constraint. Beware that most browsers cannot cope with very +# large images. + +MAX_DOT_GRAPH_HEIGHT = 1024 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes that +# lay further from the root node will be omitted. Note that setting this option to +# 1 or 2 may greatly reduce the computation time needed for large code bases. Also +# note that a graph may be further truncated if the graph's image dimensions are +# not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH and MAX_DOT_GRAPH_HEIGHT). +# If 0 is used for the depth value (the default), the graph is not depth-constrained. + +MAX_DOT_GRAPH_DEPTH = 0 + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES + +#--------------------------------------------------------------------------- +# Configuration::addtions related to the search engine +#--------------------------------------------------------------------------- + +# The SEARCHENGINE tag specifies whether or not a search engine should be +# used. If set to NO the values of all tags below this one will be ignored. + +SEARCHENGINE = NO