예제 #1
0
파일: qsim.py 프로젝트: larioandr/pyqumo
class QueueingSystem(Model):
    """QueueingSystem model represents a simple queue */*/1/N or */*/1,
    where any distribution can be used for arrival and service times.

    This model consists of four children:
    - `queue`: a model representing the packets queue (`Queue`)
    - `source`: a model representing the packets source (`Source`)
    - `server`: a model representing the packets server (`Server`)
    - `sink`: a model which collects served packets (`Sink`)
    """
    def __init__(self, sim):
        super().__init__(sim)

        arrival = sim.params.arrival
        service = sim.params.service
        queue_capacity = sim.params.queue_capacity

        self.children['queue'] = Queue(sim, queue_capacity)
        self.children['source'] = Source(sim, arrival, index=0)
        self.children['server'] = Server(sim, service)
        self.children['sink'] = Sink(sim)

        # Building connections:
        self.source.connections['queue'] = self.queue
        self.queue.connections['server'] = self.server
        self.server.connections['queue'] = self.queue
        self.server.connections['next'] = self.sink

        # Statistics:
        self.system_size_trace = Trace()
        self.system_size_trace.record(self.sim.stime, 0)
        self.system_wait_intervals = Statistic()

    @property
    def queue(self):
        return self.children['queue']

    @property
    def source(self):
        return self.children['source']

    @property
    def server(self):
        return self.children['server']

    @property
    def sink(self):
        return self.children['sink']

    @property
    def system_size(self):
        return self.queue.size + (1 if self.server.busy else 0)

    def update_system_size(self, index=0):
        assert index == 0
        self.system_size_trace.record(self.sim.stime, self.system_size)

    def add_system_wait_interval(self, value, index=0):
        assert index == 0
        self.system_wait_intervals.append(value)
예제 #2
0
def test_record_adds_data():
    trace = Trace()
    assert trace.as_tuple() == ()

    trace.record(t=1, v=10)
    assert trace.as_tuple() == ((1, 10), )

    trace.record(t=2, v=13)
    assert trace.as_tuple() == (
        (1, 10),
        (2, 13),
    )
예제 #3
0
class QueueingSystem(Model):
    """QueueingSystem model is the top-level model for this test.

    This model consists of four children:
    - `queue`: a model representing the packets queue (`Queue`)
    - `source`: a model representing the packets source (`Source`)
    - `server`: a model representing the packets server (`Server`)
    - `sink`: a model which collects served packets (`Sink`)
    """
    def __init__(self, sim):
        super().__init__(sim)
        self.children['queue'] = Queue(sim, sim.params.capacity)
        self.children['source'] = Source(sim, sim.params.arrival_mean)
        self.children['server'] = Server(sim, sim.params.service_mean)
        self.children['sink'] = Sink(sim)

        # Building connections:
        self.source.connections['queue'] = self.queue
        self.queue.connections['server'] = self.server
        self.server.connections['queue'] = self.queue
        self.server.connections['sink'] = self.sink

        # Statistics:
        self.system_size_trace = Trace()
        self.system_size_trace.record(self.sim.stime, 0)
    
    @property
    def queue(self):
        return self.children['queue']
    
    @property
    def source(self):
        return self.children['source']
    
    @property
    def server(self):
        return self.children['server']
    
    @property
    def sink(self):
        return self.children['sink']
    
    @property
    def system_size(self):
        return self.queue.size + (1 if self.server.busy else 0)
    
    def update_system_size(self):
        self.system_size_trace.record(self.sim.stime, self.system_size)
예제 #4
0
class Server(Model):
    """Server module represents a packet server with exponential service time.

    Connections: queue, sink

    Handlers:
    - on_service_end(): called upon service timeout

    Methods:
    - start_service(): start new packet service; generate error if busy.

    Statistics:
    - delays: `Statistic`, stores a set of service intervals
    - busy_trace: `Trace`, stores a vector of server busy status

    Parent: `QueueingSystem`
    """
    def __init__(self, sim, service_mean):
        super().__init__(sim)
        self.__service_mean = service_mean
        self.__busy = False
        # Statistics:
        self.delays = Statistic()
        self.busy_trace = Trace()
        self.busy_trace.record(self.sim.stime, 0)
    
    @property
    def service_mean(self):
        return self.__service_mean
    
    @property
    def busy(self):
        return self.__busy
    
    def on_service_end(self):
        assert self.__busy
        queue = self.connections['queue'].module
        self.__busy = False
        self.busy_trace.record(self.sim.stime, 0)
        if queue.size > 0:
            queue.pop()
            self.start_service()
        self.connections['sink'].module.receive_packet()
        self.parent.update_system_size()

    def start_service(self):
        assert not self.__busy
        delay = exponential(self.service_mean)
        self.sim.schedule(delay, self.on_service_end)
        self.delays.append(delay)
        self.__busy = True
        self.busy_trace.record(self.sim.stime, 1)
