| /* Test Port that stacks on top of L1CTL test port and performs LAPDm encoding/decoding, so the user can send |
| * and receive LAPDm frames in decoded TTCN-3 data types. This is particularly useful for sending/receiving |
| * all kinds of hand-crafted LAPDm frames for testing of the remote LAPDm layer */ |
| |
| /* (C) 2017-2018 Harald Welte <laforge@gnumonks.org> |
| * All rights reserved. |
| * |
| * Released under the terms of GNU General Public License, Version 2 or |
| * (at your option) any later version. |
| * |
| * SPDX-License-Identifier: GPL-2.0-or-later |
| */ |
| |
| module LAPDm_RAW_PT { |
| import from GSM_Types all; |
| import from GSM_RR_Types all; |
| import from Osmocom_Types all; |
| import from L1CTL_Types all; |
| import from L1CTL_PortType all; |
| import from LAPDm_Types all; |
| import from RLCMAC_Types all; |
| |
| /* request to tune to a given ARFCN and start BCCH decoding */ |
| type record BCCH_tune_req { |
| GsmBandArfcn arfcn, |
| boolean combined_ccch |
| } |
| |
| /* ask for a dedicated channel to be established */ |
| type record DCCH_establish_req { |
| uint8_t ra |
| } |
| |
| type record DCCH_establish_res { |
| ChannelDescription chan_desc optional, |
| charstring err optional |
| } |
| |
| /* directly switch to a dedicated channel (without RACH/IMM.ASS */ |
| type record DCCH_switch_req { |
| ChannelDescription chan_desc, |
| L1ctlMA ma optional |
| } |
| |
| type record DCCH_switch_res { |
| charstring err optional |
| } |
| |
| type record length(8) of uint8_t TfiList; |
| type record TbfPars { |
| GsmArfcn arfcn optional, |
| /* Temporary Flow Identifier for each TN */ |
| TfiList tfi |
| } |
| type record length(8) of TbfPars TbfParsPerTs; |
| |
| template TbfPars t_TbfParsInit := { |
| arfcn := omit, |
| tfi := { 255, 255, 255, 255, 255, 255, 255, 255 } |
| } |
| |
| type record TBF_UL_establish_res { |
| TbfPars pars optional, |
| charstring err optional |
| } |
| |
| type record DCCH_release_req { |
| } |
| |
| /* PH-DATA.ind / PH-DATA.req */ |
| type record LAPDm_ph_data { |
| boolean sacch, |
| GsmSapi sapi, |
| LapdmFrame lapdm |
| } |
| |
| type integer TbfNr (0..7); /* maximum of 8 concurrent TBF per direction */ |
| type record TBF_UL_establish_req { |
| TbfNr tbf_nr, |
| uint8_t ra |
| } |
| |
| type record TBF_DL_establish_req { |
| TbfNr tbf_nr, |
| TbfPars pars |
| } |
| |
| /* PH-DATA.ind / PH-DATA.req */ |
| type record RLCMAC_ph_data_ind { |
| GprsCodingScheme cs, |
| uint8_t ts_nr, |
| GsmFrameNumber fn, |
| RlcmacDlBlock block |
| } |
| type record RLCMAC_ph_data_req_dyn { |
| uint8_t tbf_id, |
| GprsCodingScheme cs, |
| RlcmacUlBlock block |
| } |
| type record RLCMAC_ph_data_req_abs { |
| uint8_t tbf_id, |
| GprsCodingScheme cs, |
| uint8_t ts_nr, |
| GsmFrameNumber fn, |
| GsmBandArfcn arfcn, |
| RlcmacUlBlock block |
| } |
| type union RLCMAC_ph_data_req { |
| RLCMAC_ph_data_req_dyn dyn, |
| RLCMAC_ph_data_req_abs abs |
| } |
| |
| /* port from our (internal) point of view */ |
| type port LAPDm_SP_PT message { |
| in BCCH_tune_req, |
| DCCH_establish_req, |
| DCCH_switch_req, |
| DCCH_release_req, |
| TBF_UL_establish_req, |
| TBF_DL_establish_req, |
| RLCMAC_ph_data_req, |
| LAPDm_ph_data; |
| out DCCH_establish_res, |
| DCCH_switch_res, |
| TBF_UL_establish_res, |
| RLCMAC_ph_data_ind, |
| LAPDm_ph_data; |
| } with {extension "internal"}; |
| |
| /* port from user (external) point of view */ |
| type port LAPDm_PT message { |
| in DCCH_establish_res, |
| DCCH_switch_res, |
| TBF_UL_establish_res, |
| RLCMAC_ph_data_ind, |
| LAPDm_ph_data; |
| out BCCH_tune_req, |
| DCCH_establish_req, |
| DCCH_switch_req, |
| DCCH_release_req, |
| TBF_UL_establish_req, |
| TBF_DL_establish_req, |
| RLCMAC_ph_data_req, |
| LAPDm_ph_data; |
| } with {extension "internal"}; |
| |
| function LAPDmStart() runs on lapdm_CT { |
| f_init(); |
| ScanEvents(); |
| } |
| |
| /* TS 44.004 Figure 5.1 */ |
| type enumerated ph_state_enum { |
| PH_STATE_NULL, |
| PH_STATE_BCH, |
| PH_STATE_SEARCHING_BCH, |
| PH_STATE_TUNING_DCH, |
| PH_STATE_DCH, |
| PH_STATE_TBF |
| } |
| |
| type component lapdm_CT { |
| |
| /* L1CTL port towards the bottom */ |
| port L1CTL_PT L1CTL; |
| /* Port towards L2 */ |
| port LAPDm_SP_PT LAPDM_SP; |
| |
| /* physical layer state */ |
| var ph_state_enum ph_state := PH_STATE_NULL; |
| |
| /* channel description of the currently active DCH */ |
| var ChannelDescription chan_desc; |
| |
| /* last SACCH downlink L1 header we received */ |
| var uint5_t ms_power_lvl := 0; |
| var uint8_t timing_adv := 0; |
| |
| var TbfParsPerTs g_tbf_ul; |
| var TbfParsPerTs g_tbf_dl; |
| }; |
| |
| /* wrapper function to log state transitions */ |
| private function set_ph_state(ph_state_enum new_state) runs on lapdm_CT { |
| log("PH-STATE ", ph_state, " -> ", new_state); |
| ph_state := new_state; |
| } |
| |
| private function f_init() runs on lapdm_CT { |
| f_connect_reset(L1CTL); |
| set_ph_state(PH_STATE_NULL); |
| } |
| |
| /* release the dedicated radio channel */ |
| private function f_release_dcch() runs on lapdm_CT { |
| L1CTL.send(ts_L1CTL_DM_REL_REQ(chan_desc.chan_nr)); |
| set_ph_state(PH_STATE_BCH); |
| } |
| |
| /* tune to given ARFCN and start BCCH/CCCH decoding */ |
| private function f_tune_bcch(GsmBandArfcn arfcn, boolean combined) |
| runs on lapdm_CT { |
| var L1ctlCcchMode mode := CCCH_MODE_NON_COMBINED; |
| if (combined) { |
| mode := CCCH_MODE_COMBINED; |
| } |
| |
| if (ph_state == PH_STATE_DCH) { |
| /* release any previous DCH */ |
| f_release_dcch(); |
| } else if (ph_state == PH_STATE_TBF) { |
| f_release_tbf(); |
| } |
| |
| set_ph_state(PH_STATE_SEARCHING_BCH); |
| |
| /* send FB/SB req to sync to cell */ |
| f_L1CTL_FBSB(L1CTL, arfcn, mode); |
| set_ph_state(PH_STATE_BCH); |
| } |
| |
| /* master function establishing a dedicated radio channel */ |
| private function f_establish_dcch(uint8_t ra) runs on lapdm_CT { |
| var ImmediateAssignment imm_ass; |
| var GsmFrameNumber rach_fn; |
| |
| /* send RACH request and obtain FN at which it was sent */ |
| rach_fn := f_L1CTL_RACH(L1CTL, ra); |
| //if (not rach_fn) { return; } |
| |
| /* wait for receiving matching IMM ASS */ |
| imm_ass := f_L1CTL_WAIT_IMM_ASS(L1CTL, ra, rach_fn) |
| //if (not imm_ass) { return; } |
| set_ph_state(PH_STATE_TUNING_DCH); |
| |
| /* store/save channel description */ |
| chan_desc := imm_ass.chan_desc; |
| |
| /* send DM_EST_REQ, TODO: Mobile Allocation */ |
| f_L1CTL_DM_EST_REQ_IA(L1CTL, imm_ass); |
| set_ph_state(PH_STATE_DCH); |
| } |
| |
| /* switching directly to a dedicated channel *without RACH/IMM-ASS */ |
| private function f_switch_dcch(in DCCH_switch_req sw_req) runs on lapdm_CT { |
| set_ph_state(PH_STATE_TUNING_DCH); |
| /* store/save channel description */ |
| chan_desc := sw_req.chan_desc; |
| |
| /* tune the L1 to the indicated channel */ |
| if (chan_desc.h) { |
| L1CTL.send(ts_L1CTL_DM_EST_REQ_H1(chan_desc.chan_nr, |
| chan_desc.tsc, |
| chan_desc.maio_hsn.hsn, |
| chan_desc.maio_hsn.maio, |
| sw_req.ma)); |
| } else { |
| L1CTL.send(ts_L1CTL_DM_EST_REQ_H0(chan_desc.chan_nr, |
| chan_desc.tsc, |
| chan_desc.arfcn)); |
| } |
| |
| set_ph_state(PH_STATE_DCH); |
| } |
| |
| /* initialize a tfi_usf array with "not used" value 255 for all TN */ |
| function f_TfiUsfArrInit() return TfiUsfArr { |
| var TfiUsfArr tua := { 255, 255, 255, 255, 255, 255, 255, 255 }; |
| return tua; |
| } |
| |
| /* set TFI/USF value for one given timeslot number (index) */ |
| function f_TfiUsfArrSet(inout TfiUsfArr a, in uint8_t idx, in uint8_t tfi_usf) { |
| a[idx] := tfi_usf; |
| } |
| |
| template (value) RLCMAC_ph_data_req ts_PH_DATA_ABS(uint8_t tbf_id, GprsCodingScheme cs, |
| uint8_t ts, uint32_t fn, |
| GsmBandArfcn arfcn, |
| RlcmacUlBlock block) := { |
| abs := { |
| tbf_id := tbf_id, |
| cs := CS1, /* FIXME */ |
| ts_nr := ts, |
| fn := fn, |
| arfcn := arfcn, |
| block := block |
| } |
| } |
| |
| private function f_establish_tbf(uint8_t ra) runs on lapdm_CT return boolean { |
| var template GsmRrMessage imm_ass_rr; |
| var ImmediateAssignment imm_ass; |
| var PacketUlAssign pkt_ul_ass; |
| var GsmFrameNumber rach_fn; |
| var TfiUsfArr tua := f_TfiUsfArrInit(); |
| |
| /* send RACH request and obtain FN at which it was sent */ |
| rach_fn := f_L1CTL_RACH(L1CTL, ra); |
| |
| /* wait for receiving matching IMM ASS */ |
| imm_ass := f_L1CTL_WAIT_IMM_ASS(L1CTL, ra, rach_fn); |
| |
| /* make sure we got *Packet* (Uplink) Immediate Assignment */ |
| imm_ass_rr := tr_IMM_TBF_ASS(dl := false, ra := ra, fn := rach_fn, |
| rest := tr_IaRestOctets_ULAss(?)); |
| if (not match(imm_ass, imm_ass_rr.payload.imm_ass)) { |
| log("Failed to match Packet Immediate Assignment"); |
| return false; |
| } |
| |
| /* decapsulate PacketUlAssign for further matching */ |
| pkt_ul_ass := imm_ass.rest_octets.hh.pa.uldl.ass.ul; |
| |
| /* Dynamic Block Allocation */ |
| if (match(pkt_ul_ass, tr_PacketUlDynAssign)) { |
| set_ph_state(PH_STATE_TBF); |
| |
| /* store/save channel description */ |
| //chan_desc := imm_ass.chan_desc; |
| |
| /* Important: ARFCN, TN, TSC, USF, USF_GRANULARITY, CH_CODING_CMD */ |
| f_TfiUsfArrSet(tua, imm_ass.pkt_chan_desc.tn, pkt_ul_ass.dynamic.usf); |
| f_L1CTL_TBF_CFG(L1CTL, true, tua); |
| return true; |
| /* FIXME: Single Block Allocation */ |
| } else if (match(pkt_ul_ass, tr_PacketUlSglAssign)) { |
| log("Non-dynamic UL TBF assignment not supported yet"); |
| return false; |
| } else { |
| log("Failed to match Uplink Block Allocation: ", pkt_ul_ass); |
| return false; |
| } |
| } |
| |
| private function f_release_tbf() runs on lapdm_CT { |
| var TfiUsfArr tua := f_TfiUsfArrInit(); |
| /* send "all timeslots unused" for both UL and DL */ |
| f_L1CTL_TBF_CFG(L1CTL, true, tua); |
| f_L1CTL_TBF_CFG(L1CTL, false, tua); |
| /* L1 will then fall back to BCCH/CCCH */ |
| set_ph_state(PH_STATE_BCH); |
| } |
| |
| /* Establish TBF / packet transfer mode */ |
| private altstep as_tbf_ul_est() runs on lapdm_CT { |
| var TBF_UL_establish_req tbf_ul_req; |
| [] LAPDM_SP.receive(TBF_UL_establish_req:?) -> value tbf_ul_req { |
| var TbfNr tbf_nr := tbf_ul_req.tbf_nr; |
| var TBF_UL_establish_res res; |
| if (isvalue(g_tbf_ul[tbf_nr].arfcn)) { |
| setverdict(fail, "Cannot establish UL TBF ID ", tbf_nr, ": BUSY"); |
| mtc.stop; |
| } |
| f_establish_tbf(tbf_ul_req.ra); |
| if (ph_state == PH_STATE_TBF) { |
| g_tbf_ul[tbf_nr] := valueof(t_TbfParsInit); /* FIXME: Actual TFI[s] */ |
| log("Established UL TBF ", tbf_nr); |
| res := { pars := g_tbf_ul[tbf_nr], err := omit }; |
| } else { |
| res := { pars := omit, err := "Unable to establish UL TBF" }; |
| } |
| LAPDM_SP.send(res); |
| } |
| } |
| |
| private altstep as_tbf_dl_est() runs on lapdm_CT { |
| var TBF_DL_establish_req tbf_dl_req; |
| [] LAPDM_SP.receive(TBF_DL_establish_req:?) -> value tbf_dl_req { |
| var TbfNr tbf_nr := tbf_dl_req.tbf_nr; |
| if (isvalue(g_tbf_dl[tbf_nr].arfcn)) { |
| setverdict(fail, "Cannot establish DL TBF ID ", tbf_nr, ": BUSY"); |
| mtc.stop; |
| } |
| g_tbf_dl[tbf_nr] := tbf_dl_req.pars; |
| f_L1CTL_TBF_CFG(L1CTL, false, tbf_dl_req.pars.tfi); |
| set_ph_state(PH_STATE_TBF); |
| log("Established DL TBF ", tbf_nr, ": ", tbf_dl_req.pars); |
| } |
| } |
| |
| private function f_init_tbf() runs on lapdm_CT { |
| var integer i; |
| for (i := 0; i < 8; i := i+1) { |
| g_tbf_ul[i] := valueof(t_TbfParsInit); |
| g_tbf_dl[i] := valueof(t_TbfParsInit); |
| } |
| } |
| |
| function ScanEvents() runs on lapdm_CT { |
| var L1ctlDlMessage dl; |
| var BCCH_tune_req bt; |
| var LAPDm_ph_data lpd; |
| var RLCMAC_ph_data_ind rpdi; |
| var RLCMAC_ph_data_req rpdr; |
| var DCCH_establish_req est_req; |
| var DCCH_switch_req sw_req; |
| var DCCH_establish_res est_res; |
| |
| f_init_tbf(); |
| |
| while (true) { |
| if (ph_state == PH_STATE_NULL) { |
| alt { |
| [] LAPDM_SP.receive(BCCH_tune_req:?) -> value bt { |
| f_tune_bcch(bt.arfcn, bt.combined_ccch); |
| } |
| |
| [] LAPDM_SP.receive {} |
| [] L1CTL.receive {} |
| |
| } |
| } else if (ph_state == PH_STATE_BCH or ph_state == PH_STATE_SEARCHING_BCH) { |
| alt { |
| [] LAPDM_SP.receive(BCCH_tune_req:?) -> value bt { |
| f_tune_bcch(bt.arfcn, bt.combined_ccch); |
| } |
| |
| /* forward CCCH SAPI from L1CTL to User */ |
| [] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_BCCH(0))) -> value dl { |
| lpd.sacch := false; |
| lpd.sapi := 0; |
| lpd.lapdm.bbis := dec_LapdmFrameBbis(dl.payload.data_ind.payload); |
| LAPDM_SP.send(lpd); |
| } |
| |
| /* forward BCCH SAPI from L1CTL to User */ |
| [] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_PCH_AGCH(0))) -> value dl { |
| lpd.sacch := false; |
| lpd.sapi := 0; |
| lpd.lapdm.bbis := dec_LapdmFrameBbis(dl.payload.data_ind.payload); |
| LAPDM_SP.send(lpd); |
| } |
| |
| /* Establish dedicated channel */ |
| [] LAPDM_SP.receive(DCCH_establish_req:?) -> value est_req { |
| var DCCH_establish_res res; |
| f_establish_dcch(est_req.ra); |
| if (ph_state == PH_STATE_DCH) { |
| res := { chan_desc, omit }; |
| } else { |
| res := { omit, "Unable to esetablish DCCH" }; |
| } |
| LAPDM_SP.send(res); |
| } |
| [] LAPDM_SP.receive(DCCH_switch_req:?) -> value sw_req { |
| var DCCH_switch_res res; |
| f_switch_dcch(sw_req); |
| if (ph_state == PH_STATE_DCH) { |
| res := { omit }; |
| } else { |
| res := { "Unable to switch to DCCH" }; |
| } |
| LAPDM_SP.send(res); |
| } |
| |
| |
| [] as_tbf_ul_est(); |
| [] as_tbf_dl_est(); |
| |
| [] LAPDM_SP.receive {} |
| [] L1CTL.receive {} |
| |
| } |
| |
| } else if (ph_state == PH_STATE_TUNING_DCH or ph_state == PH_STATE_DCH) { |
| alt { |
| |
| /* decode any received DATA frames for the dedicated channel and pass them up */ |
| [] L1CTL.receive(tr_L1CTL_DATA_IND(chan_desc.chan_nr)) -> value dl { |
| var octetstring l2; |
| if (dl.dl_info.link_id.c == SACCH) { |
| lpd.sacch := true; |
| var octetstring l1 := substr(dl.payload.data_ind.payload, 0, 2); |
| l2 := substr(dl.payload.data_ind.payload, 2, |
| lengthof(dl.payload.data_ind.payload)-2); |
| ms_power_lvl := oct2int(l1[0] and4b '1F'O); |
| timing_adv := oct2int(l1[1]); |
| /* FIXME: how to deal with UI frames in B4 format (lo length!) */ |
| } else { |
| lpd.sacch := false; |
| l2 := dl.payload.data_ind.payload; |
| } |
| lpd.sapi := dl.dl_info.link_id.sapi; |
| lpd.lapdm.ab := dec_LapdmFrameAB(l2); |
| LAPDM_SP.send(lpd); |
| } |
| |
| /* encode any LAPDm record from user and pass it on to L1CTL */ |
| [] LAPDM_SP.receive(LAPDm_ph_data:?) -> value lpd { |
| var octetstring buf; |
| var RslLinkId link_id; |
| if (lpd.sacch) { |
| link_id := valueof(ts_RslLinkID_SACCH(lpd.sapi)); |
| buf := f_pad_oct(enc_LapdmFrame(lpd.lapdm), 21, '2B'O); |
| var SacchL1Header l1h := valueof(ts_SacchL1Header(ms_power_lvl, |
| false, timing_adv)); |
| L1CTL.send(ts_L1CTL_DATA_REQ_SACCH(chan_desc.chan_nr, link_id, |
| l1h, buf)); |
| } else { |
| link_id := valueof(ts_RslLinkID_DCCH(lpd.sapi)); |
| buf := f_pad_oct(enc_LapdmFrame(lpd.lapdm), 23, '2B'O); |
| L1CTL.send(ts_L1CTL_DATA_REQ(chan_desc.chan_nr, link_id, buf)); |
| } |
| } |
| |
| /* Release dedicated channel */ |
| [] LAPDM_SP.receive(DCCH_release_req:?) { |
| /* go back to BCCH */ |
| f_release_dcch(); |
| } |
| |
| [] LAPDM_SP.receive {} |
| [] L1CTL.receive {} |
| |
| |
| } |
| } else if (ph_state == PH_STATE_TBF) { |
| alt { |
| |
| /* decode + forward any blocks from L1 to L23*/ |
| [] L1CTL.receive(tr_L1CTL_DATA_IND(t_RslChanNr_PDCH(?))) -> value dl { |
| rpdi.block := dec_RlcmacDlBlock(dl.payload.data_ind.payload); |
| /* FIXME: Filter based on g_tbf_dl */ |
| rpdi.fn := dl.dl_info.frame_nr; |
| rpdi.ts_nr := dl.dl_info.chan_nr.tn; |
| rpdi.cs := CS1; /* FIXME */ |
| log("RPDI: ", rpdi); |
| LAPDM_SP.send(rpdi); |
| } |
| |
| [] L1CTL.receive { } |
| |
| /* encode + forward any blocks from L23 to L1 */ |
| [] LAPDM_SP.receive(RLCMAC_ph_data_req:?) -> value rpdr { |
| var octetstring buf; |
| if (ischosen(rpdr.dyn)) { |
| buf := enc_RlcmacUlBlock(rpdr.dyn.block); |
| L1CTL.send(ts_L1CTL_DATA_TBF_REQ(buf, L1CTL_CS1, rpdr.dyn.tbf_id)); |
| } else { |
| buf := enc_RlcmacUlBlock(rpdr.abs.block); |
| L1CTL.send(ts_L1CTL_DATA_ABS_REQ(buf, rpdr.abs.arfcn, |
| rpdr.abs.ts_nr, rpdr.abs.fn, |
| L1CTL_CS1, rpdr.abs.tbf_id)); |
| } |
| } |
| |
| [] as_tbf_ul_est(); |
| [] as_tbf_dl_est(); |
| |
| /* FIXME: release TBF mode */ |
| [] LAPDM_SP.receive(DCCH_release_req:?) { |
| /* go back to BCCH */ |
| f_release_tbf(); |
| f_init_tbf(); |
| } |
| |
| } |
| } |
| |
| } /* while (1) */ |
| } |
| } |