1# Copyright 2019 Oticon A/S
2# SPDX-License-Identifier: Apache-2.0
3
4#EDTT Transport driver for BabbleSim
5# Any EDTT Transport shall implement the following interface:
6#   __init__(args)
7#   connect()
8#   send(idx, message)
9#   recv(idx, number_bytes, timeout)
10#   wait(time)
11#   close()
12#   get_time()
13#   n_devices : Number of devices it is connected to
14
15import struct;
16import os;
17import fcntl;
18
19from components.bsim_device import BSimDevice
20from components.bsim_lib import ( PB_MSG_DISCONNECT, PB_MSG_TERMINATE, PB_MSG_WAIT, PB_MSG_WAIT_END, TIME_NEVER, create_com_folder, create_fifo_if_not_there, test_and_create_lock_file);
21
22class EDTTT:
23    TO_EDTT  = 0;
24    TO_DEVICE = 1;
25    TO_PHY = 1;
26    PhyFIFOs = [-1,-1];
27    PhyFIFO_names = ["",""];
28    FIFOs = [];
29    FIFOnames = [];
30    verbosity = 0;
31    last_t =  0; #last timestamp received from the Phy
32    Connected = False;
33    n_devices = 0;
34    low_level_device = None
35    com_path = ""
36    lock_path = ""
37    autoterminate = True
38    RxWait = 10000
39
40    def __init__(self, pending_args, TraceClass):
41        self.Trace = TraceClass;
42        import argparse
43        parser = argparse.ArgumentParser(prog="BabbleSim transport options:", add_help=False)
44        parser.add_argument("-s", "--sim_id", required=True, help="Simulation id");
45        parser.add_argument("-d", "--EDTT_tool_dev_nbr", required=True, type=int, help="Device number of the EDTT tool itself");
46        parser.add_argument("-D", "--number-of-devices", required=True, type=int, help="Number of simulated devices we will connect to (the low level device does not count)");
47        parser.add_argument("-devs", "--devices-numbers", required=True, type=int, nargs='+', help="Set of simulated devices we will connect to. There should <number-of-devices> in this list, where the first one will be the \"device 0\" for EDTT and so forth");
48        parser.add_argument("-RxWait", required=False, default=10000, type=float, help="(10e3) while there is no enough data for a read, the simulation will be advanced in this steps");
49        parser.add_argument("-l", "--low-level-device", required=False, help="Enable BSim low level device; Note that this requires --low-level-device-nbr to be supplied", action='store_true');
50        parser.add_argument("--low-level-device-nbr", required=False, type=int, help="Device number of the BSim low level device");
51        parser.add_argument("--DontAutoTerminate", required=False, action='store_true', help="Do not terminate simulation when the test ends (by default it will)");
52        (args, discard) = parser.parse_known_args(pending_args)
53
54        self.sim_id = args.sim_id;
55        self.EDTT_tool_dev_nbr = args.EDTT_tool_dev_nbr;
56        self.n_devices = args.number_of_devices;
57        self.devices_numbers = args.devices_numbers;
58        self.RxWait = int(args.RxWait)
59
60        if args.low_level_device:
61            if not args.low_level_device_nbr:
62                raise Exception("--low-level-device-nbr is required when the BSim low level device is enabled")
63            self.low_level_device = BSimDevice(int(args.low_level_device_nbr), self.sim_id, TraceClass)
64
65        if len(args.devices_numbers) < args.number_of_devices:
66            raise Exception("--devices-numbers must have as many elements as configured with number-of-devices")
67
68        if args.low_level_device_nbr and (args.low_level_device_nbr <= args.EDTT_tool_dev_nbr):
69            raise Exception("The low-level-device-nbr must be higher than the EDTT tool device number (otherwise we will deadlock)")
70
71        self.FIFOs = [[-1, -1] for i in range(0,args.number_of_devices)];
72        self.FIFOnames = [["", ""] for i in range(0,args.number_of_devices)];
73
74        if args.DontAutoTerminate:
75            self.autoterminate = false
76
77    def connect(self):
78        self.Trace.trace(3,"Connecting to EDTT Phy and devices");
79        self.com_path = create_com_folder(self.sim_id);
80        self.lock_path = test_and_create_lock_file(self.com_path, self.EDTT_tool_dev_nbr, self.Trace)
81
82        #Note that python ignores SIGPIPE by default
83
84        self.PhyFIFO_names[self.TO_PHY] = "%s/%s.d%i.dtp" % (self.com_path, "2G4", self.EDTT_tool_dev_nbr)
85        self.PhyFIFO_names[self.TO_EDTT] = "%s/%s.d%i.ptd" % (self.com_path, "2G4", self.EDTT_tool_dev_nbr)
86
87        try:
88            create_fifo_if_not_there(self.PhyFIFO_names[self.TO_PHY]);
89            create_fifo_if_not_there(self.PhyFIFO_names[self.TO_EDTT]);
90
91            #Note that the order of opening them needs to be like this. To match the Phy order and avoid deadlocking the other
92            #We purposedly want to block until the other side is connected to hold until everything is ready
93            self.PhyFIFOs[self.TO_EDTT] = os.open(self.PhyFIFO_names[self.TO_EDTT], os.O_RDONLY);
94            self.PhyFIFOs[self.TO_PHY] = os.open(self.PhyFIFO_names[self.TO_PHY], os.O_WRONLY);
95
96            if (self.PhyFIFOs[self.TO_PHY] == -1):
97                raise Exception("Could not open FIFO %s"% self.PhyFIFO_names[self.TO_PHY]);
98            if (self.PhyFIFOs[self.TO_EDTT] == -1):
99                raise Exception("Could not open FIFO %s"% self.PhyFIFO_names[self.TO_EDTT]);
100        except:
101            self.cleanup()
102            raise
103
104        self.Trace.trace(6,"Connected to Phy");
105
106        if self.low_level_device:
107            self.low_level_device.connect()
108            self.Trace.trace(8,"Low level device connected to Phy");
109
110        try:
111            for i in range(0, self.n_devices):
112                d = self.devices_numbers[i];
113                self.Trace.trace(8,"Connecting to device %i"%d);
114                self.FIFOnames[i][self.TO_DEVICE] = "%s/Device%i.PTTin" % (self.com_path, d)
115                self.FIFOnames[i][self.TO_EDTT]   = "%s/Device%i.PTTout" % (self.com_path, d)
116                #Long ago the EDTT was called PTT. The FIFOs were never renamed as it would be a backwards compatibility change.
117
118                create_fifo_if_not_there(self.FIFOnames[i][self.TO_EDTT]);
119                create_fifo_if_not_there(self.FIFOnames[i][self.TO_DEVICE]);
120
121                #Note that we don't set O_NONBLOCK during the creating of the FIFO as we want to block until the device is there
122                self.FIFOs[i][self.TO_EDTT] = os.open(self.FIFOnames[i][self.TO_EDTT], os.O_RDONLY);
123                if (self.FIFOs[i][self.TO_EDTT] == -1):
124                    raise Exception("Could not open FIFO %s"% self.FIFOnames[i][self.TO_EDTT]);
125
126                #we want the read end to be non bloking (if the device didn't produce anything yet, we need to let it run a bit)
127                flags = fcntl.fcntl(self.FIFOs[i][self.TO_EDTT], fcntl.F_GETFL);
128                flags |= os.O_NONBLOCK;
129                fcntl.fcntl(self.FIFOs[i][self.TO_EDTT], fcntl.F_SETFL, flags);
130
131                self.FIFOs[i][self.TO_DEVICE] = os.open(self.FIFOnames[i][self.TO_DEVICE], os.O_WRONLY);
132                if (self.FIFOs[i][self.TO_DEVICE] == -1):
133                    raise Exception("Could not open FIFO %s"% self.FIFOnames[i][self.TO_DEVICE]);
134
135                #we want the  write end to be non bloking (if for whatever reason
136                # we fill up the FIFO, we would deadlock as the device is stalled
137                # => better to catch it in the write function)
138                flags = fcntl.fcntl(self.FIFOs[i][self.TO_DEVICE], fcntl.F_GETFL);
139                flags |= os.O_NONBLOCK;
140                fcntl.fcntl(self.FIFOs[i][self.TO_DEVICE], fcntl.F_SETFL, flags);
141
142                self.Trace.trace(8,"Connected to device %i"%d);
143        except:
144            self.cleanup()
145            raise
146
147        self.Connected = True;
148        self.Trace.trace(4,"Connected to Phy and all devices");
149
150        # Wait for dump files to have been opened by the 2G4 phy
151        # Since there isn't a method implemented for synchronizing that, use the
152        # fact that any commands to the 2G4 phy will not run until after it has
153        #opened the files for writing; So use a minimal wait as a blocking mechanism
154        self.wait_until_t(1)
155
156    def cleanup(self):
157        if self.lock_path:
158            try:
159                os.remove(self.lock_path)
160            except:
161                pass
162            self.lock_path = ""
163
164        if self.low_level_device:
165            self.Trace.trace(4,"Cleaning up low-level device");
166            self.low_level_device.cleanup()
167
168        self.Trace.trace(4,"Cleaning up transport");
169        try:
170            if self.PhyFIFOs[self.TO_EDTT]:
171                os.close(self.PhyFIFOs[self.TO_EDTT]);
172                self.PhyFIFOs[self.TO_EDTT] = 0
173                self.Trace.trace(9,"Closed FIFO to Phy (->EDTT direction)");
174            if self.PhyFIFOs[self.TO_PHY]:
175                os.close(self.PhyFIFOs[self.TO_PHY]);
176                self.PhyFIFOs[self.TO_PHY] = 0
177                self.Trace.trace(9,"Closed FIFO to Phy (->Phy direction)");
178            os.remove(self.PhyFIFO_names[self.TO_PHY]);
179            os.remove(self.PhyFIFO_names[self.TO_EDTT]);
180
181            for i in range(0, self.n_devices):
182                self.Trace.trace(9,"Cleaning up interface to Device %i"%i);
183                if self.FIFOs[i][self.TO_EDTT]:
184                    os.close(self.FIFOs[i][self.TO_EDTT]);
185                    self.FIFOs[i][self.TO_EDTT] = 0
186                    self.Trace.trace(9,"Closed FIFO to Device %i (->EDTT direction)"%i);
187                if self.FIFOs[i][self.TO_DEVICE]:
188                    os.close(self.FIFOs[i][self.TO_DEVICE]);
189                    self.FIFOs[i][self.TO_DEVICE] = 0
190                    self.Trace.trace(9,"Closed FIFO to Device %i (->Device direction)"%i);
191                os.remove(self.FIFOnames[i][self.TO_DEVICE]);
192                os.remove(self.FIFOnames[i][self.TO_EDTT]);
193        except OSError:
194            self.Trace.trace(9,"(minor) Error closing FIFO "
195                             "(most likely either file does not exist yet)");
196
197    def __disconnect(self):
198        if self.Connected:
199            self.Connected = False
200            if self.autoterminate:
201                msg = struct.pack('=I', PB_MSG_TERMINATE)
202                self.Trace.trace(4,"Terminating simulation")
203            else:
204                msg = struct.pack('=I', PB_MSG_DISCONNECT)
205                self.Trace.trace(4,"Disconnecting from Phy")
206            self.__write_to_phy(msg);
207        if self.low_level_device:
208            self.low_level_device.disconnect()
209
210    def close(self):
211        self.__disconnect();
212        self.cleanup();
213
214    def __write_to_device(self, d, content):
215        # Write content to device d
216        try:
217            written = os.write(self.FIFOs[d][self.TO_DEVICE], content);
218            if written != len(content):
219                raise;
220        except:
221            self.Trace.trace(4,"The Device %i disappeared when trying to "
222                             "write to it"%d);
223            self.close();
224            raise Exception("Abruptly disconnected from device %i"%d);
225
226    def __read_from_device(self, d, nbytes):
227        # Atttempt a non-blocking read of nbytes from device d
228        pkt=b""
229        try:
230            pkt = os.read(self.FIFOs[d][self.TO_EDTT], nbytes);
231        except BlockingIOError:
232            #No enough data available yet, we just return an empty packet
233            #or whatever we got so far
234            pass
235        except:
236            self.Trace.trace(4,"The Device %i disappeared when trying to "
237                             "read from it"%d);
238            self.close();
239            raise Exception("Abruptly disconnected from device %i"%d);
240
241        return pkt;
242
243    def __write_to_phy(self,  content):
244        # Write content to the Phy
245        try:
246            written = os.write(self.PhyFIFOs[self.TO_PHY], content);
247            if written != len(content):
248                raise;
249        except:
250            self.Trace.trace(4,"The Phy disappeared when trying to "
251                             "write to it");
252            self.Connected = False
253            self.close();
254            raise Exception("Abruptly disconnected from Phy");
255
256    def __read_from_phy(self, nbytes):
257        # Read (blocking) nbytes from the Phy.
258        # If less bytes are read, we consider it an error
259        try:
260            pkt = os.read(self.PhyFIFOs[self.TO_EDTT], nbytes);
261            if len(pkt) != nbytes:
262                raise;
263            return pkt;
264        except:
265            self.Trace.trace(4,"The Phy disappeared when trying to "
266                             "read from it");
267            self.Connected = False
268            self.close();
269            raise Exception("Abruptly disconnected from Phy");
270
271    def send(self, idx, message):
272        #Send <message> to device <idx>
273        if (idx > self.n_devices -1):
274            raise Exception("Trying to access unconnected device %i"%idx);
275
276        self.Trace.trace(8,"Writing %i bytes to device %i"%(len(message),idx));
277        self.__write_to_device(idx,message)
278        # a send is immediate (no time advance)
279        self.Trace.btsnoop.send(idx, message)
280
281    def recv(self, idx, number_bytes, to=None):
282        #Attempt to receive <number_bytes> from device <idx>, with a timeout of <to> ms
283        if (idx > self.n_devices -1):
284            raise Exception("Trying to access unconnected device %i"%idx);
285
286        if to == None:
287            to = self.default_to
288        if ( number_bytes == 0 ):
289          return b""
290
291        timeout = to*1000 + self.last_t;
292
293        self.Trace.trace(8, "Recv of %iB from dev %d, timeout %i ms"% (number_bytes, idx, to) )
294
295        # Let's try to read from the device. We will either manage right away
296        # or we will need to let time advance while we keep retrying
297        pending_to_read = number_bytes
298        readsofar = 0
299        packet = b""
300        while self.last_t < timeout:
301            segment = self.__read_from_device(idx, pending_to_read)
302            packet += segment
303            nread = len(segment)
304            readsofar += nread
305            pending_to_read -= nread;
306            if pending_to_read > 0: #we need to wait a bit
307                self.Trace.trace(6, "During recv of %iB from dev %d, pending %i, Waiting for %s us"%
308                                  (number_bytes, idx, pending_to_read, str(self.RxWait)) )
309                self.wait(self.RxWait/1000)
310            else:
311                break
312
313        if pending_to_read != 0:
314            self.Trace.trace(2, "Attempt to recv from dev %d, but only read %i out of %i bytes"%(idx, readsofar, number_bytes))
315        else:
316            self.Trace.trace(8, "Attempt to recv from dev %d, read all %i bytes"%(idx, number_bytes))
317
318        return packet
319
320    def wait(self, delay_in_ms):
321        end_of_wait = int(delay_in_ms*1000 + self.last_t);
322        return self.wait_until_t(end_of_wait)
323
324    def wait_until_t(self, end_of_wait):
325        if self.last_t >= end_of_wait:
326            self.Trace.trace(3, "Ignoring end_of_wait with a time not in the future: simulation time: %s; Requested end of wait: %s" % (self.last_t, end_of_wait))
327            return
328
329        self.Trace.trace(8, "Waiting until %d"%end_of_wait)
330
331        if self.low_level_device:
332            self.low_level_device.wait(end_of_wait)
333
334        if self.Connected:
335            #A wait request to the Phy is a 32bit PB_MSG_WAIT header followed by
336            #a 64 bit time_to_wait integer value in microseconds
337            msg = struct.pack("=IQ", PB_MSG_WAIT, end_of_wait)
338            self.__write_to_phy(msg)
339
340            # Block until we get the reply from the Phy which a 32 bit value.
341            # It can either be an indication that the wait ended PB_MSG_WAIT_END
342            # or a PB_MSG_DISCONNECT == an order to disconnected
343            raw_header = self.__read_from_phy(4)
344
345            header, = struct.unpack("=I", raw_header)
346            if header == PB_MSG_DISCONNECT:
347                self.Trace.trace(2, "Phy told us to disconnect")
348                self.Connected = False
349                self.__disconnect()
350                raise Exception("Simulated terminated by the Phy")
351            elif header != PB_MSG_WAIT_END:
352                raise Exception("Low level communication with PHY failed; Received invalid response %s" % header)
353
354        self.last_t = end_of_wait
355
356    def get_time(self):
357        #return current time in miliseconds
358        return self.last_t/1000;
359
360    def get_last_t(self):
361        #return current time in microseconds
362        return self.last_t
363