예제 #5
0
class Queue(Model):
    """Queue module represents the packets queue, stores only current size.

    Connections: server

    Methods and properties:
    - push(): increase the queue size
    - pop(): decrease the queue size
    - size: get current queue size

    Statistics:
    -  size_trace: Trace, holding the history of the queue size updates
    """
    def __init__(self, sim, capacity):
        super().__init__(sim)
        self.__capacity = capacity
        self.__size = 0
        # Statistics:
        self.size_trace = Trace()
        self.size_trace.record(self.sim.stime, 0)
    
    @property
    def capacity(self):
        return self.__capacity
    
    @property
    def size(self):
        return self.__size
    
    def push(self):
        server = self.connections['server'].module
        if self.__size == 0 and not server.busy:
            server.start_service()
        elif self.capacity < 0 or self.__size < self.capacity:
            self.__size += 1
            self.size_trace.record(self.sim.stime, self.__size)
        self.parent.update_system_size()
    
    def pop(self):
        assert self.__size > 0
        self.__size -= 1
        self.size_trace.record(self.sim.stime, self.__size)
    
    def __str__(self):
        return f'Queue({self.size})'
예제 #6
0
class ModelData(Model):
    def __init__(self, sim):
        super().__init__(sim)

        # Topology comprises:
        # - `nodes`: each having position, properties
        # - `static_connections`: optional dictionary with static connections
        # We use it as a template for network creation.
        self.network = Network(sim.params.topology)
        self.repair_started = False
        self.failed_nodes = []
        self.routing_mode = sim.params.routing_mode
        sim.logger.level = sim.params.log_level

        # Statistics:
        self.num_failed = Trace()
        self.num_offline = Trace()
        self.operable = Trace()
        self.num_failed_sampled = Trace()
        self.num_offline_sampled = Trace()

        # Record initial trace data:
        t = sim.stime
        num_offline = len(self.network.get_offline_nodes())
        self.num_failed.record(t, 0)
        self.num_failed_sampled.record(t, 0)
        self.num_offline.record(t, num_offline)
        self.num_offline_sampled.record(t, num_offline)
        self.operable.record(t, 1)

        # Build network routes:
        self.network.build_routing_table(self.routing_mode)
        for sensor in self.network.sensors():
            self.schedule_failure(sensor)

        # Check whether we record samples:
        if sim.params.record_samples:
            sim.schedule(sim.params.sample_interval,
                         self.handle_sample_timeout)

    def handle_failure(self, node):
        """Handle node failure. Upon failure the following actions take place:

        1) a node is marked as turned off in the network;

        2) if repair hasn't started yet, we check whether enough nodes
           already failed:
           if num_offline_nodes > num_offline_till_repair, start the repair.
        """
        self.network.turn_off(node)
        self.failed_nodes.append(node)

        # If static routing is used, no actual re-routing will take place.
        # However, in case of dynamic routing using, this can lead to
        # network reconfiguration and new alternative paths
        self.network.build_routing_table(self.routing_mode)

        # Count failed and offline nodes and record statistics:
        num_offline_nodes = len(self.network.get_offline_nodes())
        num_failed_nodes = len(self.failed_nodes)
        self.num_failed.record(self.sim.stime, num_failed_nodes)
        self.num_offline.record(self.sim.stime, num_offline_nodes)

        # We treat all offline nodes as broken, while some of them are
        # offline since the routers they were connected to became unavailable:
        self.sim.logger.debug(f'node {node} failed. {num_failed_nodes} nodes '
                              f'failed, {num_failed_nodes} are offline. '
                              f'Failed nodes: {self.failed_nodes}')
        if num_offline_nodes >= self.sim.params.num_offline_till_repair:
            if not self.repair_started:
                # print(f'num failed node: {num_failed_nodes}, offline: {num_offline_nodes}, start repair')
                self.schedule_next_repair()
            self.operable.record(self.sim.stime, 0)

    def handle_repair_finished(self, node):
        """Handle repair of all nodes finished.

        Here we assume that during repair all failed nodes were fixed,
        so we schedule their failures again.
        """
        # Turn on the repaired node, rebuild a routing table and schedule the
        # next failure:
        self.sim.logger.debug(f'node {node} repair finished')
        self.network.turn_on(node)
        self.network.build_routing_table(self.routing_mode)
        self.schedule_failure(node)

        # Remove the repaired node from the failed list, count failed and
        # offline nodes:
        self.failed_nodes.remove(node)
        num_failed = len(self.failed_nodes)
        num_offline = len(self.network.get_offline_nodes())

        # Record statistics:
        t = self.sim.stime
        self.num_failed.record(t, num_failed)
        self.num_offline.record(t, num_offline)

        # Check whether there are other failed nodes. If they exist, schedule
        # the next repair. Otherwise, mark repair end.
        if num_failed > 0:
            self.schedule_next_repair()
        else:
            self.repair_started = False

        if num_offline >= self.sim.params.num_offline_till_repair:
            self.operable.record(t, 0)
        else:
            self.operable.record(t, 1)

    def handle_sample_timeout(self):
        t = self.sim.stime
        num_offline = len(self.network.get_offline_nodes())
        self.num_failed_sampled.record(t, len(self.failed_nodes))
        self.num_offline_sampled.record(t, num_offline)
        self.sim.schedule(self.sim.params.sample_interval,
                          self.handle_sample_timeout)

    def schedule_failure(self, node):
        """Schedule next failure event for a given node.

        :param node: network node
        """
        interval = self.sim.params.failure_interval()
        self.sim.schedule(interval, self.handle_failure, args=(node, ))

    def schedule_next_repair(self):
        """Schedule next repair duration. Since all broken nodes are
        repaired at once, we do not distinguish nodes here.
        """
        if not self.failed_nodes:
            raise RuntimeError('can not schedule repair - no failed nodes')

        # Select a random node to repair, and schedule repair end:
        node_index = np.random.randint(len(self.failed_nodes))
        node = self.failed_nodes[node_index]

        if not self.repair_started:
            interval = self.sim.params.repair_interval()
            self.repair_started = True
        else:
            interval = self.sim.params.cons_repair_interval()

        self.sim.logger.debug(f'started node {node} repair for {interval}s')
        self.sim.schedule(interval, self.handle_repair_finished, args=(node, ))
