├── tests ├── __init__.py ├── test_celc_crc_for_byte_array.py ├── test_receivedPacket.py ├── test_serial2pjon_proxy_arduino.py ├── test_pjonBaseClient.py ├── test_pjonPiperClient.py ├── test_PJONserialStrategy.py ├── test_pjonPiperClients_packetsProcessor.py ├── test_serialOverRedis.py └── test_pjonProtocol.py ├── pjon_python ├── __init__.py ├── protocol │ ├── __init__.py │ ├── pjon_protocol_constants.py │ └── pjon_protocol.py ├── utils │ ├── __init__.py │ ├── serial_utils.py │ ├── crc8.py │ ├── RedisConn.py │ └── fakeserial.py ├── strategies │ ├── __init__.py │ └── pjon_hwserial_strategy.py ├── pjon_piper_bin │ ├── rpi │ │ └── pjon_piper │ └── win │ │ └── PJON-piper.exe ├── over_redis_mock_client.py ├── base_client.py └── wrapper_client.py ├── setup.cfg ├── requirements.txt ├── requirements-dev.txt ├── specification └── specification.md ├── convert_readme_to_rst.py ├── LICENSE ├── setup.py ├── raspi_example_main.py ├── arduino_firmware ├── serial2pjon_proxy_v5_1 │ └── serial2pjon_proxy_v5_1.ino ├── serial2pjon_proxy │ └── serial2pjon_proxy.ino └── serial2pjon_proxy_v4_3 │ └── serial2pjon_proxy_v4_3.ino ├── README.md └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pjon_python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pjon_python/protocol/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pjon_python/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pjon_python/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mock 2 | nose 3 | pyserial >= 3.1.1 4 | retrying -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mock 2 | nose 3 | pyserial >= 3.1.1 4 | unittest2 -------------------------------------------------------------------------------- /pjon_python/pjon_piper_bin/rpi/pjon_piper: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Girgitt/PJON-python/HEAD/pjon_python/pjon_piper_bin/rpi/pjon_piper -------------------------------------------------------------------------------- /pjon_python/pjon_piper_bin/win/PJON-piper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Girgitt/PJON-python/HEAD/pjon_python/pjon_piper_bin/win/PJON-piper.exe -------------------------------------------------------------------------------- /specification/specification.md: -------------------------------------------------------------------------------- 1 | The PJON protocol's specification is available in [PJON's GitHub repository](https://github.com/gioblu/PJON/tree/master/specification) -------------------------------------------------------------------------------- /convert_readme_to_rst.py: -------------------------------------------------------------------------------- 1 | from pypandoc import convert 2 | 3 | read_rst = convert('./README.md', 'rst') 4 | 5 | 6 | with open('./README.rst', 'w') as fo_h: 7 | fo_h.writelines(read_rst) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Zbigniew Zasieczny 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. */ -------------------------------------------------------------------------------- /tests/test_celc_crc_for_byte_array.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pjon_python.utils import crc8 4 | 5 | 6 | class TestCelc_crc_for_byte_array(TestCase): 7 | def test_celc_crc_for_byte_array(self): 8 | self.assertEquals(106, crc8.calc_crc_for_byte_array([1, 21, 2, 45, 49, 97, 50, 115, 51, 100, 52, 102, 53, 103, 54, 104, 55, 106, 56, 107])) 9 | self.assertEquals(198, crc8.calc_crc_for_byte_array([1, 8, 2, 45, 65, 66, 67])) 10 | self.assertEquals(71, crc8.calc_crc_for_byte_array([1, 9, 2, 45, 65, 65, 65, 65])) 11 | self.assertEquals(57, crc8.calc_crc_for_byte_array([35, 8, 1, 67, 49, 50, 51])) 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | read_md = lambda f: open(f, 'r').read() 4 | 5 | 6 | setup( 7 | name='pjon_python', 8 | packages=['pjon_python', 'pjon_python.protocol', 'pjon_python.strategies', 'pjon_python.utils'], 9 | version='4.2.6', 10 | description='Python implementation of the PJON communication protocol.', 11 | long_description=read_md('./README.rst'), 12 | author='Zbigniew Zasieczny', 13 | author_email='z.zasieczny@gmail.com', 14 | url='https://github.com/Girgitt/PJON-python', 15 | download_url='https://github.com/Girgitt/PJON-python/tarball/4.2.6', 16 | keywords=['PJON', 'multimaster', 'serial', 'RS485', 'arduino'], 17 | classifiers=[], 18 | ) -------------------------------------------------------------------------------- /tests/test_receivedPacket.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pjon_python.protocol.pjon_protocol import ReceivedPacket 3 | 4 | 5 | class TestReceivedPacket(TestCase): 6 | def setUp(self): 7 | self._rcv_packet = ReceivedPacket('ABC123', 6, None) 8 | 9 | def test_payload(self): 10 | self.assertEqual('ABC123', self._rcv_packet.payload) 11 | 12 | def test_payload_as_string(self): 13 | self.assertEqual('ABC123', self._rcv_packet.payload_as_string) 14 | 15 | def test_payload_as_chars(self): 16 | self.assertEqual(['A', 'B', 'C', '1', '2', '3'], self._rcv_packet.payload_as_chars) 17 | 18 | def test_payload_as_bytes(self): 19 | self.assertEqual([65, 66, 67, 49, 50, 51], self._rcv_packet.payload_as_bytes) 20 | 21 | def test_packet_length(self): 22 | self.assertEqual(6, self._rcv_packet.packet_length) 23 | 24 | def test_packet_info(self): 25 | self.assertEqual(None, self._rcv_packet.packet_info) 26 | -------------------------------------------------------------------------------- /pjon_python/utils/serial_utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import glob 3 | import serial 4 | 5 | 6 | def get_serial_ports(): 7 | # credits to Thomas (http://stackoverflow.com/questions/12090503/listing-available-com-ports-with-python) 8 | """ Lists serial port names 9 | 10 | :raises EnvironmentError: 11 | On unsupported or unknown platforms 12 | :returns: 13 | A list of the serial ports available on the system 14 | """ 15 | if sys.platform.startswith('win'): 16 | ports = ['COM%s' % (i + 1) for i in range(256)] 17 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 18 | # this excludes your current terminal "/dev/tty" 19 | ports = glob.glob('/dev/tty[A-Za-z]*') 20 | elif sys.platform.startswith('darwin'): 21 | ports = glob.glob('/dev/tty.*') 22 | else: 23 | raise EnvironmentError('Unsupported platform') 24 | 25 | result = [] 26 | for port in ports: 27 | try: 28 | s = serial.Serial(port) 29 | s.close() 30 | result.append(port) 31 | except (OSError, serial.SerialException): 32 | pass 33 | return result -------------------------------------------------------------------------------- /raspi_example_main.py: -------------------------------------------------------------------------------- 1 | from pjon_python.base_client import PjonBaseSerialClient 2 | import time 3 | 4 | # load serial2pjon_proxy_v5_1 to arduino 5 | # adjust serial port below to match your arduino's port 6 | # run this example; expected output: 7 | # 8 | # pakets in outgoing queue: 0 9 | # received from 35 payload: [66, 49, 50, 51, 52, 53, 54, 55, 56, 57] 10 | # pakets in outgoing queue: 0 11 | # pakets in outgoing queue: 0 12 | # received from 35 payload: [66, 49, 50, 51, 52, 53, 54, 55, 56, 57] 13 | # pakets in outgoing queue: 0 14 | # 15 | 16 | cli = PjonBaseSerialClient(1, '/dev/ttyUSB1', write_timeout=0.005, timeout=0.005) 17 | 18 | 19 | def error_handler(self, error_code=0, parameter=0): 20 | error_text = "code: %s, parameter: %s" % (error_code, parameter) 21 | if error_code == 101: 22 | error_text = "connection lost to node: %s" % parameter 23 | elif error_code == 101: 24 | error_text = "outgoing buffer full" 25 | 26 | if error_code > 0: 27 | print(error_text) 28 | 29 | def receive_handler(payload, packet_length, packet_info): 30 | print "received from %s payload: %s" % (packet_info.sender_id, payload) 31 | 32 | cli.set_receive(receive_handler) 33 | cli.set_error(error_handler) 34 | cli.start_client() 35 | 36 | while True: 37 | print "pakets in outgoing queue: %s" % cli.send(35, "C123") 38 | time.sleep(.1) -------------------------------------------------------------------------------- /pjon_python/utils/crc8.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] == 3: 4 | import codecs 5 | 6 | 7 | def calc_crc_for_byte_array(byte_array): 8 | crc = 0 9 | for b in byte_array: 10 | crc = AddToCRC(b, crc) 11 | return crc 12 | 13 | 14 | def calc_crc_for_hex_string(incoming): 15 | # convert to bytearray 16 | if sys.version_info[0] == 3: 17 | hex_data = codecs.decode(incoming, "hex_codec") 18 | else: 19 | hex_data = incoming.decode("hex") 20 | msg = bytearray(hex_data) 21 | result = 0 22 | for i in msg: 23 | result = AddToCRC(i, result) 24 | return hex(result) 25 | 26 | 27 | def AddToCRC(b, crc): 28 | """ computes crc iteratively by returning updated crc input""" 29 | if b < 0: 30 | b += 256 31 | for i in range(8): 32 | odd = ((b ^ crc) & 1) == 1 33 | crc >>= 1 34 | b >>= 1 35 | if odd: 36 | crc ^= 0x8C # this means crc ^= 140 37 | return crc 38 | 39 | 40 | def check(incoming): 41 | """Returns True if CRC Outcome Is 0xx or 0x0""" 42 | result = calc_crc_for_hex_string(incoming) 43 | if result == "0x0" or result == "0x00": 44 | return True 45 | else: 46 | return False 47 | 48 | 49 | def append(incoming): 50 | """Returns the Incoming message after appending it's CRC CheckSum""" 51 | result = calc_crc_for_hex_string(incoming).split('x')[1].zfill(2) 52 | return incoming + result 53 | -------------------------------------------------------------------------------- /pjon_python/protocol/pjon_protocol_constants.py: -------------------------------------------------------------------------------- 1 | ''' Communication modes ''' 2 | SIMPLEX = 150 3 | HALF_DUPLEX = 151 4 | 5 | ''' Maximum randon delay on collision ''' 6 | COLLISION_MAX_DELAY = 48 7 | 8 | ''' Protocol symbols ''' 9 | ACK = 6 10 | ACQUIRE_ID = 63 11 | BUSY = 666 12 | NAK = 21 13 | 14 | ''' Reserved addresses ''' 15 | BROADCAST = 0 16 | NOT_ASSIGNED = 255 17 | 18 | ''' Internalconstants ''' 19 | FAIL = 0x100 # 256 20 | TO_BE_SENT = 74 21 | 22 | ''' HEADER CONFIGURATION ''' 23 | ''' Packet header bits ''' 24 | #FIXME: it's actually not bit order but corresponding value 25 | MODE_BIT = 1 # 1 - Shared | 0 - Local 26 | SENDER_INFO_BIT = 2 # 1 - Sender device id + Sender bus id if shared | 0 - No info inclusion 27 | ACK_REQUEST_BIT = 4 # 1 - Request synchronous acknowledge | 0 - Do not request acknowledge 28 | 29 | ''' Errors ''' 30 | CONNECTION_LOST = 101 31 | PACKETS_BUFFER_FULL = 102 32 | MEMORY_FULL = 103 33 | CONTENT_TOO_LONG = 104 34 | ID_ACQUISITION_FAIL = 105 35 | 36 | ''' Constraints ''' 37 | MAX_ATTEMPTS = 125 38 | 39 | ''' Packets buffer length ''' 40 | MAX_PACKETS = 128 41 | 42 | ''' Max packet length, higher if necessary( and you have free memory ''' 43 | PACKET_MAX_LENGTH = 50 44 | 45 | ''' Maximum id scan time(5 seconds) ''' 46 | MAX_ID_SCAN_TIME = 5000000 47 | 48 | 49 | ''' additional constants compared to PJON v4.2''' 50 | RECEIVER_ID_BYTE_ORDER = 0 51 | RECEIVER_HEADER_BYTE_ORDER = 2 52 | SENDER_ID_WITH_NET_INFO_BYTE_ORDER = 11 53 | RECEIVER_BUS_ID_WITH_NET_INFO_BYTE_ORDER = 3 54 | 55 | SENDER_ID_WITHOUT_NET_INFO_BYTE_ORDER = 3 -------------------------------------------------------------------------------- /tests/test_serial2pjon_proxy_arduino.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pjon_python.base_client import PjonBaseSerialClient 3 | import platform 4 | from unittest2.compatibility import wraps 5 | import time 6 | 7 | def skip_if_condition(condition, reason): 8 | def deco(f): 9 | @wraps(f) 10 | def wrapper(self, *args, **kwargs): 11 | if condition: 12 | self.skipTest(reason) 13 | else: 14 | f(self, *args, **kwargs) 15 | return wrapper 16 | return deco 17 | 18 | 19 | class TestSerial2pjonProxy(TestCase): 20 | def setUp(self): 21 | if platform.platform().find('armv') < 0: 22 | return 23 | self.cli = PjonBaseSerialClient(1, '/dev/ttyUSB1', write_timeout=0.005, timeout=0.005) 24 | self.cli.set_error(self.test_error_handler) 25 | self.cli.start_client() 26 | 27 | 28 | def test_error_handler(self, error_code=0, parameter=0): 29 | error_text = "code: %s, parameter: %s" % (error_code, parameter) 30 | if error_code == 101: 31 | error_text = "connection lost to node: %s" % parameter 32 | elif error_code == 101: 33 | error_text = "outgoing buffer full" 34 | 35 | if error_code > 0: 36 | self.fail(error_text) 37 | 38 | @skip_if_condition(platform.platform().find('armv') < 0, 'skipping on non-ARM as it is in-hardware test') 39 | def test_proxy_should_respond(self): 40 | time.sleep(5) 41 | for i in range(2): 42 | #self.cli.send_without_ack(35, 'C12345') 43 | self.cli.send(35, 'C12345') 44 | time.sleep(1) 45 | time.sleep(2) 46 | -------------------------------------------------------------------------------- /tests/test_pjonBaseClient.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skip 2 | from unittest2.compatibility import wraps 3 | from pjon_python import base_client 4 | 5 | import time 6 | import platform 7 | import logging 8 | log = logging.getLogger("tests") 9 | 10 | 11 | def skip_if_condition(condition, reason): 12 | def deco(f): 13 | @wraps(f) 14 | def wrapper(self, *args, **kwargs): 15 | if condition: 16 | self.skipTest(reason) 17 | else: 18 | f(self, *args, **kwargs) 19 | return wrapper 20 | return deco 21 | 22 | 23 | class TestPjonBaseClient(TestCase): 24 | def setUp(self): 25 | self.is_arm = True if platform.platform().find('armv') > 0 else False 26 | self.cli_1 = base_client.PjonBaseSerialClient(bus_addr=1, com_port='fakeserial') 27 | self.cli_1.start_client() 28 | self.cli_2 = base_client.PjonBaseSerialClient(bus_addr=2, com_port='fakeserial') 29 | self.cli_2.start_client() 30 | self.cli_3 = base_client.PjonBaseSerialClient(bus_addr=3, com_port='fakeserial') 31 | self.cli_3.start_client() 32 | 33 | def test_fake_serial_should_pass_messages_between_clients_no_ack(self): 34 | self.cli_1.send_without_ack(2, 'test1') 35 | self.cli_1.send_without_ack(2, 'test2') 36 | time.sleep(.4) 37 | self.assertEquals(2, len(self.cli_2._protocol._stored_received_packets)) 38 | 39 | @skip_if_condition(platform.platform().find('armv') > 0, 'skipping on ARM due to performance') 40 | def test_fake_serial_should_pass_messages_between_clients_with_ack(self): 41 | self.cli_1.send(2, 'test1') 42 | self.cli_1.send(2, 'test2') 43 | self.cli_1.send(3, 'test3') 44 | 45 | time.sleep(1.4) 46 | self.assertEquals(2, len(self.cli_2._protocol._stored_received_packets)) 47 | self.assertEquals(1, len(self.cli_3._protocol._stored_received_packets)) 48 | 49 | -------------------------------------------------------------------------------- /tests/test_pjonPiperClient.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from unittest import TestCase 3 | from pjon_python import wrapper_client 4 | from unittest2.compatibility import wraps 5 | import mock 6 | import time 7 | 8 | 9 | def skip_if_condition(condition, reason): 10 | def deco(f): 11 | @wraps(f) 12 | def wrapper(self, *args, **kwargs): 13 | if condition: 14 | self.skipTest(reason) 15 | else: 16 | f(self, *args, **kwargs) 17 | return wrapper 18 | return deco 19 | 20 | 21 | class TestPjonPiperClient(TestCase): 22 | def setUp(self): 23 | with mock.patch('pjon_python.wrapper_client.PjonPiperClient.get_coms', create=True) as get_coms_mock: 24 | get_coms_mock.return_value = ['COM1'] 25 | self._pjon_wrapper_cli = wrapper_client.PjonPiperClient(com_port='COM1') 26 | 27 | @skip_if_condition('win' not in platform.platform(), 'skipping on non-windows') 28 | def test_is_string_valid_com_port_name__should_return_true_for_expected_com_names__windows(self): 29 | self.assertTrue(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM1")) 30 | self.assertTrue(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM99")) 31 | self.assertTrue(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM11")) 32 | 33 | @skip_if_condition('win' not in platform.platform(), 'skipping on non-windows') 34 | def test_is_string_valid_com_port_name__should_return_false_for_wrong_com_names__windows(self): 35 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM1 ")) 36 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM0")) 37 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name("COM100")) 38 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name("coom")) 39 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name(" com")) 40 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name(" com3")) 41 | self.assertFalse(self._pjon_wrapper_cli.is_string_valid_com_port_name(" com323")) 42 | 43 | def test_client_should_initialize_with_stdout_watchdog_disabled(self): 44 | self.assertEqual(self._pjon_wrapper_cli.is_piper_stdout_watchdog_enabled, False) 45 | 46 | def test_set_stdout_watchdog__should_change_enable_property(self): 47 | self._pjon_wrapper_cli.set_piper_stdout_watchdog(timeout_sec=0.250) 48 | self.assertEqual(self._pjon_wrapper_cli.is_piper_stdout_watchdog_enabled, True) 49 | 50 | def test_reset_piper_stdout_watchdog__should_set_received_ts_to_current_time(self): 51 | self._pjon_wrapper_cli.reset_piper_stdout_watchdog() 52 | self.assertLess(self._pjon_wrapper_cli._piper_stdout_last_received_ts - time.time(), 2) 53 | -------------------------------------------------------------------------------- /arduino_firmware/serial2pjon_proxy_v5_1/serial2pjon_proxy_v5_1.ino: -------------------------------------------------------------------------------- 1 | #define MAX_PACKETS 4 2 | #include 3 | #include 4 | 5 | SoftwareSerial sSerial(10, 11); // RX, TX 6 | 7 | 8 | //PJON bus(45); 9 | //PJON bus_hw(35); 10 | 11 | PJON bus(45); 12 | PJON bus_hw(35); 13 | 14 | long start_ts; 15 | int last_received_address = 0; 16 | 17 | void setup() { 18 | pinModeFast(13, OUTPUT); 19 | digitalWriteFast(13, LOW); // Initialize LED 13 to be off 20 | 21 | //bus.set_pins(2, 1); 22 | //bus.set_rx_vcc_pin(12); 23 | bus.set_receiver(receiver_function); 24 | bus_hw.set_receiver(receiver_function_hw); 25 | //bus_hw.set_error(error_handler_hw); 26 | 27 | //bus.begin(); 28 | Serial.begin(115200); 29 | sSerial.begin(76800); 30 | bus.strategy.set_serial(&sSerial); 31 | bus_hw.strategy.set_serial(&Serial); 32 | 33 | 34 | 35 | //bus.include_sender_info(false); 36 | //bus.set_acknowledge(false); 37 | //bus_hw.set_acknowledge(true); 38 | //bus.set_packet_auto_deletion(true); 39 | //bus.set_router(false); 40 | 41 | 42 | 43 | //bus.send_repeatedly(44, "B1234567891234567890", 20, 100000); // Send B to device 44 every second 44 | start_ts = millis(); 45 | } 46 | 47 | 48 | void receiver_function(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 49 | if(payload[0] == 'C') { 50 | //Serial.println("BLINK"); 51 | digitalWriteFast(13, HIGH); 52 | delay(1); 53 | digitalWriteFast(13, LOW); 54 | delay(1); 55 | digitalWriteFast(13, HIGH); 56 | delay(1); 57 | digitalWriteFast(13, LOW); 58 | } 59 | } 60 | 61 | void receiver_function_hw(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 62 | if(payload[0] == 'C') { 63 | last_received_address = packet_info.sender_id; 64 | //Serial.println("BLINK"); 65 | digitalWriteFast(13, HIGH); 66 | delay(1); 67 | digitalWriteFast(13, LOW); 68 | delay(1); 69 | digitalWriteFast(13, HIGH); 70 | delay(1); 71 | digitalWriteFast(13, LOW); 72 | } 73 | } 74 | 75 | void error_handler_hw(uint8_t code, uint8_t data) { 76 | Serial.print("err:"); 77 | Serial.println(code); 78 | Serial.println(data); 79 | } 80 | 81 | boolean fired=false; 82 | 83 | boolean updated=false; 84 | long last_update_ts = 0; 85 | 86 | void loop() { 87 | if (millis() % 5 < 2){ 88 | if(!updated){ 89 | updated = true; 90 | last_update_ts=millis(); 91 | bus.update(); 92 | bus_hw.update(); 93 | }; 94 | }else{ 95 | updated = false; 96 | }; 97 | //bus.update(); 98 | //bus_hw.update(); 99 | bus.receive(100); 100 | bus_hw.receive(100); 101 | 102 | if (millis() % 500 < 100){ 103 | if(!fired){ 104 | fired = true; 105 | 106 | start_ts=millis(); 107 | 108 | digitalWriteFast(13, HIGH); 109 | delay(1); 110 | digitalWrite(13, LOW); 111 | 112 | //bus.send(44, "B12345689012345678901234567789", 30); 113 | //bus.send(44, "B123456789", 10); 114 | if(last_received_address > 0){ 115 | bus_hw.send(last_received_address, "B123456789", 10); 116 | }; 117 | }; 118 | }else{ 119 | fired = false; 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /pjon_python/utils/RedisConn.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from redis import ConnectionError 4 | import logging 5 | import jsonpickle 6 | from retrying import retry 7 | 8 | log = logging.getLogger("redis-conn") 9 | 10 | 11 | def retry_if_connection_error(exception): 12 | return isinstance(exception, ConnectionError) 13 | 14 | 15 | class RedisConn(object): 16 | def __init__(self, redis_conn, sub_channel='rtu-cmd', pub_channel='rtu-cmd', cli_id=None): 17 | self._redis_conn = redis_conn 18 | 19 | self._pubsub = self._redis_conn.pubsub(ignore_subscribe_messages=False) 20 | log.error("subscribing: %s" % sub_channel) 21 | self._pubsub.subscribe(sub_channel) 22 | 23 | self._sub_channel_name = sub_channel 24 | self._pub_channel_name = pub_channel 25 | 26 | self._cli_id = cli_id 27 | 28 | def subscribe(self, channel_name): 29 | self._pubsub.subscribe(channel_name) 30 | 31 | def listen(self, rcv_timeout=0.01): 32 | 33 | message = True 34 | while message: 35 | try: 36 | message = self._pubsub.get_message(timeout=rcv_timeout) 37 | except ConnectionError: 38 | log.error("lost connection to Redis") 39 | time.sleep(1) 40 | break 41 | if message: 42 | log.debug("%s - receied pub message: %s" % (self._cli_id, message)) 43 | if message['type'] == 'message': 44 | try: 45 | return jsonpickle.loads(message['data']) 46 | except(ValueError, KeyError): 47 | return message['data'] 48 | return None 49 | 50 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 51 | def publish(self, payload, channel=None): 52 | if channel is None: 53 | channel = self._pub_channel_name 54 | log.debug("publishing to channel: %s \n %s" % (channel, payload)) 55 | try: 56 | payload = jsonpickle.dumps(payload) 57 | except ValueError: 58 | pass 59 | self._redis_conn.publish(channel, payload) 60 | 61 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 62 | def hgetall(self, *args, **kwargs): 63 | return self._redis_conn.hgetall(*args, **kwargs) 64 | 65 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 66 | def hget(self, *args, **kwargs): 67 | return self._redis_conn.hget(*args, **kwargs) 68 | 69 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 70 | def hmset(self, *args, **kwargs): 71 | return self._redis_conn.hmset(*args, **kwargs) 72 | 73 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 74 | def delete(self, *args, **kwargs): 75 | return self._redis_conn.delete(*args, **kwargs) 76 | 77 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 78 | def hdel(self, *args, **kwargs): 79 | return self._redis_conn.hdel(*args, **kwargs) 80 | 81 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 82 | def hset(self, *args, **kwargs): 83 | return self._redis_conn.hset(*args, **kwargs) 84 | 85 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 86 | def set(self, *args, **kwargs): 87 | return self._redis_conn.set(*args, **kwargs) 88 | 89 | @retry(wait_fixed=1000, stop_max_attempt_number=3) 90 | def get(self, *args, **kwargs): 91 | return self._redis_conn.get(*args, **kwargs) 92 | -------------------------------------------------------------------------------- /arduino_firmware/serial2pjon_proxy/serial2pjon_proxy.ino: -------------------------------------------------------------------------------- 1 | #define MAX_PACKETS 4 2 | #include 3 | #include 4 | 5 | SoftwareSerial sSerial(10, 11); // RX, TX 6 | 7 | 8 | //PJON bus(45); 9 | //PJON bus_hw(35); 10 | 11 | PJON bus(45); 12 | PJON bus_hw(35); 13 | 14 | long start_ts; 15 | int last_received_address = 0; 16 | 17 | void setup() { 18 | pinModeFast(13, OUTPUT); 19 | digitalWriteFast(13, LOW); // Initialize LED 13 to be off 20 | 21 | bus.set_pins(2, 1); 22 | //bus.set_rx_vcc_pin(12); 23 | bus.set_receiver(receiver_function); 24 | bus_hw.set_receiver(receiver_function_hw); 25 | //bus_hw.set_error(error_handler_hw); 26 | 27 | //bus.begin(); 28 | Serial.begin(115200); 29 | sSerial.begin(76800); 30 | //bus.strategy.set_serial(&sSerial); 31 | bus_hw.strategy.set_serial(&Serial); 32 | 33 | 34 | 35 | //bus.include_sender_info(false); 36 | //bus.set_acknowledge(false); 37 | //bus_hw.set_acknowledge(true); 38 | //bus.set_packet_auto_deletion(true); 39 | //bus.set_router(false); 40 | 41 | 42 | 43 | //bus.send_repeatedly(44, "B1234567891234567890", 20, 100000); // Send B to device 44 every second 44 | start_ts = millis(); 45 | } 46 | 47 | 48 | void receiver_function(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 49 | if(payload[0] == 'C') { 50 | //Serial.println("BLINK"); 51 | digitalWriteFast(13, HIGH); 52 | delay(1); 53 | digitalWriteFast(13, LOW); 54 | delay(1); 55 | digitalWriteFast(13, HIGH); 56 | delay(1); 57 | digitalWriteFast(13, LOW); 58 | } 59 | } 60 | 61 | void receiver_function_hw(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 62 | if(payload[0] == 'C') { 63 | last_received_address = packet_info.sender_id; 64 | //Serial.println("BLINK"); 65 | digitalWriteFast(13, HIGH); 66 | delay(1); 67 | digitalWriteFast(13, LOW); 68 | delay(1); 69 | digitalWriteFast(13, HIGH); 70 | delay(1); 71 | digitalWriteFast(13, LOW); 72 | } 73 | } 74 | 75 | void error_handler_hw(uint8_t code, uint8_t data) { 76 | Serial.print("err:"); 77 | Serial.println(code); 78 | Serial.println(data); 79 | } 80 | 81 | boolean fired=false; 82 | 83 | boolean updated=false; 84 | long last_update_ts = 0; 85 | 86 | void loop() { 87 | if (millis() % 5 < 2){ 88 | if(!updated){ 89 | updated = true; 90 | last_update_ts=millis(); 91 | //bus.update(); 92 | bus_hw.update(); 93 | }; 94 | }else{ 95 | updated = false; 96 | }; 97 | //bus.update(); 98 | //bus_hw.update(); 99 | //bus.receive(100); 100 | bus_hw.receive(100); 101 | 102 | if (millis() % 500 < 100){ 103 | if(!fired){ 104 | fired = true; 105 | 106 | start_ts=millis(); 107 | 108 | digitalWriteFast(13, HIGH); 109 | delay(1); 110 | digitalWrite(13, LOW); 111 | 112 | //bus.send(44, "B12345689012345678901234567789", 30); 113 | //bus.send(44, "B123456789", 10); 114 | if(last_received_address > 0){ 115 | bus_hw.send(last_received_address, "B123456789", 10); 116 | }; 117 | }; 118 | }else{ 119 | fired = false; 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /arduino_firmware/serial2pjon_proxy_v4_3/serial2pjon_proxy_v4_3.ino: -------------------------------------------------------------------------------- 1 | #define MAX_PACKETS 4 2 | #include 3 | #include 4 | 5 | SoftwareSerial sSerial(10, 11); // RX, TX 6 | 7 | 8 | //PJON bus(45); 9 | //PJON bus_hw(35); 10 | 11 | PJON bus(45); 12 | PJON bus_hw(35); 13 | 14 | long start_ts; 15 | int last_received_address = 0; 16 | 17 | void setup() { 18 | pinModeFast(13, OUTPUT); 19 | digitalWriteFast(13, LOW); // Initialize LED 13 to be off 20 | 21 | //bus.set_pins(2, 1); 22 | //bus.set_rx_vcc_pin(12); 23 | bus.set_receiver(receiver_function); 24 | bus_hw.set_receiver(receiver_function_hw); 25 | //bus_hw.set_error(error_handler_hw); 26 | 27 | //bus.begin(); 28 | Serial.begin(115200); 29 | sSerial.begin(76800); 30 | //bus.strategy.set_serial(&sSerial); 31 | bus_hw.strategy.set_serial(&Serial); 32 | 33 | 34 | 35 | //bus.include_sender_info(false); 36 | //bus.set_acknowledge(false); 37 | //bus_hw.set_acknowledge(true); 38 | //bus.set_packet_auto_deletion(true); 39 | //bus.set_router(false); 40 | 41 | 42 | 43 | //bus.send_repeatedly(44, "B1234567891234567890", 20, 100000); // Send B to device 44 every second 44 | start_ts = millis(); 45 | } 46 | 47 | 48 | void receiver_function(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 49 | if(payload[0] == 'C') { 50 | //Serial.println("BLINK"); 51 | digitalWriteFast(13, HIGH); 52 | delay(1); 53 | digitalWriteFast(13, LOW); 54 | delay(1); 55 | digitalWriteFast(13, HIGH); 56 | delay(1); 57 | digitalWriteFast(13, LOW); 58 | } 59 | } 60 | 61 | void receiver_function_hw(uint8_t *payload, uint8_t length, const PacketInfo &packet_info) { 62 | if(payload[0] == 'C') { 63 | last_received_address = packet_info.sender_id; 64 | //Serial.println("BLINK"); 65 | digitalWriteFast(13, HIGH); 66 | delay(1); 67 | digitalWriteFast(13, LOW); 68 | delay(1); 69 | digitalWriteFast(13, HIGH); 70 | delay(1); 71 | digitalWriteFast(13, LOW); 72 | } 73 | } 74 | 75 | void error_handler_hw(uint8_t code, uint8_t data) { 76 | Serial.print("err:"); 77 | Serial.println(code); 78 | Serial.println(data); 79 | } 80 | 81 | boolean fired=false; 82 | 83 | boolean updated=false; 84 | long last_update_ts = 0; 85 | 86 | void loop() { 87 | if (millis() % 5 < 2){ 88 | if(!updated){ 89 | updated = true; 90 | last_update_ts=millis(); 91 | //bus.update(); 92 | bus_hw.update(); 93 | }; 94 | }else{ 95 | updated = false; 96 | }; 97 | //bus.update(); 98 | //bus_hw.update(); 99 | //bus.receive(100); 100 | bus_hw.receive(100); 101 | 102 | if (millis() % 500 < 100){ 103 | if(!fired){ 104 | fired = true; 105 | 106 | start_ts=millis(); 107 | 108 | digitalWriteFast(13, HIGH); 109 | delay(1); 110 | digitalWrite(13, LOW); 111 | 112 | //bus.send(44, "B12345689012345678901234567789", 30); 113 | //bus.send(44, "B123456789", 10); 114 | if(last_received_address > 0){ 115 | bus_hw.send(last_received_address, "B123456789", 10); 116 | }; 117 | }; 118 | }else{ 119 | fired = false; 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /tests/test_PJONserialStrategy.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skip 2 | 3 | import mock 4 | 5 | from pjon_python.strategies.pjon_hwserial_strategy import PJONserialStrategy, UnsupportedPayloadType 6 | 7 | 8 | class TestPJONserialStrategy(TestCase): 9 | 10 | def test_send_byte_should_convert_int_to_chr(self): 11 | with mock.patch('serial.Serial', create=True) as ser: 12 | serial_strategy = PJONserialStrategy(serial_port=ser) 13 | 14 | self.assertEquals(serial_strategy.send_byte(11), 0) 15 | 16 | def test_send_byte_should_convert_hex_to_chr(self): 17 | with mock.patch('serial.Serial', create=True) as ser: 18 | serial_strategy = PJONserialStrategy(serial_port=ser) 19 | self.assertEquals(serial_strategy.send_byte(0x22), 0) 20 | 21 | def test_send_byte_should_accept_char(self): 22 | with mock.patch('serial.Serial', create=True) as ser: 23 | serial_strategy = PJONserialStrategy(serial_port=ser) 24 | self.assertEquals(serial_strategy.send_byte('a'), 0) 25 | 26 | def test_send_byte_should_raise_on_unsupported_type(self): 27 | with mock.patch('serial.Serial', create=True) as ser: 28 | serial_strategy = PJONserialStrategy(serial_port=ser) 29 | self.assertRaises(UnsupportedPayloadType, serial_strategy.send_byte, ['a', 'b']) 30 | self.assertRaises(UnsupportedPayloadType, serial_strategy.send_byte, 'abc') 31 | self.assertRaises(UnsupportedPayloadType, serial_strategy.send_byte, [1, 2]) 32 | self.assertRaises(UnsupportedPayloadType, serial_strategy.send_byte, {'a': 'b'}) 33 | @skip("skipped because receive buffer was disabled") 34 | def test_serial_client_should_read_all_available_bytes_to_receive_buffer(self): 35 | with mock.patch('serial.Serial', create=True) as ser: 36 | def arr_return(size): 37 | vars = [chr(item) for item in [1, 9, 2, 45]] 38 | vars.reverse() 39 | return vars[:size] 40 | ser.read.side_effect = arr_return 41 | ser.inWaiting.side_effect = [4, 3, 2, 1] 42 | serial_strategy = PJONserialStrategy(serial_port=ser) 43 | self.assertEquals(serial_strategy.receive_byte(), 1) 44 | self.assertEquals(len(serial_strategy._read_buffer), 3) 45 | self.assertEquals(serial_strategy.receive_byte(), 9) 46 | self.assertEquals(len(serial_strategy._read_buffer), 2) 47 | self.assertEquals(serial_strategy.receive_byte(), 2) 48 | self.assertEquals(len(serial_strategy._read_buffer), 1) 49 | self.assertEquals(serial_strategy.receive_byte(), 45) 50 | self.assertEquals(len(serial_strategy._read_buffer), 0) 51 | 52 | @skip("skipped because receive buffer was disabled") 53 | def test_serial_client_should_trim_serial_buffer(self): 54 | with mock.patch('serial.Serial', create=True) as ser: 55 | serial_strategy = PJONserialStrategy(serial_port=ser) 56 | 57 | def return_payload_twice_the_buffer_length(size): 58 | vars = [chr(13)] * serial_strategy._READ_BUFFER_SIZE * 2 59 | return vars[:size] 60 | 61 | ser.read.side_effect = return_payload_twice_the_buffer_length 62 | ser.inWaiting.side_effect = [len(return_payload_twice_the_buffer_length(serial_strategy._READ_BUFFER_SIZE * 2))] 63 | 64 | self.assertEqual(serial_strategy.receive_byte(), 13) 65 | 66 | self.assertEqual(len(serial_strategy._read_buffer), 32767) 67 | 68 | -------------------------------------------------------------------------------- /pjon_python/utils/fakeserial.py: -------------------------------------------------------------------------------- 1 | # fakeSerial.py 2 | # PySerial functional mocks 3 | import uuid 4 | from pjon_python.utils.RedisConn import RedisConn 5 | import logging 6 | import random 7 | import fakeredis 8 | 9 | log = logging.getLogger("fakeredis") 10 | 11 | fake_redis_cli = fakeredis.FakeStrictRedis() 12 | instance_id = 0 13 | 14 | class Serial: 15 | """ class which uses redis (or fakeredis) pub/sub to communicate with other serial 16 | mocks. It's purpose is to enable half-duplex communication testing without 17 | OS-level serial port emulators. 18 | """ 19 | 20 | def __init__(self, port='COM1', baudrate=19200, transport=None, 21 | timeout=1, write_timeout=1, bytesize=8, parity='N', 22 | stopbits=1, xonxoff=0, rtscts=0): 23 | 24 | #self._uuid = uuid.uuid1(clock_seq=random.randint(0, 1000000)) 25 | global instance_id 26 | instance_id += 1 27 | self._uuid = instance_id 28 | if transport is None: 29 | 30 | self.transport = RedisConn(fake_redis_cli, 31 | sub_channel='pjon-serial', 32 | pub_channel='pjon-serial', 33 | cli_id=self._uuid) 34 | else: 35 | self.transport = RedisConn(transport, 36 | sub_channel='pjon-serial', 37 | pub_channel='pjon-serial') 38 | 39 | self.transport.subscribe('pjon-serial') 40 | 41 | self.name = port 42 | self.port = port 43 | self.timeout = timeout 44 | self.parity = parity 45 | self.baudrate = baudrate 46 | self.bytesize = bytesize 47 | self.stopbits = stopbits 48 | self.xonxoff = xonxoff 49 | self.rtscts = rtscts 50 | self._isOpen = True 51 | self._data = [] 52 | 53 | 54 | @property 55 | def closed(self): 56 | return not self.isOpen() 57 | 58 | def isOpen(self): 59 | return self._isOpen 60 | 61 | def open(self): 62 | self._isOpen = True 63 | 64 | def close(self): 65 | self._isOpen = False 66 | 67 | def write(self, string): 68 | message = dict() 69 | message['originator_uuid'] = self._uuid 70 | message['payload'] = string 71 | self.transport.publish(message) 72 | 73 | def update_input_queue(self): 74 | new_byte_data = True 75 | while new_byte_data: 76 | new_byte_data = self.transport.listen(rcv_timeout=0.0001) 77 | if new_byte_data: 78 | if new_byte_data['originator_uuid'] != self._uuid: 79 | #log.debug("received bytes: %s" % new_byte_data['payload']) 80 | self._data.extend(new_byte_data['payload']) 81 | 82 | def read(self, size=1): 83 | self.update_input_queue() 84 | s = self._data[0:size] 85 | self._data = self._data[size:] 86 | return s 87 | 88 | def inWaiting(self): 89 | self.update_input_queue() 90 | return len(self._data) 91 | 92 | def readline(self): 93 | raise NotImplementedError 94 | 95 | def flushInput(self): 96 | self.update_input_queue() 97 | self._data = [] 98 | 99 | def flushOutput(self): 100 | self._data = [] 101 | 102 | def __str__(self): 103 | return "Serial( port='%s', baudrate=%d," \ 104 | % (str(self.isOpen), self.port, self.baudrate) \ 105 | + " bytesize=%d, parity='%s', stopbits=%d, xonxoff=%d, rtscts=%d)"\ 106 | % (self.bytesize, self.parity, self.stopbits, self.xonxoff, 107 | self.rtscts) 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PJON-python 2 | Pythonic interface to PJON communication protocol. 3 | 4 | PJON (Github: [PJON](https://github.com/gioblu/PJON/) ) is an open-source, multi-master, multi-media (one-wire, two-wires, radio) communication protocol available for various platforms (Arduino/AVR, ESP8266, Teensy). 5 | 6 | PJON is one of very few open-source implementations of multi-master communication protocols for microcontrollers. 7 | 8 | PJON-python module in the current status enables communication with other PJON devices directly over UART (serial communication): 9 | 10 | - In a basic scenario PJON + PJON-python can be a viable alternative to more complex protocols like Firmata (Arduino firmware on Github: [firmata/Arduino](https://github.com/firmata/arduino) for direct (SIMPLEX) communication host-uC. 11 | - If RS485 drivers with auto-tx setup are used the host (e.g. Raspberry PI) can join multi-master PJON bus and python programs can communicate with multiple uC. 12 | - If uC with proxy firmware (sending packets between serial and other type of PJON bus) is used the host can communicate with PJON buses other than serial through that serial2pjon proxy uC. 13 | 14 | PJON-python module opens popular uC platforms (Arduino, ESP8266, Teensy) to the whole range of applications: 15 | - multi-master automation (reporting by exception lowers latency compared to polling-based protocols like Modbus) 16 | - open-hardware IoT (thanks to integration flexibility of other python modules) 17 | 18 | 19 | ## Current status: 20 | - work in progress, minimal client operational with PJON v4.2 or v4.3 (ThroughHardwareSerial strategy is required) 21 | - initially PHY abstraction to BitBang and OverSampling strategies provided by a serial-PJON bridge implemented as Arduino sketch 22 | - support for ThroughHardwareSerial strategy in HALF_DUPLEX, multi-master communication mode e.g. over RS485 bus is provided directly (serial-RS485 converter required) 23 | - support for ThroughHardwareSerial strategy in SIMPLEX communication mode will be provided directly (e.g. to talk to a single Arduino). 24 | - Communication with a single arduino connected to USB works in HALF_DUPLEX mode out of the box without any additional hardware 25 | 26 | ## Outstading features 27 | 28 | - **PJON serial strategy** 29 | - [x] receive without ACK from local bus 30 | - [x] receive with ACK 31 | - [x] send without ACK to local bus 32 | - [x] send with ACK 33 | 34 | - **PJON protocol** 35 | - [x] receive 36 | - [x] send 37 | - [x] update 38 | - [ ] repetitive send 39 | - [x] local bus support 40 | - [x] including sender ID 41 | - [ ] shared bus support 42 | - [ ] auto addressing (PJON v5 feature) 43 | 44 | - **Public API** 45 | - [ ] blocking [implementing] 46 | - [x] non-blocking 47 | 48 | - Auto-discover of serial-PJON bridge 49 | 50 | PJON-python versions are aligned with PJON versions to indicate compatibility with C implementation for uC platforms. 51 | 52 | #### v4 goals: 53 | - local and remote serial port support with auto-discovery of the serial2pjon proxy arduino 54 | - PJON serial strategy for local bus with ACK support [done] 55 | - full PJON serial protocol for serial strategy (remote buses support) 56 | 57 | #### v5 goals: 58 | - auto addressing 59 | 60 | 61 | ## Minimal client example 62 | ```python 63 | from pjon_python.base_client import PjonBaseSerialClient 64 | import time 65 | 66 | pjon_cli = PjonBaseSerialClient(1, 'COM6') 67 | pjon_cli.start_client() 68 | 69 | 70 | def receive_handler(payload, packet_length, packet_info): 71 | print "received packet from device %s with payload: %s" % (packet_info.sender_id, payload) 72 | 73 | pjon_cli.set_receive(receive_handler) 74 | 75 | while True: 76 | # recipient id payload 77 | pjon_cli.send(35, 'C123456789') # payload can be string or an array of bytes (or any type suitable for casting to byte) 78 | time.sleep(1) 79 | ``` 80 | -------------------------------------------------------------------------------- /tests/test_pjonPiperClients_packetsProcessor.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pjon_python.wrapper_client import ReceivedPacketsProcessor 3 | from pjon_python.wrapper_client import PjonPiperClient 4 | import mock 5 | 6 | 7 | class TestPipierClientPacketsProcessor(TestCase): 8 | def setUp(self): 9 | self.valid_packet_str = "#RCV snd_id=44 snd_net=204.204.204.204 rcv_id=45 rcv_net=205.205.205.205 id=0 hdr=6 pckt_cnt=38 len=8 data=ABC test" 10 | self.valid_error_str = "#ERR code=5 data=10" 11 | with mock.patch('pjon_python.wrapper_client.PjonPiperClient.get_coms', create=True) as get_coms_mock: 12 | get_coms_mock.return_value=["test"] 13 | with mock.patch('pjon_python.wrapper_client.PjonPiperClient.Watchdog', create=True) as watchdog_mock: 14 | self._rcvd_packets_processor = ReceivedPacketsProcessor(PjonPiperClient(com_port="test")) 15 | 16 | def test_is_text_line_received_error_info__should_detect_error_lines(self): 17 | self.assertFalse(self._rcvd_packets_processor.is_text_line_received_error_info(" ")) 18 | self.assertFalse(self._rcvd_packets_processor.is_text_line_received_error_info("error something")) 19 | 20 | self.assertTrue(self._rcvd_packets_processor.is_text_line_received_error_info("#ERR something")) 21 | 22 | def test_get_from_error_string_code(self): 23 | self.assertEqual(5, self._rcvd_packets_processor.get_from_error_string__code(self.valid_error_str)) 24 | 25 | def test_get_from_error_string_data(self): 26 | self.assertEqual(10, self._rcvd_packets_processor.get_from_error_string__data(self.valid_error_str)) 27 | 28 | def test__is_text_line_received_packet_info__should_detect_rcvd_lines(self): 29 | 30 | self.assertFalse(self._rcvd_packets_processor.is_text_line_received_packet_info(" ")) 31 | self.assertFalse(self._rcvd_packets_processor.is_text_line_received_packet_info("received something")) 32 | 33 | self.assertTrue(self._rcvd_packets_processor.is_text_line_received_packet_info("#RCV something")) 34 | 35 | def test_get_from_packet_string__snd_id(self): 36 | self.assertEqual(44, self._rcvd_packets_processor.get_from_packet_string__snd_id(self.valid_packet_str)) 37 | 38 | def test_get_from_packet_string__snd_net(self): 39 | self.assertEqual([204, 204, 204, 204], self._rcvd_packets_processor.get_from_packet_string__snd_net(self.valid_packet_str)) 40 | 41 | def test_get_from_packet_string__rcv_id(self): 42 | self.assertEqual(45, self._rcvd_packets_processor.get_from_packet_string__rcv_id(self.valid_packet_str)) 43 | 44 | def test_get_from_packet_string__rcv_net(self): 45 | self.assertEqual([205, 205, 205, 205], self._rcvd_packets_processor.get_from_packet_string__rcv_net(self.valid_packet_str)) 46 | 47 | def test_get_from_packet_string__data_len(self): 48 | self.assertEqual(8, self._rcvd_packets_processor.get_from_packet_string__data_len(self.valid_packet_str)) 49 | 50 | def test_get_from_packet_string__data(self): 51 | self.assertEqual('ABC test', self._rcvd_packets_processor.get_from_packet_string__data(self.valid_packet_str)) 52 | 53 | def test_get_packet_info_obj_for_packet_string__should_convert_packet_string(self): 54 | packet = self._rcvd_packets_processor.get_packet_info_obj_for_packet_string(self.valid_packet_str) 55 | self.assertNotEquals(None, packet) 56 | self.assertEqual(44, packet.packet_info.sender_id) 57 | self.assertEqual(45, packet.packet_info.receiver_id) 58 | self.assertEqual([205, 205, 205, 205], packet.packet_info.receiver_bus_id) 59 | self.assertEqual([204, 204, 204, 204], packet.packet_info.sender_bus_id) 60 | self.assertEqual('ABC test', packet.payload) 61 | self.assertEqual('ABC test', packet.payload_as_string) 62 | self.assertEqual(['A', 'B', 'C', ' ', 't', 'e', 's', 't'], packet.payload_as_chars) 63 | self.assertEqual([65, 66, 67, 32, 116, 101, 115, 116], packet.payload_as_bytes) 64 | -------------------------------------------------------------------------------- /tests/test_serialOverRedis.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import mock 3 | import time 4 | import fakeredis 5 | 6 | from pjon_python.utils import fakeserial 7 | from pjon_python import over_redis_mock_client 8 | 9 | 10 | class TestSerialOverRedis(TestCase): 11 | def setUp(self): 12 | redis_cli = fakeredis.FakeStrictRedis() 13 | self.ser_cli_1 = fakeserial.Serial(port='COM1', baudrate=9600, 14 | transport=redis_cli) 15 | 16 | self.ser_cli_2 = fakeserial.Serial(port='COM1', baudrate=9600, 17 | transport=redis_cli) 18 | 19 | def test_write__should_deliver_to_other_clients_but_not_to_originator(self): 20 | self.ser_cli_1.write('a') 21 | self.assertEquals(['a'], self.ser_cli_2.read()) 22 | self.assertEquals([], self.ser_cli_1.read()) 23 | 24 | self.ser_cli_1.write('abc') 25 | self.assertEquals(3, self.ser_cli_2.inWaiting()) 26 | self.assertEquals(['a'], self.ser_cli_2.read()) 27 | self.assertEquals(['b'], self.ser_cli_2.read()) 28 | self.assertEquals(['c'], self.ser_cli_2.read()) 29 | self.assertEquals(0, self.ser_cli_2.inWaiting()) 30 | 31 | self.assertEquals(0, self.ser_cli_1.inWaiting()) 32 | self.assertEquals([], self.ser_cli_1.read()) 33 | 34 | def test_redis_client__should_call_receive_function_when_receives_publish_msg_with_own_id_as_receiver(self): 35 | redis_cli = fakeredis.FakeStrictRedis() 36 | with mock.patch('pjon_python.over_redis_mock_client.OverRedisClient.dummy_receiver_forward', create=True) as rcv_fwd_mock: 37 | with mock.patch('pjon_python.over_redis_mock_client.OverRedisClient.dummy_receiver', create=True) as rcv_mock: 38 | rc = over_redis_mock_client.OverRedisClient(com_port="COMX", bus_addr=99, baud=115200, 39 | transport=redis_cli) 40 | rc.start_client() 41 | self.assertFalse(rcv_fwd_mock.called) 42 | self.assertFalse(rcv_mock.called) 43 | test_msg = { 44 | "sender_bus_id": [0, 0, 0, 0], 45 | "receiver_id": 99, 46 | "payload_length": 10, 47 | "sender_id": 22, 48 | "receiver_bus_id": [0, 0, 0, 0], 49 | "originator_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 50 | "payload": "B test 123"} 51 | redis_cli.publish('pjon-python-redis', test_msg) 52 | time.sleep(0.1) 53 | self.assertFalse(rcv_fwd_mock.called) 54 | self.assertTrue(rcv_mock.called) 55 | 56 | def test_redis_client__should_call_forward_function_when_receives_publish_msg_with_own_id_as_sender(self): 57 | redis_cli = fakeredis.FakeStrictRedis() 58 | with mock.patch('pjon_python.over_redis_mock_client.OverRedisClient.dummy_receiver_forward', create=True) as rcv_fwd_mock: 59 | with mock.patch('pjon_python.over_redis_mock_client.OverRedisClient.dummy_receiver', create=True) as rcv_mock: 60 | rc = over_redis_mock_client.OverRedisClient(com_port="COMX", bus_addr=99, baud=115200, 61 | transport=redis_cli) 62 | rc.start_client() 63 | self.assertFalse(rcv_fwd_mock.called) 64 | self.assertFalse(rcv_mock.called) 65 | test_msg = { 66 | "sender_bus_id": [0, 0, 0, 0], 67 | "receiver_id": 22, 68 | "payload_length": 10, 69 | "sender_id": 99, 70 | "receiver_bus_id": [0, 0, 0, 0], 71 | "originator_uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", 72 | "payload": "B test 123"} 73 | redis_cli.publish('pjon-python-redis', test_msg) 74 | time.sleep(0.1) 75 | self.assertTrue(rcv_fwd_mock.called) 76 | self.assertFalse(rcv_mock.called) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | PJON-python 2 | =========== 3 | 4 | Pythonic interface to PJON communication protocol. 5 | 6 | PJON (Github: `PJON `__ ) is an 7 | open-source, multi-master, multi-media (one-wire, two-wires, radio) 8 | communication protocol available for various platforms (Arduino/AVR, 9 | ESP8266, Teensy). 10 | 11 | PJON is one of very few open-source implementations of multi-master 12 | communication protocols for microcontrollers. 13 | 14 | PJON-python module in the current status enables communication with 15 | other PJON devices directly over UART (serial communication): 16 | 17 | - In a basic scenario PJON + PJON-python can be a viable alternative to 18 | more complex protocols like Firmata (Arduino firmware on Github: 19 | `firmata/Arduino `__ for direct 20 | (SIMPLEX) communication host-uC. 21 | - If RS485 drivers with auto-tx setup are used the host (e.g. Raspberry 22 | PI) can join multi-master PJON bus and python programs can 23 | communicate with multiple uC. 24 | - If uC with proxy firmware (sending packets between serial and other 25 | type of PJON bus) is used the host can communicate with PJON buses 26 | other than serial through that serial2pjon proxy uC. 27 | 28 | PJON-python module opens popular uC platforms (Arduino, ESP8266, Teensy) 29 | to the whole range of applications: - multi-master automation (reporting 30 | by exception lowers latency compared to polling-based protocols like 31 | Modbus) - open-hardware IoT (thanks to integration flexibility of other 32 | python modules) 33 | 34 | Current status: 35 | --------------- 36 | 37 | - work in progress, minimal client operational with PJON v4.2 or v4.3 38 | (ThroughHardwareSerial strategy is required) 39 | - initially PHY abstraction to BitBang and OverSampling strategies 40 | provided by a serial-PJON bridge implemented as Arduino sketch 41 | - support for ThroughHardwareSerial strategy in HALF\_DUPLEX, 42 | multi-master communication mode e.g. over RS485 bus is provided 43 | directly (serial-RS485 converter required) 44 | - support for ThroughHardwareSerial strategy in SIMPLEX communication 45 | mode will be provided directly (e.g. to talk to a single Arduino). 46 | - Communication with a single arduino connected to USB works in 47 | HALF\_DUPLEX mode out of the box without any additional hardware 48 | 49 | outstading features 50 | ------------------- 51 | 52 | - PJON serial strategy 53 | - receive without ACK from local bus [done] 54 | - receive with ACK [done] 55 | - send without ACK to local bus [done] 56 | - send with ACK [done] 57 | - PJON protocol 58 | - receive [done] 59 | - send [done] 60 | - update [done] 61 | - repetitive send 62 | - local bus support [done] 63 | - including sender ID [done] 64 | - shared bus support 65 | - auto addressing (PJON v5 feature) 66 | - public api 67 | - blocking [implementing] 68 | - non-blocking [done] 69 | - auto-discover of serial-PJON bridge 70 | 71 | PJON-python versions are aligned with PJON versions to indicate 72 | compatibility with C implementation for uC platforms. 73 | 74 | v4 goals: 75 | ^^^^^^^^^ 76 | 77 | - local and remote serial port support with auto-discovery of the 78 | serial2pjon proxy arduino 79 | - PJON serial strategy for local bus with ACK support [done] 80 | - full PJON serial protocol for serial strategy (remote buses support) 81 | 82 | v5 goals: 83 | ^^^^^^^^^ 84 | 85 | - auto addressing 86 | 87 | minimal client example 88 | 89 | .. code:: python 90 | 91 | from pjon_python.base_client import PjonBaseSerialClient 92 | import time 93 | 94 | pjon_cli = PjonBaseSerialClient(1, 'COM6') 95 | pjon_cli.start_client() 96 | 97 | 98 | def receive_handler(payload, packet_length, packet_info): 99 | print "received packet from device %s with payload: %s" % (packet_info.sender_id, payload) 100 | 101 | pjon_cli.set_receive(receive_handler) 102 | 103 | while True: 104 | # recipient id payload 105 | pjon_cli.send(35, 'C123456789') # payload can be string or an array of bytes (or any type suitable for casting to byte) 106 | time.sleep(1) 107 | -------------------------------------------------------------------------------- /pjon_python/strategies/pjon_hwserial_strategy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | from serial import SerialTimeoutException 5 | 6 | from pjon_python.protocol import pjon_protocol_constants 7 | 8 | log = logging.getLogger("ser-strat") 9 | 10 | THROUGH_HARDWARE_SERIAL_MAX_TIME_TO_WAIT_FOR_INCOMING_BYTE = 0.01 11 | THROUGH_HARDWARE_SERIAL_MAX_TIME_TO_WAIT_FOR_RESPONSE_BYTE = 0.5 12 | THROUGH_HARDWARE_SERIAL_MIN_TIME_CHANNEL_CLEARANCE = 0.0001 13 | 14 | 15 | class UnsupportedPayloadType(Exception): 16 | pass 17 | 18 | 19 | class PJONserialStrategy(object): 20 | def __init__(self, serial_port=None): 21 | if serial_port is None: 22 | raise NotImplementedError("serial==None but autodiscovery of serial-pjon proxy is not imeplemented yet") 23 | else: 24 | log.info("passed serial port %s" % serial_port) 25 | self._ser = serial_port 26 | if self._ser.closed: 27 | log.debug("openning serial") 28 | self._ser.open() 29 | 30 | # time.sleep(3) 31 | self._ser.flushInput() 32 | self._ser.flushOutput() 33 | self._read_buffer = [] 34 | self._READ_BUFFER_SIZE = 32768 35 | 36 | self._last_received_ts = 0 37 | 38 | def can_start(self): 39 | if self._ser: 40 | if self._ser.inWaiting() == 0: 41 | if time.time() - self._last_received_ts > THROUGH_HARDWARE_SERIAL_MIN_TIME_CHANNEL_CLEARANCE: 42 | return True 43 | return False 44 | 45 | def send_byte(self, b): 46 | try: 47 | if type(b) is str and len(b) == 1: 48 | log.debug("sending byte: %s (%s)" % (b, ord(b))) 49 | self._ser.write(b) 50 | elif type(b) is int: 51 | b = chr(b) 52 | if type(b) is str and len(b) == 1: 53 | log.debug("sending byte: %s (%s)" % (b, ord(b))) 54 | self._ser.write(b) 55 | else: 56 | raise TypeError 57 | else: 58 | raise TypeError 59 | except TypeError: 60 | raise UnsupportedPayloadType("byte type should be str length 1 or int but %s found" % type(b)) 61 | 62 | except SerialTimeoutException: 63 | log.exception("write timeout") 64 | 65 | return 0 66 | 67 | def receive_byte(self, is_ack_response=False): 68 | # FIXME: move serial port reading to thread reading input to queue and change receive_byte to read from queue 69 | ##log.debug(" >>> rcv byte") 70 | if len(self._read_buffer) > 0: 71 | return self._read_buffer.pop() 72 | 73 | start_time = time.time() 74 | receive_wait_time = THROUGH_HARDWARE_SERIAL_MAX_TIME_TO_WAIT_FOR_INCOMING_BYTE 75 | if is_ack_response: 76 | receive_wait_time = THROUGH_HARDWARE_SERIAL_MAX_TIME_TO_WAIT_FOR_RESPONSE_BYTE 77 | while time.time() - start_time < receive_wait_time: 78 | try: 79 | if True: 80 | rcv_vals = self._ser.read(size=1) 81 | for rcv_val in rcv_vals: 82 | if rcv_val != '': 83 | # log.debug(" > received byte: %s (%s)" % (ord(rcv_val), rcv_val)) 84 | self._last_received_ts = time.time() 85 | return ord(rcv_val) 86 | ''' 87 | bytes_waiting = self._ser.inWaiting() 88 | if bytes_waiting > 0: # bug in pyserial? for single byte 0 is returned 89 | #log.debug(" >> waiting bytes: %s" % bytes_waiting) 90 | rcv_vals = self._ser.read(size=1) 91 | for rcv_val in rcv_vals: 92 | if rcv_val != '': 93 | #log.debug(" > received byte: %s (%s)" % (ord(rcv_val), rcv_val)) 94 | self._last_received_ts = time.time() 95 | #self._read_buffer.append(ord(rcv_val)) 96 | return ord(rcv_val) 97 | #if len(self._read_buffer) > 0: 98 | # if len(self._read_buffer) > self._READ_BUFFER_SIZE: 99 | # log.error("serial read buffer over max size trimming last bytes") 100 | # self._read_buffer = self._read_buffer[:self._READ_BUFFER_SIZE] 101 | # return self._read_buffer.pop() 102 | #time.sleep(0.0005) 103 | ''' 104 | 105 | except StopIteration: # needed for mocking in unit tests 106 | pass 107 | return pjon_protocol_constants.FAIL 108 | 109 | def receive_response(self): 110 | return self.receive_byte(is_ack_response=True) 111 | 112 | def send_response(self, response): 113 | self.send_byte(response) 114 | 115 | 116 | -------------------------------------------------------------------------------- /pjon_python/over_redis_mock_client.py: -------------------------------------------------------------------------------- 1 | from pjon_python.utils.RedisConn import RedisConn 2 | import uuid 3 | import time 4 | import logging 5 | import threading 6 | import fakeredis 7 | from pjon_python.protocol.pjon_protocol import PacketInfo 8 | from pjon_python.protocol.pjon_protocol import ReceivedPacket 9 | from redis import ConnectionError 10 | 11 | log = logging.getLogger("over_redis") 12 | 13 | fake_redis_cli = fakeredis.FakeStrictRedis() 14 | instance_id = 0 15 | 16 | 17 | class OverRedisClient(object): 18 | """ class which uses redis (or fakeredis) pub/sub to communicate with other serial 19 | redis clients. It's purpose is to enable PJON-like communication without 20 | OS-level serial port emulators. 21 | """ 22 | 23 | def __init__(self, bus_addr=1, com_port=None, baud=115200, transport=None): 24 | 25 | global instance_id 26 | instance_id += 1 27 | self._uuid = str(uuid.uuid4()) 28 | if transport is None: 29 | 30 | self._transport = RedisConn(fake_redis_cli, 31 | sub_channel='pjon-python-redis', 32 | pub_channel='pjon-python-redis', 33 | cli_id=self._uuid) 34 | log.debug("using fakeredis transport") 35 | else: 36 | while True: 37 | try: 38 | self._transport = RedisConn(transport, 39 | sub_channel='pjon-python-redis', 40 | pub_channel='pjon-python-redis') 41 | break 42 | except ConnectionError: 43 | log.exception("connection to Redis failed, retrying") 44 | time.sleep(1) 45 | 46 | log.debug("using transport: %s" % str(transport)) 47 | 48 | #self.transport.subscribe('pjon-serial') 49 | self._transport.subscribe('pjon-python-redis') 50 | 51 | self._data = [] 52 | self._started = False 53 | self._bus_addr = bus_addr 54 | self._receiver_function = self.dummy_receiver 55 | self._receiver_function_forward = self.dummy_receiver_forward 56 | self._error_function = self.dummy_error 57 | 58 | @staticmethod 59 | def dummy_receiver(*args, **kwargs): 60 | pass 61 | 62 | @staticmethod 63 | def dummy_receiver_forward(*args, **kwargs): 64 | pass 65 | 66 | @staticmethod 67 | def dummy_error(*args, **kwargs): 68 | pass 69 | 70 | def set_receiver(self, receiver_function): 71 | self._receiver_function = receiver_function 72 | 73 | def set_receiver_forward(self, receiver_function): 74 | self._receiver_function_forward = receiver_function 75 | 76 | def set_error(self, error_function): 77 | self._error_function = error_function 78 | 79 | def start_client(self): 80 | if self._started: 81 | log.info('client already started') 82 | return 83 | log.debug("starting update redis input thd") 84 | self._started = True 85 | run_thd = threading.Thread(target=self.update_redis_input) 86 | run_thd.daemon = True 87 | run_thd.start() 88 | 89 | def stop_client(self): 90 | self._started = False 91 | 92 | def write(self, string): 93 | message = dict() 94 | message['originator_uuid'] = self._uuid 95 | message['payload'] = string 96 | try: 97 | self._transport.publish(message) 98 | except ConnectionError: 99 | log.exception("could not publish to redis") 100 | 101 | def send(self, receiver_id, payload, sender_id=None): 102 | log.debug("sending %s to %s" % (payload, receiver_id)) 103 | packet_message = dict() 104 | packet_message['originator_uuid'] = self._uuid 105 | packet_message['receiver_id'] = receiver_id 106 | packet_message['receiver_bus_id'] = [0, 0, 0, 0] 107 | if sender_id is None: 108 | packet_message['sender_id'] = self._bus_addr 109 | else: 110 | packet_message['sender_id'] = sender_id 111 | packet_message['sender_bus_id'] = [0, 0, 0, 0] 112 | packet_message['payload'] = payload 113 | packet_message['payload_length'] = len(payload) 114 | try: 115 | self._transport.publish(packet_message) 116 | except ConnectionError: 117 | log.exception("could not publish to redis") 118 | 119 | def send_without_ack(self, device_id, payload): 120 | self.send(device_id, payload) 121 | 122 | def send_with_forced_sender_id(self, receiver_id, sender_id, payload): 123 | self.send(receiver_id, payload, sender_id=sender_id) 124 | 125 | @staticmethod 126 | def get_packet_info_obj_for_packet_message(packet_message): 127 | packet_info = PacketInfo() 128 | PacketInfo.receiver_id = packet_message['receiver_id'] 129 | PacketInfo.receiver_bus_id = packet_message['receiver_bus_id'] 130 | PacketInfo.sender_id = packet_message['sender_id'] 131 | PacketInfo.sender_bus_id = packet_message['sender_bus_id'] 132 | payload = packet_message['payload'] 133 | length = packet_message['payload_length'] 134 | 135 | return ReceivedPacket(payload=payload, packet_length=length, packet_info=packet_info) 136 | 137 | def update_redis_input(self): 138 | while True: 139 | if not self._started: 140 | return 141 | new_message = self._transport.listen(rcv_timeout=0.01) 142 | if new_message: 143 | if new_message['originator_uuid'] != self._uuid: 144 | log.debug("received message from someone") 145 | packet = self.get_packet_info_obj_for_packet_message(new_message) 146 | payload = packet.payload_as_string 147 | packet_length = packet.packet_length 148 | packet_info = packet.packet_info 149 | if packet_info.receiver_id != 0: 150 | if packet_info.sender_id == self._bus_addr: 151 | log.debug("packet to be forwarded to: %s" % packet_info.receiver_id) 152 | self._receiver_function_forward(payload, packet_length, packet_info) 153 | continue 154 | elif packet_info.receiver_id != self._bus_addr: 155 | log.debug("packet for someone else: %s" % str(packet_info)) 156 | continue 157 | log.debug("packet for me; calling receiver function") 158 | self._receiver_function(payload, packet_length, packet_info) 159 | else: 160 | log.debug("received own msg") 161 | 162 | def __str__(self): 163 | return "OverRedisClient, transport: %s" % str(self._transport) 164 | -------------------------------------------------------------------------------- /pjon_python/base_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import serial 4 | import logging 5 | import fakeredis 6 | from threading import Thread 7 | from pjon_python.utils import fakeserial 8 | 9 | from pjon_python.protocol import pjon_protocol 10 | from pjon_python.utils import serial_utils, crc8 11 | from pjon_python.strategies import pjon_hwserial_strategy 12 | 13 | bridge_id_response = [hex(ord(item)) for item in 'i_am_serial2pjon'] 14 | bridge_id_query = [hex(ord(item)) for item in 'are_you_serial2pjon'] 15 | 16 | log = logging.getLogger("base-cli") 17 | 18 | 19 | class PjonIoUpdateThread(Thread): 20 | def __init__(self, pjon_protocol): 21 | super(PjonIoUpdateThread, self).__init__() 22 | self._pjon_protocol = pjon_protocol 23 | 24 | def run(self): 25 | iter_cnt = 0 26 | while True: 27 | #if iter_cnt % 1 == 0: 28 | self._pjon_protocol.update() 29 | self._pjon_protocol.receive() 30 | #time.sleep(0.0008) 31 | iter_cnt += 1 32 | 33 | fake_redis_cli = fakeredis.FakeStrictRedis() 34 | 35 | class PjonBaseSerialClient(object): 36 | """ 37 | This class is a base for sync and async clients 38 | It provides reading and writing threads communicating through queues 39 | 40 | If com port is not specified it's assumed serial2pjon proxy is used and all available 41 | COM ports are scanned trying to discover the proxy. 42 | """ 43 | def __init__(self, bus_addr=1, com_port=None, baud=115200, write_timeout=0.005, timeout=0.005, transport=None): 44 | if com_port is None: 45 | raise NotImplementedError("COM port not defined and serial2proxy not supported yet") 46 | #self._com_port = self.discover_proxy() 47 | available_com_ports = serial_utils.get_serial_ports() 48 | if com_port != 'fakeserial': 49 | if com_port not in available_com_ports: 50 | raise EnvironmentError("specified COM port is one of available ports: %s" % available_com_ports) 51 | 52 | if sys.platform.startswith('win'): 53 | self._serial = serial.Serial(com_port, baud, write_timeout=write_timeout, timeout=timeout) 54 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 55 | self._serial = serial.Serial(com_port, baud, writeTimeout=write_timeout, timeout=timeout) 56 | elif sys.platform.startswith('darwin'): 57 | self._serial = serial.Serial(com_port, baud, writeTimeout=write_timeout, timeout=timeout) 58 | else: 59 | raise EnvironmentError('Unsupported platform') 60 | 61 | else: 62 | if transport is None: 63 | self._serial = fakeserial.Serial(com_port, baud, write_timeout=write_timeout, timeout=timeout, 64 | transport=fake_redis_cli) 65 | else: 66 | self._serial = fakeserial.Serial(com_port, baud, write_timeout=write_timeout, timeout=timeout, 67 | transport=transport) 68 | 69 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(self._serial) 70 | self._protocol = pjon_protocol.PjonProtocol(bus_addr, strategy=serial_hw_strategy) 71 | 72 | self._started = False 73 | 74 | def set_receive(self, receive_function): 75 | self._protocol.set_receiver(receive_function) 76 | 77 | def set_error(self, error_function): 78 | self._protocol.set_error(error_function) 79 | 80 | def send(self, device_id, payload): 81 | return self._protocol.send(device_id, payload) 82 | 83 | def send_without_ack(self, device_id, payload): 84 | log.debug("send_without_ack: crafting header >") 85 | header = self._protocol.get_overridden_header(request_ack=False) 86 | log.debug("send_without_ack: crafting header <") 87 | return self._protocol.dispatch(device_id, payload, header=header) 88 | 89 | def send_with_forced_sender_id(self, device_id, sender_id, payload): 90 | header = self._protocol.get_overridden_header(include_sender_info=True) 91 | return self._protocol.dispatch(device_id, payload, header=header, forced_sender_id=sender_id) 92 | 93 | def start_client(self): 94 | if self._started: 95 | log.info('client already started') 96 | return 97 | io_thd = PjonIoUpdateThread(self._protocol) 98 | 99 | io_thd.setDaemon(True) 100 | 101 | io_thd.start() 102 | 103 | def discover_proxy(self): 104 | for com_port_name in serial_utils.get_serial_ports(): 105 | print("checking port: %s" % com_port_name) 106 | ser = serial.Serial(com_port_name, 115200, write_timeout=0.2, timeout=0.5) 107 | try: 108 | if ser.closed: 109 | print("openning") 110 | ser.open() 111 | 112 | time.sleep(2.5) 113 | ser.flushInput() 114 | ser.flushOutput() 115 | 116 | # remaining_bytes : session | CMD | d.a.t.a : CRC8 \n 117 | 118 | packet_input = [ord('\r'), 119 | 14, 120 | ord(':'), ord('R'), 121 | ord('|'), ord('C'), 122 | ord('|'), 1, 2, 3, 4, 5, 6, 123 | ord('|'), ord('8'), 124 | ord('\n')] 125 | hex_string = "".join("%02x" % b for b in packet_input) 126 | 127 | #ser.write(hex_string.decode('hex')) 128 | print("hex arr: %s" % packet_input) 129 | 130 | print("writing") 131 | #ser.flushInput() 132 | #ser.flushOutput() 133 | ser.write(bytearray(packet_input)) 134 | #ser.flushInput() 135 | 136 | resp = "" 137 | read_val = "" 138 | print("reading") 139 | while True: 140 | read_val = ser.read(1) 141 | if len(read_val) == 0: 142 | break 143 | resp += read_val 144 | 145 | print("%s resp>%s<" % (com_port_name, resp)) 146 | return resp 147 | except serial.SerialException: 148 | import traceback 149 | traceback.print_exc(100) 150 | finally: 151 | try: 152 | ser.close() 153 | print("closing") 154 | except serial.SerialException: 155 | traceback.print_exc(100) 156 | 157 | return None 158 | 159 | 160 | def listen_serial(self, timeout=5): 161 | # for com_port_name in serial_utils.get_serial_ports(): 162 | for com_port_name in ['COM31']: 163 | print("checking port: %s" % com_port_name) 164 | ser = serial.Serial(com_port_name, 115200, write_timeout=0.2, timeout=0.5) 165 | try: 166 | if ser.closed: 167 | print("openning") 168 | ser.open() 169 | 170 | #time.sleep(3) 171 | ser.flushInput() 172 | ser.flushOutput() 173 | start_ts = time.time() 174 | resp = "" 175 | while True: 176 | read_val = ser.read(1) 177 | if len(read_val) == 0: 178 | break 179 | resp += str(ord(read_val))+"|" 180 | if time.time() - start_ts > timeout: 181 | break 182 | print(resp) 183 | 184 | #crc = crc8.crc8() 185 | #crc.update(''.join([chr(item) for item in [1,8,2,45,66,66,66,66]])) 186 | #print ord(crc.digest()) 187 | crc = 0 188 | for b in [1,9,2,45,65,65,65,65]: 189 | #for b in [1,8,2,45,65,66,67]: 190 | #for b in [1,21,2,45,49,97,50,115,51,100,52,102,53,103,54,104,55,106,56,107]: 191 | crc = crc8.AddToCRC(b, crc) 192 | print(crc) 193 | 194 | 195 | except serial.SerialException: 196 | import traceback 197 | traceback.print_exc(100) 198 | finally: 199 | try: 200 | ser.close() 201 | print("closing") 202 | except serial.SerialException: 203 | traceback.print_exc(100) -------------------------------------------------------------------------------- /tests/test_pjonProtocol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from unittest import TestCase, skip 4 | 5 | import mock 6 | import serial 7 | 8 | from pjon_python.protocol import pjon_protocol, pjon_protocol_constants 9 | from pjon_python.strategies import pjon_hwserial_strategy 10 | 11 | try: 12 | xrange 13 | except NameError: 14 | xrange = range 15 | 16 | 17 | log = logging.getLogger("tests") 18 | 19 | 20 | class TestPjonProtocol(TestCase): 21 | def setUp(self): 22 | self.rcvd_packets = [] 23 | 24 | def print_args(*args, **kwargs): 25 | log.debug(">> print args") 26 | for arg in args: 27 | log.debug("arg: %s" % arg) 28 | try: 29 | for key, value in kwargs.iteritems(): 30 | log.debug("kwarg: %s=%s" % (key, value)) 31 | except AttributeError: 32 | for key, value in kwargs.items(): 33 | log.debug("kwarg: %s=%s" % (key, value)) 34 | 35 | def test_crc_should_be_calcualted_for_single_char_str(self): 36 | with mock.patch('serial.Serial', create=True) as ser: 37 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 38 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 39 | self.assertEquals(59, proto.compute_crc_8_for_byte('a', 0)) 40 | 41 | def test_crc_should_be_calcualted_for_int(self): 42 | with mock.patch('serial.Serial', create=True) as ser: 43 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 44 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 45 | self.assertEquals(28, proto.compute_crc_8_for_byte(37, 0)) 46 | 47 | def test_crc_should_raise_on_unsupported_types(self): 48 | with mock.patch('serial.Serial', create=True) as ser: 49 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 50 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 51 | self.assertRaises(TypeError, proto.compute_crc_8_for_byte, ['a', 'b'], 0) 52 | self.assertRaises(TypeError, proto.compute_crc_8_for_byte, 'ab', 0) 53 | self.assertRaises(TypeError, proto.compute_crc_8_for_byte, {'a': 2}, 0) 54 | 55 | def test_packet_info__should_interpret_bits_correctly(self): 56 | with mock.patch('serial.Serial', create=True) as ser: 57 | ser.read.side_effect = [chr(item) for item in [1, 9, 2, 45, 65, 65, 65, 65, 71]] 58 | ser.inWaiting.side_effect = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 59 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 60 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 61 | 62 | local_packet_no_ack = [1, 8, 2, 45, 65, 66, 67, 198] 63 | 64 | packet_info = proto.get_packet_info(local_packet_no_ack) 65 | 66 | self.assertEquals(45, packet_info.sender_id) 67 | self.assertEquals(1, packet_info.receiver_id) 68 | self.assertEquals(2, packet_info.header) 69 | 70 | def test_receive_should_get_packet_from_local_bus(self): 71 | with mock.patch('serial.Serial', create=True) as ser: 72 | ser.read.side_effect = [chr(item) for item in [1, 9, 2, 45, 65, 65, 65, 65, 71]] 73 | ser.inWaiting.side_effect = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 74 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 75 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 76 | proto.receiver_function(self.print_args) 77 | 78 | timeout = 0.05 79 | start_ts = time.time() 80 | while True: 81 | proto.receive() 82 | if time.time() - start_ts > timeout: 83 | break 84 | 85 | self.assertEquals(1, len(proto._stored_received_packets)) 86 | self.assertEquals(1, proto._stored_received_packets[-1].packet_info.receiver_id) 87 | self.assertEquals(45, proto._stored_received_packets[-1].packet_info.sender_id) 88 | self.assertEquals([65, 65, 65, 65], proto._stored_received_packets[-1].payload) 89 | self.assertEquals(9, proto._stored_received_packets[-1].packet_length) 90 | 91 | def test_receive_should_get_multiple_packets_from_local_bus(self): 92 | with mock.patch('serial.Serial', create=True) as ser: 93 | ser.read.side_effect = [chr(item) for item in [1, 9, 2, 45, 65, 65, 65, 65, 71, 1, 8, 2, 100, 61, 62, 63, 65]] 94 | ser.inWaiting.side_effect = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1, 0] 95 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 96 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 97 | proto.receiver_function(self.print_args) 98 | 99 | timeout = 0.28 100 | start_ts = time.time() 101 | while True: 102 | proto.receive() 103 | if time.time() - start_ts > timeout: 104 | break 105 | 106 | self.assertEquals(2, len(proto._stored_received_packets)) 107 | self.assertEquals(1, proto._stored_received_packets[-1].packet_info.receiver_id) 108 | self.assertEquals(100, proto._stored_received_packets[-1].packet_info.sender_id) 109 | self.assertEquals([61, 62, 63], proto._stored_received_packets[-1].payload) 110 | self.assertEquals(8, proto._stored_received_packets[-1].packet_length) 111 | 112 | def test_protocol_client_should_truncate_received_packets_buffer(self): 113 | with mock.patch('serial.Serial', create=True) as ser: 114 | single_packet = [chr(item) for item in [1, 9, 2, 45, 65, 65, 65, 65, 71]] 115 | multiple_packets = [] 116 | packets_count = 50 117 | for i in xrange(packets_count): 118 | multiple_packets.extend(single_packet) 119 | 120 | self.assertEquals(packets_count * len(single_packet), len(multiple_packets)) 121 | 122 | ser.read.side_effect = multiple_packets 123 | log.debug(multiple_packets) 124 | bytes_count_arr = [] 125 | for i in xrange(len(multiple_packets)+1): 126 | bytes_count_arr.append(i) 127 | 128 | bytes_count_arr.reverse() 129 | log.debug(bytes_count_arr) 130 | self.assertEquals(bytes_count_arr[0], len(multiple_packets)) 131 | self.assertEquals(bytes_count_arr[-1], 0) 132 | ser.inWaiting.side_effect = bytes_count_arr 133 | 134 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 135 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 136 | proto.receiver_function(self.print_args) 137 | proto._received_packets_buffer_length = 8 138 | 139 | timeout = 1.15 140 | start_ts = time.time() 141 | while True: 142 | proto.receive() 143 | if time.time() - start_ts > timeout: 144 | break 145 | 146 | self.assertEquals(proto._received_packets_buffer_length, len(proto._stored_received_packets)) 147 | self.assertEquals(1, proto._stored_received_packets[-1].packet_info.receiver_id) 148 | self.assertEquals(45, proto._stored_received_packets[-1].packet_info.sender_id) 149 | self.assertEquals([65, 65, 65, 65], proto._stored_received_packets[-1].payload) 150 | self.assertEquals(9, proto._stored_received_packets[-1].packet_length) 151 | 152 | @skip("hardware-dependant test skipped") 153 | def test_receive_should_get_packets_from_real_hardware(self): 154 | ser = serial.Serial('COM31', 115200, write_timeout=0.2, timeout=0.5) 155 | 156 | log.debug(">>> strategy init") 157 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 158 | log.debug("<<< strategy init") 159 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 160 | # self.assertEquals(pjon_protocol_constants.ACK, proto.receive()) 161 | 162 | timeout = 1.5 163 | start_ts = time.time() 164 | while True: 165 | proto.receive() 166 | if time.time() - start_ts > timeout: 167 | break 168 | 169 | self.assertEquals(1, proto._stored_received_packets[-1].packet_info.receiver_id) 170 | self.assertEquals(45, proto._stored_received_packets[-1].packet_info.sender_id) 171 | self.assertEquals([65, 65, 65, 65], proto._stored_received_packets[-1].payload) 172 | self.assertEquals(9, proto._stored_received_packets[-1].packet_length) 173 | 174 | @skip("hardware-dependant test skipped") 175 | def test_receive_should_get_packets_from_real_hardware_2(self): 176 | ser = serial.Serial('COM6', 115200, write_timeout=0.2, timeout=0.5) 177 | time.sleep(2.5) 178 | log.debug(">>> strategy init") 179 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 180 | log.debug("<<< strategy init") 181 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 182 | # self.assertEquals(pjon_protocol_constants.ACK, proto.receive()) 183 | 184 | timeout = 2 185 | start_ts = time.time() 186 | while True: 187 | proto.receive() 188 | if time.time() - start_ts > timeout: 189 | break 190 | 191 | self.assertEquals(1, proto._stored_received_packets[-1].packet_info.receiver_id) 192 | self.assertEquals(35, proto._stored_received_packets[-1].packet_info.sender_id) 193 | self.assertEquals([65, 65, 65, 65], proto._stored_received_packets[-1].payload) 194 | self.assertEquals(9, proto._stored_received_packets[-1].packet_length) 195 | 196 | def test_protocol_client_should_send_packets_with_ack(self): 197 | with mock.patch('serial.Serial', create=True) as ser: 198 | log.debug(">>> strategy init") 199 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 200 | log.debug("<<< strategy init") 201 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 202 | 203 | proto.set_sender_info(False) 204 | proto.set_acknowledge(True) 205 | ser.read.side_effect = [chr(pjon_protocol_constants.ACK)] 206 | ser.inWaiting.side_effect = [0, 1] 207 | 208 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(1, "test", sender_id=11)) 209 | 210 | self.assertEquals(8, ser.write.call_count) 211 | self.assertEquals(1, ser.read.call_count) 212 | 213 | def test_protocol_client_should_send_packets_without_ack(self): 214 | with mock.patch('serial.Serial', create=True) as ser: 215 | log.debug(">>> strategy init") 216 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 217 | log.debug("<<< strategy init") 218 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 219 | proto.set_sender_info(False) 220 | proto.set_acknowledge(False) 221 | ser.read.side_effect = ['1'] 222 | ser.inWaiting.side_effect = [0] 223 | 224 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(1, "test", sender_id=2)) 225 | 226 | self.assertEquals(8, ser.write.call_count) 227 | self.assertEquals(0, ser.read.call_count) 228 | 229 | def test_protocol_client_should_send_packets_with_sender_info_without_ack(self): 230 | with mock.patch('serial.Serial', create=True) as ser: 231 | log.debug(">>> strategy init") 232 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 233 | log.debug("<<< strategy init") 234 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 235 | proto.set_sender_info(True) 236 | proto.set_acknowledge(False) 237 | ser.read.side_effect = ['1'] 238 | ser.inWaiting.side_effect = [0] 239 | 240 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(1, "test", sender_id=13)) 241 | 242 | self.assertEquals(9, ser.write.call_count) 243 | self.assertEquals(0, ser.read.call_count) 244 | 245 | def test_protocol_client_should_send_packets_with_sender_info_with_ack(self): 246 | with mock.patch('serial.Serial', create=True) as ser: 247 | log.debug(">>> strategy init") 248 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 249 | log.debug("<<< strategy init") 250 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 251 | proto.set_sender_info(True) 252 | proto.set_acknowledge(True) 253 | ser.read.side_effect = [chr(pjon_protocol_constants.ACK)] 254 | ser.inWaiting.side_effect = [0, 1] 255 | 256 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(1, "test", sender_id=2)) 257 | 258 | self.assertEquals(9, ser.write.call_count) 259 | self.assertEquals(1, ser.read.call_count) 260 | 261 | @skip("hardware-dependant test skipped") 262 | def test_send_string__should_send_packet_without_ack_to_real_hardware(self): 263 | ser = serial.Serial('COM6', 115200, write_timeout=0.2, timeout=0.5) 264 | ser.flushInput() 265 | ser.flushInput() 266 | time.sleep(2.5) 267 | log.debug(">>> strategy init") 268 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 269 | log.debug("<<< strategy init") 270 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 271 | proto.set_acknowledge(False) 272 | for i in range(20): 273 | print(proto.send_string(35, "C123")) 274 | time.sleep(0.1) 275 | 276 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(35, "C123")) 277 | 278 | @skip("hardware-dependant test skipped") 279 | def test_send_string__should_send_packet_with_ack_to_real_hardware(self): 280 | ser = serial.Serial('COM6', 115200, write_timeout=0.2, timeout=0.5) 281 | ser.flushInput() 282 | ser.flushInput() 283 | time.sleep(2.5) 284 | log.debug(">>> strategy init") 285 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 286 | log.debug("<<< strategy init") 287 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 288 | proto.set_acknowledge(True) 289 | 290 | ''' 291 | #define MODE_BIT 1 // 1 - Shared | 0 - Local 292 | #define SENDER_INFO_BIT 2 // 1 - Sender device id + Sender bus id if shared | 0 - No info inclusion 293 | #define ACK_REQUEST_BIT 4 // 1 - Request synchronous acknowledge | 0 - Do not request acknowledge 294 | ''' 295 | 296 | for i in range(20): 297 | print(proto.send_string(35, "C123", packet_header=4)) # [0, 0, 1]: Local bus | No sender info included | Acknowledge requested 298 | time.sleep(0.1) 299 | 300 | self.assertEquals(pjon_protocol_constants.ACK, proto.send_string(35, "C123", packet_header=4)) 301 | 302 | def test_get_header_from_internal_config__should_return_header_for_all_supported_internal_configurations(self): 303 | with mock.patch('serial.Serial', create=True) as ser: 304 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 305 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 306 | 307 | self.assertEquals(True, (proto.get_header_from_internal_config() & pjon_protocol_constants.ACK_REQUEST_BIT) >> proto.get_bit_index_by_value( 308 | pjon_protocol_constants.ACK_REQUEST_BIT)) 309 | self.assertEquals(True, (proto.get_header_from_internal_config() & pjon_protocol_constants.SENDER_INFO_BIT) >> proto.get_bit_index_by_value( 310 | pjon_protocol_constants.SENDER_INFO_BIT)) 311 | self.assertEquals(False, (proto.get_header_from_internal_config() & pjon_protocol_constants.MODE_BIT) >> proto.get_bit_index_by_value( 312 | pjon_protocol_constants.MODE_BIT)) 313 | 314 | proto.set_sender_info(True) 315 | self.assertEquals(True, ( 316 | proto.get_header_from_internal_config() & pjon_protocol_constants.SENDER_INFO_BIT) >> proto.get_bit_index_by_value( 317 | pjon_protocol_constants.SENDER_INFO_BIT)) 318 | 319 | proto.set_shared_network(True) 320 | self.assertEquals(True, ( 321 | proto.get_header_from_internal_config() & pjon_protocol_constants.MODE_BIT) >> proto.get_bit_index_by_value( 322 | pjon_protocol_constants.MODE_BIT)) 323 | 324 | def test_get_overridden_header__should_overwrite_header_bits(self): 325 | with mock.patch('serial.Serial', create=True) as ser: 326 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 327 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 328 | 329 | self.assertEquals(0, proto.get_overridden_header(request_ack=False, 330 | shared_network_mode=False, 331 | include_sender_info=False)) 332 | 333 | self.assertEquals(1, proto.get_overridden_header(request_ack=False, 334 | shared_network_mode=True, 335 | include_sender_info=False)) 336 | 337 | self.assertEquals(2, proto.get_overridden_header(request_ack=False, 338 | shared_network_mode=False, 339 | include_sender_info=True)) 340 | 341 | self.assertEquals(4, proto.get_overridden_header(request_ack=True, 342 | shared_network_mode=False, 343 | include_sender_info=False)) 344 | 345 | def test_send__should_call_dispatch(self): 346 | with mock.patch('serial.Serial', create=True) as ser: 347 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 348 | with mock.patch('pjon_python.protocol.pjon_protocol.PjonProtocol.dispatch', create=True) as dispatch_mock: 349 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 350 | proto.send(1, 'test') 351 | self.assertEquals(1, dispatch_mock.call_count) 352 | 353 | def test_dispatch_should_put_new_outgoing_packet_to_outgoing_packets_buffer(self): 354 | with mock.patch('serial.Serial', create=True) as ser: 355 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 356 | 357 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 358 | self.assertEquals(0, proto.send(17, 'test')) 359 | 360 | self.assertEquals(1, len(proto.outgoing_packets)) 361 | 362 | self.assertEquals(proto.outgoing_packets[-1].state, pjon_protocol_constants.TO_BE_SENT) 363 | self.assertEquals(proto.outgoing_packets[-1].content, 'test') 364 | self.assertEquals(proto.outgoing_packets[-1].device_id, 17) 365 | 366 | def test_dispatch_should_fail_on_too_many_outgoing_packets(self): 367 | with mock.patch('serial.Serial', create=True) as ser: 368 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 369 | 370 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 371 | for i in range(pjon_protocol_constants.MAX_PACKETS + 1): 372 | self.assertEquals(i, proto.send(i, 'test')) 373 | 374 | self.assertEquals(pjon_protocol_constants.FAIL, proto.send(1, 'test')) 375 | 376 | def test_update_should_send_new_packet(self): 377 | with mock.patch('serial.Serial', create=True) as ser: 378 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 379 | 380 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 381 | proto.set_acknowledge(False) 382 | 383 | serial_hw_strategy.can_start = mock.Mock() 384 | serial_hw_strategy.can_start.return_value = True 385 | 386 | self.assertEquals(0, proto.send(1, 'test0')) 387 | self.assertEquals(1, proto.send(2, 'test1')) 388 | self.assertEquals(2, proto.send(3, 'test2')) 389 | self.assertEquals(3, proto.send(4, 'test3')) 390 | self.assertEquals(4, proto.send(5, 'test4')) 391 | self.assertEquals(5, proto.send(6, 'test5')) 392 | self.assertEquals(6, proto.send(7, 'test6')) 393 | 394 | self.assertEquals(7, len(proto.outgoing_packets)) 395 | proto.update() 396 | self.assertEquals(0, len(proto.outgoing_packets)) 397 | 398 | def test_update_should_execute_error_handled_on_failed_max_attempts_send__and_delete_on_exceeded_max_attempts(self): 399 | with mock.patch('serial.Serial', create=True) as ser: 400 | serial_hw_strategy = pjon_hwserial_strategy.PJONserialStrategy(ser) 401 | single_packet = [''] 402 | multiple_packets = [] 403 | pjon_protocol_constants.MAX_ATTEMPTS = 3 404 | packets_count = pjon_protocol_constants.MAX_ATTEMPTS + 1 405 | for i in xrange(packets_count): 406 | multiple_packets.extend(single_packet) 407 | 408 | self.assertEquals(packets_count * len(single_packet), len(multiple_packets)) 409 | 410 | ser.read.side_effect = multiple_packets # return values (empty) simulating no response to cause failure 411 | 412 | proto = pjon_protocol.PjonProtocol(1, strategy=serial_hw_strategy) 413 | proto.set_acknowledge(True) 414 | 415 | self.assertEquals(0, proto.send(1, 'test0')) 416 | self.assertEquals(1, proto.send(2, 'test1')) 417 | 418 | self.assertEquals(2, len(proto.outgoing_packets)) 419 | with mock.patch('pjon_python.protocol.pjon_protocol.time', create=True) as time_mock: 420 | time_mock.time.side_effect = [time.time() + item for item in range(5 * pjon_protocol_constants.MAX_ATTEMPTS)] 421 | 422 | serial_hw_strategy.can_start = mock.Mock() 423 | serial_hw_strategy.can_start.return_value = True 424 | 425 | ser.inWaiting.return_value = 1 # needed for python 3.5 426 | 427 | for i in xrange(pjon_protocol_constants.MAX_ATTEMPTS): 428 | proto.update() 429 | 430 | self.assertEquals(2, len(proto.outgoing_packets)) 431 | 432 | error_function_mock = mock.Mock() 433 | proto.set_error(error_function_mock) 434 | proto.update() # now failed outstanding packets should be deleted 435 | self.assertEquals(0, len(proto.outgoing_packets)) 436 | 437 | error_function_mock.assert_any_call(pjon_protocol_constants.CONNECTION_LOST, 1) 438 | error_function_mock.assert_any_call(pjon_protocol_constants.CONNECTION_LOST, 2) 439 | self.assertEquals(2, error_function_mock.call_count) 440 | 441 | 442 | -------------------------------------------------------------------------------- /pjon_python/protocol/pjon_protocol.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | import random 4 | import time 5 | 6 | from pjon_python.protocol import pjon_protocol_constants 7 | from pjon_python.utils import crc8 8 | 9 | try: 10 | xrange 11 | except NameError: 12 | xrange = range 13 | 14 | 15 | log = logging.getLogger("pjon-prot") 16 | ''' 17 | PROTOCOL SPEC: 18 | PJON v4.2 19 | 20 | Transmission Response 21 | ____________________________________________________ _____ 22 | | ID | LENGTH | HEADER | SENDER ID | CONTENT | CRC | | ACK | 23 | <-|----|--------|--------|-------------|---------|-----|--> <--|-----| 24 | | 12 | 5 | 001 | ID 11 | 64 | | | 6 | 25 | |____|________|________|_____________|_________|_____| |_____| 26 | 27 | DEFAULT HEADER CONFIGURATION: 28 | [0, 1, 1]: Local bus | Sender info included | Acknowledge requested 29 | 30 | BUS CONFIGURATION: 31 | bus.set_acknowledge(true); 32 | bus.include_sender_info(true); 33 | _________________________________________________________________________________________ 34 | 35 | Transmission 36 | ______________________________________ 37 | | ID | LENGTH | HEADER | CONTENT | CRC | 38 | <-|----|--------|--------|---------|-----|--> 39 | | 12 | 5 | 000 | 64 | | 40 | |____|________|________|_________|_____| 41 | 42 | HEADER CONFIGURATION: 43 | [0, 0, 0]: Local bus | Sender info included | Acknowledge requested 44 | 45 | BUS CONFIGURATION: 46 | bus.set_acknowledge(false); 47 | bus.include_sender_info(false); 48 | _________________________________________________________________________________________ 49 | 50 | A Shared packet transmission example handled in HALF_DUPLEX mode, with acknowledge 51 | request, including the sender info: 52 | 53 | Channel analysis Transmission Response 54 | _____ __________________________________________________________________ _____ 55 | | C-A | | ID | LENGTH | HEADER | BUS ID | BUS ID | ID | CONTENT | CRC | | ACK | 56 | <-|-----|--< >--|----|--------|--------|-------------|--------|----|---------|-----|--> <--|-----| 57 | | 0 | | 12 | 5 | 111 | 0001 | 0001 | 11 | 64 | | | 6 | 58 | |_____| |____|________|________|_____________|________|____|_________|_____| |_____| 59 | |Receiver info| Sender info | 60 | 61 | ''' 62 | 63 | 64 | class PacketInfo(object): 65 | id = 0 66 | header = 0 67 | receiver_id = 0 68 | receiver_bus_id = [0, 0, 0, 0] 69 | sender_id = 0 70 | sender_bus_id = [0, 0, 0, 0] 71 | 72 | def __str__(self): 73 | return "%s -> %s" % (self.sender_id, self.receiver_id) 74 | 75 | 76 | class ReceivedPacket(object): 77 | def __init__(self, payload, packet_length, packet_info): 78 | self._payload = payload 79 | self._packet_length = packet_length 80 | self._packet_info = packet_info 81 | self._receive_ts = None 82 | self._send_ts = None 83 | self._send_attempts_count = None 84 | 85 | @property 86 | def payload(self): 87 | return self._payload 88 | 89 | @property 90 | def payload_as_string(self): 91 | return self.payload 92 | 93 | @property 94 | def payload_as_chars(self): 95 | return [item for item in self._payload] 96 | 97 | @property 98 | def payload_as_bytes(self): 99 | return [ord(item) for item in self._payload] 100 | 101 | @property 102 | def packet_length(self): 103 | return self._packet_length 104 | 105 | @property 106 | def packet_info(self): 107 | return self._packet_info 108 | 109 | def __str__(self): 110 | return "%s [%s]" % (str(self._packet_info), self.payload) 111 | 112 | 113 | class OutgoingPacket(object): 114 | def __init__(self): 115 | self.header = None 116 | self.content = None 117 | self.device_id = None 118 | self.sender_id = None 119 | self.length = None 120 | self.state = 0 121 | self.registration = None 122 | self.timing = None 123 | self.attempts = 0 124 | 125 | def __str__(self): 126 | return "registration: %s. device_id: %s, payload: %s, state: %s, attempts: %s" % (self.registration, self.device_id, self.content, self.state, self.attempts) 127 | 128 | 129 | class PjonProtocol(object): 130 | def __init__(self, device_id, strategy): 131 | self._acknowledge = True 132 | self._sender_info = True 133 | self._router = False 134 | self._strategy = strategy 135 | self._device_id = device_id 136 | self._constanta = pjon_protocol_constants 137 | self._shared = False 138 | self._mode = pjon_protocol_constants.HALF_DUPLEX 139 | self._localhost = [0, 0, 0, 0] 140 | self._bus_id = [0, 0, 0, 0] 141 | self._receiver_function = self.dummy_receiver 142 | self._error_function = self.dummy_error 143 | self._store_packets = True 144 | self._stored_received_packets = [] 145 | self._received_packets_buffer_length = 32 146 | self._bit_index_by_value = { 147 | 1: 0, 148 | 2: 1, 149 | 4: 2, 150 | 8: 3, 151 | 16: 4, 152 | 32: 5, 153 | 64: 6 154 | } 155 | self.outgoing_packets = [] 156 | self._auto_delete = True 157 | 158 | def begin(self): 159 | pass 160 | 161 | @staticmethod 162 | def compute_crc_8_for_byte(input_byte, crc): 163 | try: 164 | return crc8.AddToCRC(input_byte, crc) 165 | except TypeError: 166 | if type(input_byte) is str: 167 | if len(input_byte) == 1: 168 | return crc8.AddToCRC(ord(input_byte), crc) 169 | raise TypeError("unsupported type for crc calculation; should be byte or str length==1") 170 | 171 | def receiver_function(self, new_ref): 172 | self._receiver_function = new_ref 173 | 174 | @property 175 | def bus_id(self): 176 | return self._bus_id 177 | 178 | @property 179 | def localhost(self): 180 | return self._localhost 181 | 182 | @property 183 | def device_id(self): 184 | return self._device_id 185 | 186 | @property 187 | def router(self): 188 | return self._router 189 | 190 | def set_router(self, val): 191 | if val: 192 | self._router = True 193 | self._router = False 194 | 195 | def set_receiver(self, receiver_function): 196 | self._receiver_function = receiver_function 197 | 198 | def set_error(self, error_function): 199 | self._error_function = error_function 200 | 201 | @property 202 | def shared(self): 203 | return self._shared 204 | 205 | @property 206 | def mode(self): 207 | return self._mode 208 | 209 | @property 210 | def strategy(self): 211 | return self._strategy 212 | 213 | def bus_id_equality(self, id_1, id_2): 214 | '''not implemented''' 215 | return False 216 | 217 | def set_acknowledge(self, new_val): 218 | self._acknowledge = new_val 219 | 220 | def set_sender_info(self, new_val): 221 | self._sender_info = new_val 222 | 223 | def set_shared_network(self, new_val): 224 | self._shared = new_val 225 | 226 | @staticmethod 227 | def dummy_receiver(*args, **kwargs): 228 | pass 229 | 230 | @staticmethod 231 | def dummy_error(*args, **kwargs): 232 | pass 233 | 234 | @staticmethod 235 | def get_packet_info(packet): 236 | packet_info = PacketInfo() 237 | packet_info.receiver_id = packet[pjon_protocol_constants.RECEIVER_ID_BYTE_ORDER] 238 | packet_info.header = packet[pjon_protocol_constants.RECEIVER_HEADER_BYTE_ORDER] 239 | if packet_info.header & pjon_protocol_constants.MODE_BIT != 0: 240 | packet_info.receiver_bus_id = packet[pjon_protocol_constants.RECEIVER_BUS_ID_WITH_NET_INFO_BYTE_ORDER] 241 | if packet_info.header & pjon_protocol_constants.SENDER_INFO_BIT != 0: 242 | packet_info.sender_bus_id = 0 # packet + 7); 243 | packet_info.sender_id = packet[pjon_protocol_constants.SENDER_ID_WITH_NET_INFO_BYTE_ORDER] 244 | 245 | elif packet_info.header & pjon_protocol_constants.SENDER_INFO_BIT != 0: 246 | packet_info.sender_id = packet[pjon_protocol_constants.SENDER_ID_WITHOUT_NET_INFO_BYTE_ORDER] 247 | 248 | return packet_info 249 | 250 | def get_bit_index_by_value(self, bit_value): 251 | return self._bit_index_by_value[bit_value] 252 | 253 | def receive(self): 254 | data = [None for item in xrange(pjon_protocol_constants.PACKET_MAX_LENGTH)] 255 | state = 0 256 | packet_length = pjon_protocol_constants.PACKET_MAX_LENGTH 257 | CRC = 0 258 | shared = False 259 | includes_sender_info = False 260 | acknowledge_requested = False 261 | log.debug(">>> new packet assembly") 262 | for i in xrange(pjon_protocol_constants.PACKET_MAX_LENGTH): 263 | log.debug(" >> i: %s" % i) 264 | 265 | data[i] = state = self._strategy.receive_byte() 266 | if state == pjon_protocol_constants.FAIL: 267 | log.debug(" > returning fail - byte was not read") 268 | return pjon_protocol_constants.FAIL 269 | 270 | if i == 0 and data[i] != self._device_id and data[i] != pjon_protocol_constants.BROADCAST and not self.router: 271 | log.debug(" > returning busy; packet for someone else [i: %s, sreceiver: %s target: %s]" % (i, self._device_id, data[i])) 272 | return pjon_protocol_constants.BUSY 273 | 274 | if i == 1: 275 | if data[i] > 4 and data[i] < pjon_protocol_constants.PACKET_MAX_LENGTH: 276 | packet_length = data[i] 277 | else: 278 | log.debug(" > returning fail on wrong packet length") 279 | return pjon_protocol_constants.FAIL 280 | 281 | if i == 2: # Packet header 282 | shared = data[2] & pjon_protocol_constants.MODE_BIT 283 | includes_sender_info = data[2] & pjon_protocol_constants.SENDER_INFO_BIT 284 | acknowledge_requested = data[2] & pjon_protocol_constants.ACK_REQUEST_BIT 285 | if(shared != self.shared) and not self.router: 286 | log.debug(" > ret busy") 287 | return pjon_protocol_constants.BUSY # Keep private and shared buses apart 288 | 289 | """ 290 | If an id is assigned to this bus it means that is potentially 291 | sharing its medium, or the device could be connected in parallel 292 | with other buses. Bus id equality is checked to avoid collision 293 | i.e. id 1 bus 1, should not receive a message for id 1 bus 2. 294 | """ 295 | ''' 296 | if self.shared and shared and not self.router and i > 2 and i < 7: 297 | if bus_id[i - 3] != data[i]: 298 | return pjon_protocol_constants.BUSY 299 | ''' 300 | if i == packet_length - 1: 301 | break 302 | CRC = self.compute_crc_8_for_byte(data[i], CRC) 303 | 304 | data = data[:packet_length] 305 | log.info(" >> packet: %s" % data) 306 | log.info(" >> calc CRC: %s" % CRC) 307 | 308 | if data[-1] == CRC: 309 | log.debug("CRC OK") 310 | if acknowledge_requested and data[0] != pjon_protocol_constants.BROADCAST and self.mode != pjon_protocol_constants.SIMPLEX: 311 | if not self.shared or (self.shared and shared and self.bus_id_equality(data + 3, self.bus_id)): 312 | self.strategy.send_response(pjon_protocol_constants.ACK) 313 | 314 | last_packet_info = self.get_packet_info(data) 315 | payload_offset = 3 316 | 317 | if shared: 318 | if includes_sender_info: 319 | payload_offset += 9 320 | else: 321 | payload_offset += 4 322 | else: 323 | if includes_sender_info: 324 | payload_offset += 1 325 | payload = data[payload_offset:-1] 326 | 327 | log.info(" >> payload: %s" % payload) 328 | 329 | if self._receiver_function is not None: 330 | # payload, length, packietInfo 331 | self._receiver_function(payload, packet_length, last_packet_info) 332 | 333 | if self._store_packets: 334 | packet_to_store = ReceivedPacket(payload, packet_length, last_packet_info) 335 | self._stored_received_packets.append(packet_to_store) 336 | if len(self._stored_received_packets) > self._received_packets_buffer_length: 337 | log.debug("truncating received packets") 338 | self._stored_received_packets = self._stored_received_packets[ 339 | len(self._stored_received_packets) - self._received_packets_buffer_length:] 340 | 341 | return pjon_protocol_constants.ACK 342 | else: 343 | if acknowledge_requested and data[0] != pjon_protocol_constants.BROADCAST and self.mode != pjon_protocol_constants.SIMPLEX: 344 | if not self.shared and ( self.shared and shared and self.bus_id_equality(data + 3, self.bus_id)): 345 | self.strategy.send_response(pjon_protocol_constants.NAK) 346 | return pjon_protocol_constants.NAK 347 | 348 | def send_string(self, recipient_id, string_to_send, sender_id=None, string_length=None, packet_header=None): 349 | log.debug("send_string to device: %s payload: %s header: %s" % (recipient_id, string_to_send, packet_header)) 350 | if packet_header is None: 351 | log.warning("send_string: packed_header is None") 352 | packet_header = self.get_header_from_internal_config() 353 | 354 | if string_length is None: 355 | log.debug("calculating str length") 356 | string_length = len(string_to_send) 357 | 358 | if string_to_send is None: 359 | log.debug("string is None; ret FAIL") 360 | return pjon_protocol_constants.FAIL 361 | 362 | if self.mode != pjon_protocol_constants.SIMPLEX and not self.strategy.can_start(): #FIXME: mode does not chec 363 | log.debug("HALF_DUPLEX and BUSY: ret BUSY") 364 | return pjon_protocol_constants.BUSY 365 | 366 | includes_sender_info = packet_header & pjon_protocol_constants.SENDER_INFO_BIT 367 | 368 | CRC = 0 369 | 370 | # Transmit recipient device id 371 | self.strategy.send_byte(recipient_id) 372 | CRC = self.compute_crc_8_for_byte(recipient_id, CRC) 373 | 374 | packet_meta_size_bytes = 4 375 | if includes_sender_info: 376 | packet_meta_size_bytes += 1 377 | 378 | # Transmit packet length 379 | self.strategy.send_byte(string_length + packet_meta_size_bytes) 380 | CRC = self.compute_crc_8_for_byte(string_length + packet_meta_size_bytes, CRC) 381 | 382 | # Transmit header header 383 | self.strategy.send_byte(packet_header) 384 | CRC = self.compute_crc_8_for_byte(packet_header, CRC) 385 | 386 | ''' If an id is assigned to the bus, the packet's content is prepended by 387 | the ricipient's bus id. This opens up the possibility to have more than 388 | one bus sharing the same medium. ''' 389 | 390 | # transmit sender id if included in header 391 | if includes_sender_info: 392 | self.strategy.send_byte(sender_id) 393 | CRC = self.compute_crc_8_for_byte(sender_id, CRC) 394 | 395 | for i in xrange(string_length): 396 | self.strategy.send_byte(string_to_send[i]) 397 | CRC = self.compute_crc_8_for_byte(string_to_send[i], CRC) 398 | 399 | self.strategy.send_byte(CRC) 400 | 401 | if not (packet_header & pjon_protocol_constants.ACK_REQUEST_BIT > 0): 402 | log.debug("packet_header: %s" % packet_header) 403 | log.debug("no ACK required; ret ACK") 404 | return pjon_protocol_constants.ACK 405 | if (recipient_id == pjon_protocol_constants.BROADCAST): 406 | log.debug("BROADCAST; ret ACK") 407 | return pjon_protocol_constants.ACK 408 | if (self.mode == pjon_protocol_constants.SIMPLEX): 409 | log.debug("SIMPLEX; ret ACK") 410 | return pjon_protocol_constants.ACK 411 | 412 | log.debug(">>> receiving response") 413 | response = self.strategy.receive_response() 414 | log.info("<<< receiving response; received: %s" % response) 415 | 416 | if response == pjon_protocol_constants.ACK: 417 | log.debug("received ACK resp; ret ACK") 418 | return pjon_protocol_constants.ACK 419 | 420 | ''' Random delay if NAK, corrupted ACK/NAK or collision ''' 421 | 422 | if response != pjon_protocol_constants.FAIL: 423 | log.debug("collision or corruption; sleeping") 424 | time.sleep(random.randint(0, pjon_protocol_constants.COLLISION_MAX_DELAY / 1000)) 425 | 426 | # FIXME: original PJON lib does not return anything 427 | return pjon_protocol_constants.BUSY 428 | 429 | if response == pjon_protocol_constants.NAK: 430 | return pjon_protocol_constants.NAK 431 | 432 | return pjon_protocol_constants.FAIL 433 | 434 | def send(self, recipient_id, payload): 435 | return self.dispatch(recipient_id, payload) 436 | 437 | def dispatch(self, recipient_id, payload, header=None, target_net=None, timing=None, forced_sender_id=None): 438 | if header is None: 439 | log.debug("dispatch: getting header from internal config") 440 | header = self.get_header_from_internal_config() 441 | 442 | payload_length = len(payload) 443 | 444 | if payload_length >= pjon_protocol_constants.PACKET_MAX_LENGTH: 445 | log.error("payload too big") 446 | return pjon_protocol_constants.FAIL 447 | 448 | if timing is None: 449 | timing = 0 450 | 451 | if self.shared: 452 | raise NotImplementedError("operation on shared bus (multiple networks) not implemented") 453 | 454 | if len(self.outgoing_packets) <= pjon_protocol_constants.MAX_PACKETS: 455 | outgoing_packet = OutgoingPacket() 456 | outgoing_packet.header = header 457 | outgoing_packet.content = payload 458 | outgoing_packet.device_id = recipient_id 459 | if forced_sender_id is None: 460 | outgoing_packet.sender_id = self.device_id 461 | else: 462 | outgoing_packet.sender_id = forced_sender_id 463 | outgoing_packet.length = len(payload) 464 | outgoing_packet.state = pjon_protocol_constants.TO_BE_SENT 465 | outgoing_packet.registration = time.time() 466 | outgoing_packet.timing = timing 467 | outgoing_packet.attempts = 0 468 | log.debug("adding packet to the outgoing packets list: %s" % str(outgoing_packet)) 469 | self.outgoing_packets.append(outgoing_packet) 470 | 471 | return len(self.outgoing_packets) - 1 472 | 473 | self._error_function(pjon_protocol_constants.PACKETS_BUFFER_FULL, pjon_protocol_constants.MAX_PACKETS) 474 | 475 | return pjon_protocol_constants.FAIL 476 | 477 | def get_header_from_internal_config(self): 478 | header = 0 479 | if self.shared: 480 | log.debug("header for shared bus") 481 | header |= pjon_protocol_constants.MODE_BIT 482 | if self._sender_info: 483 | log.debug("header with sender info") 484 | header |= pjon_protocol_constants.SENDER_INFO_BIT 485 | if self._acknowledge: 486 | log.debug("header requiring ACK") 487 | header |= pjon_protocol_constants.ACK_REQUEST_BIT 488 | 489 | return header 490 | 491 | def get_overridden_header(self, request_ack=True, include_sender_info=False, shared_network_mode=False): 492 | header = self.get_header_from_internal_config() 493 | 494 | if request_ack: 495 | header |= pjon_protocol_constants.ACK_REQUEST_BIT 496 | else: 497 | #header |= ~(0xFF & (1 << self.get_bit_index_by_value(pjon_protocol_constants.ACK_REQUEST_BIT))) 498 | header &= ~(1 << self.get_bit_index_by_value(pjon_protocol_constants.ACK_REQUEST_BIT)) 499 | 500 | if include_sender_info: 501 | header |= pjon_protocol_constants.SENDER_INFO_BIT 502 | else: 503 | header &= ~(1 << self.get_bit_index_by_value(pjon_protocol_constants.SENDER_INFO_BIT)) 504 | 505 | if shared_network_mode: 506 | header |= pjon_protocol_constants.MODE_BIT 507 | else: 508 | header &= ~(1 << self.get_bit_index_by_value(pjon_protocol_constants.MODE_BIT)) 509 | 510 | return header 511 | 512 | def update(self): 513 | log.debug(">>> update") 514 | for outgoing_packet in self.outgoing_packets: 515 | log.debug(" >> processing packet: %s" % outgoing_packet) 516 | if outgoing_packet.state == 0: 517 | log.debug(" > continue on outgoing_packet.state == 0") 518 | continue 519 | 520 | if (time.time() - outgoing_packet.registration)*1000 >= \ 521 | outgoing_packet.timing + math.pow(outgoing_packet.attempts, 3): 522 | log.debug(" sending packet with header: %s" % outgoing_packet.header) 523 | outgoing_packet.state = self.send_string(outgoing_packet.device_id, 524 | outgoing_packet.content, 525 | sender_id=outgoing_packet.sender_id, 526 | packet_header=outgoing_packet.header) 527 | log.info(" > send_string returned: %s" % outgoing_packet.state) 528 | else: 529 | log.debug(" > continue on time.time() - registration > timing + pow(attempts, 3)") 530 | continue 531 | 532 | was_packet_deleted = True 533 | while was_packet_deleted: 534 | was_packet_deleted = False 535 | for outgoing_packet in self.outgoing_packets: 536 | if outgoing_packet.state == pjon_protocol_constants.ACK: 537 | if not outgoing_packet.timing: 538 | if self._auto_delete: 539 | log.debug(" > deleting packet from outgoing buffer") 540 | log.debug(" > buffer before deletion: %s" % str(self.outgoing_packets)) 541 | self.outgoing_packets[:] = [item for item in self.outgoing_packets if item is not outgoing_packet] 542 | log.debug(" > buffer after deletion: %s" % str(self.outgoing_packets)) 543 | was_packet_deleted = True 544 | break 545 | else: 546 | outgoing_packet.attempts = 0 547 | outgoing_packet.registration = time.time() 548 | outgoing_packet.state = pjon_protocol_constants.TO_BE_SENT 549 | 550 | if outgoing_packet.state == pjon_protocol_constants.FAIL: 551 | outgoing_packet.attempts += 1 552 | if outgoing_packet.attempts > pjon_protocol_constants.MAX_ATTEMPTS: 553 | if outgoing_packet.content[0] == pjon_protocol_constants.ACQUIRE_ID: 554 | # FIXME: not really understand why outgoing packets queue would ever get ID acquisition packet? 555 | self._device_id = outgoing_packet.device_id 556 | self.outgoing_packets[:] = [item for item in self.outgoing_packets if 557 | item is not outgoing_packet] 558 | log.debug(" > continue on ACQUIRE ID") 559 | was_packet_deleted = True 560 | break 561 | #continue 562 | else: 563 | self._error_function(pjon_protocol_constants.CONNECTION_LOST, 564 | outgoing_packet.device_id) 565 | 566 | if not outgoing_packet.timing: 567 | if self._auto_delete: 568 | log.debug(" > auto deleting failed packet") 569 | self.outgoing_packets[:] = [item for item in self.outgoing_packets if 570 | item is not outgoing_packet] 571 | was_packet_deleted = True 572 | break 573 | else: 574 | outgoing_packet.attempts = 0 575 | outgoing_packet.registration = time.time() 576 | outgoing_packet.state = pjon_protocol_constants.TO_BE_SENT 577 | #FIXME: original PJON is not re-scheduling failed packets for re-sending 578 | # if delivery failed but attempts below maximum allowable count; fixed below 579 | #else: 580 | # outgoing_packet.state = pjon_protocol_constants.TO_BE_SENT 581 | 582 | return len(self.outgoing_packets) 583 | -------------------------------------------------------------------------------- /pjon_python/wrapper_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | todo: 3 | 1. put PJON-piper compiled binaries to 4 | /pjon_piper_bin/win 5 | /pjon_piper_bin/rpi 6 | 7 | OR maybe require setting PJON_PIPER_PATH ? 8 | 9 | solution: add binary (required path auto-discovery) 10 | but enable path overwriting 11 | 12 | 2. add "subprocess watchdog" to utils 13 | 3. add fakeredis client to utils 14 | or skip and use queues 15 | 16 | 3. inherit from base client in wrapper client 17 | - overload set sync/async 18 | - overload send 19 | - overload receive 20 | 21 | detect platform and launch proper client binary 22 | 23 | communicate with subprocess over fakeredis as done 24 | for supervisor_win 25 | OR through queues as already available in watchdog ? 26 | """ 27 | import os 28 | import sys 29 | import time 30 | import psutil 31 | import atexit 32 | import logging 33 | import platform 34 | import threading 35 | import subprocess 36 | from pjon_python.protocol.pjon_protocol import PacketInfo 37 | from pjon_python.protocol.pjon_protocol import ReceivedPacket 38 | 39 | try: 40 | from Queue import Queue, Empty 41 | except ImportError: 42 | from queue import Queue, Empty # python 3.x 43 | 44 | log = logging.getLogger("pjon-cli") 45 | log.addHandler(logging.NullHandler()) 46 | 47 | 48 | class ComPortUndefinedExc(Exception): 49 | pass 50 | 51 | 52 | class ComPortNotAvailableExc(Exception): 53 | pass 54 | 55 | 56 | class PjonPiperClient(threading.Thread): 57 | def __init__(self, bus_addr=1, com_port=None, baud=115200): 58 | super(PjonPiperClient, self).__init__() 59 | self._pipe = None 60 | self._bus_addr = bus_addr 61 | self._serial_baud = baud 62 | self._piper_client_stdin_queue = Queue() 63 | self._piper_client_stdout_queue = Queue() 64 | self._receiver_function = self.dummy_receiver 65 | self._error_function = self.dummy_error 66 | self._last_watchdog_poll_ts = 0 67 | self._piper_stdout_watchdog_timeout = 0 68 | self._piper_stdout_last_received_ts = 0 69 | 70 | 71 | if sys.platform == 'win32': 72 | self._startupinfo = subprocess.STARTUPINFO() 73 | self._startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 74 | self._startupinfo.wShowWindow = subprocess.SW_HIDE 75 | self._pjon_piper_path = os.path.join(self.get_self_path(), 'pjon_piper_bin', 'win', 'PJON-piper.exe') 76 | elif sys.platform == 'linux2': 77 | #os.setpgrp() 78 | if(self.is_arm_platform()): 79 | if self.is_raspberry(): 80 | self._pjon_piper_path = os.path.join(self.get_self_path(), 'pjon_piper_bin', 'rpi', 81 | 'pjon_piper') 82 | #print(self._pjon_piper_path) 83 | else: 84 | NotImplementedError("Only Linux on Raspberry is supported") 85 | else: 86 | raise NotImplementedError("this version of Linux is not supported yet") 87 | else: 88 | raise NotImplementedError("platform not supported; currently provided support only for: win32") 89 | 90 | if sys.platform == 'win32': 91 | self._pipier_client_subproc_cmd = "%s %s %s %s\n" % (self._pjon_piper_path, com_port.strip(), baud, bus_addr) 92 | elif sys.platform == 'linux2': 93 | self._pipier_client_subproc_cmd = [self._pjon_piper_path, com_port.strip(), str(baud), str(bus_addr)] 94 | if com_port is None: 95 | raise ComPortUndefinedExc("missing com_port kwarg: serial port name is required") 96 | 97 | available_coms = self.get_coms() 98 | if com_port not in available_coms: 99 | raise ComPortNotAvailableExc("com port %s is not available in this system; available ports are: %s" % (com_port, str(available_coms))) 100 | else: 101 | log.info("COM OK: %s" % com_port) 102 | 103 | self._pipier_client_watchdog = WatchDog(suproc_command=self._pipier_client_subproc_cmd, 104 | stdin_queue=self._piper_client_stdin_queue, 105 | stdout_queue=self._piper_client_stdout_queue, 106 | parent = self) 107 | 108 | self._packets_processor = ReceivedPacketsProcessor(self) 109 | 110 | atexit.register(self.stop_client) 111 | 112 | # TODO: 113 | # 3. implement periodic checks if com is available 114 | # if not: restart watchdog (can be a permanent restart; no state machine required) 115 | 116 | @staticmethod 117 | def is_raspberry(): 118 | call_result = subprocess.Popen(['cat', '/sys/firmware/devicetree/base/model'], stdout=subprocess.PIPE) 119 | if call_result.stdout.readline().strip().lower().startswith('raspberry'): 120 | return True 121 | return False 122 | 123 | @staticmethod 124 | def is_arm_platform(): 125 | return bool(sum([item.lower().startswith('armv') for item in platform.uname()])) 126 | 127 | @staticmethod 128 | def get_self_path(): 129 | if sys.platform == 'win32': 130 | return os.path.dirname(os.path.abspath(__file__)) 131 | else: 132 | return os.path.dirname(os.path.abspath(__file__)) 133 | 134 | @staticmethod 135 | def is_string_valid_com_port_name(com_name): 136 | try: 137 | if sys.platform == 'win32': 138 | if com_name.upper().startswith('COM'): 139 | if not com_name.upper().endswith(' '): 140 | if len(com_name) in (4, 5): 141 | if 1 <= int(com_name.upper().split("COM")[-1]) <= 99: 142 | log.debug("COM name matches expected form for Windows OS") 143 | return True 144 | else: 145 | log.debug("com number outside 1-99") 146 | else: 147 | log.debug("wrong length: %s" % len(com_name)) 148 | else: 149 | log.debug("ends with space") 150 | else: 151 | log.debug("not starts with COM") 152 | elif 'linux' in sys.platform: 153 | if '/dev/tty' in com_name: 154 | return True 155 | else: 156 | raise NotImplementedError("platform not supported; currently provided support only for: win32") 157 | except ValueError: 158 | pass 159 | 160 | return False 161 | 162 | @staticmethod 163 | def is_string_valid_pjon_piper_version(version_str): 164 | if version_str.upper().startswith('VERSION:'): 165 | return True 166 | return False 167 | 168 | @staticmethod 169 | def dummy_receiver(*args, **kwargs): 170 | pass 171 | 172 | @staticmethod 173 | def dummy_error(*args, **kwargs): 174 | pass 175 | 176 | @property 177 | def is_piper_stdout_watchdog_enabled(self): 178 | if self._piper_stdout_watchdog_timeout > 0: 179 | return True 180 | return False 181 | 182 | def set_piper_stdout_watchdog(self, timeout_sec=3): 183 | self._piper_stdout_watchdog_timeout = timeout_sec 184 | 185 | def reset_piper_stdout_watchdog(self): 186 | self._piper_stdout_last_received_ts = time.time() 187 | 188 | def should_piper_stdout_watchdog_issue_restart(self): 189 | if time.time() - self._piper_stdout_last_received_ts > self._piper_stdout_watchdog_timeout: 190 | return True 191 | return False 192 | 193 | def set_receiver(self, receiver_function): 194 | self._receiver_function = receiver_function 195 | 196 | def set_error(self, error_function): 197 | self._error_function = error_function 198 | 199 | def get_coms(self): 200 | close_fds = False if sys.platform == 'win32' else True 201 | 202 | if sys.platform == 'win32': 203 | cmd_subprc_pipe = subprocess.Popen("%s coms" % self._pjon_piper_path, shell=False, close_fds=close_fds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, startupinfo=self._startupinfo, env=os.environ) 204 | else: 205 | cmd_subprc_pipe = subprocess.Popen([self._pjon_piper_path, "coms"], shell=False, close_fds=close_fds, 206 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 207 | bufsize=0, env=os.environ) 208 | 209 | coms = [] 210 | #log.debug(">> cmd pipe out") 211 | while cmd_subprc_pipe: 212 | try: 213 | nex_tline = cmd_subprc_pipe.stdout.readline() 214 | if nex_tline == '': 215 | break 216 | #log.debug(nex_tline.strip()) 217 | if self.is_string_valid_com_port_name(nex_tline.strip()): 218 | #log.debug("\_got a com port in the stdout") 219 | coms.append(nex_tline.strip()) 220 | else: 221 | pass 222 | #log.error("suspicious COM name in the output: %s" % nex_tline.strip()) 223 | except AttributeError: 224 | pass 225 | #log.debug("<< cmd pipe out") 226 | cmd_subprc_pipe.terminate() 227 | 228 | if coms == []: 229 | log.warn("PJON-piper returned no serial ports; falling back to pyserial to enumerate available serials") 230 | from serial.tools import list_ports 231 | if sys.platform == 'win32': 232 | coms = [item.device for item in list_ports.comports()] 233 | elif sys.platform == 'linux2': 234 | coms = [item[0] for item in list_ports.comports()] 235 | 236 | return coms 237 | 238 | def get_pjon_piper_version(self): 239 | close_fds = False if sys.platform == 'win32' else True 240 | 241 | if sys.platform == 'win32': 242 | cmd_subprc_pipe = subprocess.Popen("%s version" % self._pjon_piper_path, shell=False, close_fds=close_fds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, startupinfo=self._startupinfo, env=os.environ) 243 | elif sys.platform == 'linux2': 244 | cmd_subprc_pipe = subprocess.Popen([self._pjon_piper_path, 'version'], shell=False, close_fds=close_fds, 245 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 246 | bufsize=0, startupinfo=self._startupinfo, env=os.environ) 247 | 248 | possible_version_output = "unknown" 249 | 250 | log.debug(">> cmd pipe out") 251 | while cmd_subprc_pipe: 252 | nex_tline = cmd_subprc_pipe.stdout.readline() 253 | if nex_tline == '': 254 | break 255 | possible_version_output = nex_tline.strip() 256 | if self.is_string_valid_pjon_piper_version(possible_version_output): 257 | possible_version_output = possible_version_output.split("VERSION:")[-1].strip() 258 | log.debug("<< cmd pipe out") 259 | cmd_subprc_pipe.terminate() 260 | 261 | return possible_version_output 262 | 263 | def start_client(self): 264 | self._pipier_client_watchdog.start() 265 | self._packets_processor.start() 266 | time.sleep(0.05) 267 | while True: 268 | if time.time() - self._pipier_client_watchdog._birthtime < self._pipier_client_watchdog.START_SECONDS_DEFAULT: 269 | if self._pipier_client_watchdog._start_failed: 270 | raise Exception("client start failed; are you running as root? - needed to access serial port") 271 | elif self._pipier_client_watchdog._pipe.poll() is None: 272 | break 273 | return True 274 | 275 | def stop_client(self): 276 | log.info("..stopping client: %s" % str(self)) 277 | self._pipier_client_watchdog.stop() 278 | self._packets_processor.stop() 279 | try: 280 | self._pipier_client_watchdog.join() 281 | except RuntimeError: 282 | pass 283 | try: 284 | self._packets_processor.join() 285 | except RuntimeError: 286 | pass 287 | return True 288 | 289 | def send(self, device_id, payload): 290 | log.debug("sending %s to %s" % (payload, device_id)) 291 | self._piper_client_stdin_queue.put("send %s data=%s" % (device_id, payload)) 292 | 293 | def send_without_ack(self, device_id, payload): 294 | log.debug("sending %s to %s" % (payload, device_id)) 295 | self._piper_client_stdin_queue.put("send_noack %s data=%s" % (device_id, payload)) 296 | 297 | 298 | class ReceivedPacketsProcessor(threading.Thread): 299 | def __init__(self, parent): 300 | super(ReceivedPacketsProcessor, self).__init__() 301 | self._parent = parent 302 | self.setDaemon(True) 303 | self._stopped = False 304 | self.name = "piper packets processor" 305 | 306 | def run(self): 307 | try: 308 | while True: 309 | try: 310 | if self._parent._pipier_client_watchdog._start_failed: 311 | log.error("PJON-piper client start failed; Check if you run as root - needed for serial port access") 312 | self._parent.stop_client() 313 | self.stop() 314 | 315 | if self._stopped: 316 | return 317 | 318 | line = self._parent._piper_client_stdout_queue.get(timeout=0.01) 319 | 320 | if len(line) > 1: 321 | line = line.strip() 322 | log.debug('%s: %s', self.getName(), line) 323 | if self.is_text_line_received_packet_info(line): 324 | log.debug('%s: %s', self.getName(), line) 325 | try: 326 | packet = self.get_packet_info_obj_for_packet_string(line) 327 | if packet.packet_length == len(packet.payload): 328 | payload = packet.payload_as_string 329 | packet_length = packet.packet_length 330 | packet_info = packet.packet_info 331 | self._parent._receiver_function(payload, packet_length, packet_info) 332 | else: 333 | log.error("incorrect payload length for rcv string %s" % line) 334 | 335 | if self._parent.is_piper_stdout_watchdog_enabled: 336 | self._parent.reset_piper_stdout_watchdog() 337 | 338 | except ValueError: 339 | log.exception("could not process incoming paket str") 340 | finally: 341 | pass 342 | elif self.is_text_line_received_error_info(line): 343 | try: 344 | error_code = self.get_from_error_string__code(line) 345 | error_data = self.get_from_error_string__data(line) 346 | self._parent._error_function(error_code, error_data) 347 | 348 | except ValueError: 349 | log.exception("could not process incoming paket str") 350 | finally: 351 | pass 352 | 353 | except Empty: 354 | continue 355 | 356 | except (IOError, AttributeError): 357 | break 358 | except KeyboardInterrupt: 359 | pass 360 | 361 | def stop(self, skip_confirmation=False): 362 | self._stopped = True 363 | 364 | @staticmethod 365 | def is_text_line_received_error_info(text_line): 366 | if text_line.strip().startswith("#ERR"): 367 | return True 368 | return False 369 | 370 | @staticmethod 371 | def get_from_error_string__code(packet_string): 372 | return int(packet_string.split("code=")[-1].split(" ")[0]) 373 | 374 | @staticmethod 375 | def get_from_error_string__data(packet_string): 376 | return int(packet_string.split("data=")[-1]) 377 | 378 | @staticmethod 379 | def is_text_line_received_packet_info(text_line): 380 | if text_line.strip().startswith("#RCV"): 381 | return True 382 | return False 383 | 384 | @staticmethod 385 | def get_from_packet_string__snd_id(packet_string): 386 | return int(packet_string.split("snd_id=")[-1].split(" ")[0]) 387 | 388 | @staticmethod 389 | def get_from_packet_string__snd_net(packet_string): 390 | net_str = packet_string.split("snd_net=")[-1].split(" ")[0] 391 | return [int(item) for item in net_str.split(".")] 392 | 393 | @staticmethod 394 | def get_from_packet_string__rcv_id(packet_string): 395 | return int(packet_string.split("rcv_id=")[-1].split(" ")[0]) 396 | 397 | @staticmethod 398 | def get_from_packet_string__rcv_net(packet_string): 399 | net_str = packet_string.split("rcv_net=")[-1].split(" ")[0] 400 | return [int(item) for item in net_str.split(".")] 401 | 402 | @staticmethod 403 | def get_from_packet_string__data_len(packet_string): 404 | return int(packet_string.split("len=")[-1].split(" ")[0]) 405 | 406 | @staticmethod 407 | def get_from_packet_string__data(packet_string): 408 | return packet_string.split("data=")[-1] 409 | 410 | def get_packet_info_obj_for_packet_string(self, packet_str): 411 | packet_info = PacketInfo() 412 | PacketInfo.receiver_id = self.get_from_packet_string__rcv_id(packet_str) 413 | PacketInfo.receiver_bus_id = self.get_from_packet_string__rcv_net(packet_str) 414 | PacketInfo.sender_id = self.get_from_packet_string__snd_id(packet_str) 415 | PacketInfo.sender_bus_id = self.get_from_packet_string__snd_net(packet_str) 416 | payload = self.get_from_packet_string__data(packet_str) 417 | length = self.get_from_packet_string__data_len(packet_str) 418 | 419 | return ReceivedPacket(payload=payload, packet_length=length, packet_info=packet_info) 420 | 421 | 422 | class WatchDog(threading.Thread): 423 | TICK_SECONDS = .01 424 | START_SECONDS_DEFAULT = 2 425 | 426 | def __init__(self, suproc_command, stdin_queue, stdout_queue, parent): 427 | threading.Thread.__init__(self) 428 | self.setDaemon(False) # we want it to survive parent's death so it can detect innactivity and terminate subproccess 429 | self.setName('pjon_piper_thd') 430 | self._subproc_command = suproc_command 431 | self._birthtime = None 432 | self._stopped = False 433 | self._start_failed = False 434 | self._pipe = None 435 | self._stdout_queue = stdout_queue 436 | self._stdin_queue = stdin_queue 437 | self._parent = parent 438 | if sys.platform == 'win32': 439 | self._startupinfo = subprocess.STARTUPINFO() 440 | self._startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 441 | self._startupinfo.wShowWindow = subprocess.SW_HIDE 442 | 443 | self.log = logging.getLogger(self.name) 444 | self.log.handlers = [] 445 | self.log.addHandler(logging.NullHandler()) 446 | #self.log.propagate = False 447 | self.log.setLevel(logging.INFO) 448 | 449 | def start_subproc(self): 450 | close_fds = False if sys.platform == 'win32' else True 451 | if sys.platform == 'win32': 452 | self._pipe = subprocess.Popen(self._subproc_command, shell=False, close_fds=close_fds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, startupinfo=self._startupinfo, env=os.environ) 453 | elif sys.platform == 'linux2': 454 | self._pipe = subprocess.Popen(self._subproc_command, shell=False, close_fds=close_fds, 455 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 456 | bufsize=0, env=os.environ, preexec_fn=os.setpgrp) 457 | 458 | self._birthtime = time.time() 459 | 460 | def poll_on_subproc(self): 461 | close_fds = False if sys.platform == 'win32' else True 462 | try: 463 | while True: 464 | try: 465 | if self._stopped: 466 | self.log.debug("poll on subproc: killing within thread run") 467 | self._pipe.terminate() 468 | self._pipe = None 469 | return True 470 | else: 471 | 472 | if self._pipe.poll() is not None: 473 | if time.time() - self._birthtime < self.START_SECONDS_DEFAULT: 474 | self.log.error('WatchDog(%r) start failed', self.getName()) 475 | self._stopped = True 476 | self._pipe = None 477 | self._start_failed = True 478 | return False 479 | elif not self._stopped: 480 | self.log.error('WatchDog(%r) is dead, restarting', self.getName()) 481 | if sys.platform == 'win32': 482 | self._pipe = subprocess.Popen(self._subproc_command, shell=False, close_fds=close_fds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0, startupinfo=self._startupinfo, env=os.environ) 483 | elif sys.platform == 'linux2': 484 | self._pipe = subprocess.Popen(self._subproc_command, shell=False, 485 | close_fds=close_fds, stdin=subprocess.PIPE, 486 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 487 | bufsize=0, 488 | env=os.environ) 489 | self._birthtime = time.time() 490 | 491 | if self._parent.is_piper_stdout_watchdog_enabled: 492 | self._parent.reset_piper_stdout_watchdog() 493 | #else: 494 | # if time.time() - self._birthtime > self.START_SECONDS_DEFAULT: 495 | # if time.time() - self._parent._last_watchdog_poll_ts > 10: 496 | # log.critical("parent thread not active; quitting") 497 | # self._pipe.terminate() 498 | # break 499 | # 500 | # else: 501 | # pass 502 | # #log.info("OK") 503 | #self._parent._last_watchdog_poll_ts = time.time() 504 | 505 | if self._parent.is_piper_stdout_watchdog_enabled: 506 | if time.time() - self._birthtime > self.START_SECONDS_DEFAULT: 507 | if self._parent.should_piper_stdout_watchdog_issue_restart(): 508 | log.warning("PJON-piper restart issued by stdout watchdog") 509 | self._pipe.terminate() 510 | 511 | time.sleep(self.TICK_SECONDS) 512 | 513 | except Exception as e: 514 | self.log.exception('WatchDog.run error: %r', e) 515 | 516 | finally: 517 | try: 518 | self._pipe.terminate() 519 | except (AttributeError, OSError): 520 | pass 521 | try: 522 | psutil.Process(self._pipe.pid).kill() 523 | except: 524 | pass 525 | 526 | def start(self): 527 | self.start_subproc() 528 | 529 | run_thd = threading.Thread(target=self.poll_on_subproc) 530 | run_thd.daemon = True 531 | run_thd.start() 532 | 533 | stdout_thd = threading.Thread(target=self.attach_queue_to_stdout) 534 | stdout_thd.daemon = True 535 | stdout_thd.start() 536 | 537 | stdin_thd = threading.Thread(target=self.attach_queue_to_stdin) 538 | stdin_thd.daemon = True 539 | stdin_thd.start() 540 | 541 | #stdout_process_thd = threading.Thread(target=self.process_stdout_output) 542 | #stdout_process_thd.daemon = True 543 | #stdout_process_thd.start() 544 | 545 | return True 546 | 547 | def stop(self, skip_confirmation=False): 548 | try: 549 | #self.log.info("PID to kill: %s" % self._pipe.pid) 550 | self._stopped = True 551 | 552 | if skip_confirmation: 553 | return True 554 | 555 | timeout = self.START_SECONDS_DEFAULT 556 | while timeout > 0: 557 | if self._pipe is None: 558 | return True 559 | else: 560 | time.sleep(self.TICK_SECONDS) 561 | timeout -= self.TICK_SECONDS 562 | return False 563 | 564 | except AttributeError: 565 | self.log.exception("could not stop thd") 566 | return False 567 | 568 | @staticmethod 569 | def stop_by_pid(pid): 570 | process = psutil.Process(pid) 571 | for proc in process.children(recursive=True): 572 | proc.kill() 573 | process.kill() 574 | 575 | def attach_queue_to_stdout(self): 576 | start_ts = time.time() 577 | while time.time() - start_ts < self.START_SECONDS_DEFAULT: 578 | if self._pipe is not None: 579 | self.log.debug("attaching queue to stdout") 580 | while True: 581 | try: 582 | if self._stopped: 583 | break 584 | if self._start_failed: 585 | break 586 | nextline = self._pipe.stdout.readline() 587 | if nextline == '':# and self._pipe.poll() is not None: 588 | continue 589 | self._stdout_queue.put(nextline.strip()) 590 | 591 | except AttributeError: 592 | self.log.exception("stdout queue broken") 593 | break 594 | if self._pipe: 595 | self._pipe.stdout.close() 596 | else: 597 | self.log.warning("pipe is None; can't attach queue to stdout") 598 | 599 | time.sleep(0.2) 600 | 601 | def attach_queue_to_stdin(self): 602 | start_ts = time.time() 603 | while time.time() - start_ts < self.START_SECONDS_DEFAULT: 604 | try: 605 | if self._pipe is not None: 606 | self.log.debug("attaching queue to stdin") 607 | while True: 608 | try: 609 | if self._stopped: 610 | break 611 | if self._start_failed: 612 | break 613 | input_cmd = self._stdin_queue.get(timeout=.01) 614 | if input_cmd == '': # and self._pipe.poll() is not None: 615 | continue 616 | self._pipe.stdin.write(input_cmd+'\n') 617 | self._pipe.stdin.flush() 618 | continue 619 | except Empty: 620 | continue 621 | except (IOError, AttributeError): 622 | break 623 | if self._pipe: 624 | self._pipe.stdin.close() 625 | else: 626 | self.log.warning("pipe is None; can't attach queue to stdin") 627 | 628 | time.sleep(0.2) 629 | except KeyboardInterrupt: 630 | pass 631 | --------------------------------------------------------------------------------