/* 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 * 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) */ } }