예제 #7
0
def test_record_with_past_time_causes_error():
    trace = Trace([(10, 13)])
    with pytest.raises(ValueError) as excinfo:
        trace.record(5, 10)
    assert 'adding data in past prohibited' in str(excinfo.value).lower()
예제 #8
0
파일: qsim.py 프로젝트: larioandr/pyqumo
class Server(Model):
    """Server module represents a packet server with exponential service time.

    Connections: queue, sink

    Handlers:
    - on_service_end(): called upon service timeout

    Methods:
    - start_service(): start new packet service; generate error if busy.

    Statistics:
    - delays: `Statistic`, stores a set of service intervals
    - busy_trace: `Trace`, stores a vector of server busy status
    - num_served: `int`

    Parent: `QueueingSystem`
    """
    def __init__(self, sim, service_time, index=0):
        super().__init__(sim)
        self.service_time = service_time
        self.packet = None
        self.index = index

        # Statistics:
        self.service_intervals = Statistic()
        self.busy_trace = Trace()
        self.busy_trace.record(self.sim.stime, 0)
        self.departure_intervals = Intervals()
        self.departure_intervals.record(sim.stime)
        self.num_served = 0

    @property
    def busy(self):
        return self.packet is not None

    @property
    def ready(self):
        return self.packet is None

    def handle_service_end(self):
        assert self.busy
        stime = self.sim.stime

        self.connections['next'].send(self.packet)
        self.parent.add_system_wait_interval(stime - self.packet.arrived_at,
                                             self.index)

        self.packet = None

        self.busy_trace.record(stime, 0)
        self.departure_intervals.record(stime)
        self.num_served += 1

        # Requesting next packet from the queue:
        queue = self.connections['queue'].module
        if queue.size > 0:
            self.serve(queue.pop())

        self.parent.update_system_size(self.index)

    def _start_service(self):
        delay = self.service_time()
        self.sim.schedule(delay, self.handle_service_end)
        return delay

    def serve(self, packet):
        assert not self.busy
        self.packet = packet
        delay = self._start_service()
        self.service_intervals.append(delay)
        self.busy_trace.record(self.sim.stime, 1)
