From 1ccd2eaa85fb4b49c04dcc7418fd6a56912d6120 Mon Sep 17 00:00:00 2001 From: ezdac Date: Mon, 12 Nov 2018 12:54:26 +0100 Subject: [PATCH 1/3] Improve Arduino communication, fix main loop, enhance tasks --- app.py | 106 ++++--- arduino/track_control/track_control.ino | 130 +++++++-- tests/conftest.py | 11 +- tests/test_track_control.py | 55 +++- tests/test_train_app.py | 79 ++++- tests/utils/fake_raiden.py | 1 - tests/utils/test_fake_raiden.py | 34 +++ track_control.py | 364 +++++++++++++++++++----- 8 files changed, 624 insertions(+), 156 deletions(-) create mode 100644 tests/utils/test_fake_raiden.py diff --git a/app.py b/app.py index ced1ccc..595a10f 100644 --- a/app.py +++ b/app.py @@ -1,35 +1,73 @@ import asyncio import random import sys -from typing import List +from typing import List, Optional import code128 -from const import SENDER_ADDRESS, TOKEN_ADDRESS, BAR_CODE_FILE_PATH, RECEIVER_LIST +from const import BAR_CODE_FILE_PATH, RECEIVER_LIST from deployment import start_raiden_nodes from raiden import RaidenNode, RaidenNodeMock -from track_control import TrackControl, ArduinoSerial, MockSerial +from track_control import ( + TrackControl, + ArduinoSerial, + ArduinoTrackControl, + MockArduinoTrackControl, + BarrierEventTaskFactory, + BarrierLoopTaskRunner +) from network import NetworkTopology import logging -from utils import wait_for_event log = logging.getLogger() +ADDRESS_MAP = {address: index for index, address in enumerate(RECEIVER_LIST)} + + +class BarcodeHandler(): + + _address_map = ADDRESS_MAP + + def save_barcode(self, address, nonce): + address, nonce = self._process_args(address, nonce) + self._save_barcode(address, nonce) + + def _process_args(self, address, nonce): + address = self._address_map[address] + return address, nonce + + def _save_barcode(self, address, nonce): + barcode = code128.image("(" + str(address) + "," + str(nonce) + ")") + factor = 4 + barcode = barcode.resize((int(barcode.width * factor), int(barcode.height * factor))) + barcode.save(str(BAR_CODE_FILE_PATH)) + class TrainApp: def __init__(self, track_control: TrackControl, raiden_nodes: List[RaidenNode], - network_topology: NetworkTopology): + network_topology: NetworkTopology, + barcode_handler: Optional[BarcodeHandler]=None): self.track_control = track_control self.raiden_nodes = raiden_nodes self.network_topology = network_topology + self.barcode_handler = barcode_handler self._track_loop = None self._current_provider = None self._provider_nonces = {provider.address: 0 for provider in self.raiden_nodes} + self._barrier_ltr = None + self._barrier_etf = None def start(self): - self.track_control.start() + """ + NOTE: it's necessary that the asyncio related instantiations are done at runtime, + because we need a running loop! + :return: + """ + self._barrier_ltr = BarrierLoopTaskRunner(self.track_control) + self._barrier_etf = BarrierEventTaskFactory(self.track_control) + self._barrier_ltr.start() self._track_loop = asyncio.create_task(self.run()) # FIXME make awaitable so that errors can raise @@ -37,37 +75,30 @@ def start(self): def stop(self): try: self._track_loop.cancel() - # TODO implement stop - # self.track_control.stop() + self._barrier_ltr.stop() except asyncio.CancelledError: pass async def run(self): + # TODO make sure that every neccessary task is running: + # (barrier_etf, barrier_ltr instantiated, etc) log.debug("Track loop started") self.track_control.power_on() while True: # Pick a random receiver - self._choose_and_set_next_provider() + self._set_next_provider() provider = self._current_provider current_nonce = self.current_nonce - # Generate barcode with current provider and nonce - self.create_new_barcode( - provider=RECEIVER_LIST.index(self.current_provider_address), - nonce=current_nonce - ) - payment_received_task = asyncio.create_task( provider.ensure_payment_received( sender_address=self.network_topology.sender_address, token_address=self.network_topology.token_address, nonce=current_nonce, poll_interval=0.05) ) - barrier_event_task = asyncio.create_task( - wait_for_event(self.track_control.barrier_event)) + barrier_event_task = self._barrier_etf.create_await_event_task() log.info('Waiting for payment to provider={}, nonce={}'.format(provider.address, current_nonce)) - # await both awaitables but return when one of them is finished first done, pending = await asyncio.wait([payment_received_task, barrier_event_task], return_when=asyncio.FIRST_COMPLETED) @@ -76,41 +107,45 @@ async def run(self): if payment_received_task.result() is True: payment_successful = True else: + assert barrier_event_task in done + assert payment_received_task in pending # cancel the payment received task for task in pending: task.cancel() if payment_successful is True: log.info("Payment received") - self._increment_nonce_for_current_provider() assert barrier_event_task in pending await barrier_event_task + # increment the nonce after the barrier was triggered + self._increment_nonce_for_current_provider() else: log.info("Payment not received before next barrier trigger") self.track_control.power_off() payment_received_task = asyncio.create_task( - provider.ensure_payment_received(sender_address=SENDER_ADDRESS, - token_address=TOKEN_ADDRESS, - nonce=current_nonce, + provider.ensure_payment_received(sender_address=self.network_topology.sender_address, + token_address=self.network_topology.token_address, + nonce=self.current_nonce, poll_interval=0.05) ) await payment_received_task if payment_received_task.result() is True: + self._increment_nonce_for_current_provider() self.track_control.power_on() + log.info("Payment received, turning track power on again") else: # this shouldn't happen # FIXME remove assert in production code assert False - def _choose_and_set_next_provider(self): - self._current_provider = random.choice(self.raiden_nodes) + def _on_new_provider(self): + if self.barcode_handler is not None: + self.barcode_handler.save_barcode(self.current_provider_address, self.current_nonce) - def create_new_barcode(self, provider, nonce): - barcode = code128.image("(" + str(provider) + "," + str(nonce) + ")") - factor = 4 - barcode = barcode.resize((int(barcode.width * factor), int(barcode.height * factor))) - barcode.save(str(BAR_CODE_FILE_PATH)) + def _set_next_provider(self): + self._current_provider = random.choice(self.raiden_nodes) + self._on_new_provider() @property def current_provider_address(self): @@ -129,9 +164,13 @@ def build_app(cls, network: NetworkTopology, mock_arduino=False, mock_raiden=Fal raiden_node_cls = RaidenNode if mock_arduino: log.debug('Mocking Arduino serial') - serial_track_power = MockSerial() + arduino_track_control = MockArduinoTrackControl() else: - serial_track_power = ArduinoSerial(port='/dev/ttyACM0', baudrate=9600, timeout=.1) + arduino_serial = ArduinoSerial(port='/dev/ttyACM0', baudrate=9600, timeout=.1) + # arduino_serial = ArduinoSerial(port='/dev/cu.usbmodem1421', baudrate=9600, timeout=.1) + arduino_track_control = ArduinoTrackControl(arduino_serial) + arduino_track_control.connect() + if mock_raiden: raiden_node_cls = RaidenNodeMock log.debug('Mocking RaidenNode') @@ -147,6 +186,7 @@ def build_app(cls, network: NetworkTopology, mock_arduino=False, mock_raiden=Fal 'Not all raiden nodes could get started, check the log files for more info. Shutting down') sys.exit() raiden_nodes = list(raiden_nodes_dict.values()) - track_control = TrackControl(serial_track_power) + track_control = TrackControl(arduino_track_control) - return cls(track_control, raiden_nodes, network) + barcode_handler = BarcodeHandler() + return cls(track_control, raiden_nodes, network, barcode_handler) diff --git a/arduino/track_control/track_control.ino b/arduino/track_control/track_control.ino index 7825657..989fbd9 100644 --- a/arduino/track_control/track_control.ino +++ b/arduino/track_control/track_control.ino @@ -7,6 +7,24 @@ float distance; bool measuring; int value = 9; +// 0: Power is Off, +// 1: Power is On +int powerSensor = 0; + +// 0: not measuring, +// 1: object is not close, +// 2: object is close +int barrierSensor = 0; + +// 0: request Sensor Data/'ACK Signal' +// 1: power Track off +// 2: power Track on +// 3: turn barrier measuring off +// 4: turn barrier measuring on +int inByte = 0; + +int maxTriggerDistance = 20; + void setup() { pinMode(3, OUTPUT); //make the pin (3) as output pinMode(13, OUTPUT); //make the LED pin (13) as output @@ -14,42 +32,110 @@ void setup() { pinMode(echoPin, INPUT); // Sets the echoPin as an Input Serial.begin(9600); // Starts the serial communication measuring = false; + establishContact(); } void loop() { int data; - if (Serial.available()> 0){ - data = Serial.read(); - } + if (Serial.available() > 0){ + inByte = Serial.read(); - if (data == 1) // Turn Train power on - { - digitalWrite(3, HIGH); - Serial.println(digitalRead(3)); - } - if (data == 0) // Turn Train power off - { - digitalWrite(3, LOW); - Serial.println(digitalRead(3)); - } - - if (data == 2){ // Start measuiring - measuring = true; - } + switch (inByte) { + // send Sensor data + case 0: + updateSensorData(); + delayMicroseconds(10); + sendAck(); + sendSensorData(); + break; + // Turn Train power off + case 1: + turnPowerOff(); + sendAck(); + break; + // Turn Train power on + case 2: + turnPowerOn(); + sendAck(); + break; + case 3: + measuring = false; + sendAck(); + break; + case 4: + measuring = true; + sendAck(); + default: +// if no bytes are present, the client is not expecting computation from us +// TODO just continue loop + break; + } +} if (measuring == true){ int value = getAverage(); - if (value < 20){ - Serial.println(value); - measuring = false; + if (value < maxTriggerDistance){ + barrierSensor = 2; + } + else { + barrierSensor = 1; } } else { - delayMicroseconds(10); - } + barrierSensor = 0; + delayMicroseconds(10); + } } +void establishContact() { + while (Serial.available() <= 0) { + sendHandshake(); + delay(300); + } + + while (true) { + if (Serial.available() > 0){ + inByte = Serial.read(); +// we expect the ACK from the client + if (inByte == 0){ + sendAck(); + break; + } + } + } +} + +void sendSensorData() { + Serial.write(powerSensor); + Serial.write(barrierSensor); +} + +void sendAck() { + Serial.write('A'); +} + +void sendHandshake(){ + Serial.write('H'); +} + +void turnPowerOff() { + digitalWrite(3, LOW); +} + +void turnPowerOn() { + digitalWrite(3, HIGH); +} + +void updateSensorData() { + if (digitalRead(3) == HIGH) { + powerSensor = 1; + } else { + powerSensor = 0; + } +} + + int getDistance(){ // Clears the trigPin digitalWrite(trigPin, LOW); diff --git a/tests/conftest.py b/tests/conftest.py index 26af1f0..bb0aab2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from raiden import RaidenNodeMock, RaidenNode from tests.utils.fake_raiden import FakeRaiden -from track_control import TrackControl, MockSerial +from track_control import TrackControl, MockArduinoTrackControl from network import NetworkTopology from utils import random_address @@ -49,8 +49,13 @@ def raiden(address, api_endpoint, default_raiden_config_file): @pytest.fixture -def track_control(): - return TrackControl(track_power_serial=MockSerial()) +def arduino_track_control(): + return MockArduinoTrackControl() + + +@pytest.fixture +def track_control(arduino_track_control): + return TrackControl(arduino_track_control) @pytest.fixture diff --git a/tests/test_track_control.py b/tests/test_track_control.py index 0a2c5b6..663c2df 100644 --- a/tests/test_track_control.py +++ b/tests/test_track_control.py @@ -1,22 +1,57 @@ -import asyncio -from utils import wait_for_event import pytest +from track_control import BarrierEventTaskFactory, BarrierLoopTaskRunner, BarrierState, PowerState +from utils import context_switch +import asyncio + + +@pytest.fixture +async def barrier_loop_task_runner(track_control, arduino_track_control): + barrier_ltr = BarrierLoopTaskRunner(track_control) + barrier_ltr.start() + assert barrier_ltr.is_running() + yield barrier_ltr + barrier_ltr.stop() + + +@pytest.mark.asyncio +async def test_barrier_event_task(track_control): + barrier_etf = BarrierEventTaskFactory(track_control) + task = barrier_etf.create_await_event_task() + + await context_switch() + + assert len(track_control._barrier_events) == 1 + assert not task.done() + track_control.trigger_barrier() + + await context_switch() + + assert len(track_control._barrier_events) == 0 + assert task.done() + await task @pytest.mark.asyncio -async def test_wait_for_barrier_event(track_control): - wait_for_task = asyncio.create_task(wait_for_event(track_control.barrier_event)) - assert not wait_for_task.done() - await track_control._trigger_barrier() - await wait_for_task +async def test_barrier_loop(barrier_loop_task_runner, arduino_track_control, track_control): + event = asyncio.Event() + + track_control.register_barrier_event(event) + + await context_switch() + assert not event.is_set() + + arduino_track_control.mock_set_barrier_trigger() + await context_switch() + + await event.wait() + assert event.is_set() def test_power_on(track_control): track_control.power_on() - assert track_control.track_power_serial.is_high + assert track_control.is_powered def test_power_off(track_control): track_control.power_off() - assert not track_control.track_power_serial.is_high - + assert not track_control.is_powered diff --git a/tests/test_train_app.py b/tests/test_train_app.py index 550a8a6..fcde950 100644 --- a/tests/test_train_app.py +++ b/tests/test_train_app.py @@ -1,13 +1,12 @@ import pytest +import asyncio from app import TrainApp -import asyncio +from utils import context_switch # All test coroutines will be treated as marked. pytestmark = pytest.mark.asyncio -from utils import context_switch - @pytest.fixture def train_app(raiden, track_control, network_topology): @@ -28,31 +27,85 @@ async def test_query_for_started_fake_raiden(fake_raiden, raiden, api_endpoint, assert result is True -async def test_track_loop(fake_raiden, token_address, sender_address, train_app, raiden): +async def test_track_loop(fake_raiden, token_address, sender_address, train_app, raiden, track_control): # make a payment we're not waiting for fake_raiden.make_payment(token_address, sender_address, 123, 2) train_app.start() await context_switch() - assert train_app.track_control.is_powered + # test internals for now + assert train_app._barrier_ltr.is_running + assert train_app._barrier_etf is not None + assert not train_app._track_loop.done() - nonce = train_app.current_nonce - provider_address = train_app.current_provider_address + assert train_app.track_control == track_control + assert track_control.is_powered # we only have one provider, its RaidenNode instance and the corresponding FakeRaiden instance: - assert provider_address == fake_raiden.address == raiden.address + assert train_app.current_provider_address == raiden.address + assert train_app.current_nonce == 0 + + nonce = train_app.current_nonce # make the payment manually on the fake-raiden instance of the provider fake_raiden.make_payment(token_address, sender_address, nonce, 1) - await asyncio.sleep(1) - assert train_app.current_nonce == nonce + 1 + # sleep for longer than a "context_switch()", since we have a poll-interval set and we are doing real networking + await asyncio.sleep(0.1) + + # Payment should be succesful, track still powered + # the nonce is not yet increased since we are still waiting for the barrier to trigger + assert train_app.current_nonce == 0 + assert train_app.track_control.is_powered + + track_control.trigger_barrier() + + await context_switch() + + # The barrier was triggered, so now we incremented the nonce and are waiting for the next payment + # the provider address is still the same, since there are no other nodes to choose from + assert train_app.current_nonce == 1 + assert train_app.current_provider_address == raiden.address assert train_app.track_control.is_powered train_app.stop() -@pytest.mark.skip -async def test_track_loop_not_paid(): - pass +async def test_track_loop_barrier_triggered(fake_raiden, token_address, sender_address, train_app, raiden, track_control): + # make a payment we're not waiting for + fake_raiden.make_payment(token_address, sender_address, 123, 2) + + train_app.start() + await context_switch() + + assert train_app.track_control == track_control + assert track_control.is_powered + + nonce = train_app.current_nonce + provider_address = train_app.current_provider_address + + # we only have one provider, its RaidenNode instance: + assert provider_address == raiden.address + assert nonce == 0 + + track_control.trigger_barrier() + await context_switch() + + # we triggered the barrier before paying, so the nonce shouldn't be increased and + # the track should be powered off + assert train_app.current_nonce == nonce + assert not train_app.track_control.is_powered + + # now make the correct payment + fake_raiden.make_payment(token_address, sender_address, nonce, 1) + + # sleep for longer than a "context_switch()", since we have a poll-interval set and we are doing real networking + await asyncio.sleep(1) + # The payment was made in the end so now we incremented the nonce and are waiting for the next payment + # the provider address is still the same, since there are no other nodes to choose from + assert train_app.current_nonce == nonce + 1 + assert train_app.current_provider_address == raiden.address + assert train_app.track_control.is_powered + + train_app.stop() diff --git a/tests/utils/fake_raiden.py b/tests/utils/fake_raiden.py index 324d676..e0aecaa 100644 --- a/tests/utils/fake_raiden.py +++ b/tests/utils/fake_raiden.py @@ -47,7 +47,6 @@ async def on_payment_info(self, request): token_address = request.match_info['token_address'] partner_address = request.match_info['partner_address'] payments = self.payments.get((token_address, partner_address)) - print(payments, "payments") events = [] if payments is not None: for nonce, amount in payments.items(): diff --git a/tests/utils/test_fake_raiden.py b/tests/utils/test_fake_raiden.py new file mode 100644 index 0000000..297d87b --- /dev/null +++ b/tests/utils/test_fake_raiden.py @@ -0,0 +1,34 @@ +import pytest +import aiohttp + +# All test coroutines will be treated as marked. +pytestmark = pytest.mark.asyncio + + +# HACK +# FIXME, when using the same endpoint as in other tests, the aiohttp fake_raiden cannot +# double bind the port - probably due to a missing teardown in other tests +@pytest.fixture +def api_endpoint(): + return 'http://127.0.0.1:5005' + + +async def test_make_payment(fake_raiden, token_address, sender_address, api_endpoint): + fake_raiden.make_payment(token_address, sender_address, 123, 2) + assert len(fake_raiden.payments[(token_address, sender_address)]) == 1 + + expected_result = [{"event": "EventPaymentReceivedSuccess", "amount": 2, "identifier": 123}] + + url = api_endpoint + "/api/1/payments/{}/{}".format(token_address, sender_address) + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + data = await response.json() + assert response.status == 200 + assert data == expected_result + + +async def test_make_succesive_payments(fake_raiden, token_address, sender_address): + fake_raiden.make_payment(token_address, sender_address, 123, 2) + fake_raiden.make_payment(token_address, sender_address, 0, 1) + assert len(fake_raiden.payments[(token_address, sender_address)]) == 2 + diff --git a/track_control.py b/track_control.py index 21dede0..0703bd4 100644 --- a/track_control.py +++ b/track_control.py @@ -2,123 +2,339 @@ import time import asyncio import logging +from typing import List, Optional -from utils import context_switch +from enum import Enum log = logging.getLogger() +# // 0: request Sensor Data/'ACK Signal' +# // 1: power Track off +# // 2: power Track on +# // 3: turn barrier measuring off +# // 4: turn barrier measuring on +class OutMessage(Enum): + ACK = 0 + REQUEST_SENSOR = 1 + POWER_OFF = 2 + POWER_ON = 3 + DISTANCE_MEASURE_OFF = 4 + DISTANCE_MEASURE_ON = 5 + + @classmethod + def encode(cls, message: 'OutMessage'): + encoded = None + # ACK is not implemented at the moment, + # so for now this is equivalent to a sensor request + if message is cls.ACK: + encoded = bytes([0]) + elif message is cls.REQUEST_SENSOR: + encoded = bytes([0]) + elif message is cls.POWER_OFF: + encoded = bytes([1]) + elif message is cls.POWER_ON: + encoded = bytes([2]) + elif message is cls.DISTANCE_MEASURE_OFF: + encoded = bytes([3]) + elif message is cls.DISTANCE_MEASURE_ON: + encoded = bytes([4]) + else: + ValueError('Message not known') + return encoded + + +# // 0: not measuring, +# // 1: object is not close, +# // 2: object is close +class BarrierState(Enum): + NOT_MEASURING = 0 + OBJECT_NOT_CLOSE = 1 + OBJECT_CLOSE = 2 + + @staticmethod + def decode(byte): + decoded = None + if byte == bytes([0]): + decoded = BarrierState.NOT_MEASURING + elif byte == bytes([1]): + decoded = BarrierState.OBJECT_NOT_CLOSE + elif byte == bytes([2]): + decoded = BarrierState.OBJECT_CLOSE + else: + print('can;t decode') + raise ValueError('Can\'t decode data') + return decoded + + +# // 0: Power is Off, +# // 1: Power is On +class PowerState(Enum): + POWER_OFF = 0 + POWER_ON = 1 + + @staticmethod + def decode(byte): + decoded = None + if byte == bytes([0]): + decoded = PowerState.POWER_OFF + elif byte == bytes([1]): + decoded = PowerState.POWER_ON + else: + print('can;t decode') + raise ValueError('Can\'t decode data') + return decoded + + class ArduinoSerial: def __init__(self, port, baudrate, timeout): - self._serial = None - self._is_high = False self._serial = serial.Serial(port, baudrate, timeout=timeout) # open serial port - - # TODO check for initialisation instead of waiting? time.sleep(2) - def set_high(self): - self._serial.write(bytes([1])) # Sets Arduino pin 3 HIGH - self._is_high = True + def do_handshake(self): + self._wait_for_read(b'H') + send_value = OutMessage.encode(OutMessage.ACK) + self._serial.write(send_value) + # wait for ACK from Arduino + self._wait_for_read(b'A', allowed_prepending=[b'H']) + log.debug('Handshake with Arduino succesful') + return True + + def send_message(self, message): + send_value = OutMessage.encode(message) + log.debug(f'Send message to Arduino: {message}<{send_value}>') + self._serial.write(send_value) + # wait for ACK from Arduino + self._wait_for_read(b'A') + log.debug('Got ACK from Arduino') + + def read_bytes(self, nof_bytes): + values = [] + for _ in range(nof_bytes): + val = self._serial.read() + if val is b'': + raise ValueError('No bytes to read') + values.append(val) + log.debug(f"Read bytes from Arduino: {values}") + return tuple(values) + + def _wait_for_read(self, expected, max_tries: Optional[int]=None, allowed_prepending: Optional[List]=None): + val = self._serial.read() + tried = 0 + if allowed_prepending is None: + allowed_prepending = [] + allowed_prepending_signals = {b'', *allowed_prepending} + + while val in allowed_prepending_signals: + val = self._serial.read() + if max_tries is not None: + tried += 1 + if tried > max_tries: + break + time.sleep(0.05) + + was_expected = bool(val == expected) + if was_expected is False: + raise ValueError(f'Unexpected serial read from Arduino! Expected {expected}, got {val}.') + - def set_low(self): - self._serial.write(bytes([0])) # Sets Arduino pin 3 LOW - self._is_high = False +class ArduinoTrackControl: - def trigger_measure(self): - self._serial.write(bytes([2])) # Triggers a series of distance measurements - self._serial.flush() # Make sure arduino reads the above command + def __init__(self, serial: ArduinoSerial): + self._serial = serial + self._barrier_state = None + self._power_state = None - # TODO check if you can poll immediately from arduino, otherwise use internal flag @property - def is_high(self): - return self._is_high + def barrier_state(self): + return self._barrier_state @property - def status(self): - return self._serial.readline().decode( - 'utf-8').strip() + def power_state(self): + return self._power_state + def connect(self): + return self._serial.do_handshake() -class MockSerial: - # TODO implement the trigger behaviour also in mock class + def power_on(self): + self._serial.send_message(OutMessage.POWER_ON) - def __init__(self): - self._bit_set = False + def power_off(self): + self._serial.send_message(OutMessage.POWER_OFF) - def set_high(self): - self._bit_set = True + def start_measure(self): + self._serial.send_message(OutMessage.DISTANCE_MEASURE_ON) - def set_low(self): - self._bit_set = False + def stop_measure(self): + self._serial.send_message(OutMessage.DISTANCE_MEASURE_OFF) - def trigger_measure(self): - pass + def update_sensor_data(self): + self._serial.send_message(OutMessage.REQUEST_SENSOR) + power_byte, barrier_byte = self._serial.read_bytes(2) + self._power_state = PowerState.decode(power_byte) + self._barrier_state = BarrierState.decode(barrier_byte) + + +class MockArduinoTrackControl: + + def __init__(self): + self._barrier_state = None + self._power_state = None @property - def is_high(self): - return self._bit_set + def barrier_state(self): + return self._barrier_state @property - def status(self): - return '1' if self._bit_set is True else '0' + def power_state(self): + return self._power_state + def connect(self): + return True -class TrackControl: + def power_on(self): + self._power_state = PowerState.POWER_ON + + def power_off(self): + self._power_state = PowerState.POWER_OFF + + def start_measure(self): + # start with not triggering + pass + + def stop_measure(self): + self._barrier_state = BarrierState.NOT_MEASURING + + def update_sensor_data(self): + # we don't communicate with arduino, so do nothing here + pass + + def mock_set_barrier_trigger(self): + """ + only for setting the barrier state manually in the mock class, + """ + self._barrier_state = BarrierState.OBJECT_CLOSE + + def mock_unset_barrier_trigger(self): + """ + only for unsetting the barrier state manually in the mock class + """ + self._barrier_state = BarrierState.OBJECT_NOT_CLOSE + + +class EventTaskFactory: + + async def await_event(self): + raise NotImplementedError + + def create_await_event_task(self): + return asyncio.create_task(self.await_event()) + + +class LoopTaskRunner: + + def is_running(self): + raise NotImplementedError + + def start(self): + raise NotImplementedError + + def stop(self): + raise NotImplementedError + + +class BarrierEventTaskFactory(EventTaskFactory): + + def __init__(self, track_control: 'TrackControl'): + self._track_control = track_control - def __init__(self, track_power_serial): - self.track_power_serial = track_power_serial - self._barrier_event = None + async def await_event(self): + event = asyncio.Event() + self._track_control.register_barrier_event(event) + await event.wait() + self._track_control.unregister_barrier_event(event) + + +class BarrierLoopTaskRunner(LoopTaskRunner): + + def __init__(self, track_control: 'TrackControl'): + self._track_control = track_control self._barrier_task = None + def is_running(self): + return not self._barrier_task.done() + + def start(self): + self._barrier_task = asyncio.create_task(self._track_control.run_barrier_loop()) + + def stop(self): + # TODO implement + pass + + +class TrackControl: + + def __init__(self, arduino_track_control): + # the ArduinoTrackControl is assumed to be connected already! + self.arduino_track_control = arduino_track_control + self._barrier_events = set() + # sleep for x seconds when barrier was triggered + self._barrier_sleep_time = 4. + @property def is_powered(self): - return self.track_power_serial.is_high + return bool(self.arduino_track_control.power_state is PowerState.POWER_ON) @property - def barrier_event(self): - # can be awaited in a task like this: - # `await track_control.barrier_event.wait()` - if self._barrier_event is None: - # make sure the event is instantiated when there is an event loop running - assert asyncio.get_running_loop() - self._barrier_event = asyncio.Event() - return self._barrier_event + def _any_event_is_waiting(self): + for event in self._barrier_events: + if not event.is_set(): + return True + return False - def start(self): - # TODO maybe this can run in a different executor/thread - self._barrier_task = asyncio.create_task(self.run_barrier_loop()) + def register_barrier_event(self, event: asyncio.Event): + if not event.is_set(): + self._barrier_events.add(event) + else: + raise Exception('Tried to register an Event that is aleady set') + + def unregister_barrier_event(self, event: asyncio.Event): + self._barrier_events.remove(event) + + def remove_all_barrier_events(self): + self._barrier_events = set() async def run_barrier_loop(self): while True: - self.track_power_serial.trigger_measure() - while True: - data = self.track_power_serial.status - try: - data = float(data) - # if no float is provided, this assumes the distance sensor didn't trigger - except ValueError: - data = 100 - if data < 20.: # If something passes closer than 20cm - await self._trigger_barrier() - await context_switch() - - async def _trigger_barrier(self): - # set event so that all tasks waiting on it will continue - self._barrier_event.set() - # this context switch needs to happen, so that all tasks that are waiting - # on the event can advance, before the event is reset again - await context_switch() - # reset immediately so that tasks can wait for the next trigger - self._barrier_event.clear() + if self._any_event_is_waiting: + self.arduino_track_control.start_measure() + while True: + self.arduino_track_control.update_sensor_data() + barrier_state = self.arduino_track_control.barrier_state + if barrier_state is BarrierState.OBJECT_CLOSE: + self.trigger_barrier() + self.arduino_track_control.stop_measure() + await asyncio.sleep(self._barrier_sleep_time) + else: + await asyncio.sleep(0.1) + # break out of loop to trigger measure again + break + else: + await asyncio.sleep(0.1) + + def trigger_barrier(self): + for event in self._barrier_events: + event.set() def power_off(self): - self.track_power_serial.set_low() - log.debug("Serial read for track power is {}".format(self.track_power_serial.status)) + self.arduino_track_control.power_off() + self.arduino_track_control.update_sensor_data() + # assert not self.arduino_track_control.is_powered() log.info("Turned power for train off") def power_on(self): - self.track_power_serial.set_high() - log.debug("Serial read for track power is {}".format(self.track_power_serial.status)) + self.arduino_track_control.power_on() + self.arduino_track_control.update_sensor_data() + # assert self.arduino_track_control.is_powered() log.info("Turned power for train on") From 2c26c3364f47bc105e83c88dbe64f8f2ab3bd680 Mon Sep 17 00:00:00 2001 From: ezdac Date: Mon, 12 Nov 2018 15:37:22 +0100 Subject: [PATCH 2/3] Delete unneccessary TODO and code --- arduino/track_control/track_control.ino | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/arduino/track_control/track_control.ino b/arduino/track_control/track_control.ino index 989fbd9..365bb56 100644 --- a/arduino/track_control/track_control.ino +++ b/arduino/track_control/track_control.ino @@ -48,28 +48,22 @@ void loop() { delayMicroseconds(10); sendAck(); sendSensorData(); - break; // Turn Train power off case 1: turnPowerOff(); sendAck(); - break; // Turn Train power on case 2: turnPowerOn(); sendAck(); - break; case 3: measuring = false; sendAck(); - break; case 4: measuring = true; sendAck(); default: -// if no bytes are present, the client is not expecting computation from us -// TODO just continue loop - break; + break; } } From 98214c18e2994470a29a033723bdfc1e4a2b33ca Mon Sep 17 00:00:00 2001 From: ezdac Date: Mon, 12 Nov 2018 19:30:30 +0100 Subject: [PATCH 3/3] Implement keepalives for arduino reset --- app.py | 37 +++++++++++++----------- arduino/track_control/track_control.ino | 16 +++++++++++ raiden.py | 3 +- tests/test_train_app.py | 1 - track_control.py | 38 ++++++++++++++++++++++++- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/app.py b/app.py index 595a10f..8dad1cc 100644 --- a/app.py +++ b/app.py @@ -14,7 +14,8 @@ ArduinoTrackControl, MockArduinoTrackControl, BarrierEventTaskFactory, - BarrierLoopTaskRunner + BarrierLoopTaskRunner, + KeepAliveTaskRunner ) from network import NetworkTopology import logging @@ -28,6 +29,7 @@ class BarcodeHandler(): _address_map = ADDRESS_MAP + _barcode_file_path = str(BAR_CODE_FILE_PATH) def save_barcode(self, address, nonce): address, nonce = self._process_args(address, nonce) @@ -41,7 +43,8 @@ def _save_barcode(self, address, nonce): barcode = code128.image("(" + str(address) + "," + str(nonce) + ")") factor = 4 barcode = barcode.resize((int(barcode.width * factor), int(barcode.height * factor))) - barcode.save(str(BAR_CODE_FILE_PATH)) + barcode.save(self._barcode_file_path) + log.debug(f'Written current barcode to disk: {self._barcode_file_path}') class TrainApp: @@ -56,18 +59,17 @@ def __init__(self, track_control: TrackControl, raiden_nodes: List[RaidenNode], self._track_loop = None self._current_provider = None self._provider_nonces = {provider.address: 0 for provider in self.raiden_nodes} - self._barrier_ltr = None - self._barrier_etf = None - def start(self): - """ - NOTE: it's necessary that the asyncio related instantiations are done at runtime, - because we need a running loop! - :return: - """ - self._barrier_ltr = BarrierLoopTaskRunner(self.track_control) + self._task_runners = [ + KeepAliveTaskRunner(self.track_control), + BarrierLoopTaskRunner(self.track_control) + ] self._barrier_etf = BarrierEventTaskFactory(self.track_control) - self._barrier_ltr.start() + + def start(self): + self.track_control.connect() + for task in self._task_runners: + task.start() self._track_loop = asyncio.create_task(self.run()) # FIXME make awaitable so that errors can raise @@ -75,7 +77,8 @@ def start(self): def stop(self): try: self._track_loop.cancel() - self._barrier_ltr.stop() + for task in self._task_runners: + task.stop() except asyncio.CancelledError: pass @@ -94,7 +97,7 @@ async def run(self): provider.ensure_payment_received( sender_address=self.network_topology.sender_address, token_address=self.network_topology.token_address, - nonce=current_nonce, poll_interval=0.05) + nonce=self.current_nonce, poll_interval=0.05) ) barrier_event_task = self._barrier_etf.create_await_event_task() log.info('Waiting for payment to provider={}, nonce={}'.format(provider.address, @@ -140,6 +143,7 @@ async def run(self): assert False def _on_new_provider(self): + log.info(f'New provider chosen: {self.current_provider_address}') if self.barcode_handler is not None: self.barcode_handler.save_barcode(self.current_provider_address, self.current_nonce) @@ -166,10 +170,9 @@ def build_app(cls, network: NetworkTopology, mock_arduino=False, mock_raiden=Fal log.debug('Mocking Arduino serial') arduino_track_control = MockArduinoTrackControl() else: - arduino_serial = ArduinoSerial(port='/dev/ttyACM0', baudrate=9600, timeout=.1) - # arduino_serial = ArduinoSerial(port='/dev/cu.usbmodem1421', baudrate=9600, timeout=.1) + # arduino_serial = ArduinoSerial(port='/dev/ttyACM0', baudrate=9600, timeout=.1) + arduino_serial = ArduinoSerial(port='/dev/cu.usbmodem1421', baudrate=9600, timeout=.1) arduino_track_control = ArduinoTrackControl(arduino_serial) - arduino_track_control.connect() if mock_raiden: raiden_node_cls = RaidenNodeMock diff --git a/arduino/track_control/track_control.ino b/arduino/track_control/track_control.ino index 365bb56..0852038 100644 --- a/arduino/track_control/track_control.ino +++ b/arduino/track_control/track_control.ino @@ -1,3 +1,5 @@ +#include + // defines pins numbers const int trigPin = 6; const int echoPin = 7; @@ -21,6 +23,8 @@ int barrierSensor = 0; // 2: power Track on // 3: turn barrier measuring off // 4: turn barrier measuring on +// 5: Keepalive signal +// 6: request arduino reset int inByte = 0; int maxTriggerDistance = 20; @@ -33,6 +37,8 @@ void setup() { Serial.begin(9600); // Starts the serial communication measuring = false; establishContact(); + //Aktiviere Watchdog mit 8s Zeitkonstante + wdt_enable(WDTO_8S); } void loop() { @@ -48,20 +54,30 @@ void loop() { delayMicroseconds(10); sendAck(); sendSensorData(); + break; // Turn Train power off case 1: turnPowerOff(); sendAck(); + break; // Turn Train power on case 2: turnPowerOn(); sendAck(); + break; case 3: measuring = false; sendAck(); + break; case 4: measuring = true; sendAck(); + break; + case 5: + // received keepalive, reset the watchdog timer + wdt_reset(); + sendAck(); + break; default: break; } diff --git a/raiden.py b/raiden.py index 4dd96a4..54b93e4 100644 --- a/raiden.py +++ b/raiden.py @@ -82,6 +82,7 @@ async def query_for_payment_received(self, sender_address, token_address, nonce) if event["event"] == "EventPaymentReceivedSuccess" and \ event["amount"] == 1 and \ event["identifier"] == nonce: + log.debug(f"{self} has payment-event for nonce: {nonce}") return True # Event not found in event list: return False @@ -133,6 +134,6 @@ async def query_for_started(self): async def query_for_payment_received(self, sender_address, token_address, nonce): # TODO remove - await asyncio.sleep(0.1) + await asyncio.sleep(3) # always say the payment was received for now return True diff --git a/tests/test_train_app.py b/tests/test_train_app.py index fcde950..92e927a 100644 --- a/tests/test_train_app.py +++ b/tests/test_train_app.py @@ -35,7 +35,6 @@ async def test_track_loop(fake_raiden, token_address, sender_address, train_app, await context_switch() # test internals for now - assert train_app._barrier_ltr.is_running assert train_app._barrier_etf is not None assert not train_app._track_loop.done() diff --git a/track_control.py b/track_control.py index 0703bd4..78bbae4 100644 --- a/track_control.py +++ b/track_control.py @@ -21,6 +21,7 @@ class OutMessage(Enum): POWER_ON = 3 DISTANCE_MEASURE_OFF = 4 DISTANCE_MEASURE_ON = 5 + KEEPALIVE = 6 @classmethod def encode(cls, message: 'OutMessage'): @@ -39,6 +40,8 @@ def encode(cls, message: 'OutMessage'): encoded = bytes([3]) elif message is cls.DISTANCE_MEASURE_ON: encoded = bytes([4]) + elif message is cls.KEEPALIVE: + encoded = bytes([5]) else: ValueError('Message not known') return encoded @@ -157,6 +160,9 @@ def power_state(self): def connect(self): return self._serial.do_handshake() + def send_keepalive(self): + self._serial.send_message(OutMessage.KEEPALIVE) + def power_on(self): self._serial.send_message(OutMessage.POWER_ON) @@ -193,6 +199,9 @@ def power_state(self): def connect(self): return True + def send_keepalive(self): + pass + def power_on(self): self._power_state = PowerState.POWER_ON @@ -273,15 +282,35 @@ def stop(self): pass +class KeepAliveTaskRunner(LoopTaskRunner): + + def __init__(self, track_control: 'TrackControl'): + self._track_control = track_control + self._barrier_task = None + + def is_running(self): + return not self._barrier_task.done() + + def start(self): + self._barrier_task = asyncio.create_task(self._track_control.run_keepalive_loop()) + + def stop(self): + # TODO implement + pass + + class TrackControl: def __init__(self, arduino_track_control): - # the ArduinoTrackControl is assumed to be connected already! + # the ArduinoTrackControl is assumed to NOT be connected already! self.arduino_track_control = arduino_track_control self._barrier_events = set() # sleep for x seconds when barrier was triggered self._barrier_sleep_time = 4. + def connect(self): + return self.arduino_track_control.connect() + @property def is_powered(self): return bool(self.arduino_track_control.power_state is PowerState.POWER_ON) @@ -323,6 +352,13 @@ async def run_barrier_loop(self): else: await asyncio.sleep(0.1) + async def run_keepalive_loop(self): + while True: + self.arduino_track_control.send_keepalive() + # send keepalive every second - + # after 8s of no keepalive, the arduino will reset + await asyncio.sleep(1.) + def trigger_barrier(self): for event in self._barrier_events: event.set()