├── LICENSE ├── README.md ├── pyproject.toml ├── src └── ppm_reader │ ├── __init__.py │ └── __main__.py └── tests └── empty_file /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 redoxcode 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi version shield](https://img.shields.io/pypi/v/micropython-ppm_reader)](https://pypi.org/project/micropython-ppm_reader/) [![pypi downloads per month shield](https://img.shields.io/pypi/dm/micropython-ppm_reader?color=brightgreen)](https://pypi.org/project/micropython-ppm_reader/) 2 | ## Description 3 | A micropython library to decode PPM signals coming from a RC receiver (as used for rc planes and drones). 4 | 5 | This library is focused on savety and includes functions that can be used to detect a faulty or lost signal. 6 | **For this it is required to init the PpmReader class with the correct number of channels in the PPM signal. This might be a different number than the amount of servo connectors on the RC receiver hardware!** 7 | 8 | Created for the use with pi pico, but should work on other boards as well. 9 | You can find the [API documentation](https://github.com/redoxcode/micropython-ppm_reader/#API) and a few [examples](https://github.com/redoxcode/micropython-ppm_reader/#Examples) below. 10 | 11 | ## Examples 12 | 13 | ### Print the values of all channels 14 | ```Python 15 | from ppm_reader import PpmReader 16 | ppm_pin_id=28 17 | ppm_channels=8 18 | ppmReader=PpmReader(ppm_pin_id,ppm_channels) 19 | while True: 20 | time.sleep(0.5) 21 | print(ppmReader.get_values()) 22 | ``` 23 | ### Find the number of channels 24 | ```Python 25 | #the number of channels should be known before you init PpmReader 26 | #if the channel number is incorrect only guess_channel_count will work 27 | from ppm_reader import PpmReader 28 | ppm_pin_id=28 29 | ppmReader=PpmReader(ppm_pin_id,channels=0) 30 | while True: 31 | time.sleep(0.5) 32 | print(ppmReader.guess_channel_count()) 33 | ``` 34 | ### Find values for min_value and max_value 35 | ```Python 36 | #move the controls to the extreme positions and observe the values 37 | from ppm_reader import PpmReader 38 | ppm_pin_id=28 39 | ppm_channels=8 40 | ppmReader=PpmReader(ppm_pin_id,ppm_channels) 41 | while True: 42 | time.sleep(0.5) 43 | print(ppmReader.get_raw_values()) 44 | ``` 45 | ### Check for a loss of signal 46 | ```Python 47 | from ppm_reader import PpmReader 48 | ppm_pin_id=28 49 | ppm_channels=8 50 | ppmReader=PpmReader(ppm_pin_id,ppm_channels) 51 | 52 | #wait initial connection with the remote 53 | while ppmReader.get_valid_packets() == 0: 54 | print("waiting for connection ...") 55 | time.sleep(0.5) 56 | print("connected.") 57 | 58 | #got signal, continue to main loop 59 | while True: 60 | last_packet_time=ppmReader.time_since_last_packet() 61 | print(last_packet_time) 62 | if last_packet_time>25000: 63 | #25ms without a new packet 64 | #take security measures here (for example stop all motors) 65 | print("connection lost") 66 | #wait for connection 67 | while ppmReader.time_since_last_packet()>25000: 68 | pass 69 | print("connected again") 70 | else: 71 | #connection ok. Do something here 72 | print(ppmReader.get_values()) 73 | ``` 74 | 75 | ## API 76 | ### class PpmReader(pin_id,channels,min_value=1000,max_value=2000,packet_gap=4000) 77 | - pin_id: GPIO pin connected to the PPM signal comming from the RC receiver. 78 | - channels: Number of channels in the PPM signal. if the channel count is wrong the packts will be considered invalid. 79 | - min_value: Minimum timeframe per channel in us (this should be around 1000us for standard equipment). 80 | - max_value: Minimum timeframe per channel in us (this should be around 2000us for standard equipment). 81 | - packet_gap: Minimum time gap between packets in us (4000us should be used for standard equipment). 82 | 83 | ```time_since_last_packet()``` 84 | - returns the time passed since the last valid packet arrived in us. This will stay below about 5000us if every packet is received correctly. Missing 2 or 3 packets is usually not a problem. 85 | 86 | ```get_valid_packets()``` 87 | - returns the number of valid packets received 88 | 89 | ```get_inalid_packets()``` 90 | - returns the number of invalid packets received 91 | 92 | ```reset_packet_counters()``` 93 | - resets counters for valid and invalid packets received 94 | 95 | ```get_raw_values()``` 96 | - returns a list of all raw timeframes in us in the last valid packet received 97 | 98 | ```get_raw_value(channel)``` 99 | - returns the raw timeframe in us in the last valid packet received for a given channel 100 | - channel: channel to get the value from 101 | 102 | ```get_values()``` 103 | - returns a list of all values in the last valid packet maped to a range of 0.0 to 1.0 (values are not clipped) 104 | 105 | ```get_value(channel)``` 106 | - the value for a given channel in the last valid packet maped to a range of 0.0 to 1.0 (values are not clipped) 107 | - channel: channel to get the value from 108 | 109 | ```get_values_bi()``` 110 | - returns a list of all values in the last valid packet maped to a range of -1.0 to 1.0 (values are not clipped) 111 | 112 | ```get_value_bi(channel)``` 113 | - the value for a given channel in the last valid packet maped to a range of -1.0 to 1.0 (values are not clipped) 114 | - channel: channel to get the value fro 115 | 116 | 117 | ```guess_channel_count()``` 118 | - returns the number of channels in the last packet (incase you are not sure how many channels your signal has) 119 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | 3 | [build-system] 4 | requires = ["setuptools>=61.0.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "micropython-ppm_reader" 9 | version = "1.0.2" 10 | description = "Decodes PPM signals from RC receivers" 11 | readme = "README.md" 12 | authors = [{ name = "redoxcode", email = "redoxcode@github.com" }] 13 | license = { file = "LICENSE" } 14 | classifiers = [ 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: Implementation :: MicroPython" 17 | ] 18 | keywords = ["PPM", "reader", "rc", "remote", "receiver"] 19 | dependencies = [] 20 | 21 | [project.optional-dependencies] 22 | dev = [] 23 | 24 | [project.urls] 25 | Homepage = "https://github.com/redoxcode/micropython-ppm_reader" 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ppm_reader/__init__.py: -------------------------------------------------------------------------------- 1 | import machine 2 | import time 3 | 4 | 5 | class PpmReader: 6 | def __init__(self,pin_id,channels,min_value=1000,max_value=2000,packet_gap=4000): 7 | self.channels=channels 8 | self.min_value=min_value 9 | self.max_value=max_value 10 | self.packet_gap=packet_gap 11 | self.pin = machine.Pin(pin_id, machine.Pin.IN) 12 | self.pin.irq(trigger=machine.Pin.IRQ_RISING,handler=self._irq_handler,hard=True) 13 | 14 | self.timer=time.ticks_us() 15 | self.last_valid_time=0 16 | self.valid_packets=0 17 | self.invalid_packets=0 18 | self.last_packet_length=0 19 | self.current_packet=[] 20 | self.current_channel=0 21 | self.last_valid_packet=[] 22 | 23 | for i in range(self.channels): 24 | self.current_packet.append(0) 25 | self.last_valid_packet.append(0) 26 | 27 | def _irq_handler(self,_p): 28 | now = time.ticks_us() 29 | delta = time.ticks_diff(now,self.timer) 30 | self.timer = now 31 | if delta > self.packet_gap: 32 | #end of a packet 33 | self.last_packet_length=self.current_channel 34 | #check packet length 35 | if self.last_packet_length == self.channels: 36 | #packet is good 37 | self.valid_packets+=1 38 | self.last_valid_packet=self.current_packet 39 | self.last_valid_time=now 40 | else: 41 | #something went wrong 42 | self.invalid_packets+=1 43 | 44 | #start new packet 45 | self.current_channel=0 46 | else: 47 | if self.current_channel < self.channels: 48 | #save time between pulses 49 | self.current_packet[self.current_channel]=delta 50 | self.current_channel+=1 51 | 52 | def time_since_last_packet(self): 53 | return time.ticks_diff(time.ticks_us(),self.last_valid_time) 54 | 55 | def get_valid_packets(self): 56 | return self.valid_packets 57 | 58 | def get_inalid_packets(self): 59 | return self.invalid_packets 60 | 61 | def reset_packet_counters(self): 62 | self.valid_packets=0 63 | self.invalid_packets=0 64 | 65 | def get_raw_value(self,channel): 66 | return self.last_valid_packet[channel] 67 | 68 | def get_raw_values(self): 69 | return self.last_valid_packet 70 | 71 | def get_values(self): 72 | values=[] 73 | for i in range(self.channels): 74 | values.append(self.get_value(i)) 75 | return values 76 | 77 | def get_value(self,channel): 78 | return (self.last_valid_packet[channel]-self.min_value)/(self.max_value-self.min_value) 79 | 80 | def get_values_bi(self): 81 | values=[] 82 | for i in range(self.channels): 83 | values.append(self.get_value_bi(i)) 84 | return values 85 | 86 | def get_value_bi(self,channel): 87 | return self.get_value(channel)*2.0-1.0 88 | 89 | def guess_channel_count(self): 90 | return self.last_packet_length 91 | -------------------------------------------------------------------------------- /src/ppm_reader/__main__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redoxcode/micropython-ppm_reader/f93f8a81e1f42e6743d80dba0a8f0ba8d8848e56/src/ppm_reader/__main__.py -------------------------------------------------------------------------------- /tests/empty_file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redoxcode/micropython-ppm_reader/f93f8a81e1f42e6743d80dba0a8f0ba8d8848e56/tests/empty_file --------------------------------------------------------------------------------