예제 #9
0
파일: qsim.py 프로젝트: larioandr/pyqumo
class Queue(Model):
    """Queue module represents the packets queue, stores only current size.

    Connections: server

    Methods and properties:
    - push(): increase the queue size
    - pop(): decrease the queue size
    - size: get current queue size

    Statistics:
    - size_trace: Trace, holding the history of the queue size updates
    - num_arrived: `int`
    - num_dropped: `int`
    - drop_ratio: `float`
    """
    def __init__(self, sim, capacity, index=0):
        super().__init__(sim)
        self.__capacity = capacity
        self.packets = deque()
        self.index = index
        # Statistics:
        self.size_trace = Trace()
        self.size_trace.record(self.sim.stime, 0)
        self.num_dropped = 0
        self.arrival_intervals = Intervals()
        self.arrival_intervals.record(sim.stime)
        self.num_arrived = 0
        self.wait_intervals = Statistic()

    @property
    def capacity(self):
        return self.__capacity

    @property
    def size(self):
        return len(self.packets)

    @property
    def drop_ratio(self):
        if self.num_arrived == 0:
            return 0
        return self.num_dropped / self.num_arrived

    def handle_message(self, message, connection=None, sender=None):
        self.push(message)

    def push(self, packet):
        self.arrival_intervals.record(self.sim.stime)
        self.num_arrived += 1
        server = self.connections['server'].module

        packet.arrived_at = self.sim.stime

        if self.size == 0 and not server.busy:
            server.serve(packet)
            self.wait_intervals.append(self.sim.stime - packet.arrived_at)

        elif self.capacity is None or self.size < self.capacity:
            self.packets.append(packet)
            self.size_trace.record(self.sim.stime, self.size)

        else:
            self.num_dropped += 1

        self.parent.update_system_size(self.index)

    def pop(self):
        packet = self.packets.popleft()
        self.size_trace.record(self.sim.stime, self.size)
        self.wait_intervals.append(self.sim.stime - packet.arrived_at)
        return packet

    def __str__(self):
        return f'Queue({self.size})'
예제 #10
0
class WiredTransceiver(Model):
    """This module models a simple wired full-duplex transceiver.

    `WiredTransceiver` simulates a simple wired network card. It sends packets
    in `WireFrame`s instances with constant bitrate. Each frame can have
    a preamble and some header. After transmission, `WiredTransceiver` waits
    IFS before sending another frame.

    `WiredTransceiver` receives data from the queue. Upon construction, it
    schedules `_start()` call, which will request data from the queue. Note
    that `'queue'` connection MUST be added right after creation to make this
    work. Upon `_start()` call, `WiredTransceiver` will request packets with
    `queue.get_next(self)` call.

    `WiredTransceiver` requests other packets when TX operation finishes and
    it waits IFS. To request additional packets, it calls `queue.get_next()`.

    `WiredTransceiver` is expected to have the only one peer, which typically
    is another `WiredTransceiver` module. They MUST be connected with `'peer'`
    bi-directional connection. Reception is performed in two steps: when
    the frame reaches the transceiver, it marks RX as busy, and schedules
    frame reception end at `frame.duration`. Upon frame RX end, RX is marked
    as ready.

    If `WiredTransceiver` receives a packet from the `Queue` during another
    TX operation running, it will throw a `RuntimeError` exception.

    `WiredTransceiver` needs three connections:

    - `'peer'`: a bi-directional connection to another transceiver (mandatory);
    - `'queue'`: a bi-directional connection to the packets queue (mandatory);
    - `'up'`: a bi-directional connection to the switch (optional).

    If `'up'` connection is defined, the received packets are sent through it.
    Otherwise, they are silently dropped. Typically, `'up'` connects a
    `NetworkSwitch` module, or parent `NetworkInterface`.
    """
    def __init__(self, sim, bitrate=inf, header_size=0, preamble=0, ifs=0):
        super().__init__(sim)
        self.bitrate = bitrate
        self.header_size = header_size
        self.preamble = preamble
        self.ifs = ifs
        # State variables:
        self.__started = False
        self.__tx_frame = None
        self.__wait_ifs = False
        self.__rx_frame = None
        # Statistics:
        self.__num_received_frames = 0
        self.__num_received_bits = 0
        self.__rx_busy_trace = Trace()
        self.__rx_busy_trace.record(0, 0)
        self.__num_transmitted_packets = 0
        self.__num_transmitted_bits = 0
        self.__tx_busy_trace = Trace()
        self.__tx_busy_trace.record(0, 0)
        self.__service_time = Statistic()
        self.__service_started_at = None
        # Initialization:
        self.sim.schedule(self.sim.stime, self.start)

    @property
    def started(self):
        return self.__started

    @property
    def tx_ready(self):
        return not self.tx_busy

    @property
    def tx_busy(self):
        return self.__tx_frame is not None or self.__wait_ifs

    @property
    def rx_ready(self):
        return self.__rx_frame is None

    @property
    def rx_busy(self):
        return not self.rx_ready

    @property
    def num_received_frames(self):
        return self.__num_received_frames

    @property
    def num_received_bits(self):
        return self.__num_received_bits

    @property
    def rx_busy_trace(self):
        return self.__rx_busy_trace

    @property
    def num_transmitted_packets(self):
        return self.__num_transmitted_packets

    @property
    def num_transmitted_bits(self):
        return self.__num_transmitted_bits

    @property
    def tx_busy_trace(self):
        return self.__tx_busy_trace

    @property
    def service_time(self):
        return self.__service_time

    @property
    def tx_frame(self):
        return self.__tx_frame

    def start(self):
        self.connections['queue'].module.get_next(self)
        self.__started = True

    def handle_message(self, message, connection=None, sender=None):
        if connection.name == 'queue':
            if self.tx_busy:
                raise RuntimeError(
                    'new NetworkPacket while another TX running')
            duration = ((self.header_size + message.size) / self.bitrate +
                        self.preamble)
            frame = WireFrame(packet=message,
                              duration=duration,
                              header_size=self.header_size,
                              preamble=self.preamble)
            self.connections['peer'].send(frame)
            self.sim.schedule(duration, self.handle_tx_end)
            self.__tx_frame = frame
            self.__tx_busy_trace.record(self.sim.stime, 1)
            self.__service_started_at = self.sim.stime
            self.sim.logger.debug(f'start transmitting frame {frame}',
                                  src=self)
        elif connection.name == 'peer':
            self.sim.schedule(message.duration,
                              self.handle_rx_end,
                              args=(message, ))
            self.__rx_frame = message
            self.__rx_busy_trace.record(self.sim.stime, 1)
            self.sim.logger.debug(f'start receiving frame {message}', src=self)

    def handle_tx_end(self):
        self.sim.schedule(self.ifs, self.handle_ifs_end)
        # Record statistics:
        self.__num_transmitted_packets += 1
        self.__num_transmitted_bits += self.__tx_frame.size
        # Update state variables:
        self.__wait_ifs = True
        self.__tx_frame = None
        self.sim.logger.debug(f'finish transmitting, waiting IFS', src=self)

    def handle_ifs_end(self):
        self.__wait_ifs = False
        self.connections['queue'].module.get_next(self)
        # Record statistics:
        self.__tx_busy_trace.record(self.sim.stime, 0)
        self.__service_time.append(self.sim.stime - self.__service_started_at)
        self.__service_started_at = None
        self.sim.logger.debug(f'IFS end, ready to transmit', src=self)

    def handle_rx_end(self, frame):
        if 'up' in self.connections:
            self.connections['up'].send(frame.packet)
        self.__rx_frame = None
        self.__num_received_frames += 1
        self.__num_received_bits += frame.size
        self.__rx_busy_trace.record(self.sim.stime, 0)
        self.sim.logger.debug(f'finish receiving frame', src=self)

    def __str__(self):
        prefix = f'{self.parent}.' if self.parent else ''
        return f'{prefix}RxTx'
예제 #11
0
class Receiver(Model):
    """Module simulating MAC (+PHY) DCF receiver.

    Connections:
    - `'radio'`:
    - `'channel'`:
    - `'transmitter'`
    - `'up'`:
    """
    class State(Enum):
        IDLE = 0
        RX = 1
        TX1 = 2
        TX2 = 3
        COLLIDED = 4
        WAIT_SEND_ACK = 5
        SEND_ACK = 6

    def __init__(
        self,
        sim,
        address=None,
        sifs=None,
        phy_header_size=None,
        ack_size=None,
    ):
        super().__init__(sim)

        # Properties:
        self.__address = address
        self.__sifs = sifs if sifs is not None else sim.params.sifs
        self.__phy_header_size = (phy_header_size if phy_header_size
                                  is not None else sim.params.phy_header_size)
        self.__ack_size = (ack_size
                           if ack_size is not None else sim.params.ack_size)

        # State variables:
        self.__state = Receiver.State.IDLE
        self.__rxbuf = set()
        self.__cur_tx_pdu = None

        # Statistics:
        self.__num_collisions = 0
        self.__num_received = 0
        self.__busy_trace = Trace()
        self.__busy_trace.record(sim.stime, 0)

    @property
    def state(self):
        return self.__state

    @state.setter
    def state(self, state):
        if state != self.__state:
            if self.__state == Receiver.State.IDLE:
                self.__busy_trace.record(self.sim.stime, 1)
            elif state == Receiver.State.IDLE:
                self.__busy_trace.record(self.sim.stime, 0)

            self.sim.logger.debug(f'{self.__state.name} -> {state.name}',
                                  src=self)
            if state is Receiver.State.COLLIDED:
                self.__num_collisions += 1
            self.__state = state

    @property
    def address(self):
        return self.__address

    @address.setter
    def address(self, address):
        self.__address = address

    @property
    def sifs(self):
        return self.__sifs

    @property
    def phy_header_size(self):
        return self.__phy_header_size

    @property
    def ack_size(self):
        return self.__ack_size

    @property
    def num_received(self):
        return self.__num_received

    @property
    def num_collisions(self):
        return self.__num_collisions

    @property
    def collision_ratio(self):
        num_ops = self.__num_received + self.__num_collisions
        if num_ops > 0:
            return self.__num_collisions / num_ops
        return 0

    @property
    def busy_trace(self):
        return self.__busy_trace

    @property
    def radio(self):
        return self.connections['radio'].module

    @property
    def channel(self):
        return self.connections['channel'].module

    @property
    def transmitter(self):
        return self.connections['transmitter'].module

    @property
    def up(self):
        return self.connections['up'].module

    @property
    def collision_probability(self):
        if self.num_received > 0 or self.num_collisions > 0:
            return self.num_collisions / (self.num_collisions +
                                          self.num_received)
        return 0

    def start_receive(self, pdu):
        if pdu in self.__rxbuf:
            self.sim.logger.error(
                f"PDU {pdu} is already in the buffer:\n{self.__rxbuf}",
                src=self)
            raise RuntimeError(f'PDU is already in the buffer, PDU={pdu}')

        if self.state is Receiver.State.IDLE and not self.__rxbuf:
            self.state = Receiver.State.RX
            self.channel.set_busy()

        elif (self.state is Receiver.State.RX
              or (self.state is Receiver.State.IDLE and self.__rxbuf)):
            self.state = Receiver.State.COLLIDED

        self.__rxbuf.add(pdu)

        # In all other states (e.g. TX2, WAIT_SEND_ACK, SEND_ACK) we just add
        # the packet to RX buffer.

    def finish_receive(self, pdu):
        assert pdu in self.__rxbuf
        self.__rxbuf.remove(pdu)

        if self.state is Receiver.State.RX:
            assert not self.__rxbuf
            if pdu.receiver_address == self.address:
                if pdu.type is DataPDU.Type.DATA:
                    self.state = Receiver.State.WAIT_SEND_ACK
                    self.__cur_tx_pdu = pdu
                    self.sim.schedule(self.sifs, self.handle_timeout)
                elif pdu.type is DataPDU.Type.ACK:
                    self.transmitter.acknowledged()
                    self.state = Receiver.State.IDLE
                    self.channel.set_ready()
                else:
                    raise RuntimeError(f'unsupported packet type {pdu.type}')
            else:
                self.state = Receiver.State.IDLE
                self.channel.set_ready()

        elif self.state is Receiver.State.COLLIDED:
            if not self.__rxbuf:
                self.state = Receiver.State.IDLE
                self.channel.set_ready()
            # Otherwise stay in COLLIDED state

        # In all other states (e.g. IDLE, TX1, TX2, ...) we just purge the
        # packet from the RX buffer.

    def start_transmit(self):
        if self.state is Receiver.State.IDLE:
            self.state = Receiver.State.TX1

        elif self.state in (Receiver.State.RX, Receiver.State.COLLIDED):
            self.state = Receiver.State.TX2

        assert self.state is not self.state.WAIT_SEND_ACK

    def finish_transmit(self):
        if self.state is Receiver.State.TX1:
            if self.__rxbuf:
                self.channel.set_busy()
                self.state = Receiver.State.COLLIDED
            else:
                self.state = Receiver.State.IDLE

        elif self.state is Receiver.State.TX2:
            if self.__rxbuf:
                self.state = Receiver.State.COLLIDED
            else:
                self.channel.set_ready()
                self.state = Receiver.State.IDLE

        elif self.state is Receiver.State.SEND_ACK:
            payload = self.__cur_tx_pdu.packet
            self.connections['up'].send(payload)
            self.__num_received += 1
            self.__cur_tx_pdu = None
            self.channel.set_ready()
            self.state = Receiver.State.IDLE

    def handle_timeout(self):
        assert self.state is Receiver.State.WAIT_SEND_ACK
        ack = AckPDU(
            header_size=self.phy_header_size,
            ack_size=self.ack_size,
            sender_address=self.address,
            receiver_address=self.__cur_tx_pdu.sender_address,
        )
        self.state = Receiver.State.SEND_ACK
        self.radio.transmit(ack)

    def __str__(self):
        prefix = f'{self.parent}.' if self.parent else ''
        return f'{prefix}receiver'
예제 #12
0
class Transmitter(Model):
    """Models transmitter module at MAC layer.

    Connections:
    - `'channel'`: mandatory, to `Channel` instance;
    - `'radio'`: mandatory, to `Radio` module
    - `'queue'`: optional, to `Queue` module
    """
    class State(Enum):
        IDLE = 0
        BUSY = 1
        BACKOFF = 2
        TX = 3
        WAIT_ACK = 4

    def __init__(
        self,
        sim,
        address=None,
        phy_header_size=None,
        mac_header_size=None,
        ack_size=None,
        bitrate=None,
        preamble=None,
        max_propagation=0,
    ):
        super().__init__(sim)

        # Properties:
        self.__address = address
        self.__phy_header_size = (phy_header_size if phy_header_size
                                  is not None else sim.params.phy_header_size)
        self.__mac_header_size = (mac_header_size if mac_header_size
                                  is not None else sim.params.mac_header_size)
        self.__bitrate = bitrate if bitrate is not None else sim.params.bitrate
        self.__preamble = (preamble
                           if preamble is not None else sim.params.preamble)
        self.__max_propagation = max_propagation
        self.__ack_size = (ack_size
                           if ack_size is not None else sim.params.ack_size)

        # State variables:
        self.timeout = None
        self.cw = 65536
        self.backoff = -1
        self.num_retries = None
        self.pdu = None
        self.__state = Transmitter.State.IDLE
        self.__seqn = 0

        # Statistics:
        self.backoff_vector = Statistic()
        self.__start_service_time = None
        self.service_time = Statistic()
        self.num_sent = 0
        self.num_retries_vector = Statistic()
        self.__busy_trace = Trace()
        self.__busy_trace.record(sim.stime, 0)

        # Initialize:
        sim.schedule(0, self.start)

    @property
    def state(self):
        return self.__state

    @state.setter
    def state(self, state):
        if self.state != state:
            self.sim.logger.debug(f'{self.state.name} -> {state.name}',
                                  src=self)
        self.__state = state

    @property
    def address(self):
        return self.__address

    @address.setter
    def address(self, address):
        self.__address = address

    @property
    def phy_header_size(self):
        return self.__phy_header_size

    @property
    def mac_header_size(self):
        return self.__mac_header_size

    @property
    def ack_size(self):
        return self.__ack_size

    @property
    def bitrate(self):
        return self.__bitrate

    @property
    def preamble(self):
        return self.__preamble

    @property
    def max_propagation(self):
        return self.__max_propagation

    @property
    def busy_trace(self):
        return self.__busy_trace

    @property
    def channel(self):
        return self.connections['channel'].module

    @property
    def radio(self):
        return self.connections['radio'].module

    @property
    def queue(self):
        return self.connections['queue'].module

    def start(self):
        self.queue.get_next(self)

    def handle_message(self, packet, connection=None, sender=None):
        if connection.name == 'queue':
            assert self.state == Transmitter.State.IDLE

            self.cw = self.sim.params.cwmin
            self.backoff = randint(0, self.cw)
            self.num_retries = 1

            #
            # Create the PDU:
            #
            self.pdu = DataPDU(packet,
                               seqn=self.__seqn,
                               header_size=self.phy_header_size +
                               self.mac_header_size,
                               sender_address=self.address,
                               receiver_address=packet.receiver_address)
            self.__seqn += 1

            self.__start_service_time = self.sim.stime
            self.backoff_vector.append(self.backoff)
            self.__busy_trace.record(self.sim.stime, 1)

            self.sim.logger.debug(
                f'backoff={self.backoff}; CW={self.cw},NR={self.num_retries}',
                src=self)

            if self.channel.is_busy:
                self.state = Transmitter.State.BUSY
            else:
                self.state = Transmitter.State.BACKOFF
                self.timeout = self.sim.schedule(self.sim.params.difs,
                                                 self.handle_backoff_timeout)
        else:
            raise RuntimeError(
                f'unexpected handle_message({packet}, connection={connection}, '
                f'sender={sender}) call')

    def channel_ready(self):
        if self.state == Transmitter.State.BUSY:
            self.timeout = self.sim.schedule(self.sim.params.difs,
                                             self.handle_backoff_timeout)
            self.state = Transmitter.State.BACKOFF

    def channel_busy(self):
        if self.state == Transmitter.State.BACKOFF:
            self.sim.cancel(self.timeout)
            self.state = Transmitter.State.BUSY

    def finish_transmit(self):
        if self.state == Transmitter.State.TX:
            self.sim.logger.debug('TX finished', src=self)
            ack_duration = (
                (self.ack_size + self.mac_header_size + self.phy_header_size) /
                self.bitrate + self.preamble + 6 * self.max_propagation)
            self.timeout = self.sim.schedule(
                self.sim.params.sifs + ack_duration, self.handle_ack_timeout)
            self.state = Transmitter.State.WAIT_ACK

    def acknowledged(self):
        if self.state == Transmitter.State.WAIT_ACK:
            self.sim.logger.debug('received ACK', src=self)

            self.sim.cancel(self.timeout)
            self.pdu = None
            self.state = Transmitter.State.IDLE

            self.num_sent += 1
            self.service_time.append(self.sim.stime -
                                     self.__start_service_time)
            self.__start_service_time = None
            self.num_retries_vector.append(self.num_retries)
            self.num_retries = None
            self.__busy_trace.record(self.sim.stime, 0)

            #
            # IMPORTANT: Informing the queue that we can handle the next packet
            #
            self.queue.get_next(self)

    def handle_ack_timeout(self):
        assert self.state == Transmitter.State.WAIT_ACK
        self.num_retries += 1
        self.cw = min(2 * self.cw, self.sim.params.cwmax)
        self.backoff = randint(0, self.cw)

        self.backoff_vector.append(self.backoff)

        self.sim.logger.debug(
            f'backoff={self.backoff}; CW={self.cw}, NR={self.num_retries})',
            src=self)

        if self.channel.is_busy:
            self.state = Transmitter.State.BUSY
        else:
            self.state = Transmitter.State.BACKOFF
            self.timeout = self.sim.schedule(self.sim.params.difs,
                                             self.handle_backoff_timeout)

    def handle_backoff_timeout(self):
        if self.backoff == 0:
            self.state = Transmitter.State.TX
            self.sim.logger.debug(f'transmitting {self.pdu}', src=self)
            self.radio.transmit(self.pdu)
        else:
            assert self.backoff > 0
            self.backoff -= 1
            self.timeout = self.sim.schedule(self.sim.params.slot,
                                             self.handle_backoff_timeout)
            self.sim.logger.debug(f'backoff := {self.backoff}', src=self)

    def __str__(self):
        prefix = f'{self.parent}.' if self.parent else ''
        return f'{prefix}transmitter'